The previous chapter introduced simple Tkinter animation techniques (see the tour's canvasDraw 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.
9.6.1 Running PyDraw
PyDraw is essentially a Tkinter canvas with lots of keyboard and mouse event bindings to allow users to perform common drawing operations. This isn't a professional-grade paint program by any definition, but it's fun to play with. In fact, you really should -- it is impossible to capture things like object motion in the medium afforded by this book. Start PyDraw from the launcher bars (or run the file movingpics.pyfrom Example 9-18 directly). Press the ? key to view a help message giving available commands (or read the help string in the code listings).
Figure 9-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 towards. In the latter case, PyDraw performs an animated (gradual) movement of the object to the target spot. Try this on the picture of Python creator Guido van Rossum near the top to start the famous "Moving Guido Demo" (yes, he has a sense of humor, too).
Figure 9-17. PyDraw with draw objects ready to be moved
Press "p" to insert photos, and use left-button drags to draw shapes. Windows users -- middle-click is usually both mouse buttons at once, 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 like that in Figure 9-18.
Figure 9-18. PyDraw after substantial play
9.6.2 PyDraw Source Code
Like PyEdit, PyDraw lives in a single file. Two extensions that customize motion implementations are listed following the main module shown in Example 9-18.
Example 9-18. PP2EGuiMovingPicsmovingpics.py
############################################################################## # PyDraw: 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; ############################################################################## helpstr = """--PyDraw version 1.0-- 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 tkFileDialog import * from tkMessageBox 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.0') 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) 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=1): print label if fullTrace: for key in dir(event): print key, '=>', getattr(event, key) 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( )
Just as in the last chapter's canvasDraw examples, we can add support for moving more than one object at the same time with either after scheduled-callback events, or threads. Example 9-19 shows a MovingPics subclass that codes the necessary customizations to do parallel moves with after events. Run this file directly to see the difference; I could try to capture the notion of multiple objects in motion with a screen shot, but would almost certainly fail.
Example 9-19. PP2EGuiMovingPicsmovingpics_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 = reptX - 1 else: self.canvas.move(objectId, 0, incrY) reptY = 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( )
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 you want it to move to. It starts its journey immediately, even if other objects are in motion. Each object's scheduled after 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'll probably notice that movement isn't quite as fast or as smooth as in the original, but multiple moves can overlap in time.
Example 9-20 shows how to achieve such parallelism with threads. This process works, but as we learned in the last chapter, updating GUIs in spawned threads is generally a dangerous affair. On my machine, the movement that this script implements with threads is a bit more jerky than the original version -- a reflection of the overhead incurred for switching the interpreter (and CPU) between multiple threads.
Example 9-20. PP2EGuiMovingPicsmovingpics_threads.py
################################################################### # use threads to move objects; seems to work 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, 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( )
Introducing Python
Part I: System Interfaces
System Tools
Parallel System Tools
Larger System Examples I
Larger System Examples II
Part II: GUI Programming
Graphical User Interfaces
A Tkinter Tour, Part 1
A Tkinter Tour, Part 2
Larger GUI Examples
Part III: Internet Scripting
Network Scripting
Client-Side Scripting
Server-Side Scripting
Larger Web Site Examples I
Larger Web Site Examples II
Advanced Internet Topics
Part IV: Assorted Topics
Databases and Persistence
Data Structures
Text and Language
Part V: Integration
Extending Python
Embedding Python
VI: The End
Conclusion Python and the Development Cycle