Section 10.5. Canvas

10.5. Canvas

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.

10.5.1. Basic Canvas Operations

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. PP3E\Gui\Tour\

 # 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.

Figure 10-21. canvas1 hardcoded object sketches

10.5.2. Programming the Canvas Widget

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. Coordinates

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.[*]

[*] 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 canvasa useful canvas application we would explore in this book if I had a few hundred pages to spare. Object construction

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 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. Object identifiers and operations

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 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. Canvas object tags

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 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 Canvases

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. PP3E\Gui\Tour\

 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:

Scrollable versus viewable sizes

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 largerit includes the entire content, of which only part is displayed in the view window. Scrolling moves the view window over the scrollable size canvas.

Viewable to absolute coordinate mapping

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.

Figure 10-22. scrolledcanvas live

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:\...\PP3E\Gui\Tour>python  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 Thumbnails

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. PP3E\Gui\PIL\

 ############################################################## # 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; # 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 x 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:

 ...\PP3E\Gui\PIL> 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 viewer

Or, 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 directory 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, 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 Events

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. PP3E\Gui\Tour\

 ################################################################# # 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:

Clearing the canvas

To erase everything on the canvas, the script binds the double left-click event to run the canvas's delete method with the all tagagain, 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).

Dragging out object shapes

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 objectthe 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.

Object moves

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.

Figure 10-25. canvasDraw after a few drags and moves Binding events on specific items

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.

Figure 10-26. Canvas-bind window

Example 10-17. PP3E\Gui\Tour\

 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:\...\PP3E\Gui\Tour>python  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.

Programming Python
Programming Python
ISBN: 0596009259
EAN: 2147483647
Year: 2004
Pages: 270
Authors: Mark Lutz

Similar book on Amazon © 2008-2017.
If you may any questions please contact us: