Chapter 9
introduced simple tkinter animation techniques (see the
tour’scanvasDraw
variants). The PyDraw
program listed here builds upon those ideas to implement a more
feature-rich painting program in Python. It adds new trails and scribble
drawing modes, object and background color fills, embedded photos, and
more. In addition, it implements object movement and animation
techniques—drawn objects may be moved around the canvas by clicking and
dragging, and any drawn object can be gradually moved across the screen to
a target location clicked with the mouse.
PyDraw is essentially
a tkinter canvas with keyboard and mouse event bindings to
allow users to perform common drawing operations. This isn’t a
professional-grade paint program, but it’s fun to play with. In fact,
you really should—it is impossible to capture things such as object
motion in this book. Start PyDraw from the launcher bars (or run the
file
movingpics.py
from
Example 11-8
directly). Press the
? key to view a help message giving available commands (or read the help
string in the code listings).
Figure 11-17
shows
PyDraw after a few objects have been drawn on the canvas. To move any
object shown here, either click it with the middle mouse button and drag
to move it with the mouse cursor, or middle-click the object, and then
right-click in the spot you want it to move toward. In the latter case,
PyDraw performs an animated (gradual) movement of the object to the
target spot. Try this on the picture shown near the top of the figure—it
will slowly move across your display.
Figure 11-17. PyDraw with draw objects ready to be moved
Press “p” to insert photos, and use left-button drags to draw
shapes. (Note to Windows users: middle-click is often either both mouse
buttons at once or a scroll wheel, but you may need to configure this in
your control panel.) In addition to mouse events, there are 17 key-press
commands for tailoring sketches that I won’t cover here. It takes a
while to get the hang of what all the keyboard and mouse commands do,
but once you’ve mastered the bindings, you too can begin generating
senseless electronic artwork such as that in
Figure 11-18
.
Figure 11-18. PyDraw after substantial play
Like PyEdit, PyDraw lives
in a single file. Two extensions that customize motion
implementations are listed following the main module shown in
Example 11-8
.
Example 11-8. PP4E\Gui\MovingPics\movingpics.py
"""
##############################################################################
PyDraw 1.1: simple canvas paint program and object mover/animator.
Uses time.sleep loops to implement object move loops, such that only
one move can be in progress at once; this is smooth and fast, but see
the widget.after and thread-based subclasses here for other techniques.
Version 1.1 has been updated to run under Python 3.X (2.X not supported)
##############################################################################
"""
helpstr = """--PyDraw version 1.1--
Mouse commands:
Left = Set target spot
Left+Move = Draw new object
Double Left = Clear all objects
Right = Move current object
Middle = Select closest object
Middle+Move = Drag current object
Keyboard commands:
w=Pick border width c=Pick color
u=Pick move unit s=Pick move delay
o=Draw ovals r=Draw rectangles
l=Draw lines a=Draw arcs
d=Delete object 1=Raise object
2=Lower object f=Fill object
b=Fill background p=Add photo
z=Save postscript x=Pick pen modes
?=Help other=clear text
"""
import time, sys
from tkinter import *
from tkinter.filedialog import *
from tkinter.messagebox import *
PicDir = '../gifs'
if sys.platform[:3] == 'win':
HelpFont = ('courier', 9, 'normal')
else:
HelpFont = ('courier', 12, 'normal')
pickDelays = [0.01, 0.025, 0.05, 0.10, 0.25, 0.0, 0.001, 0.005]
pickUnits = [1, 2, 4, 6, 8, 10, 12]
pickWidths = [1, 2, 5, 10, 20]
pickFills = [None,'white','blue','red','black','yellow','green','purple']
pickPens = ['elastic', 'scribble', 'trails']
class MovingPics:
def __init__(self, parent=None):
canvas = Canvas(parent, width=500, height=500, bg= 'white')
canvas.pack(expand=YES, fill=BOTH)
canvas.bind('', self.onStart)
canvas.bind('', self.onGrow)
canvas.bind('', self.onClear)
canvas.bind('', self.onMove)
canvas.bind('', self.onSelect)
canvas.bind('', self.onDrag)
parent.bind('', self.onOptions)
self.createMethod = Canvas.create_oval
self.canvas = canvas
self.moving = []
self.images = []
self.object = None
self.where = None
self.scribbleMode = 0
parent.title('PyDraw - Moving Pictures 1.1')
parent.protocol('WM_DELETE_WINDOW', self.onQuit)
self.realquit = parent.quit
self.textInfo = self.canvas.create_text(
5, 5, anchor=NW,
font=HelpFont,
text='Press ? for help')
def onStart(self, event):
self.where = event
self.object = None
def onGrow(self, event):
canvas = event.widget
if self.object and pickPens[0] == 'elastic':
canvas.delete(self.object)
self.object = self.createMethod(canvas,
self.where.x, self.where.y, # start
event.x, event.y, # stop
fill=pickFills[0], width=pickWidths[0])
if pickPens[0] == 'scribble':
self.where = event # from here next time
def onClear(self, event):
if self.moving: return # ok if moving but confusing
event.widget.delete('all') # use all tag
self.images = []
self.textInfo = self.canvas.create_text(
5, 5, anchor=NW,
font=HelpFont,
text='Press ? for help')
def plotMoves(self, event):
diffX = event.x - self.where.x # plan animated moves
diffY = event.y - self.where.y # horizontal then vertical
reptX = abs(diffX) // pickUnits[0] # incr per move, number moves
reptY = abs(diffY) // pickUnits[0] # from last to event click
incrX = pickUnits[0] * ((diffX > 0) or −1) # 3.x // trunc div required
incrY = pickUnits[0] * ((diffY > 0) or −1)
return incrX, reptX, incrY, reptY
def onMove(self, event):
traceEvent('onMove', event, 0) # move current object to click
object = self.object # ignore some ops during mv
if object and object not in self.moving:
msecs = int(pickDelays[0] * 1000)
parms = 'Delay=%d msec, Units=%d' % (msecs, pickUnits[0])
self.setTextInfo(parms)
self.moving.append(object)
canvas = event.widget
incrX, reptX, incrY, reptY = self.plotMoves(event)
for i in range(reptX):
canvas.move(object, incrX, 0)
canvas.update()
time.sleep(pickDelays[0])
for i in range(reptY):
canvas.move(object, 0, incrY)
canvas.update() # update runs other ops
time.sleep(pickDelays[0]) # sleep until next move
self.moving.remove(object)
if self.object == object: self.where = event
def onSelect(self, event):
self.where = event
self.object = self.canvas.find_closest(event.x, event.y)[0] # tuple
def onDrag(self, event):
diffX = event.x - self.where.x # OK if object in moving
diffY = event.y - self.where.y # throws it off course
self.canvas.move(self.object, diffX, diffY)
self.where = event
def onOptions(self, event):
keymap = {
'w': lambda self: self.changeOption(pickWidths, 'Pen Width'),
'c': lambda self: self.changeOption(pickFills, 'Color'),
'u': lambda self: self.changeOption(pickUnits, 'Move Unit'),
's': lambda self: self.changeOption(pickDelays, 'Move Delay'),
'x': lambda self: self.changeOption(pickPens, 'Pen Mode'),
'o': lambda self: self.changeDraw(Canvas.create_oval, 'Oval'),
'r': lambda self: self.changeDraw(Canvas.create_rectangle, 'Rect'),
'l': lambda self: self.changeDraw(Canvas.create_line, 'Line'),
'a': lambda self: self.changeDraw(Canvas.create_arc, 'Arc'),
'd': MovingPics.deleteObject,
'1': MovingPics.raiseObject,
'2': MovingPics.lowerObject, # if only 1 call pattern
'f': MovingPics.fillObject, # use unbound method objects
'b': MovingPics.fillBackground, # else lambda passed self
'p': MovingPics.addPhotoItem,
'z': MovingPics.savePostscript,
'?': MovingPics.help}
try:
keymap[event.char](self)
except KeyError:
self.setTextInfo('Press ? for help')
def changeDraw(self, method, name):
self.createMethod = method # unbound Canvas method
self.setTextInfo('Draw Object=' + name)
def changeOption(self, list, name):
list.append(list[0])
del list[0]
self.setTextInfo('%s=%s' % (name, list[0]))
def deleteObject(self):
if self.object != self.textInfo: # ok if object in moving
self.canvas.delete(self.object) # erases but move goes on
self.object = None
def raiseObject(self):
if self.object: # ok if moving
self.canvas.tkraise(self.object) # raises while moving
def lowerObject(self):
if self.object:
self.canvas.lower(self.object)
def fillObject(self):
if self.object:
type = self.canvas.type(self.object)
if type == 'image':
pass
elif type == 'text':
self.canvas.itemconfig(self.object, fill=pickFills[0])
else:
self.canvas.itemconfig(self.object,
fill=pickFills[0], width=pickWidths[0])
def fillBackground(self):
self.canvas.config(bg=pickFills[0])
def addPhotoItem(self):
if not self.where: return
filetypes=[('Gif files', '.gif'), ('All files', '*')]
file = askopenfilename(initialdir=PicDir, filetypes=filetypes)
if file:
image = PhotoImage(file=file) # load image
self.images.append(image) # keep reference
self.object = self.canvas.create_image( # add to canvas
self.where.x, self.where.y, # at last spot
image=image, anchor=NW)
def savePostscript(self):
file = asksaveasfilename()
if file:
self.canvas.postscript(file=file) # save canvas to file
def help(self):
self.setTextInfo(helpstr)
#showinfo('PyDraw', helpstr)
def setTextInfo(self, text):
self.canvas.dchars(self.textInfo, 0, END)
self.canvas.insert(self.textInfo, 0, text)
self.canvas.tkraise(self.textInfo)
def onQuit(self):
if self.moving:
self.setTextInfo("Can't quit while move in progress")
else:
self.realquit() # std wm delete: err msg if move in progress
def traceEvent(label, event, fullTrace=True):
print(label)
if fullTrace:
for atrr in dir(event):
if attr[:2] != '__':
print(attr, '=>', getattr(event, attr))
if __name__ == '__main__':
from sys import argv # when this file is executed
if len(argv) == 2: PicDir = argv[1] # '..' fails if run elsewhere
root = Tk() # make, run a MovingPics object
MovingPics(root)
root.mainloop()
As is, only one object can be in motion at a time—requesting an
object move while one is already in motion pauses the original till the
new move is finished. Just as in
Chapter 9
’scanvasDraw
examples, though, we can add
support for moving more than one object at the same time with eitherafter
scheduled callback events or
threads.
Example 11-9
shows aMovingPics
subclass that codes the
necessary customizations to do parallel moves withafter
events. It allows any number of objects
in the canvas, including pictures, to be moving independently at once.
Run this file directly to see the difference; I could try to capture the
notion of multiple objects in motion with a screenshot, but I would
almost certainly fail.
Example 11-9. PP4E\Gui\MovingPics\movingpics_after.py
"""
PyDraw-after: simple canvas paint program and object mover/animator
use widget.after scheduled events to implement object move loops, such
that more than one can be in motion at once without having to use threads;
this does moves in parallel, but seems to be slower than time.sleep version;
see also canvasDraw in Tour: builds and passes the incX/incY list at once:
here, would be allmoves = ([(incrX, 0)] * reptX) + ([(0, incrY)] * reptY)
"""
from movingpics import *
class MovingPicsAfter(MovingPics):
def doMoves(self, delay, objectId, incrX, reptX, incrY, reptY):
if reptX:
self.canvas.move(objectId, incrX, 0)
reptX -= 1
else:
self.canvas.move(objectId, 0, incrY)
reptY -= 1
if not (reptX or reptY):
self.moving.remove(objectId)
else:
self.canvas.after(delay,
self.doMoves, delay, objectId, incrX, reptX, incrY, reptY)
def onMove(self, event):
traceEvent('onMove', event, 0)
object = self.object # move cur obj to click spot
if object:
msecs = int(pickDelays[0] * 1000)
parms = 'Delay=%d msec, Units=%d' % (msecs, pickUnits[0])
self.setTextInfo(parms)
self.moving.append(object)
incrX, reptX, incrY, reptY = self.plotMoves(event)
self.doMoves(msecs, object, incrX, reptX, incrY, reptY)
self.where = event
if __name__ == '__main__':
from sys import argv # when this file is executed
if len(argv) == 2:
import movingpics # not this module's global
movingpics.PicDir = argv[1] # and from* doesn't link names
root = Tk()
MovingPicsAfter(root)
root.mainloop()
To appreciate its operation, open this script’s window full screen
and create some objects in its canvas by pressing “p” after an initial
click to insert pictures, dragging out shapes, and so on. Now, while one
or more moves are in progress, you can start another by middle-clicking
on another object and right-clicking on the spot to which you want it to
move. It starts its journey immediately, even if other objects are in
motion. Each object’s scheduledafter
events are added to the same event loop queue and dispatched by tkinter
as soon as possible after a timer expiration.
If you run this subclass module directly, you might notice that
movement isn’t quite as fast or as smooth as in the original (depending
on your machine, and the many layers of software under Python), but
multiple moves can overlap in time.
Example 11-10
shows
how to achieve such parallelism with threads. This process works, but as
we learned in Chapters
9
and
10
,
updating GUIs in spawned threads is generally a dangerous affair. On one
of my machines, the movement that this script implements with threads
was a bit jerkier than the original version—perhaps a
reflection
of the overhead incurred for
switching the interpreter (and CPU) between multiple threads—but again,
this can vary.
Example 11-10. PP4E\Gui\MovingPics\movingpics_threads.py
"""
PyDraw-threads: use threads to move objects; works okay on Windows
provided that canvas.update() not called by threads (else exits with
fatal errors, some objs start moving immediately after drawn, etc.);
at least some canvas method calls must be thread safe in tkinter;
this is less smooth than time.sleep, and is dangerous in general:
threads are best coded to update global vars, not change GUI;
"""
import _thread as thread, time, sys, random
from tkinter import Tk, mainloop
from movingpics import MovingPics, pickUnits, pickDelays
class MovingPicsThreaded(MovingPics):
def __init__(self, parent=None):
MovingPics.__init__(self, parent)
self.mutex = thread.allocate_lock()
import sys
#sys.setcheckinterval(0) # switch after each vm op: doesn't help
def onMove(self, event):
object = self.object
if object and object not in self.moving:
msecs = int(pickDelays[0] * 1000)
parms = 'Delay=%d msec, Units=%d' % (msecs, pickUnits[0])
self.setTextInfo(parms)
#self.mutex.acquire()
self.moving.append(object)
#self.mutex.release()
thread.start_new_thread(self.doMove, (object, event))
def doMove(self, object, event):
canvas = event.widget
incrX, reptX, incrY, reptY = self.plotMoves(event)
for i in range(reptX):
canvas.move(object, incrX, 0)
# canvas.update()
time.sleep(pickDelays[0]) # this can change
for i in range(reptY):
canvas.move(object, 0, incrY)
# canvas.update() # update runs other ops
time.sleep(pickDelays[0]) # sleep until next move
#self.mutex.acquire()
self.moving.remove(object)
if self.object == object: self.where = event
#self.mutex.release()
if __name__ == '__main__':
root = Tk()
MovingPicsThreaded(root)
mainloop()