Writing Tk Code in the Right Order

A few days ago I was working with some code someone else had written, trying to remove a few of the most annoying bugs. One thing that I found most frustrating was that if I made the window smaller, the scrollbar disappeared. The following code illustrates the problem. Copy the code into a file then execute it with just about any version of wish:

frame .f -borderwidth 1 -relief sunken
text .t -wrap word -yscrollcommand [list .vsb set] \
    -borderwidth 0 -background white
scrollbar .vsb -command [list .t yview] \
    -borderwidth 1

pack .t -in .f -side left -fill both -expand 1
pack .vsb -in .f -side right -fill y -expand 0
pack .f -side top -fill both -expand 1

Make the window smaller and you should see the scrollbar disappear. When packing the text widget we told pack to let the text widget expand and contract to take up the available space, yet before it expands or contracts the scrollbar disappears. Why is that?

This is a case where the order of the code is important. Simply switching the order in which the text widget and scrollbar are packed makes the problem vanish:

frame .f -borderwidth 1 -relief sunken
text .t -wrap word -yscrollcommand [list .vsb set] \
    -borderwidth 0 -background white
scrollbar .vsb -command [list .t yview] \
    -borderwidth 1

pack .vsb -in .f -side right -fill y -expand 0
pack .t -in .f -side left -fill both -expand 1
pack .f -side top -fill both -expand 1

The reason for this is hidden in the pack man page [1] under the section labeled "The Pack Algorithm". First, it says:

"The packer arranges the slaves for a master by scanning the packing list in order..."

The key phrase is "in order". The packing list is simply the list of widgets that have been packed, in the order that they were packed. The first clue to this behavior, then, is that the order in which they are packed is significant in some way.

The significance of the order is described later in the same section:

"Once a given slave has been packed, the area of its parcel is subtracted from the cavity, leaving a smaller rectangular cavity for the next slave...If the cavity should become too small to meet the needs of a slave then the slave will be given whatever space is left in the cavity. If the cavity shrinks to zero size, then all remaining slaves on the packing list will be unmapped from the screen"

Thus, when we shrink the window the packer first tries to allocate space for the text widget. The text widget has a preferred size (even though we didn't explicitly give it one) and the packer tries to honor that. If we shrink the window down small enough the text widget will take up all the available space. Because the remaining cavity is too small for the next widget in the packing list — the scrollbar — no space will be allocated for it and the scrollbar vanishes from view.

Creation order can be important too

While we're on the subject of ordering code, it's important to notice the effect of ordering when it comes to creating widgets, too.

I typically try to keep the pathnames to widgets as short as possible, expecially for "significant" widgets -- widgets I'm likely to reference in other parts of the code. I don't want to have to reference something like ".mainframe.subframe.innerblock.maintext" when I could use ".text" instead. Tk supports this style of coding because I can pack or grid a widget in a widget other than its immediate parent. The sequence looks something like this:

frame .f ...
text .t ...
pack .t -in .f ...

You can see this style in practice in the code posted earlier in this article. If you choose to use this style, you must again be aware of the order in which things are done. In addition to a packing order (or a grid order...) widgets have the notion of a "stacking order". By default this is the order in which a widget is created. If you create widget ".foo" and then widget ".bar", ".bar" will be above .foo in the stacking orde and will be above .foo visually.

The practical ramifications are important. Here's the earlier example, rewritten so that the text widget is created before the frame in which it is packed:

text .t -wrap word -yscrollcommand [list .vsb set] \
    -borderwidth 0 -background white
scrollbar .vsb -command [list .t yview] \
    -borderwidth 1
frame .f -borderwidth 1 -relief sunken

pack .vsb -in .f -side right -fill y -expand 0
pack .t -in .f -side left -fill both -expand 1
pack .f -side top -fill both -expand 1

When you run the above code you shouldn't be able to see the text widget. That is because it is behind the frame since the frame was created after the text widget and thus is higher in the stacking order.

There are two ways to solve this problem. One is to simply create the widgets in order from lowest to highest as I did in the first versions of the example. By doing so, issues of stacking order become non-issues.

The second solution is to use the commands "raise" and "lower" which changes the relative stacking order between widgets. We can add a "raise" command at the end of the code to raise the text widget above the containing frame. We need to do the same thing for the scrollbar since it, too, was created before the containing frame:

text .t -wrap word -yscrollcommand [list .vsb set] \
    -borderwidth 0 -background white
scrollbar .vsb -command [list .t yview] \
    -borderwidth 1
frame .f -borderwidth 1 -relief sunken

pack .vsb -in .f -side right -fill y -expand 0
pack .t -in .f -side left -fill both -expand 1
pack .f -side top -fill both -expand 1

raise .t .f
raise .vsb .f

Final Thoughs

You could go years and never run into the sort of problems described in this article. Tk was designed to work best when you use the most obvious coding style. It's a testament to Tk's designer that it works so well out of the box, without you having to know about packing order, stacking order, the packing algorithm, etc. However, if you find that Tk doesn't quite work the way you expect there is almost always a documented reason, and a way to tweak Tk to get the behavior you desire.

Links

  1. http://www.tcl.tk/man/tcl8.4/TkCmd/pack.htm#M26