Apart from the direct
shape moves of thecanvasDraw
example we met earlier in this
chapter, 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 9-16
.
It also demonstrates and expands on 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 usetime.sleep
to pause for fractions of a second between multiple
move operations, along with manualupdate
calls. The script moves, sleeps,
moves a bit more, and so on. Atime.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 (including both new user requests
and basic window redraws).
By using thewidget.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 requirecanvas.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 thetime.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. As described
earlier, GUIs should not be updated from spawned threads in general,
but some canvas calls such asmove
seem to be thread-safe today in the
current tkinter 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 thecanvasDraw
example we met in
Example 9-16
earlier in this
chapter. Refer back to that example for its other event bindings and
basic draw, move, and clear operations; here, we customize its object
creators for tags and add new event bindings and actions.
Example 9-30
illustrates the
first approach.
Example 9-30. PP4E\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; currently running onMove callback has
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(100, 10, text='Press o and r to move shapes')
self.canvas.master.bind('', self.onMoveOvals)
self.canvas.master.bind('', 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 to drawn objects here, add a text field at the top
of the canvas, and add bindings and callbacks for motion requests.
Figure 9-42
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.)
Figure 9-42. Drag-out objects ready to be animated
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. That is, only onetime.sleep
loop callback can be running at a
time, and a new one started by anupdate
call is effectively a recursive call
which pauses another loop already in progress.
Screen updates are a bit sluggish while moves are in progress,
too, because they happen only as often as manualupdate
calls are made (try a drag-out or a
cover/uncover of the window during a move to see for yourself). In
fact, uncommenting the canvasupdate
call in
Example 9-30
makes the GUI
completely unresponsive during the move—it won’t redraw itself if
covered, doesn’t respond to new user requests, and doesn’t show any of
its progress (you only get to see the final state). This effectively
simulates the impact of long-running operations on GUIs in
general.
Example 9-31
specializes just themoveInSquares
method of the prior example to remove all such limitations—by usingafter
timer callback loops, it
schedules moves without potential pauses. It also reflects the most
common (and likely best) way that tkinter GUIs handle time-based
events at large. By breaking tasks into parts this way instead of
running them all at once, they are naturally both distributed over
time and
overlapped
.
Example 9-31. PP4E\Gui\Tour\canvasDraw_tags_after.py
"""
similar, but with widget.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; the motion gets wild if you press 'o' or 'r' while move in progress:
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 inherits the drawing customizations of the prior,
but 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. As
discussed earlier, it can be dangerous to update the screen from a
spawned thread in general, but it works in this example (on the test
platform used, at least).
Example 9-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.
Example 9-32. PP4E\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() once made this crash
badly, though some canvas calls must be thread safe or 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 9-31
, but this
time it’s a reflection of threads running in parallel. In fact, this
uses the same scheme as the firsttime.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 example works on Windows today, but it failed on Linux at
one point in this book’s lifetime—the screen was not updated as
threads changed it, so you couldn’t see any changes until later GUI
events. The usual rule of thumb about avoiding GUI updates in spawned
threads laid out earlier still holds true. 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. Even under this model,
though, the main thread can still useafter
event loops like that of
Example 9-31
to watch for
results from worker threads to appear without being blocked while
waiting (more on this in the next section and chapter).
Parts of this story are implementation details prone to change
over time, and it’s not impossible that GUI updates in threads may be
better supported by tkinter in the future, so be sure to explore the
state of threading in future releases for
more details.
We’ll revisit animation in
Chapter 11
’s PyDraw example; there, all three of
the techniques we just met—sleeps, timers, and threads—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. In closing, here are a few more words on the topic to hint
at the possibilities.
Besides
canvas-based animations, widget configuration tools
support a variety of animation effects. For example, as we saw in the
flashing and hidingalarm
scripts
earlier (see
Example 9-28
), it is also easy
to change the appearance of other kinds of widgets dynamically withafter
timer-event loops. With
timer-based loops, you can periodically flash widgets, completely
erase and redraw widgets and windows on the fly, reverse or change
widget colors, and so on. See
For a Good Time…
for
another example in this category which changes fonts and colors on the
fly (albeit, with questionable ergonomic intentions).
Techniques for
running long-running tasks in parallel threads become
more important if animations must remain active while your program
waits. For instance, imagine a program that spends minutes downloading
data from a network, calculating the output of a numeric model, or
performing other long-running tasks. If such a program’s GUI must
display an animation or otherwise show progress while waiting for the
task, it can do so by either altering a widget’s appearance or by
moving objects in a canvas periodically—simply use theafter
method to wake up intermittently to
modify the GUI as we’ve seen. A progress bar or counter, for instance,
may be updated duringafter
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. Duringafter
timer-event processing, the
main GUI thread might check variables or objects 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 PythonQueue
object, to be picked up and handled by
the GUI duringafter
events. For
generality, theQueue
might even
contain function objects that are run by the GUI to update the
display.
Again, we’ll study such threaded GUI programming and
communication techniques in
Chapter 10
,
and employ them in the PyMailGUI example later in the book. For now,
keep in mind that spawning computational tasks in threads can allow
the GUI itself to both perform animations and remain active in general
during wait states.
Unless you
stopped playing video games shortly after the ascent of
Pong, you probably also know that the sorts of movement and animation
techniques shown in this chapter and book are suitable for some simple
game-like programs, but not all. For more demanding tasks, Python also
has additional graphics and gaming support we haven’t studied
here.
For more advanced 3-D animation needs, also see the support in
the
PIL extension package for common animation and movie
file formats such as
FLI and MPEG. Other third-party toolkits
such as OpenGL, Blender, PyGame, Maya, and
VPython
provide even higher-level
graphics and animation toolkits. The PyOpenGL system also offers Tk
support for GUIs. See the PyPI websites for links or search the
Web
.
If you’re interest in gaming specifically,
PyGame and other packages support game development in
Python, and other books and web resources focus on this topic.
Although Python is not widely used as the sole implementation language
of graphics-intensive game programs, it is used as both a prototyping
and a scripting language for such
products
.
[
37
]
When integrated with 3D graphics libraries, it can serve
even broader roles. See
http://www.python.org
for links to available extensions in this
domain
.