Drawing on a canvas

We have already encountered several examples of objects drawn on canvases. However, these objects were drawn to represent physical objects on front panels and to create images pro-grammatically. Now we need to allow the user to create drawn objects on the canvas.

Almost all drawing operations define a bounding box which encloses the object. The bounding box is expressed as a pair of x/y coordinates at the top-left and bottom-right corners. Lines are special cases; they have a start and end coordinate which does not have to correspond to the coordinates of its bounding box. The bounding box for a line will always be the top-left and bottom-right coordinates. It is important to note that Tk does not guarantee that the bounding box exactly bounds the object, so some allowances may have to be made in critical code. This is illustrated in figure 10.1.

Curved lines (not arcs) are defined as a series of straight lines, each with its own bounding box. Although we will see the application of these object types in some of the examples, they really require special consideration.

Let's start with a very simple drawing program, inspired by one of the examples in Douglas A. Young's The X Window System: Programming and Applications with Xt. This example allows the user to draw lines, rectangles and ovals on a canvas and then select each of these objects. The original example was written in C using X Window, so I have obviously Tkin-terized it. It does not allow editing of the resulting drawn objects, so it is somewhat akin to drawing on soft paper with a very hard pencil!

draw.py from Tkinter import * import Pmw, AppShell, math class Draw(AppShell.AppShell): usecommandarea = 1

appname = 'Drawing Program - Version 1'

frameWidth = 800

frameHeight = 600

def createButtons(self):

self.buttonAdd('Close', helpMessage='Close Screen', statusMessage='Exit', command=self.close)

Python Tkinter Canvas Example

def createBase(self):

self.width = self.root.winfo_width()-10 self.height = self.root.winfo_height()-95 self.command= self.createcomponent('command', (), None, Frame, (self.interior(),), width=self.width*0.25, height=self.height, background=Mgray90M) self.command.pack(side=LEFT, expand=YES, fill=BOTH)

