Updated 2008-10-24 16:29:19 by nem

Bryan Oakley wrote in comp.lang.tcl on 2004-04-19:

John Douglas wrote:
 > There are numerous examples on the tcl wiki, in this group archive, and in
 > books, etc. answering very specific questions about widgets.  I'd like to
 > see some examples of styles and philosophies used for larger applications
 > than the simplified examples.

I don't have any small examples readily available, but I'll try to describe roughly how I create Tk apps. The nutshell summary is this:

  1. never hard-code widget paths anywhere except where they are created
       (see update dated May 14, 2008)

  1. separate widget creation from widget layout
  2. never use inline code as the value for a -command option or key binding
  3. use Actions whenever possible

I've been wanting to get some of this stuff written down, so I'm taking the opportunity. This is a long post and I hope nobody minds, but I lack the time to make it shorter :-)

Rule 1: never hard-code widget paths except where they are created edit

If I've learned anything as a long time tk developer, it's that widget paths change over time. You'll need to add a border via a frame, or you'll need to move something from one frame and put it in another, or you'll want to rename a widget or delete a frame that is no longer necessary, or ...

The technique I use is to always store widget paths in an array. I don't store the name of every widget, just "important" ones. The rule of thumb is that if I ever expect to reference the widget outside of where it is created I will store its name.

Example:
     proc toolbar.create {} {
	 global widgets
	 set widgets(toolbar) .toolbar
	 set widgets(toolbar.cutButton) $widgets(toolbar).cutButton
	 set widgets(toolbar.copyButton) $widgets(toolbar).copyButton
	 ...
	 frame $widgets(toolbar) ...
	 button $widgets(toolbar.copyButton) ...
	 ....
     }

