10.5. CanvasWhen 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. 10.5.1. Basic Canvas OperationsCanvases 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. PP3E\Gui\Tour\canvas1.py
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. Figure 10-21. canvas1 hardcoded object sketches10.5.2. Programming the Canvas WidgetCanvases 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. 10.5.2.1. CoordinatesAll 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.[*]
10.5.2.2. Object constructionThe 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 youwhen 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. 10.5.2.3. Object identifiers and operationsAlthough 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 nameraise 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. 10.5.2.4. Canvas object tagsBut 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 coordinateswhen 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 coordinateshandy 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. 10.5.3. Scrolling CanvasesAs 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. PP3E\Gui\Tour\scrolledcanvas.py
This script makes the window in Figure 10-22. It is similar to prior scroll examples, but scrolled canvases introduce two kinks:
Figure 10-22. scrolledcanvas liveSizes 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:\...\PP3E\Gui\Tour>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 occursbut 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. 10.5.4. Scrollable Canvases and Image ThumbnailsAt 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. PP3E\Gui\PIL\viewer_thumbs_scrolled.py
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: ...\PP3E\Gui\PIL>viewer_thumbs_scrolled.py c:\mark\camera\jun1705\DCIM\100CANON 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. Figure 10-23. Scrolled thumbnail image viewerOr, simply run the script as is from a command line by clicking its icon or within IDLEwithout 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. Figure 10-24. Displaying the default images directory10.5.4.1. Scrolling images too: PyPhoto (ahead)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. 10.5.5. Using Canvas EventsLike 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. PP3E\Gui\Tour\canvasDraw.py
This script intercepts and processes three mouse-controlled actions:
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. Figure 10-25. canvasDraw after a few drags and moves10.5.5.1. Binding events on specific itemsMuch 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. Figure 10-26. Canvas-bind windowExample 10-17. PP3E\Gui\Tour\canvas-bind.py
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:\...\PP3E\Gui\Tour>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. |