The last stop on our widget tour is the most unique. Tkinter also comes with a handful of tools that have to do with the event-driven programming model, not graphics displayed on a computer screen.
Some GUI applications need to perform background activities periodically. For example, to “blink” a widget’s appearance, we’d like to register a callback handler to be invoked at regular time intervals. Similarly, it’s not a good idea to let a long-running file operation block other activity in a GUI; if the event loop could be forced to update periodically, the GUI could remain responsive. Tkinter comes with tools for both scheduling such delayed actions and forcing screen updates:
widget.after(
milliseconds, function, *args
)
This tool schedules the function to be called after a
number of milliseconds. function
can be any callable Python
object: a function, bound method, and so on. This form of the
call does not pause the program—the callback function is run
later from the normal Tkinter event loop. The
milliseconds
value can be a
floating-point number, to specify fractions of a second. This
returns an ID that can be passed to after_cancel
to cancel the callback.
Since this method is so commonly used, I’ll say more about it by
example in a moment.
widget.after(
milliseconds
)
This tool pauses the program for a number of
milliseconds—for example, an argument of 5,000 pauses for 5
seconds. This is essentially the same as Python’s library
function, time.sleep
, and
both calls can be used to add a delay in time-sensitive displays
(e.g., animation programs such as PyDraw and the simpler
examples ahead).
widget.after_idle(
function, *args
)
This tool schedules the function to be called when there
are no more pending events to process. That is, function
becomes an idle handler,
which is invoked when the GUI isn’t busy doing anything
else.
widget.after_cancel(
id
)
This tool cancels a pending after
callback event before it
occurs.
widget.update( )
This tool forces Tkinter to process all pending events in the event queue, including geometry resizing and widget updates and redraws. You can call this periodically from a long-running callback handler to refresh the screen and perform any updates to it that your handler has already requested. If you don’t, your updates may not appear on-screen until your callback handler exits. In fact, your display may hang completely during long-running handlers if not manually updated (and handlers are not run in threads, as described in the next section); the window won’t even redraw itself until the handler returns if covered and uncovered by another. For instance, programs that animate by repeatedly moving an object and pausing must call for an update before the end of the animation or only the final object position will appear on-screen; worse, the GUI will be completely inactive until the animation callback returns (see the simple animation examples later in this chapter, and see PyDraw in Chapter 12).
widget.update_idletasks(
)
This tool processes any pending idle events. This may
sometimes be safer than after
which has the potential to set up race (looping) conditions in
some scenarios. Tk widgets use idle events to display
themselves.
_tkinter.createfilehandler(
file, mask, function
)
This tool schedules the function to be called when a
file’s status changes. The function may be invoked when the file
has data for reading, is available for writing, or triggers an
exception. The file
argument
is a Python file or socket object (technically, anything with a
fileno( )
method) or an
integer file descriptor; mask
is Tkinter.READABLE
or
Tkinter.WRITABLE
to specify
the mode; and the callback function
takes two arguments—the file
ready to converse, and a mask. File handlers are often used to
process pipes or sockets, since normal input/output requests can
block the caller.
Because this call is not available on Windows under Tk
8.0—and is still not available on Windows XP under Python 2.4
and Tk 8.4 as I write this third edition—it won’t be used in
this book. Because it is currently a Unix-only alternative,
portable GUIs may be better off using after
timer loops to poll for data and
spawning threads to read data and place it on queues if
needed—see Chapter 11 for
more details. Threads are a much more general solution to
nonblocking data transfers.
widget.wait_variable(var)
widget.wait_window(win)
widget.wait_visibility(win)
These tools pause the caller until a Tkinter variable
changes its value, a window is destroyed, or a window becomes
visible. All of these enter a local event loop, such that the
application’s mainloop
continues to handle events. Note that var
is a Tkinter variable object
(discussed earlier), not a simple Python variable. To use for
modal dialogs, first call widget.focus(
)
(to set input focus) and widget.grab( )
(to make a window be
the only one active).
We won’t go into further details on all of these tools here; see other Tk and Tkinter documentation for more information.
Keep in mind that for many programs, Python’s thread
support that we discussed in Chapter
5 can serve some of the same roles as the Tkinter tools
listed in the preceding section. For instance, to avoid blocking a
GUI during a long-running file or socket transfer, the transfer can
simply be run in a spawned thread, while the rest of the program
continues to run normally. Similarly, GUIs that must watch for
inputs on pipes or sockets can do so in spawned threads (or after
callbacks), without blocking the GUI
itself. We’ll explore GUI threading in more detail in Chapter 11, and we’ll meet more
realistic threaded GUI programs in Part IV (e.g., PyMailGUI in Chapter 15).
If you do use threads in Tkinter programs, however, only the
main thread (the one that built the GUI and started the mainloop
) can make GUI calls. Things like
the update
method described in
the preceding section cannot be called from spawned threads in a GUI
program—they’ll likely trigger very strange program crashes. This
GUI thread story may be improved in future Python and Tkinter
releases, but it imposes a few structural and platform-specific
constraints today.
For example, because spawned threads cannot perform GUI
processing, they must generally communicate with the main thread
using global variables, as required by the application. A thread
that watches a socket, for instance, might simply set global
variables that trigger GUI changes in after
event callbacks. Note that this is
not a Python or Tkinter limitation (it’s much lower in the software
hierarchy that runs your GUI), and it may go away in the future. In
addition, some Tkinter canvas calls may actually be thread safe (see
the animation script later in Example 10-32). We’ll revisit
this limitation later in this book; PyMailGUI, for instance, will
collect data produced by threads and stored on a queue.
The after
method
allows scripts to schedule a callback handler to be run at some time
in the future, and we’ll use this often, in later examples in this
book. For instance, in Chapter
12, we’ll meet a clock program that uses after
to wake up 10 times per second and
check for a new time, and we’ll use an image slideshow program that
uses after
to schedule the next
photo display (see PyClock and PyView). To illustrate the basics of
scheduled callbacks, Example
10-27 does something a bit different.
Example 10-27. PP3EGuiTouralarm.py
#!/usr/local/bin/python from Tkinter import * class Alarm(Frame): def repeater(self): # on every N millisecs self.bell( ) # beep now self.stopper.flash( ) # flash button now self.after(self.msecs, self.repeater) # reschedule handler def _ _init_ _(self, msecs=1000): # default = 1 second Frame._ _init_ _(self) self.msecs = msecs self.pack( ) stopper = Button(self, text='Stop the beeps!', command=self.quit) stopper.pack( ) stopper.config(bg='navy', fg='white', bd=8) self.stopper = stopper self.repeater( ) if _ _name_ _ == '_ _main_ _': Alarm(msecs=1000).mainloop( )
This script builds the window in Figure 10-39 and periodically
calls both the button widget’s flash
method to make the button flash
momentarily (it alternates colors quickly) and the Tkinter bell
method to call your system’s sound
interface. The repeater
method
beeps and flashes once and schedules a callback to be invoked after
a specific amount of time with the after
method.
But after
doesn’t pause the
caller: callbacks are scheduled to occur in the background, while
the program performs other processing—technically, as soon as the Tk
event loop is able to notice the time rollover. To make this work,
repeater
calls after
each time through, to reschedule the
callback. Delayed events are one-shot callbacks; to repeat the
event, we need to reschedule.
The net effect is that when this script runs, it starts
beeping and flashing once its one-button window pops up. And it
keeps beeping and flashing. And beeping. And flashing. Other
activities and GUI operations don’t affect it. Even if the window is
iconified, the beeping continues because Tkinter timer events fire
in the background. You need to kill the window or press the button
to stop the alarm. By changing the msecs
delay, you can make this beep as
fast or as slow as your system allows (some platforms can’t beep as
fast as others). This may or may not be the best demo to launch in a
crowded office, but at least you’ve been warned.
The button flash
method
flashes the widget, but it’s easy to dynamically change other
appearance options of widgets, such as buttons, labels, and text,
with the widget config
method.
For instance, you can also achieve a flash-like effect by manually
reversing foreground and background colors with the widget
config
method in scheduled
after
callbacks. Just for fun,
Example 10-28
specializes the alarm to go a step further.
Example 10-28. PP3EGuiTouralarm-hide.py
from Tkinter import * import alarm class Alarm(alarm.Alarm): # change alarm callback def repeater(self): # on every N millisecs self.bell( ) # beep now if self.shown: self.stopper.pack_forget( ) # hide or erase button now else: # or reverse colors, flash... self.stopper.pack( ) self.shown = not self.shown # toggle state for next time self.after(self.msecs, self.repeater) # reschedule handler def _ _init_ _(self, msecs=1000): # default = 1 second self.shown = 0 alarm.Alarm._ _init_ _(self, msecs) if _ _name_ _ == '_ _main_ _': Alarm(msecs=500).mainloop( )
When this script is run, the same window appears, but the
button is erased or redrawn on alternating timer events. The
widget pack_forget
method
erases (unmaps) a drawn widget, and pack
makes it show up again; grid_forget
and grid
similarly hide and show widgets in
a grid. The pack_forget
method
is useful for dynamically drawing and changing a running GUI. For
instance, you can be selective about which components are
displayed, and you can build widgets ahead of time and show them
only as needed. Here, it just means that users must press the
button while it’s displayed, or else the noise keeps going.
Example 10-29 goes even further. There are a handful of methods for hiding and unhiding entire top-level windows:
To hide and unhide the entire window instead of just one
widget within it, use the top-level window widget withdraw
and deiconify
methods. The withdraw
method, demonstrated in
Example 10-29,
completely erases the window and its icon (use iconify
if you want the window’s
icon to appear during a hide).
The lift
method
raises a window above all its siblings, or relative to another
you pass in (this method is also known as tkraise
, but not raise
—its name in Tk—because
raise
is a reserved word in
Python).
The state
method
returns or changes the window’s current state (normal
, iconic
, zoomed
[full screen], or withdrawn
).
Experiment with these methods on your own to see how they differ. They are also useful to pop up prebuilt dialog windows dynamically, but are perhaps less practical here.
Example 10-29. PP3EGuiTouralarm-withdraw.py
from Tkinter import * import alarm class Alarm(alarm.Alarm): def repeater(self): # on every N millisecs self.bell( ) # beep now if self.master.state( ) == 'normal': # is window displayed? self.master.withdraw( ) # hide entire window, no icon else: # iconify shrinks to an icon self.master.deiconify( ) # else redraw entire window self.master.lift( ) # and raise above others self.after(self.msecs, self.repeater) # reschedule handler if _ _name_ _ == '_ _main_ _': Alarm().mainloop( ) # master = default Tk root
This works the same, but the entire window appears or disappears on beeps—you have to press it when it’s shown. You could add lots of other effects to the alarm. Whether your buttons and windows should flash and disappear, though, probably depends less on Tkinter technology than on your users’ patience.
Apart from the direct shape moves in the canvasDraw
example, all of the GUIs
presented so far in this part of the book have been fairly static.
This last section shows you how to change that, by adding simple
shape movement animations to the canvas drawing example listed in
Example 10-16. It also
demonstrates the notion of canvas tags—the move operations performed
here move all canvas objects associated with a tag at once. All oval
shapes move if you press “o,” and all rectangles move if you press
“r”; as mentioned earlier, canvas operation methods accept both
object IDs and tag names.
But the main goal here is to illustrate simple animation techniques using the time-based tools described earlier in this section. There are three basic ways to move objects around a canvas:
By loops that use time.sleep
to pause for fractions of a
second between multiple move operations, along with manual
update
calls. The script
moves, sleeps, moves a bit more, and so on. A time.sleep
call pauses the caller and
so fails to return control to the GUI event loop—any new
requests that arrive during a move are deferred. Because of
that, canvas.update
must be
called to redraw the screen after each move, or else updates
don’t appear until the entire movement loop callback finishes
and returns. This is a classic long-running callback scenario;
without manual update calls, no new GUI events are handled until
the callback returns in this scheme (even window
redraws).
By using the widget.after
method to schedule
multiple move operations to occur every few milliseconds.
Because this approach is based upon scheduled events dispatched
by Tkinter to your handlers, it allows multiple moves to occur
in parallel and doesn’t require canvas.update
calls. You rely on the
event loop to run moves, so there’s no reason for sleep pauses,
and the GUI is not blocked while moves are in progress.
By using threads to run multiple copies of the time.sleep
pausing loops of the first
approach. Because threads run in parallel, a sleep in any thread
blocks neither the GUI nor other motion threads. GUIs should not
be updated from spawned threads in general (in fact, calling
canvas.update
from a spawned
thread will likely crash the GUI today), but some canvas calls
such as movement
seem to be
thread safe in the current implementation.
Of these three schemes, the first yields the smoothest animations but makes other operations sluggish during movement, the second seems to yield slower motion than the others but is safer than using threads in general, and the second and third allow multiple objects to be in motion at the same time.
The next three sections demonstrate the code structure of
all three approaches in turn, with new subclasses of the canvasDraw
example we met in Example 10-16. Example 10-30 illustrates
the first approach.
Example 10-30. PP3EGuiTourcanvasDraw_tags.py
################################################################## # add tagged moves with time.sleep (not widget.after or threads); # time.sleep does not block the GUI event loop while pausing, but # screen not redrawn until callback returns or widget.update call; # the currently running onMove callback gets exclusive attention # until it returns: others pause if press 'r' or 'o' during move; ################################################################## from Tkinter import * import canvasDraw, time class CanvasEventsDemo(canvasDraw.CanvasEventsDemo): def _ _init_ _(self, parent=None): canvasDraw.CanvasEventsDemo._ _init_ _(self, parent) self.canvas.create_text(75, 8, text='Press o and r to move shapes') self.canvas.master.bind('<KeyPress-o>', self.onMoveOvals) self.canvas.master.bind('<KeyPress-r>', self.onMoveRectangles) self.kinds = self.create_oval_tagged, self.create_rectangle_tagged def create_oval_tagged(self, x1, y1, x2, y2): objectId = self.canvas.create_oval(x1, y1, x2, y2) self.canvas.itemconfig(objectId, tag='ovals', fill='blue') return objectId def create_rectangle_tagged(self, x1, y1, x2, y2): objectId = self.canvas.create_rectangle(x1, y1, x2, y2) self.canvas.itemconfig(objectId, tag='rectangles', fill='red') return objectId def onMoveOvals(self, event): print 'moving ovals' self.moveInSquares(tag='ovals') # move all tagged ovals def onMoveRectangles(self, event): print 'moving rectangles' self.moveInSquares(tag='rectangles') def moveInSquares(self, tag): # 5 reps of 4 times per sec for i in range(5): for (diffx, diffy) in [(+20, 0), (0, +20), (-20, 0), (0, -20)]: self.canvas.move(tag, diffx, diffy) self.canvas.update( ) # force screen redraw/update time.sleep(0.25) # pause, but don't block GUI if _ _name_ _ == '_ _main_ _': CanvasEventsDemo( ) mainloop( )
All three of the scripts in this section create a window of blue ovals and red rectangles as you drag new shapes out with the left mouse button. The drag-out implementation itself is inherited from the superclass. A right-mouse-button click still moves a single shape immediately, and a double-left click still clears the canvas too—other operations inherited from the original superclass. In fact, all this new script really does is change the object creation calls to add tags and colors here, add a text field, and add bindings and callbacks for motion. Figure 10-40 shows what this subclass’s window looks like after dragging out a few shapes to be animated.
The “o” and “r” keys are set up to start animation of all the ovals and rectangles you’ve drawn, respectively. Pressing “o,” for example, makes all the blue ovals start moving synchronously. Objects are animated to mark out five squares around their location and to move four times per second. New objects drawn while others are in motion start to move too because they are tagged. You need to run these live to get a feel for the simple animations they implement, of course. (You could try moving this book back and forth and up and down, but it’s not quite the same, and might look silly in public places.)
The main drawback of this first approach is that only one
animation can be going at once: if you press “r” or “o” while a
move is in progress, the new request puts the prior movement on
hold until it finishes because each move callback handler assumes
the only thread of control while it runs. Screen updates are a bit
sluggish while moves are in progress too, because they happen only
as often as manual update
calls
are made (try a drag-out or a cover/uncover of the window during a
move to see for yourself). Example 10-31 specializes
just the moveInSquares
method
to remove such limitations.
Example 10-31. PP3EGuiTourcanvasDraw_tags_after.py
######################################################################## # similar, but with .after scheduled events, not time.sleep loops; # because these are scheduled events, this allows both ovals and # rectangles to be moving at the _same_ time and does not require # update calls to refresh the GUI (only one time.sleep loop callback # can be running at once, and blocks others started until it returns); # the motion gets wild if you press 'o' or 'r' while move in progress, # though--multiple move updates start firing around the same time; ######################################################################## from Tkinter import * import canvasDraw_tags class CanvasEventsDemo(canvasDraw_tags.CanvasEventsDemo): def moveEm(self, tag, moremoves): (diffx, diffy), moremoves = moremoves[0], moremoves[1:] self.canvas.move(tag, diffx, diffy) if moremoves: self.canvas.after(250, self.moveEm, tag, moremoves) def moveInSquares(self, tag): allmoves = [(+20, 0), (0, +20), (-20, 0), (0, -20)] * 5 self.moveEm(tag, allmoves) if _ _name_ _ == '_ _main_ _': CanvasEventsDemo( ) mainloop( )
This version lets you make both ovals and rectangles move at the same time—drag out a few ovals and rectangles, and then press “o” and then “r” right away to make this go. In fact, try pressing both keys a few times; the more you press, the more the objects move, because multiple scheduled events are firing and moving objects from wherever they happen to be positioned. If you drag out a new shape during a move, it starts moving immediately as before.
Running animations in threads can sometimes achieve the same effect; it can be dangerous to update the screen from a spawned thread in general, but it works in this example, at least on Windows. Example 10-32 runs each animation task as an independent and parallel thread. That is, each time you press the “o” or “r” key to start an animation, a new thread is spawned to do the work. This works on Windows, but it failed on Linux at the time of writing this book—the screen is not updated as threads change it, so you won’t see any changes until later GUI events.
Example 10-32. PP3EGuiTourcanvasDraw_tags_thread.py
######################################################################## # similar, but run time.sleep loops in parallel with threads, not # .after events or single active time.sleep loop; because threads run # in parallel, this also allows ovals and rectangles to be moving at # the _same_ time and does not require update calls to refresh the GUI: # in fact, calling .update( ) can make this _crash_ today, though some # canvas calls seem to be thread safe or else this wouldn't work at all; ######################################################################## from Tkinter import * import canvasDraw_tags import thread, time class CanvasEventsDemo(canvasDraw_tags.CanvasEventsDemo): def moveEm(self, tag): for i in range(5): for (diffx, diffy) in [(+20, 0), (0, +20), (-20, 0), (0, -20)]: self.canvas.move(tag, diffx, diffy) time.sleep(0.25) # pause this thread only def moveInSquares(self, tag): thread.start_new_thread(self.moveEm, (tag,)) if _ _name_ _ == '_ _main_ _': CanvasEventsDemo( ) mainloop( )
This version lets you move shapes at the same time, just
like Example 10-31,
but this time it’s a reflection of threads running in parallel. In
fact, this uses the same scheme as the first time.sleep
version. Here, though, there
is more than one active thread of control, so move handlers can
overlap in time—time.sleep
blocks only the calling thread, not the program at large. This
seems to work (at least on Windows), but it is usually safer to
have your threads do number crunching only and let the main thread
(the one that built the GUI) handle any screen updates. It’s not
impossible that GUI threads may be better supported in later
Tkinter releases, so see more recent releases for more
details.
We’ll revisit animation in Chapter 12’s PyDraw example; there, all three techniques will be resurrected to move shapes, text, and photos to arbitrary spots on a canvas marked with a mouse click. And, although the canvas widget’s absolute coordinate system makes it the workhorse of most nontrivial animations, Tkinter animation in general is limited mostly by your imagination.
Besides canvas-based animations, widget configuration tools
support a variety of animation effects. For example, as we saw in
the flashing and hiding alarm
scripts earlier (see Example
10-28), it is also easy to change the appearance of other
kinds of widgets dynamically with after
timer-event loops. With timer-based
loops, you can periodically flash widgets, completely erase and
redraw widgets and windows on the fly, reverse widget colors, and so
on.
Furthermore, the techniques for running long-running tasks in parallel threads (which we will study in the next chapter) become more important if animations must remain active while your program waits.
For instance, imagine that your program will spend minutes
downloading data from a network, calculating the output of a numeric
model, or performing other long-running tasks. If you want your
program’s GUI to display an animation or otherwise show progress
while waiting for the task, you can do so by either altering a
widget’s appearance or by moving objects in a canvas
periodically—simply use the after
method to wake up intermittently to modify your GUI as we’ve seen. A
progress bar or counter, for instance, may be updated during
after
timer-event
handling.
In addition, though, the long-running task itself will likely
have to be run in a spawned parallel thread so
that your GUI remains active and performs the animation during the
wait. Otherwise, no GUI updates will occur until the task returns
control to the GUI. During after
timer-event processing, the main GUI thread might check global
variables set by the long-running task’s thread to determine
completion or progress.
Especially if more than one long-running task may be active at
the same time, the spawned thread might also communicate with the
GUI thread by storing information in a Python Queue
object, to be picked up and handled
by the GUI during after
events.
For generality, the Queue
might
even contain function objects that are run by the GUI to update the
display. We will study such techniques in Chapter 11. For now, keep in mind
that spawning tasks in threads allows the GUI itself to perform
animations and to remain active in general during wait
states.
I should also note that the sorts of movement and animation techniques shown in this chapter and the next are suitable for some game-like programs, but not all. For more advanced 3-D animation needs, be sure to also see the support in the PIL extension package for common animation and movie file formats such as FLI and MPEG. Other third-party systems such as OpenGL, Blender, PyGame, and VPython provide even higher-level graphics and animation toolkits; the PyOpenGL system also offers Tk support for GUIs. See the Vaults of Parnassus and PyPI sites for links, or search on Google.com.
As currently implemented, Python is not widely used as the sole implementation language of graphics-intensive game programs, but it can still be used as both a prototyping and a scripting language for such products.[*] When integrated with 3D graphics libraries, it can serve even broader roles. See http://www.python.org for links to other available extensions in this domain.
Finally, be sure to also watch for more on GUIs, threads, and thread communication queues in the next chapter, as well as check out the PyMailGUI example later in this book.
[*] Origin Systems, a major game software development company, used Python in this role to script the animation in some of its games. At last report, its online game product, Ultima Online II, was to be scripted with Python. In fact, many games utilize Python. Civilization IV uses Python as a scripting language, and Temple of Elemental Evil uses Python as well. Eve Online uses Python for scripting and much of the functionality.
3.145.50.222