(by the way... the dot notation used above has no special significance. Lately I prefer "toolbar.create" over "toolbar_create" or "createToolbar", but that's just personal preference)

In addition to the benefit of being able to rename widgets without having to hunt down references all over your code, it also allows you to have more readable symbolic names for widgets. In theory, this makes your code easier to read and thus easier to maintain.

Rule 2: separate widget creation from widget layout  edit

My applications look something like this:
     proc main {} {
	 init
	 widgets
	 layout
	 bindings
     }
     proc init {} {...}
     proc widgets {} {
	 toolbar.create
	 menubar.create
	 ...
     }
     proc toolbar.create {} {...}
     proc menubar.create {} {...}
     proc layout {} {
	 global widgets
	 pack $widgets(toolbar) -side top -fill x ...
	 ....
     }
     proc bindings {} {...}

Sticking to this rule makes it easy to create several layouts for your app. For example, you could give your user the ability to choose a "two pane" layout or a "3 pane" layout, or maybe icon vs. details layout, or simple vs. complex, etc. Maybe the app you're working on now doesn't require (or have time in the schedule for) such a feature, but if you write the code this way from the start, adding such a feature later becomes relatively trivial.

I also believe (though have no facts to back up this belief) that it makes the code more maintainable. If all of the layout is in one spot (or several layout-specific spots) it's easier to know where to go to fix a layout-related problem. As a side benefit it also helps to enforce rule #1. Any widget you need to lay out should have a symbolic name rather than a hard-coded widget path.

An exception I make to this is that sometimes my layout proc will create its own frames when it needs to. These are frames used only by the layout proc and never accessed from anywhere else in the code.

This doesn't mean you need a single monolithic proc filled with grid, pack and place statements in a series of nested if statements. Consider an application that can have a vertical or horizontal toolbar, and a 2-pane or 3-pane layout; the logic might look something like this:
     proc layout {} {
	 global options
	 layout.$options(layout)
	 toolbar.layout.$options(toolbarType)
     }
     proc layout.2pane {} {...}
     proc layout.3pane {} {...}
     proc toolbar.layout.vertical {} {...}
     proc toolbar.layout.horizontal {} {...}

This rule can lead to a situation where you may create widgets that aren't used by the current layout. I personally don't see this as a big problem. If it is, create pairs of widget creation and layout procedures that work together while still keeping widget creation and layout separate (eg: widgets.2pane, layout.2pane, widgets.3pane, layout.3pane, etc).

Rule 3: never use inline code as the value for a -command argument or key binding edit

The advice I give on comp.lang.tcl is usually phrased "never have more than one command as the value of a -command argument or key binding. You may have two if and only if the second command is "break"

Many times I've seen questions in comp.lang.tcl that looks something like this:
     Why is $i always the value 5 when I press my button?

     for {set i 0} {$i < 5} {incr i} {
	 button .b$i -command {
	    borka borka borka
	    yada yada yada
	    puts "you pressed button $i"
	 }
     }

I'll be a little bold and say that _all_ quoting problems with such code can be avoided by putting the code in a procedure. Using this method, items that must be dereferenced at the time the button is created can be passed in as arguments; all other values can be determined by the proc when it runs. For example:
     for {set i 0} ... {
	 button .b$i -command [list doButton $i]
     }

More importantly, however, is the fact that over time these blocks of code tend to grow, shrink, or get shared. You may have a button to perform the "copy" operation. At some point you may want a keyboard accelerator or a menu item to invoke the same code. If it's in a procedure the sharing is trivial. If you had initially hardcoded it in the definition of the button it takes a bit more work.

I've found in my experience that virtually every time I inlined code this way to save myself some time, I've had to come back later and move that code to a proc because the code grew to be more complex.

NEM: Note that you can use lambdas if you want to inline code but still get the quoting benefits of a proc:
proc lambda {params body args} {
    list ::apply [list $params $body] {*}$args
}
for {set i 0} ... {
    button .b$i -command [lambda i {
        puts "You pressed button $i"
    } $i]
}

Generally, though, a named proc is a better choice, as the name can abstract from the details of the implementation, and can be reused in multiple places.

Rule 4: use actions edit

The problem is this: many of the things your users can do can be done in more than one way. For example, consider the "copy" operation found on most edit menus. In a full featured app the user might be able to select Edit->Copy from the menubar, click right and select "Copy" from a context senstitive menu, click on the copy button on the toolbar, or press control-c from the keyboard. There may be other ways to do this too -- mouse gestures, voice recognition, etc.

A well behaved application will disable all these methods if, for example, no text is selected. Ideally the toolbar button and menu items will be grayed out, the accelerators will do nothing or perhaps ring a bell, etc.

If you follow none of the advice in this message, when your program decides there is nothing to copy it might do something like this:
     proc no_selection {} {
	 .toolbar.copy configure -state disabled
	 .menubar.editMenu entryconfigure Copy -state disabled
	 .popup entryconfigure Copy -state disabled
     }
     proc doCopy {} {
	 if {<there's nothing selected>} {
	     bell
	 } else {
	     <do the copy>
	 }
     }

If you use actions, it becomes this:
     proc no_selection {} {
	 action disable copy
     }

Neat, isn't it? Plus, imagine if someone later requests that the menu items should be "Copy Selection" rather than copy. Without actions you not only have to change the labels, but you have to hunt down the places in your code where you use those labels as indicies and change them, too.

This should probably be rule number one.

There is no official definition of "actions", but Bryan Schofield provides a package that provides a handy implementation, and he started a nice thread [1] in this newsgroup that discussed them.

http://groups.google.com/groups?th=ddf110816a167ca9

Whether you use that package or write your own doesn't matter; what matters is that you use a similar concept. Personally I recommend writing your own code so you understand exactly how they work. The implementation doesn't have to be as fancy as Bryan Schofield's; my production version for example weighs in at 177 lines, with about 20% of that comments. It's not the implementation that's important, it's the concept.

I hope some of this helps. It is by no means the best way to write a tk based app; it is simply what works best for me. Sticking to these rules makes me more efficient but they may not all work for you. Fortunately, Tk is very flexible; do some experimentation and find what works best for you.

KPV I don't agree with Rule 1 and Rule 2. Two reasons given for Rule 1 are that "widget paths change over time" and it makes it easier to rename of widgets. Well, I avoid both these problems simply by using short widgets paths and using pack's and grid's "-in" option. So instead of having a widget named .outFrame.inFrame.spacingFrame.copyButton I simply have .copyButton.

As for Rule 2, I find that a vast majority of my widgets are only ever referenced twice: creation and layout (this is especially true if you exploit a widgets -textvariable type option). So what I find has worked well for me is have one routine that builds a particular display--which I typically call DoDisplay or DoPrintDialog. This way if I want to tweak the display by say adding another button, I have only one place to go to do so, as opposed to two disparate procedures.

Rule 3 I agree with whole-heartedly; Rule 4 sounds intriguing, especially it seems to me in automation and scripting (scripting a scripting language?).

rdt I see your 'dislike' of rules 1 & 2. Using your style, how would you handle the goal of 'different layout schemes' for a user? This does seem to be an advantage of what is explained here.

KPV To change my GUI I just modify the grid/pack lines. Since I don't use full paths, my widgets can be gridded/packed anywhere (in the same toplevel). For me, widget path's don't change over time.

DKF: Do the layout well once, and users won't complain. Or at least they'll focus on complaining about other aspects of the app instead... ;^) (KPV I wish I was that good :)

