www.gibmonks.com




  Previous section   Next section

Practical Programming in Tcl & Tk, Third Edition
By Brent B. Welch

Table of Contents
Chapter 27.  Buttons and Menus


Button Commands and Scope Issues

Perhaps the trickiest issue with button commands has to do with variable scoping. A button command is executed at the global scope, which is outside of any procedure. If you create a button while inside a procedure, then the button command executes in a different scope later. The commands used in event bindings also execute later at the global scope.

I think of this as the "now" (i.e., button definition) and "later" (i.e., button use) scope problem. For example, you may want to use the values of some variables when you define a button command but use the value of other variables when the button command is used. When these two contexts are mixed, it can be confusing. The next example illustrates the problem. The button's command involves two variables: x and val. The global variable x is needed later, when the button's command executes. The local variable val is needed now, in order to define the command. Example 27-1 shows this awkward mixture of scopes:

Example 27-1 A troublesome button command.

graphics/27fig01.gif

proc Trouble {args} {
   set b 0
   # Display the value of x, a global variable
   label .label -textvariable x
   set f [frame .buttons -borderwidth 10]
   # Create buttons that multiply x by their value
   foreach val $args {
      button $f.$b -text $val \
         -command "set x \[expr \$x * $val\]"
      pack $f.$b -side left
      incr b
   }
   pack .label $f
}
set x 1
Trouble -1 4 7 36

The example uses a label widget to display the current value of x. The textvariable attribute is used so that the label displays the current value of the variable, which is always a global variable. It is not necessary to have a global command inside Trouble because the value of x is not used there. The button's command is executed later at the global scope.

The definition of the button's command is ugly, though. The value of the loop variable val is needed when the button is defined, but the rest of the substitutions need to be deferred until later. The variable substitution of $x and the command substitution of expr are suppressed by quoting with backslashes:

set x \[expr \$x * $val\]

In contrast, the following command assigns a constant expression to x each time the button is clicked, and it depends on the current value of x, which is not defined the first time through the loop. Clearly, this is incorrect:

button $f.$b -text $val \
    -command "set x [expr $x * $val]"

Another incorrect approach is to quote the whole command with braces. This defers too much, preventing the value of val from being used at the correct time.

graphics/tip_icon.gif

Use procedures for button commands.


The general technique for dealing with these sorts of scoping problems is to introduce Tcl procedures for use as the button commands. Example 27-2 introduces a little procedure to encapsulate the expression:

Example 27-2 Fixing the troublesome situation.
proc LessTrouble { args } {
   set b 0
   label .label -textvariable x
   set f [frame .buttons -borderwidth 10]
   foreach val $args {
      button $f.$b -text $val \
         -command "UpdateX $val"
      pack $f.$b -side left
      incr b
   }
   pack .label $f
}
proc UpdateX { val } {
   global x
   set x [expr $x * $val]
}
set x 1
LessTrouble -1 4 7 36

It may seem just like extra work to introduce the helper procedure, UpdateX. However, it makes the code clearer in two ways. First, you do not have to struggle with backslashes to get the button command defined correctly. Second, the code is much clearer about the function of the button. Its job is to update the global variable x.

You can generalize UpdateX to work on any variable by passing the name of the variable to update. Now it becomes much like the incr command:

button $f.$b -text $val -command "Update x $val"

The definition of Update uses upvar, which is explained on page 85, to manipulate the named variable in the global scope:

proc Update {varname val} {
   upvar #0 $varname x
   set x [expr $x * $val]
}

Double quotes are used in the button command to allow $val to be substituted. Whenever you use quotes like this, you have to be aware of the possible values for the substitutions. If you are not careful, the command you create may not be parsed correctly. The safest way to generate the command is with list:

button $f.$b -text $val -command [list UpdateX $val]

Using list ensures that the command is a list of two elements, UpdateX and the value of val. This is important because UpdateX takes only a single argument. If val contained white space, then the resulting command would be parsed into more words than you expected. Of course, in this case we plan to always call LessTrouble with an integer value, which does not contain white space.

Example 27-3 provides a more straightforward application of procedures for button commands. In this case the advantage of the procedure MaxLineLength is that it creates a scope for the local variables used during the button action. This ensures that the local variables do not accidentally conflict with global variables used elsewhere in the program. There is also the standard advantage of a procedure, which is that you may find another use for the action in another part of your program.

Example 27-3 A button associated with a Tcl procedure.

graphics/27fig02.gif

proc MaxLineLength { file } {
   set max 0
   if [catch {open $file}in] {
      return $in
   }
   foreach line [split [read $in] \n] {
      set len [string length $line]
      if {$len > $max} {
         set max $len
      }
   }
   return "Longest line is $max characters"
}
# Create an entry to accept the file name,
# a label to display the result
# and a button to invoke the action
. config -borderwidth 10
entry .e -width 30 -bg white -relief sunken
button .doit -text "Max Line Length" \
   -command {.label config -text [MaxLineLength [.e get]]}
label .label -text "Enter file name"
pack .e .doit .label -side top -pady 5

The example is centered around the MaxLineLength procedure. This opens a file and loops over the lines finding the longest one. The file open is protected with catch in case the user enters a bogus file name. In that case, the procedure returns the error message from open. Otherwise, the procedure returns a message about the longest line in the file. The local variables in, max, and len are hidden inside the scope of the procedure.

The user interface has three widgets: an entry for user input, the button, and a label to display the result. These are packed into a vertical stack, and the main window is given a border. Obviously, this simple interface can be improved in several ways. There is no Quit button, for example.

All the action happens in the button command:

.label config -text [MaxLineLength [.e get]]

Braces are used when defining the button command so that the command substitutions all happen when the button is clicked. The value of the entry widget is obtained with .e get. This value is passed into MaxLineLength, and the result is configured as the text for the label. This command is still a little complex for a button command. For example, suppose you wanted to invoke the same command when the user pressed <Return> in the entry. You would end up repeating this command in the entry binding. It might be better to introduce a one-line procedure to capture this action so that it is easy to bind the action to more than one user action. Here is how that might look:

proc Doit {} {
     .label config -text [MaxLineLength [.e get]]
}
button .doit -text "Max Line Length" -command Doit
bind .e <Return> Doit

Chapter 26 describes the bind command in detail, Chapter 29 describes the label widget, and Chapter 32 describes the entry widget.


      Previous section   Next section
    Top