Binding and -command scripts
Binding and -command scripts the life-blood of Tk-based applications. These are scripts that typically run as a result of an event generated by the user rather than being called directly from some other command. Although there are no special rules for constructing these scripts, there are some aspects of these scripts that may not be obvious to a new Tk programmer.
In my experience, the best piece of advice I can give is this rule of thumb:
Never have a binding or "-command" script call more than one command. The only exception is if the second command is "break".
The rule: never call more than one command
The practical implication of this rule of thumb is that you should normally create a proc for each binding or -command script that needs to execute more than a single command. That doesn't mean you need a 1:1 correspondence between a binding and a proc; you can certainly call the same proc from multiple bindings.
Also, you don't always have to create a proc if all you need is to call an existing command. For example, there's usually no need to create a proc to connect a scrollbar to a widget. However, in many cases where you are only calling a single command it can still be useful to create a specialized proc.
This rule has two benefits. For one, it greatly reduces or eliminates quoting problems. As a consequence of the first benefit, it makes long term maintenance easier because you can add additional code without having to worry about unusual quoting.
A simple example
Consider a simple example where you need to create a series of checkbuttons and entry widgets. When a checkbutton is checked you want to enable the entry widget, and when unchecked you want to disable the entry widget. The code might look like this:
for {set i 0} {$i < 10} {incr i} {
set checked($i) 0
frame .f$i
entry .e$i -state disabled
checkbutton .rb$i -text "Field $i: " -variable checked($i) -command {
if {$checked($i)} {set state "normal"} {set state "disabled"}
.e$i configure -state $state
}
pack .rb$i -in .f$i -side left
pack .e$i -in .f$i -side left -fill x -expand 1
pack .f$i -side top -fill x
}
If you run this code it will create the widgets, but the first time you try to click on a checkbutton you'll get the error "can't read "checkbutton(10)": no such element in array. If you put the code in a proc you might instead get the error message "can't read "i": no such variable".
Why do we get this error? Remember that curly braces inhibit variable expansion, and we're using curly braces to define the script associated with the -command option. Because of that, any occurance of "$i" will not be dereferenced until the script runs at which point it almost certainly won't be the same value as when the widget is created.
Since you only need curly braces when you need curly braces we could use double quotes instead of curly braces to work around this problem. Notice, however, that this creates a different problem in that we have to make sure we protect special characters we do not want to be dereferenced until the script actually runs:
for {set i 0} {$i < 10} {incr i} {
set checked($i) 0
frame .f$i
entry .e$i -state disabled
checkbutton .rb$i -text "Field $i: " -variable checked($i) -command "
if {\$checked($i)} {set state \"normal\"} {set state \"disabled\"}
.e$i configure -state \$state
"
pack .rb$i -in .f$i -side left
pack .e$i -in .f$i -side left -fill x -expand 1
pack .f$i -side top -fill x
}
Use a proc rather than quoting tricks
We can completely eliminate the quoting problem by creating a proc rather than trying to inline the code. Even though in this case we could in fact combine the two lines into one and thus adhere to the literal meaning of the rule, the spirit of the rule suggests we should create a specialized proc. By doing so it becomes easy to differentiate between variables that need to be dereferenced at create time (variables passed to the proc) and those that need to be dereferenced at the time the proc runs (variables inside the proc):
for {set i 0} {$i < 10} {incr i} {
set checked($i) 0
frame .f$i
entry .e$i -state disabled
checkbutton .rb$i -text "Field $i: " -variable checked($i) \
-command [list checkEnable $i]
pack .rb$i -in .f$i -side left
pack .e$i -in .f$i -side left -fill x -expand 1
pack .f$i -side top -fill x
}
proc checkEnable {i} {
global checked
.e$i configure -state [expr {$checked($i) ? "normal" : "disabled"}]
}
As you can see, by creating the proc we've eliminated the need to do any special quoting tricks. As an added benefit it becomes easier to add additional functionality without having to think extra hard about the quoting, or without having to refactor the code into a proc at a later date.
The exception: break
The rule of thumb makes an exception for the "break" command which is used by the binding mechaism to stop the chain of event processing provided by the bindtag [1] [2] mechanism. This behavior of break is documented on the bind manpage [3].
Consider the case where you want <Tab> in an entry widget to do word completion rather than the default behavior of going to the next focusable widget. You could accomplish this by using break in your widget binding to prevent the default <Tab> binding on "all" from firing. For example:
bind .entry <Tab> "[list doCompletion .entry];break"
Because this is a fairly common thing to do, it is a reasonable exception to the rule of thumb of never calling more than one command in a binding script. If you wish though, you can avoid using "break" in a binding by having the proc do "return -code break" instead. I personally prefer seeing the "break" in the binding itself. There's no technical reason to choose one technique over the other.
A second rule of thumb
In addition to the first rule of thumb presented in this article, another good rule of thumb related to bindings and -command scripts is:
Always use [list ...] to construct a binding or -command script
If you look back at some of the examples presented earlier in this article, when constructing the command for a binding or -command I used the list command. While it is possible to use double quotes, using double quotes can have unexpected side effects. Consider the following example:
set message "Hello, world"
button .b1 -text b1 -command "doSomething $message"
button .b2 -text b2 -command [list doSomething $message]
pack .b1 .b2
proc doSomething {message} {
tk_messageBox "doSomething: $message"
}
If you run the above code and click on b1 you'll get an error, whereas button b2 will work just fine. The reason is that the procedure "doSomething" expects a single argument. Because we used double quotes when defining b1, the command that b1 sees is actually "doSomething Hello, world", which is incorrect. In the case of b2, however, because we used list the word boundaries are preserved and everything works as expected.
Conclusion
Tk is remarkably flexible in letting you choose the programming style and quoting methods that work best for you. As this article shows, it's possible to create binding and -command scripts using list, double quotes and curly braces. It's even possible to use no quoting or lots of backslashes with no other quoting under some circumstances.
The choice of quoting can have an impact on how — or if — a script will work. One can certainly argue that the choice of quoting should be made based on immediate requirements. However, if you get in the habit of always placing your binding and -command scripts in procs and using [list ...] to call those procs you can almost universally avoid quoting problems and give yourself code that is easier to maintain over time.