rdt It sounds like you are saying that you know better what layout the user wants than the user does? :)

NEM: A good user interface designer probably does. For instance, compare the large amount of crappy themes and open source user interfaces for programs on Linux to the well-designed interfaces on Mac... :) (I know I'm going to get some stick for that blatant flame-bait, but I think there is some truth in it regardless). One piece of user-interface design advice I picked up at some point was that you should limit the choices available in the interface to just those which are actually important, everything else is just a distraction. Being able to change the user interface isn't important (assuming it was designed ok in the first place). I'm no UI guru though...

Bryan Oakley: that's the point -- very few of us are UI gurus. I'm a (*ahem*) good user interface designer but I don't always know what's best for every user. Sometimes they like things oriented left-to-right, sometimes top-to-bottom. I will also disagree with NEM's statement that good UI designers do know -- *great* ones maybe know, but there aren't very many great UI designers in the world.

MG - I can't claim that I've written a great amount of Tcl code, but as someone who started a fairly big project when I frankly didn't know I was doing (that being Potato MUSH (MUD) Client, needless to say with much fixing since then;), I know that following the rules on this page from the start would've saved a lot of heartache (not to mention a lot of Find+Replace and hours of re-writing/debugging).

  • Rule 1 I definitely agree with - if you need to reference widgets anywhere after creation and packing*, store their paths in a global array.
  • Rule 2 I do disagree with, though, for exactly the same reasons as KPV mentioned above (hence why I don't store the names of widgets that are only created and packed, as I do the two together).
  • Rule 3 is something I've seen mentioned time and again by people who know what they're talking about far better than I do, and something I've taken as gospel since I spent three hours debugging a problem a single extra procedure for each binding would cure.
  • And as for Rule 4... heh, as it happens, I do the first, so-called "bad" method, but it has it's advantages, I think. While, true, the second piece of code is a heck of a lot neater and more concise, with the first form you a) don't need to include the code for defining these "actions", and b) you get the advantage that menu entries, toolbar buttons, etc, are actually -deactivated-, rather than their just not working because the action is disabled. I'd much rather see the "Copy" entry greyed out, than have it appear active but not work. (And if you reference menu entries, etc, by the name of their label, you're probably not being very i18n compliant :) I tend to use an array for those, too - set menu(edit,copy) 5 - so I can reference the menu easily without relying on the label saying a particular thing. And finally, for what it's worth :) I tend to go for the createToolbar style of proc naming, since create.toolbar with the dots looks, at a glance, a little like a widget pathname. All confusion is best avoided at 3am ;) Wow, I really did ramble there...

DKF: FWIW, a complete implementation of Actions will handle the greying out of buttons/menu-items. That's why it is quite tricky to set up in the first place.

MG - Surely for it to do that, the actions could would either need to have the list of widgets itself, or be very intelligent (read as: very long), though? Or, at least, a heck of a lot more time-consuming (at least in the short term)...

Bryan Oakley - actions as I implement them know which widgets they are associated with. I'm not quite sure why Donal says it is "quite tricky", as my current implementation is not even a couple hundred lines. The primary task of the actions is, in fact, to gray out the options. That's why I first started using them half a dozen years ago. I've found, though, that it's also mighty convenient that the actions themselves don't do anything so that accelerators (which are invisible and thus can't be grayed out) won't work when they're not supposed to.

I don't understand MG's comment about them being more time-consuming. Time consuming at runtime? Time consuming to implement? I find neither of these to be true. Defining an action takes no more time than defining a proc. Associating them with widgets takes either a half a line or a line of code. Actions rock.

