PyDraw: Painting and Moving Graphics

PyDraw Painting and Moving Graphics

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

figs/ppy2_0917.gif

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

figs/ppy2_0918.gif

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



Programming Python
Python Programming for the Absolute Beginner, 3rd Edition
ISBN: 1435455002
EAN: 2147483647
Year: 2000
Pages: 245

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