10.7. Time Tools, Threads, and Animation
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:
We won't go into further details on all of these tools here; see other Tk and Tkinter documentation for more information.
10.7.1. Using Threads with GUIs
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 programthey'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.
10.7.2. Using the after Method
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. PP3E\Gui\Tour\alarm.py
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.
Figure 10-39. Stop the beeps!
But after doesn't pause the caller: callbacks are scheduled to occur in the background, while the program performs other processingtechnically, 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.
10.7.2.1. Hiding and redrawing widgets and windows
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. PP3E\Gui\Tour\alarm-hide.py
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:
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. PP3E\Gui\Tour\alarm-withdraw.py
This works the same, but the entire window appears or disappears on beepsyou 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.
10.7.3. Simple Animation Techniques
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 tagsthe 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:
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.
10.7.3.1. Using time.sleep loops
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. PP3E\Gui\Tour\canvasDraw_tags.py
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 tooother 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.
Figure 10-40. Drag-out objects ready 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.)
10.7.3.2. Using widget.after events
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. PP3E\Gui\Tour\canvasDraw_tags_after.py
This version lets you make both ovals and rectangles move at the same timedrag 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.
10.7.3.3. Using multiple time.sleep loop threads
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 bookthe screen is not updated as threads change it, so you won't see any changes until later GUI events.
Example 10-32. PP3E\Gui\Tour\canvasDraw_tags_thread.py
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 timetime.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.
10.7.4. Other Animation Concepts: Threads and Toolkits
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 periodicallysimply 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.