MG - I meant more time-consuming in times of writing the code which handles the actions (defining them, (en/dis)abling them, etc). But, if the action knows which widgets are associated with them.. well, unless the code is more complicated than I'm thinking of atm and they 'work out' which widgets are assoicated with them, then you'd need to still give it the list manually, in which case I can't really see an advantage over using actions instead. Is the code you use on the wiki, or elsewhere on the net anywhere, so I can see if I'm missing something?

Bryan Oakley - see the page Actions for a discussion

JE: All good advice, though personally I rarely follow Rule 2. The reason: keyboard traversal. Keyboard traversal order is determined by stacking order, and stacking order is (initially) determined by creation order. So the order in which widgets are created can't be completely decoupled from the layout unless you want to do a third pass to [raise] or [lower] widgets into the right order for navigation. The easiest way I've found to keep the two in sync is to do both at the same time:
  grid \
    [button $cmd.yes ... ] \
    [button $cmd.no  ... ] \
    [button $cmd.cancel ... ] \
    ''...grid options here ...'' '

This idiom isn't always applicable, of course -- usually the packing order is different from the navigation order -- but when it is, it's the easiest to write and maintain.

DKF: That's a reason for at least short-range decoupling of creation and packing/gridding. The -in option is very useful in complex layouts BTW.

Bryan Oakley - tab completion isn't as big an issue as it may seem. For example, I've never had multiple layouts where the widgets within a particular section were reordered. Different layouts to me mean things like "do you want headers on top and mail messages on the bottom, or headers on the left and messages on the right". And for the times when the layout does affect tab traversal it's a simple thing to rearrange the stacking order.

wdb With Rule 3, I agree that inlining of -command options have traps, but using a procedure enforces me to remember what I wanted in the procedure definition. Worse: it wastes the namespace of procedures with a proc which is used only once. -- My approach was using an anonymus lambda term as follows:
 button .b -command [list [lambda {x} {
     puts "$x -- ouch"
 }] $justCurrentValue]

So, the code I meant is exactly at the position it is called. Plus, no additional proc name to care of. -- Of course, lambda must be defined, but I think but that's not difficult.

Bryan Oakley May 14, 2008 - I've since changed my opinion about point number one. In fact, I started reconsidering my approach shortly after I first wrote this page based on the comments of others. I now prefer to hard-code widget paths as much as possible, and almost never more than one or two levels deep in the hierarchy. I do, however, consider this an advanced technique and still consider storing widget paths in variables to be a good technique for beginners.

What I've found in my own experience is that typically a GUI will only have a very few "primary" widgets -- widgets you need to reference in other places in the code. Examples are the text widget in a text editor, a canvas in a drawing program, and so on. I find it easier to maintain code if these widgets live as children of "." and packed or gridded using the -in option.

One exception to the rule of putting most widgets as children of "." is when there is a logical hierarchy. For example, I usually will name toolbar widgets as children of .toolbar (eg: .toolbar.cut, .toolbar.copy, etc). I do this even if the layout dictates intermediate frames for organization. For example, I may have .toolbar.cut, .toolbar.copy and .toolbar.paste packed in .toolbar.group1. In this way I don't have to invent artificial names just to keep the widget namespace tidy. For example I may have a .prefsDialog.ok, a .confirmDialog.ok, etc.

The common thread running through all this is simply this: strive to minimize the likelihood you'll have to rename widgets when you change the layout. Do this either by storing widget paths in variables, or using the shortest possible widget paths and judicious use of -in with the geometry manager of your choice.

peterc 2008-10-23: Heartily agree with Rule 3. See also The (command bound to event) error for some good reasons to take Rule 3 as gospel.

DKF: Thinking in more depth about Rule 1, the thing which I settled on for simpler layouts was creating the "interesting" widgets with something like this:
  set w [toplevel .some.name]
  frame $w.f; frame $w.f.f ;# etc.
  canvas $w.c

And then using the -in option to pack/grid to arrange things. Like that, the pathnames of the widgets I have to interact with are simple (can just save the root name of the dialog); the frames don't normally need their names saving once you've done the layout.

For complex layouts (i.e. ones where you have to make custom-interactive widgets the children of a ttk::notebook, text or canvas) you need other techniques. But that's surprisingly rare; most dialogs don't do anything excessively complex (since you can usually use -textvariable etc. to connect a Model to the widgets rather than needing to work at the Controller level directly).