self.canvas = self.createcomponent('canvas', (), None, Canvas, (self.interior(),), width=self.width*0.73, height=self.height, background=Mwhite") self.canvas.pack(side=LEFT, expand=YES, fill=BOTH) O

Widget.bind(self.canvas, "<Button-1>", self.mouseDown) Widget.bind(self.canvas, M<Button1-Motion>", self.mouseMotion) _ Widget.bind(self.canvas, M<Button1-ButtonRelease>M, self.mouseUp)

self.radio = Pmw.RadioSelect(self.command, labelpos = None, buttontype = 'radiobutton1, orient = VERTICAL, command = self.selectFunc, hull_borderwidth = 2, hull_relief = RIDGE,) self.radio.pack(side = TOP, expand = 1)

self.radio.add(text) self.func[text] = func self.radio.invoke('Rectangle')

def selectFunc(self, tag):

self.currentFunc = self.func[tag]

def mouseDown(self, event):

self.currentObject = None JQ

self.lastx = self.startx = self.canvas.canvasx(event.x) self.lasty = self.starty = self.canvas.canvasy(event.y) if not self.currentFunc:

self.selObj = self.canvas.find_closest(self.startx, self.starty)[0] self.canvas.itemconfig(self.selObj, width=2) self.canvas.lift(self.selObj)

def mouseMotion(self, event):

self.lastx = self.canvas.canvasx(event.x) self.lasty = self.canvas.canvasy(event.y) if self.currentFunc:

self.canvas.delete(self.currentObject) self.currentFunc(self.startx, self.starty, self.lastx, self.lasty, self.foreground, self.background)

def mouseUp(self, event):

self.lastx = self.canvas.canvasx(event.x) self.lasty = self.canvas.canvasy(event.y) self.canvas.delete(self.currentObject) self.currentObject = None if self.currentFunc:

self.currentFunc(self.startx, self.starty, self.lastx, self.lasty, self.foreground, self.background)

else:

if self.selObj:

self.canvas.itemconfig(self.selObj, width=1)

self.currentObject = self.canvas.create_line(x,y,x2,y2, fill=fg)

self.currentObject = self.canvas.create_rectangle(x, y, x2, y2, outline=fg, fill=bg)

self.currentObject = self.canvas.create_oval(x, y, x2, y2, outline=fg, fill=bg)

def initData(self):

self.currentFunc = None self.currentObject = None self.selObj = None self.foreground = 'black'

self.background = 'white'

def close(self): self.quit()

def createlnterface(self):

AppShell.AppShell.createlnterface(self)

self.createButtons()

self.initData()

self.createBase()

Code comments

1 This example is completely pointer-driven so it relies on binding functionality to mouse events. We bind click, movement and release to appropriate member functions.

Widget.bind(self.canvas, "<Button-1>", self.mouseDown) Widget.bind(self.canvas, "<Button1-Motion>", self.mouseMotion) Widget.bind(self.canvas, "<Button1-ButtonRelease>", self.mouseUp)

2 This simple example supports three basic shapes. We build Pmw.RadioSelect buttons to link each of the shapes with an appropriate drawing function. Additionally, we define a selection option which allows us to click on the canvas without drawing.

3 The mouseDown method deselects any currently selected object. The event returns x- and y-coordinates for the mouse-click as screen coordinates. The canvasx and canvasy methods of the Canvas widget convert these screen coordinates into coordinates relative to the canvas.

def mouseDown(self, event):

self.currentObject = None self.lastx = self.startx = self.canvas.canvasx(event.x)

self.lasty = self.starty = self.canvas.canvasy(event.y)

Converting the x- and y-coordinates to canvas coordinates is a step that is often forgotten when first coding canvas-based applications. Figure 10.3 illustrates what this means.

Python Canvas Coordinates

Figure 10.3 Relationship between screen and canvas coordinates

When the user clicks on the canvas, the click effectively goes through to the desktop and these coordinates are returned in the event. Converting to canvas coordinates returns the coordinates relative to the canvas origin, regardless of where the canvas is on the screen.

4 If no drawing function is selected, we are in select mode, and we search to locate the nearest object on the canvas and select it. This method of selection may not be appropriate for all drawing applications, since the method will always find an object, no matter where the canvas is clicked. This can lead to some confusing behavior in certain complex diagrams, so the selection model might require direct clicking on an object to select it. if not self.currentFunc:

self.selObj = self.canvas.find_closest(self.startx, self.starty)) self.canvas.itemconfig(self.selObj, width=2) self.canvas.lift(self.selObj)

Having selected the object, we thicken its outline and raise (lift) it to the top of the drawing stack, as shown in figure 10.4.

5 As the mouse is moved (with the button down), we receive a stream of motion events. Each of these represents a change in the bounding box for the object. Having converted the x- and y-coordinates to canvas points, we delete the existing canvas object and redraw it using the current function and the new bounding box.

self.canvas.delete(self.currentObject) self.currentFunc(self.startx, self.starty, self.lastx, self.lasty, self.foreground, self.background)

6 The drawing methods are quite simple; they're just creating canvas primitives within the bounding box.

self.currentObject = self.canvas.create_line(x,y,x2,y2, fill=fg)

10.1.1 Moving canvas objects

The selection of objects in the first example simply raises them in the display stack. If you were to raise a large object above smaller objects you could quite possibly prevent access to those objects. Clearly, we need to provide a more useful means of manipulating the drawn objects. Typically, draw tools move objects in response to a mouse drag. Adding this to the example is very easy. Here are the modifications which have been applied to draw.py:

Pyqt Drawing Box Line

draw2.py def mouseMotion(self, event):

cx = self.canvas.canvasx(event.x) cy = self.canvas.canvasy(event.y) if self.currentFunc: self.lastx = cx self.lasty = cy self.canvas.delete(self.currentObject) self.currentFunc(self.startx, self.starty, self.lastx, self.lasty, self.foreground, self.background)

else:

if self.selObj:

self.canvas.move(self.selObj, cx-self.lastx, cy-self.lasty)

self.lastx = cx self.lasty = cy

Code comments

O We need to store the x- and y-coordinates in intermediate variables, since we need to determine how far the mouse moved since the last time we updated the screen.

2 If we are drawing the object, we use the x- and y-coordinates as the second coordinate of the bounding box.

3 If we are moving the object, we calculate the difference between the current location and the last bounding box location.

+5 -5

Responses

  • dina
    How to make a simple drawing application python tkinter?
    6 years ago

Post a comment