a
simple working example of an event-driven program.
Look further down for a version using 8.6's
coroutine.
See Also edit
- event loop
- Keep a GUI alive during a long calculation
Description edit
KBK: Many times, users ask how to keep a Tk user interface "live" while some long-running calculation is being performed, or some I/O is proceeding in the background -- in general, how to keep a Tk application running while it's waiting for something. Often people, point to the
update command in reply.
The update command is not the Tcl Way.Let's try writing a script that counts a label down from 10 to 1. Here's a version of the program that uses
update:
# Create a simple GUI to monitor the clock
label .counter -font {Helvetica 72} -width 3 -textvariable count
grid .counter -padx 100 -pady 100
# Run the countdown
for { set count 10 } { $count >= 0 } { incr count -1 } {
# Make sure that the GUI stays up to date
update
# Wait one second between time ticks
after 1000
}
exit
If you run this program, you'll see that it displays numbers counting down from 10 to 0 and then exits. The problem, though, is that it isn't really live. During the 'after', it isn't interacting with the user. Updating your UI this way is a really bad idea.
Consider, rather, structuring your application like the program below. Like the one above, it counts down from 10 to 0 and then exits.
To understand it, it's best to look first at the main program (at the
bottom of the file, below the
countdown procedure. It does two things: it initiates the countdown by calling
countdown for the first time, and it creates a trivial user interface, consisting of just a label widget, to display the result.
Everything interesting happens within the
countdown procedure. On the first trip through, it finds that the
count variable does not exist, and sets it to 10. It then executes the
after 1000 countdown
statement, which causes the event loop to call
countdown again one second later.
At this point, the GUI gets created; the label widget finds that the value of its text variable is 10, and displays it.
One second later,
countdown enters the second time. This time, it finds that
count exists, and decrements it from 10 to 9. The magic of Tk (Tcl variable traces, if you must know) causes the label widget to update automatically. The
countdown procedure then executes that
after statement again, so that it will wake up one second later.
[ ... ]
On its final trip,
countdown enters with the value of
count at 0. It decrements it to -1, discovers that it has gone negative, and unceremoniously exits.
# Chain of events that manages the countdown
proc countdown {} {
variable count
# The first time through, 'count' is 10; thereafter, it
# decrements on each trip
if { ![info exists count] } {
set count 10
} else {
incr count -1
}
# When the count goes negative, exit
if { $count < 0 } {
exit
}
# Schedule the next tick of the clock
after 1000 countdown
return
}
# Start the clock
countdown
# Create a simple GUI to monitor the clock
label .counter -font {Helvetica 72} -width 3 -textvariable count
grid .counter -padx 100 -pady 100
The more concise version of the countdown procedure can be written as:
proc countdown2 {{cnt 10}} {
set ::count $cnt
if {$cnt < 0} exit
incr cnt -1
after 1000 [list countdown2 $cnt]
}
Exercises edit
- How would you add Stop and Reset buttons to the GUI and interface them with the countdown? (Hint: Look up after cancel in the Tcl manual.)
- Oh, by the way, did I tell you that I wanted the Stop button to be enabled only when the countdown is running, and the Reset button only when it's stopped? Modify the program to keep track of its state.
- Develop a version of the program that handles several countdowns launched from the same script. (Hint: Pass the path name of the label widget as a parameter to the countdown procedure.)
- How difficult would the first three exercises be in the version of the program that's based on the update command?
- If you want a countdown of several hours, rather than ten seconds, what are the problems with this program? What might you do about them?
Coroutine edit
EG: With help from
MS, this is my attempt using 8.6's
coroutine:
package require Tcl 8.6
package require Tk
label .counter -font {Helvetica 72} -width 3 -textvariable count
grid .counter -padx 100 -pady 100
proc countdown {} {
upvar #0 [info coroutine] myvar
for { set myvar 10 } { $myvar >= 0 } { incr myvar -1 } {
if {$myvar > 0} {
after 1000 [info coroutine]
}
yield
}
}
coroutine count countdown
MS the original task waits a second and exits after reaching zero; so maybe this is closer
package require Tcl 8.6
package require Tk
label .counter -font {Helvetica 72} -width 3 -textvariable count
grid .counter -padx 100 -pady 100
proc countdown {} {
upvar #0 [info coroutine] myvar
for { set myvar 10 } { $myvar >= 0 } { incr myvar -1 } {
after 1000 [info coroutine]
yield
}
exit
}
coroutine count countdown
NOTE there is one tricky thing going on in there: we create a coroutine with the same name
::count as the counter variable. This is of course not a required feature.