writing readable, manageable, reusable Tk

aricb Too often I find that when I write Tk code to do anything non-trivial, I end up with a sprawling disaster that is a pain to modify or even understand six months later. As I'm sure other programmers have encountered this problem, I searched for some guidelines on the Wiki and on the web. I'll share what I found below. However, I came to the conclusion that much more could (and should) be said on this topic.

Here's what I've found so far (please add to this list):

Tom Tromey's style guide [L1 ] has an excellent section on Tk. By contrast, Ray Johnson's Tcl style guide [L2 ], which has become the de facto standard for Tcl, doesn't address Tk specifically.

A handful of wiki pages address this topic:

aricb wants to note that the comments below have been most helpful and that his Tk style in 2005 is quite improved from how it was when this page was created.


Here's what little I currently do to make my code more readable. Please share any reactions to these:

  • In all but the most trivial scripts, I create new toplevels instead of using .
  • I try to combine geometry management with widget creation when possible, i.e.
 grid [text .mytoplevel.text] -row 0 -column 0

However, when I have to specify lots of options for the widget and/or the geometry manager, I end up with long lines which IMHO look bad even if I break them up with backslashes.

  • I try to break things up into small procs, where (for example) one proc will create a frame and its children and another proc will create bindings for those widgets.

PWQ 12 Aug 2003 I use a table driven approach to gui creation. As an example

 set widgetlist { {button $par.b -command xx} {entry $par.e -width 6}}
 foreach w $widgetlist {
   pack [eval $w] -anc nw
 }

The command as listed in the list (which can be spread out over multiple lines) become much easier to read.

