Hmm...
marsd made an interesting suggestion, but I don't think it's a path we should investigate. Although it might be fascinating to see if we
could, the point is that we
shouldn't.
(In case you're interested, I was musing as to whether we could do something weird with a variable trace that would force a
break or
return condition in the loop via
return -code break or
return -code return. But like I said, I'm not going there.)
In Tcl, there's no good way to force a tight loop like that to abort. But I don't think Tcl is unique in that; I can't think of a good way to write a C program or a Java program that could externally abort a loop.
If I step back and look at the larger question, I think what you're really asking is: "How do you structure a sequence of actions that the user can abort at any step?"
One relatively easy way in your case would be to modify your
type_it procedure to return 2 peices of information: the characters typed and an Ok/Cancel indicator. Then your loop could test whether or not the user clicked Cancel, and if so terminate the loop. You've got several different ways to return 2 values from a procedure. Here are the ones that immediately occur to me:[ul][li]Return a 2-element list[/li][li]Return one (or more) of the values as global variables. in this case, you could already do this with the characters typed, as you already have
var declared global.[/li][li]Interpret a return value of an empty string as meaning the user clicked Cancel. (Tk's built-in file selection dialogs take this approach.)[/li][li]Pass the name(s) of one or more variables as arguments to the procedure, use
upvar to create aliases in the proper scope, and then store the return values in those variables.[/li][/ul]This type of approach is usually best if you want the user to go through a series of steps. Do the steps one at a time, allow the user to abort, test for the abort signal in your code, and handle it appropriately.
On the other hand, sometimes you have a series of events that need to occur without user input, but you want the user to be able to abort at any time. In this case, one of the best techniques in Tcl is to break down the activity into a series of steps, and then use the
after command to run those steps in sequence. Let's take a look at an example:
Code:
# Increment the value of "x" once per second.
# "x" is used as a -textvariable for a label.
proc UpdateTimer {val} {
global x timer
set x $val
incr val
# Schedule the next call to UpdateTimer to
# occur in 1 second. Pass the updated value
# of "var" as the argument to that call.
# Save the ID of the scheduled after event
# in the global variable "timer". We can
# use this ID with the "after cancel"
# command to cancel the pending event.
set timer [after 1000 [list UpdateTimer $val]]
}
# Stop the timer by canceling the pending after
# event.
proc StopTimer {} {
global x timer
set x "Stopped"
if {[info exists timer]} {
after cancel $timer
unset timer
}
}
set x "Stopped"
label .value -textvariable x
button .start -text "Start" -command {
.stop configure -state normal
.start configure -state disabled
# Start the timer running
UpdateTimer 0
}
button .stop -text "Stop" -state disabled -command {
.start configure -state normal
.stop configure -state disabled
# Cancel the timer
StopTimer
}
pack .value -anchor w -padx 4 -pady 4
pack .start .stop -side left -padx 4 -pady 4
You can adapt this technique to a variety of tasks. For example, you might have an operation that requires many, many iterations and takes a long time to complete. Simply putting this into a loop will freeze your application's GUI while the calculation takes place. You could use
update idletasks inside the loop to refresh the screen, but other events wouldn't be processed. You could use
update inside the loop to process other events, but recursively invoking the event loop is always a bad idea because it can lead to difficult-to-debug side effects.
The best approach in this case (without resorting to multi-threaded Tcl scripts) is to break the operation into smaller chunks, and use
after to schedule each of the chunks in sequence. Here's an example:
Code:
# Execute several iterations of a calculation.
# We get the starting value, the number of
# iterations to run, the ending value as
# arguments, and the number of milliseconds
# to pause between chunks as arguments.
proc UpdateTimer {start iterations stop pause} {
global x timer
# Stop either at the end or after the
# given number of iterations, whichever
# comes first.
if {$start + $iterations > $stop} {
set bound $stop
} else {
set bound [expr {$start + $iterations}]
}
# Here's our long-running calculation.
# We're just summing a lot of random numbers.
for {set i $start} {$i <= $bound} {incr i} {
set x [expr {$x + rand()}]
}
# If we have more iterations to run, schedule
# the next call to ourselves, starting where
# we left off.
if {$bound < $stop} {
incr bound
set timer [after $pause [list UpdateTimer $bound $iterations $stop $pause]]
} else {
set x "$x (done)"
.start configure -state normal
.stop configure -state disabled
}
}
proc StopTimer {} {
global x timer
set x "Stopped"
if {[info exists timer]} {
after cancel $timer
unset timer
}
}
set x "Stopped"
label .value -width 20 -anchor w -textvariable x
button .start -text "Start" -command {
.stop configure -state normal
.start configure -state disabled
set x 0
# Add 100,000 random numbers, doing it
# in chunks of 100 at a time, pausing 10
# milliseconds between chunks. We can change
# the chunk size and the pause to tune the
# responsiveness of our application.
UpdateTimer 1 100 100000 10
}
button .stop -text "Stop" -state disabled -command {
.start configure -state normal
.stop configure -state disabled
StopTimer
}
pack .value -padx 4 -pady 4
pack .start .stop -side left -padx 4 -pady 4
- Ken Jones, President, ken@avia-training.com
Avia Training and Consulting,
866-TCL-HELP (866-825-4357) US Toll free
415-643-8692 Voice
415-643-8697 Fax