When it comes to graphics, the Tkinter Canvas
widget is the most free-form device
in the library. It’s a place to draw shapes, move objects dynamically,
and place other kinds of widgets. The canvas is based on a structured graphic object model:
everything drawn on a canvas can be processed as an object. You can
get down to the pixel-by-pixel level in a canvas, but you can also
deal in terms of larger objects such as shapes, photos, and embedded
widgets. And the canvas is powerful enough to support everything from
simple paint programs to full-scale visualization and
animation.
Canvases are ubiquitous in much nontrivial GUI work, and we’ll see larger canvas examples show up later in this book under the names PyDraw, PyView, PyClock, and PyTree. For now, let’s jump right into an example that illustrates the basics. Example 10-13 runs most of the major canvas drawing methods.
Example 10-13. PP3EGuiTourcanvas1.py
# demo all basic canvas interfaces from Tkinter import * canvas = Canvas(width=300, height=300, bg='white') # 0,0 is top left corner canvas.pack(expand=YES, fill=BOTH) # increases down, right canvas.create_line(100, 100, 200, 200) # fromX, fromY, toX, toY canvas.create_line(100, 200, 200, 300) # draw shapes for i in range(1, 20, 2): canvas.create_line(0, i, 50, i) canvas.create_oval(10, 10, 200, 200, width=2, fill='blue') canvas.create_arc(200, 200, 300, 100) canvas.create_rectangle(200, 200, 300, 300, width=5, fill='red') canvas.create_line(0, 300, 150, 150, width=10, fill='green') photo=PhotoImage(file='../gifs/guido.gif') canvas.create_image(250, 0, image=photo, anchor=NW) # embed a photo widget = Label(canvas, text='Spam', fg='white', bg='black') widget.pack( ) canvas.create_window(100, 100, window=widget) # embed a widget canvas.create_text(100, 280, text='Ham') # draw some text mainloop( )
When run, this script draws the window captured in Figure 10-21. We saw how to
place a photo on canvas and size a canvas for a photo earlier on
this tour (see Chapter 9). This
script also draws shapes, text, and even an embedded Label
widget. Its window gets by on looks
alone; in a moment, we’ll learn how to add event callbacks that let
users interact with drawn items.
Canvases are easy to use, but they rely on a coordinate system, define unique drawing methods, and name objects by identifier or tag. This section introduces these core canvas concepts.
All items drawn on a canvas are distinct objects, but they
are not really widgets. If you study the canvas1
script closely, you’ll notice
that canvases are created and packed (or gridded or placed) within
their parent container just like any other widget in Tkinter. But
the items drawn on a canvas are not. Shapes, images, and so on,
are positioned and moved on the canvas by coordinates,
identifiers, and tags. Of these, coordinates are the most
fundamental part of the canvas model.
Canvases define an (X,Y) coordinate system for their drawing area; x means the horizontal scale, y means vertical. By default, coordinates are measured in screen pixels (dots), the upper-left corner of the canvas has coordinates (0,0), and x and y coordinates increase to the right and down, respectively. To draw and embed objects within a canvas, you supply one or more (X,Y) coordinate pairs to give absolute canvas locations. This is different from the constraints we’ve used to pack widgets thus far, but it allows very fine-grained control over graphical layouts, and it supports more free-form interface techniques such as animation.[*]
The canvas allows you to draw and display common shapes such
as lines, ovals, rectangles, arcs, and polygons. In addition, you
can embed text, images, and other kinds of Tkinter widgets such as
labels and buttons. The canvas1
script demonstrates all the basic graphic object constructor
calls; to each, you pass one or more sets of (X,Y) coordinates to
give the new object’s location, start point and endpoint, or
diagonally opposite corners of a bounding box that encloses the
shape:
id = canvas.create_line(fromX, fromY, toX, toY) # line start, stop id = canvas.create_oval(fromX, fromY, toX, toY) # two opposite box corners id = canvas.create_arc( fromX, fromY, toX, toY) # two opposite oval corners id = canvas.create_rectangle(fromX, fromY, toX, toY) # two opposite corners
Other drawing calls specify just one (X,Y) pair, to give the location of the object’s upper-left corner:
id = canvas.create_image(250, 0, image=photo, anchor=NW) # embed a photo id = canvas.create_window(100, 100, window=widget) # embed a widget id = canvas.create_text(100, 280, text='Ham') # draw some text
The canvas also provides a create_polygon
method that accepts an
arbitrary set of coordinate arguments defining the endpoints of
connected lines; it’s useful for drawing more arbitrary kinds of
shapes composed of straight lines.
In addition to coordinates, most of these drawing calls let
you specify common configuration options, such as outline width
, fill
color, outline
color, and so on. Individual
object types have unique configuration options all their own too;
for instance, lines may specify the shape of an optional arrow,
and text, widgets, and images may be anchored to a point of the
compass (this looks like the packer’s anchor
, but really it gives a point on
the object that is positioned at the [X,Y] coordinates given in
the create
call; NW
puts the upper-left corner at
[X,Y]).
Perhaps the most important thing to notice here, though, is that Tkinter does most of the “grunt” work for you—when drawing graphics, you provide coordinates, and shapes are automatically plotted and rendered in the pixel world. If you’ve ever done any lower-level graphics work, you’ll appreciate the difference.
Although not used by the canvas1
script, every object you put on
a canvas has an identifier, returned by the create_
method that draws or embeds the
object (what was coded as id
in
the last section’s examples). This identifier can later be passed
to other methods that move the object to new coordinates, set its
configuration options, delete it from the canvas, raise or lower
it among other overlapping objects, and so on.
For instance, the canvas move
method accepts both an object
identifier and X and Y offsets (not coordinates), and it moves the
named object by the offsets given:
canvas.move(objectIdOrTag, offsetX, offsetY) # move object(s) by offset
If this happens to move the object off-screen, it is simply clipped (not shown). Other common canvas operations process objects too:
canvas.delete(objectIdOrTag) # delete object(s) from canvas canvas.tkraise(objectIdOrTag) # raise object(s) to front canvas.lower(objectIdOrTag) # lower object(s) below others canvas.itemconfig(objectIdOrTag, fill='red') # fill object(s) with red color
Notice the tkraise
name—raise
by itself is a
reserved word in Python. Also note that the itemconfig
method is used to configure
objects drawn on a canvas after they have been created; use
config
to set configuration
options for the canvas itself. The best thing to notice here,
though, is that because Tkinter is based on structured objects,
you can process a graphic object all at once; there is no need to
erase and redraw each pixel manually to implement a move or a
raise.
But it gets even better. In addition to object identifiers,
you can also perform canvas operations on entire sets of objects
at once, by associating them all with a tag,
a name that you make up and apply to objects on the display.
Tagging objects in a Canvas
is
at least similar in spirit to tagging substrings in the Text
widget we studied in the prior
section. In general terms, canvas operation methods accept either
a single object’s identifier or a tag name.
For example, you can move an entire set of drawn objects by
associating all with the same tag and passing the tag name to the
canvas move
method. In fact,
this is why move
takes offsets,
not coordinates—when given a tag, each object associated with the
tag is moved by the same (X,Y) offsets; absolute coordinates would
make all the tagged objects appear on top of each other
instead.
To associate an object with a tag, either specify the tag
name in the object drawing call’s tag
option or call the addtag_withtag(tag, objectIdOrTag)
canvas method (or its relatives). For instance:
canvas.create_oval(x1, y1, x2, y2, fill='red', tag='bubbles') canvas.create_oval(x3, y3, x4, y4, fill='red', tag='bubbles') objectId = canvas.create_oval(x5, y5, x6, y6, fill='red') canvas.addtag_withtag('bubbles', objectId) canvas.move('bubbles', diffx, diffy)
This makes three ovals and moves them at the same time by associating them all with the same tag name. Many objects can have the same tag, many tags can refer to the same object, and each tag can be individually configured and processed.
As in Text
, Canvas
widgets have predefined tag names
too: the tag all
refers to all
objects on the canvas, and current
refers to whatever object is
under the mouse cursor. Besides asking for an object under the
mouse, you can also search for objects with the find_
canvas methods: canvas.find_closest(X,Y)
, for instance,
returns a tuple whose first item is the identifier of the closest
object to the supplied coordinates—handy after you’ve received
coordinates in a general mouse-click event callback.
We’ll revisit the notion of canvas tags by example later in
this chapter (see the animation scripts near the end if you can’t
wait). Canvases support additional operations and options that we
don’t have space to cover here (e.g., the canvas postscript
method lets you save the
canvas in a PostScript file). See later examples in this book,
such as PyDraw, for more details, and consult other Tk or Tkinter
references for an exhaustive list of canvas object options.
As demonstrated in Example 10-14, scroll bars can be cross-linked with a canvas using the same protocols we used to add them to listboxes and text earlier, but with a few unique requirements.
Example 10-14. PP3EGuiTourscrolledcanvas.py
from Tkinter import * class ScrolledCanvas(Frame): def _ _init_ _(self, parent=None, color='brown'): Frame._ _init_ _(self, parent) self.pack(expand=YES, fill=BOTH) # make me expandable canv = Canvas(self, bg=color, relief=SUNKEN) canv.config(width=300, height=200) # display area size canv.config(scrollregion=(0,0,300, 1000)) # canvas size corners canv.config(highlightthickness=0) # no pixels to border sbar = Scrollbar(self) sbar.config(command=canv.yview) # xlink sbar and canv canv.config(yscrollcommand=sbar.set) # move one moves other sbar.pack(side=RIGHT, fill=Y) # pack first=clip last canv.pack(side=LEFT, expand=YES, fill=BOTH) # canv clipped first for i in range(10): canv.create_text(150, 50+(i*100), text='spam'+str(i), fill='beige') canv.bind('<Double-1>', self.onDoubleClick) # set event handler self.canvas = canv def onDoubleClick(self, event): print event.x, event.y print self.canvas.canvasx(event.x), self.canvas.canvasy(event.y) if _ _name_ _ == '_ _main_ _': ScrolledCanvas().mainloop( )
This script makes the window in Figure 10-22. It is similar to prior scroll examples, but scrolled canvases introduce two kinks:
You can specify the size of the displayed view window, but you must specify the size of the scrollable canvas at large. The size of the view window is what is displayed, and it can be changed by the user by resizing. The size of the scrollable canvas will generally be larger—it includes the entire content, of which only part is displayed in the view window. Scrolling moves the view window over the scrollable size canvas.
In addition, you may need to map between event view area coordinates and overall canvas coordinates if the canvas is larger than its view area. In a scrolling scenario, the canvas will almost always be larger than the part displayed, so mapping is often needed when canvases are scrolled. In some applications, this mapping is not required, because widgets embedded in the canvas respond to users directly (e.g., buttons in the PyPhoto example in Chapter 12). If the user interacts with the canvas directly, though (e.g., in a drawing program), mapping from view coordinates to scrollable size coordinates may be necessary.
Sizes are given as configuration options. To specify a view
area size, use canvas width
and
height
options. To specify an
overall canvas size, give the (X,Y) coordinates of the upper-left
and lower-right corners of the canvas in a four-item tuple passed to
the scrollregion
option. If no
view area size is given, a default size is used. If no scrollregion
is given, it defaults to the
view area size; this makes the scroll bar useless, since the view is
assumed to hold the entire canvas.
Mapping coordinates is a bit subtler. If the scrollable view
area associated with a canvas is smaller than the canvas at large,
the (X,Y) coordinates returned in event objects are view area
coordinates, not overall canvas coordinates. You’ll generally want
to scale the event coordinates to canvas coordinates, by passing
them to the canvasx
and canvasy
canvas methods before using them
to process objects.
For example, if you run the scrolled canvas script and watch the messages printed on mouse double-clicks, you’ll notice that the event coordinates are always relative to the displayed view window, not to the overall canvas:
C:...PP3EGuiTour>python scrolledcanvas.py
2 0 event x,y when scrolled to top of canvas
2.0 0.0 canvas x,y -same, as long as no border pixels
150 106
150.0 106.0
299 197
299.0 197.0
3 2 event x,y when scrolled to bottom of canvas
3.0 802.0 canvas x,y -y differs radically
296 192
296.0 992.0
152 97 when scrolled to a midpoint in the canvas
152.0 599.0
16 187
16.0 689.0
Here, the mapped canvas X is always the same as the canvas X
because the display area and canvas are both set at 300 pixels wide
(it would be off by 2 pixels due to automatic borders if not for the
script’s highlightthickness
setting). But notice that the mapped Y is wildly different from the
event Y if you click after a vertical scroll. Without scaling, the
event’s Y incorrectly points to a spot much higher in the
canvas.
Most of this book’s canvas examples need no such scaling—(0,0) always maps to the upper-left corner of the canvas display in which a mouse click occurs—but just because canvases are not scrolled. See the next section for a canvas with both horizontal and vertical scrolls; the PyTree program later in this book is similar, but it also uses dynamically changed scrollable region sizes when new trees are viewed.
As a rule of thumb, if your canvases scroll, be sure to scale event coordinates to true canvas coordinates in callback handlers that care about positions. Some handlers might not care whether events are bound to individual drawn objects or embedded widgets instead of the canvas at large, but we need to move on to the next two sections to see why.
At the end of Chapter 9, we looked at a collection of scripts that display thumbnail image links for all photos in a directory. There, we noted that scrolling is a major requirement for large photo collections. Now that we know about scrolling canvases, we can finally put them to work to implement this final extension.
Example 10-15 is a
customization of the last chapter’s code, which displays thumbnails
in a scrollable canvas. See the prior chapter for more details on its
operation (including the ImageTk
module imported from the required Python Imaging Library [PIL]
third-party extension). Here, we are just adding a canvas
positioning the thumbnail buttons at absolute coordinates in the
canvas, and computing the scrollable size using concepts outlined in
the prior section.
Example 10-15. PP3EGuiPILviewer_thumbs_scrolled.py
############################################################## # image viewer extension: uses fixed size for thumbnails # for uniform layout, and adds scrolling for large image sets # by displaying thumbs in a canvas widget with scroll bars; # requires PIL to view image formats such as JPEG, and reuses # thumbs maker and single photo viewer in viewer_thumbs.py; # caveat/to do: this could also scroll popped-up images that # are too large for the screen, cropped on Windows as is; # see PyPhoto later in book for a much more complete version; ############################################################## import sys, math from Tkinter import * from ImageTk import PhotoImage from viewer_thumbs import makeThumbs, ViewOne def viewer(imgdir, kind=Toplevel, numcols=None, height=300, width=300): """ use fixed-size buttons, scrollable canvas; sets scrollable (full) size, and places thumbs at abs x,y coordinates in canvas; caveat: assumes all thumbs are same size """ win = kind( ) win.title('Simple viewer: ' + imgdir) quit = Button(win, text='Quit', command=win.quit, bg='beige') quit.pack(side=BOTTOM, fill=X) canvas = Canvas(win, borderwidth=0) vbar = Scrollbar(win) hbar = Scrollbar(win, orient='horizontal') vbar.pack(side=RIGHT, fill=Y) # pack canvas after bars hbar.pack(side=BOTTOM, fill=X) # so clipped first canvas.pack(side=TOP, fill=BOTH, expand=YES) vbar.config(command=canvas.yview) # call on scroll move hbar.config(command=canvas.xview) canvas.config(yscrollcommand=vbar.set) # call on canvas move canvas.config(xscrollcommand=hbar.set) canvas.config(height=height, width=width) # init viewable area size # changes if user resizes thumbs = makeThumbs(imgdir) # [(imgfile, imgobj)] numthumbs = len(thumbs) if not numcols: numcols = int(math.ceil(math.sqrt(numthumbs))) # fixed or N × N numrows = int(math.ceil(numthumbs / float(numcols))) linksize = max(thumbs[0][1].size) # (width, height) fullsize = (0, 0, # upper left X,Y (linksize * numcols), (linksize * numrows) ) # lower right X,Y canvas.config(scrollregion=fullsize) # scrollable area size rowpos = 0 savephotos = [] while thumbs: thumbsrow, thumbs = thumbs[:numcols], thumbs[numcols:] colpos = 0 for (imgfile, imgobj) in thumbsrow: photo = PhotoImage(imgobj) link = Button(canvas, image=photo) handler = lambda savefile=imgfile: ViewOne(imgdir, savefile) link.config(command=handler, width=linksize, height=linksize) link.pack(side=LEFT, expand=YES) canvas.create_window(colpos, rowpos, anchor=NW, window=link, width=linksize, height=linksize) colpos += linksize savephotos.append(photo) rowpos += linksize return win, savephotos if _ _name_ _ == '_ _main_ _': imgdir = (len(sys.argv) > 1 and sys.argv[1]) or 'images' main, save = viewer(imgdir, kind=Tk) main.mainloop( )
To see this program in action, install the PIL extension described at the end of Chapter 9 and launch the script from a command line, passing the name of the image directory to be viewed as a command-line argument:
...PP3EGuiPIL>viewer_thumbs_scrolled.py c:markcamerajun1705DCIM100CANON
As before, clicking on a thumbnail image opens the corresponding image at its full size in a new pop-up window. Figure 10-23 shows the viewer at work on a directory copied from my digital camera.
Or, simply run the script as is from a command line by clicking its icon or within IDLE—without command-line arguments, it displays the contents of the default images subdirectory in the book’s source code tree, as captured in Figure 10-24.
As is, the scrollable thumbnail viewer in Example 10-15 has a major limitation: images that are larger than the physical screen are simply truncated on Windows when popped up. Moreover, there is no way to resize images once opened, to open other directories, and so on. It’s a fairly simplistic demonstration of canvas programming.
In Chapter 12, we’ll learn how to do better when we meet the PyPhoto example program. PyPhoto will scroll the full size of images well. In addition, it has tools for a variety of resizing effects, and it supports saving images to files and opening other image directories on the fly. At its core, though, PyPhoto will reuse the techniques of our simple browser here, as well as the thumbnail generation code we wrote in the prior chapter.
For the rest of this story, watch for PyPhoto in Chapter 12 or study the source code of the example program pyphoto1.py, in the source directory.
For the purposes of this chapter, notice how in Example 10-15, the thumbnail viewer’s actions are associated with embedded button widgets, not with the canvas itself. To see how to implement the latter, let’s move on to the next section.
Like Text
and
Listbox
, there is no notion of a
single command
callback for
Canvas
. Instead, canvas programs
generally use other widgets, as in Example 10-15 and in the
earlier section "Scrolling
Canvases,” or the lower-level bind
call to set up handlers for mouse
clicks, key presses, and the like. Example 10-16 shows how to
bind events for the canvas itself, in order to implement a few of
the more common canvas drawing operations.
Example 10-16. PP3EGuiTourcanvasDraw.py
################################################################# # draw elastic shapes on a canvas on drag, move on right click; # see canvasDraw_tags*.py for extensions with tags and animation ################################################################# from Tkinter import * trace = 0 class CanvasEventsDemo: def _ _init_ _(self, parent=None): canvas = Canvas(width=300, height=300, bg='beige') canvas.pack( ) canvas.bind('<ButtonPress-1>', self.onStart) # click canvas.bind('<B1-Motion>', self.onGrow) # and drag canvas.bind('<Double-1>', self.onClear) # delete all canvas.bind('<ButtonPress-3>', self.onMove) # move latest self.canvas = canvas self.drawn = None self.kinds = [canvas.create_oval, canvas.create_rectangle] def onStart(self, event): self.shape = self.kinds[0] self.kinds = self.kinds[1:] + self.kinds[:1] # start dragout self.start = event self.drawn = None def onGrow(self, event): # delete and redraw canvas = event.widget if self.drawn: canvas.delete(self.drawn) objectId = self.shape(self.start.x, self.start.y, event.x, event.y) if trace: print objectId self.drawn = objectId def onClear(self, event): event.widget.delete('all') # use tag all def onMove(self, event): if self.drawn: # move to click spot if trace: print self.drawn canvas = event.widget diffX, diffY = (event.x - self.start.x), (event.y - self.start.y) canvas.move(self.drawn, diffX, diffY) self.start = event if _ _name_ _ == '_ _main_ _': CanvasEventsDemo( ) mainloop( )
This script intercepts and processes three mouse-controlled actions:
To erase everything on the canvas, the script binds the
double left-click event to run the canvas’s delete
method with the all
tag—again, a built-in tag that
associates every object on the screen. Notice that the
Canvas
widget clicked is
available in the event object passed in to the callback
handler (it’s also available as self.canvas
).
Pressing the left mouse button and dragging (moving it while the button is still pressed) creates a rectangle or oval shape as you drag. This is often called dragging out an object—the shape grows and shrinks in an elastic rubber-band fashion as you drag the mouse and winds up with a final size and location given by the point where you release the mouse button.
To make this work in Tkinter, all you need to do is delete the old shape and draw another as each drag event fires; both delete and draw operations are fast enough to achieve the elastic drag-out effect. Of course, to draw a shape to the current mouse location you need a starting point; to delete before a redraw you also must remember the last drawn object’s identifier. Two events come into play: the initial button press event saves the start coordinates (really, the initial press event object, which contains the start coordinates), and mouse movement events erase and redraw from the start coordinates to the new mouse coordinates and save the new object ID for the next event’s erase.
When you click the right mouse button (button 3), the
script moves the most recently drawn object to the spot you
clicked in a single step. The event
argument gives the (X,Y)
coordinates of the spot clicked, and we subtract the saved
starting coordinates of the last drawn object to get the (X,Y)
offsets to pass to the canvas move
method (again, move
does not take positions).
Remember to scale event coordinates first if your canvas is
scrolled.
The net result creates a window like that shown in Figure 10-25 after user
interaction. As you drag out objects, the script alternates between
ovals and rectangles; set the script’s trace
global to watch object identifiers
scroll on stdout
as new objects
are drawn during a drag. This screenshot was taken after a few
object drag-outs and moves, but you’d never tell from looking at it;
run this example on your own computer to get a better feel for the
operations it supports.
Much like we did for the Text
widget, it is also possible to bind
events for one or more specific objects drawn on a Canvas
with its tag_bind
method. This call accepts
either a tag name string or an object ID in its first argument.
For instance, you can register a different callback handler for
mouse clicks on every drawn item or on any in a group of drawn and
tagged items, rather than for the entire canvas at large. Example 10-17 binds a
double-click handler in both the canvas itself and on two specific
text items within it, to illustrate the interfaces. It generates
Figure 10-26 when
run.
Example 10-17. PP3EGuiTourcanvas-bind.py
from Tkinter import * def onCanvasClick(event): print 'Got canvas click', event.x, event.y, event.widget def onObjectClick(event): print 'Got object click', event.x, event.y, event.widget, print event.widget.find_closest(event.x, event.y) # find text object's ID root = Tk( ) canv = Canvas(root, width=100, height=100) obj1 = canv.create_text(50, 30, text='Click me one') obj2 = canv.create_text(50, 70, text='Click me two') canv.bind('<Double-1>', onCanvasClick) # bind to whole canvas canv.tag_bind(obj1, '<Double-1>', onObjectClick) # bind to drawn item canv.tag_bind(obj2, '<Double-1>', onObjectClick) # a tag works here too canv.pack( ) root.mainloop( )
Object IDs are passed to tag_bind
here, but a tag name string
would work too. When you click outside the text items in this
script’s window, the canvas event handler fires; when either text
item is clicked, both the canvas and the text object handlers
fire. Here is the stdout
result
after clicking on the canvas twice and on each text item once; the
script uses the canvas find_closest
method to fetch the object
ID of the particular text item clicked (the one closest to the
click spot):
C:...PP3EGuiTour>python canvas-bind.py
Got canvas click 3 6 .8217952 canvas clicks
Got canvas click 46 52 .8217952
Got object click 51 33 .8217952 (1,) first text click
Got canvas click 51 33 .8217952
Got object click 55 69 .8217952 (2,) second text click
Got canvas click 55 69 .8217952
We’ll revisit the notion of events bound to canvases in the
PyDraw example in Chapter
12, where we’ll use them to implement a feature-rich paint
and motion program. We’ll also return to the canvasDraw
script later in this chapter,
to add tag-based moves and simple animation with time-based tools,
so keep this page bookmarked for reference. First, though, let’s
follow a promising side road to explore another way to lay out
widgets within windows.
[*] Animation techniques are covered at the end of this tour. Because you can embed other widgets in a canvas’s drawing area, their coordinate system also makes them ideal for implementing GUIs that let users design other GUIs by dragging embedded widgets around on the canvas—a useful canvas application we would explore in this book if I had a few hundred pages to spare.
52.15.129.90