Section 10.7. Time Tools, Threads, and Animation


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:


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 programthe 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 millisecondsfor 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 argumentsthe 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.0and is still not available on Windows XP under Python 2.4 and Tk 8.4 as I write this third editionit 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 neededsee 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.

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

 #!/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.

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

 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 raiseits name in Tkbecause 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. PP3E\Gui\Tour\alarm-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 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:

  • 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 loopany 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.

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

 ################################################################## # 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 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

 ######################################################################## # 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 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

 ######################################################################## # 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 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.

[*] 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.

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.




Programming Python
Programming Python
ISBN: 0596009259
EAN: 2147483647
Year: 2004
Pages: 270
Authors: Mark Lutz

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net