Version 14 of writing readable, manageable, reusable Tk

Updated 2003-08-12 22:59:20

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:


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
 }

(Peter Lewerin did minor edit to make it executable.)

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?

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.


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


Category Design | Category Concept | Category GUI