AJJ
AJJ

Reputation: 143

Tck after command, vwait, and the event loop

My Tk app has many "wait" windows or pauses in a functions that allow time for other backgrounds commands to do their job. The problem is that using "after 5000" within a function disables all the buttons in the application. I've found a lot of information, the most helpful was at http://wiki.tcl.tk/808. First lesson learned is that "after" without a script won't process the event loop, and second is that vwaits are nested.

So, I use the following simple "pause" function in place of "after":

proc pause {ms {waitvar WAITVAR}} {
   global $waitvar
   after $ms "set $waitvar 1"
   puts "waiting $ms for $waitvar"
   vwait $waitvar
   puts "pause $ms returned"
}

button .b -text PressMe -command {pause 5000 but[incr i]}; # everyone waits on this
pack .b
after 0 {pause 1000 var1}; pause 3000 var2; # works as expected
after 0 {pause 3000 var3}; pause 1000 var4; # both return after 3 secs

My button is always responsive, but if pressed, all other vwaits are held up for at least another 5 seconds. And a second press within 5 seconds also delays the first one. Understanding that vwaits are nested, this is now expected and not really problematic.

This seems almost too simple a solution, so I'd like to get comments as to what issues I might not have though of.

Upvotes: 3

Views: 1180

Answers (1)

Donal Fellows
Donal Fellows

Reputation: 137567

You've listed the main issue, that a vwait call will merrily nest inside another vwait call. (They're implemented using a recursive call to the event loop engine, so that's to be expected.) This can be a particular problem when you get something that ends up nesting inside itself; you can blow up the stack this way very easily. The traditional way of fixing this is with careful interlocking, such as disabling the button that invokes this particular callback while you're processing the vwait; that also gives quite a good way to indicate to the user that you're busy.

The other approach (which you might well still use with the button disabling) is to break up the code so that instead of:

proc callback {} {
    puts "do some stuff 1"
    pause 5000
    puts "do some stuff 2"
}

You instead do:

proc callback {} {
    puts "do some stuff 1"
    after 5000 callback2
}
proc callback2 {} {
    puts "do some stuff 2"
}

This allows you to avoid the vwait itself. It's called continuation-passing style programming, and it's pretty common in high-quality Tcl code. It does get a bit tricky though. Consider this looping version:

proc callback {} {
    for {set i 1} {$i <= 5} {incr i} {
        puts "This is iteration $i"
        pause 1000
    }
    puts "all done"
}

In continuation-passing style, you'd do something like this:

proc callback {{i 1}} {
    if {$i <= 5} {
        puts "This is iteration $i"
        after 1000 [list callback [incr i]]
    } else {
        puts "all done"
    }
}

The more local state you've got, the trickier it is to transform the code!


With Tcl/Tk 8.6 you've got some extra techniques.

Firstly, you can use a coroutine to simplify that tricky continuation-passing stuff.

proc callback {} {
    coroutine c[incr ::coroutines] apply {{} {
        for {set i 1} {$i <= 5} {incr i} {
            puts "This is iteration $i"
            after 1000 [info coroutine]
            yield
        }
        puts "all done"
    }}
}

This is a bit longer, but is much easier as the size and complexity of the state increases.

The other new 8.6 facility is the tk busy command, which can be used to make convenient modal dialogs that you can't interact with while some operation is happening (via clever tricks with invisible windows). It's still up to your code to ensure that the user is told that things are busy, again by marking things disabled, etc., but tk busy can make it much easier to implement (and can help avoid the nest of little tricky problems with grab).

Upvotes: 4

Related Questions