I have a system that extends this concept but is to complex to outline here, here is an example screen as an example (Stored in a file/database etc is the gui definition ):

 Options {

        *LogoEntry*Entry*font {Times 24}
        *LogoPanel*Canvas*width 600
        *LogoPanel*Canvas*height 400
        *LogoMenu*Button*ipadx 0
        *LogoMenu*Button*padX 2
        *LogoMenu*Button*relief solid
 }

 Form Logo {
        Script { namespace eval Logo {set __create 1.0} }
 V3S#LogoPanel {
        H#LogoMenu {
          V3s {
           H3r {
                PackOptions {-anc w -padx 1}
                H1s {
                        PackOptions {-anc w -padx 1}
                        T { }
                        T {Test:}
                        B {{Clear} {::Logo::test:clear}  }
                        B {{Check} {::Logo::test:check}  }
                        B {{Load} {::Logo::test:load}  }
                        B {{Save} {::Logo::test:save}  }
                }
                Sb/Lv {{{-orient v -command {!Logo yview}  -width 10 }}} {-fill y}
                Lb/Logo:Logo::data(_cmds) {-font {{{Times 18 bold}}} -width 0 {{
 -yscrollc {!Lv set}}} } {-fill y -expand 0}
                V+b {
                        E/Vars:Logo::data(_vars) {0} {-fill x}
                        H+b {
               T { }
                T {Programme: }
                B {{Save} {turtleSave [!Prog get 0 end] [!Vars get]} }
                B {{Load} {turtleLoad !Prog}}

      ........

Notes:

H means arrange horizontally

V means vertically.

The # defines the class.

The second item in each list are short cuts for the most common widget option.

Ie B {{Save} -justify c} equals button -text Save -justify c.

The third list item are the manager options (ie pack).

/xx associates a variable with the widget.

:xx defines a reference to the widget that can be used in other widget definitions

Names above are abbreviations (is B is button, Sb scrollbar, Lb listbox etc.


Bryan Oakley 12-Aug-2003

I keep widget creation and geometry management separate. I also break up my UI into manageable units and build them separately. Within the unit things are managed however it makes sense for the unit, but the overall layout of these units is done in a separate proc. That makes it easy to create a View menu with items like "show toolbar", 'show statusbar", etc.

I also store in a global array any widget paths that I use in other parts of the program. This way I can change the widget hierarchy as needed without having to track down hard-coded path names in other parts of the code.

My initialization code, then, looks something like this:

    proc main {} {
        ...
        widgets
        widgets.layout
        ....
    }

    proc widgets {} {
        widgets.menubar
        widgets.toolbar
        widgets.statusbar
        widgets.main
    }
    proc widgets.toolbar {} {
        global widgets
        set widgets(toolbar) .toolbar
        button $widgets(toolbar).cut ...
        button $widgets(toolbar).copy ...
        ...
        pack $widgets(toolbar).cut $widgets(toolbar).copy \
            -side left
    }
    ...
    proc widgets.layout {} {
        global widgets
        global options
        . configure -menu $widgets(menubar)
        grid $widgets(toolbar) -row 0 -column ...
        grid $widgets(main) -row 1 -column ...
        grid $widgets(statusbar) -row 2 -column ...
        if {!$options(-showtoolbar)} {
            grid remove $widgets(toolbar)
        }
        ....
    }

In reality I pass in the toplevel window to each of the procs so that, in theory, I can reparent the widgets if later I choose to embed them in a larger program. I left that out of this example to make the example a little easier to understand.


Bryan Oakley 12-Aug-2003

here's another one: never put more than two statements in a widget callback or binding, and if you use more than one, the second one must always be "break".

PWQ 13 Aug Why must the second argument be break, that stops any class binding from firing?

(Bryan Oakley: I'm not saying there must be a second command, only that if there is it should be "break" (for the very reason you suggest). A more clear way to say this is, perhaps, "you should never have more than one command; an exception can be made to add ';break' to inhibit other bindings")

Put another way, always have your callbacks and bindings call procs. It makes quoting easier, makes it easier to modify what bindings do, and makes it easier to share code between bindings and callbacks (ie: the "cut" toolbar can call the same proc as the "cut" menubar item and the "cut" accelerator).


PWQ 13 Aug 2003

I also use a table driven approach to event bindings (when not using the forms system I have devised). This can be seen in my CAD package [L3 ].

A Structure holds all the event bindings and one procedure is called for all events. This then matches the event with current program state and dispatches the appropriate proc to handle the event.

What this means is that the event is isolated from the widget, it does not matter which widget (or what it's path name is) generates the event.

As an example, the <MOTION> event normally does nothing, but if say an object on a canvas has been grabbed, it now is dragged around with the mouse.

An example for managing canvas items. From the table (a list of lists in a variable):

{ - - <<SELECT>> {procGrab Move} }

{Move * <<MOTION>> {procMove} }

Explaination:

   1 If no object (-) has been grabed in any mode (-) the <<SELECT>> event (B1) calles procGrab and changes mode to Move.
   2 if in ''Move'' mode and any object has been grabbed (*)  the <<MOTION>>  (Motion) event calls procMove.

While it seems overly complicated, it is far simpler to do this than to bind individual events to each widget. Also it groups all bindings in one place and allows them to be loaded from a file without having to change code.

Lastly, This technique adds state information to the bindings so allows multiple event chaining to be implemented without having to change the event bindings on the widgets. Ie emacs two key type bindings can easily be implemented in this structure.


MAK (13 Aug 2003) - Like others, I (at least nearly) always separate widget creation from geometry management. The only usual exception is for simple frames that I don't care much about. But, for the most part, I keep them separate not only so that layout can be changed moderately easy, but so that I can maintain variables with the paths for various widgets, both for specifying the path prefix for other contained widgets and so that any code elsewhere that uses thise widgets can do so without hard-coding of the paths.

Rather than globals, however, I tend to use Namespace MegaWidgets with variables within the appropriate namespace for keeping track of any child widgets and other data. These are usually arrays keyed off either the toplevel (for megawidgets that are intended to appear only once per toplevel) or off the megawidget's path (for child widgets of a particular instance of the megawidget). Each of these megawidgets has a function to create an instance of it that takes a widget path as an argument (as what to create the megawidget as, which also becomes the return value for the constructor), and sometimes options for configuration, so that they behave pretty close to the way normal Tk widgets work even if they're a primary area the GUI.

The nice thing about this is that it's much more lightweight than a full OOP system but suits its purpose pretty well, and I can keep calling the MegaWidget proc on the same widget, but from different namespaces, to add functionality that is more specific for a particular use. I use a slightly extended version from the one in the Wiki that sets up some other things automatically, like automatically calling a destructor function in each namespace where the MegaWidget proc was called, for semi-automatic garbage collection. The drawback, though, is all the eval calls involved in calling the widget subcommands, but hopefully this will change when some argument expansion solution is adopted (hence my keen interest in [lexpand] or other possible TIP #144/103 solutions).

Of course, naming conventions and formatting styles are always important for readability, but they tend to be somewhat subjective. I usually name my procs for various events (bindings etc.) with a consistent "onSomethingEvent" convention (for when Something happens), except when it's the result of clicking on a button or menu entry, in which case it usually starts with "cmd" (for command). I use to a two- to three-letter prefix (plus descriptor) for nearly all of my widget names such that, given a path, I know what each of the elements are (e.g. fr for frame, bu for button, etc.) at a glance. For configuration arguments specified at widget creation time, I usually split it apart into many lines -- one for each option, with option keywords and values vertically aligned for (subjective, of course) readability.

Hmm.. What else.. Well, I try to focus on modularity, of course, if that wasn't clear from what I said about Namespace MegaWidgets. These would be more readable still if [package require] let you import the package into other than the global namespace (see [L4 ]), but oh well. (Which brings up another point: try to avoid using hard-coded namespaces. Use [namespace current] for widget textvariables etc., and [namespace code] for bindings.)


Please contribute any principles that help you write Tk code in a more readable, manageable, and/or reusable way.