11.3 Animated Graphics with Canvas Widgets
11.3.6 Dragging Planets to New Positions
There is no user interface to ourplanet1.py script. A requirement is clearly buttons for starting and stopping the animation. The user should also be able to specify the planet’s positions. A canvas widget allows movement of its items in response to click-and-drag events. By explaining the programming of dragging a planet to a new position, the reader will have enough basic
4 Withoutupdate no animation is performed and the planet is just moved to its final position after the animate method has terminated. However, update can have undesired side effects, cf. Welch [38, p. 440].
Fig. 11.9.Example on moving canvas items (planet1.py script).
knowledge to proceed with creating quite sophisticated user interactions in graphical applications.
Dragging a planet to a new position can be implemented as follows.
1. Bind the eventleft mouse button click to a call to the methodmark. 2. Themarkmethod finds the item (i.e. planet) in the canvas that is closest
to the current mouse position.
3. Bind the event mouse motion while pressing left mouse button to a call to the method drag.
4. Thedrag method finds the mouse position and moves the planet to this position.
Binding events is done in the constructor of classPlanetarySystem: self.canvas.bind(’<Button-1>’, self.mark)
self.canvas.bind(’<B1-Motion>’, self.drag)
Functions bound to events take an event object as first parameter. In the markmethod we can extract the widget containing the mouse and the screen coordinates of the position of the mouse from the event object’s widget, x, and y data. The screen coordinates must then be transformed to canvas coordinates.
562 11. More Advanced GUI Programming def mark(self, event):
w = event.widget # hopefully a reference to self.canvas!
# find the canvas coords from screen coords:
xc = w.canvasx(event.x); yc = w.canvasx(event.y)
Finding the planet that is closest to the mouse can be performed by calling the canvas widget’sfind_closestmethod. The method returns a tuple containing the canvas identifications of the items that are closest to the (xc,yc) position.
If the object is unique, only one tuple entry is returned, and we store the value in a class variable:
self.clicked_item = w.find_closest(xc, yc)[0]
Unfortunately, there is a basic problem with this approach. As we draw lines illustrating the planet’s path in space, there are numerous canvas items, and after having moved planets around and created lots of lines,find_closestwill easily return a line segment as the closest item. If we instead require that the user must clickinside a planet, the canvas methodfind_withtag(’current’) returns the correct item5. The find_withtag method returns the identifica- tions of all canvas items marked with a specified tag (tags are introduced in Chapter 11.2.2). There is a tag calledcurrentthat is automatically associated with the item that is currently under the mouse pointer. Usingfind_withtag with thecurrenttag, we write
self.clicked_item = self.canvas.find_withtag(’current’)[0]
A combination offind_closestandfind_withtagis also possible, at least for an illustration of Python programming:
self.clicked_item = w.find_closest(xc, yc)[0]
# did we get a planet or a line segment?
if not self.planets.has_key(self.clicked_item):
# find_closest did not find a planet, use current tag
# (requires the mouse pointer to be inside a planet):
self.clicked_item = self.canvas.find_withtag(’current’)[0]
The final task to be performed in the markmethod is to record the coor- dinates of the clicked item:
self.clicked_item_xc_prev = xc; self.clicked_item_yc_prev = yc Thedragmethod is to be called when the left mouse button is pressed while moving the mouse. The method is called numerous times during the mouse movement, and for each call we need to update the clicked item’s position accordingly. The current mouse position is found by processing the event object. Thereafter, we let the Planetobject be responsible for moving itself in the canvas widget.
5 If several items are overlapping at the mouse position, a tuple of all identifications is returned.
def drag(self, event):
w = event.widget # could test that this is self.canvas...
xc = w.canvasx(event.x); yc = w.canvasx(event.y) self.planets[self.clicked_item].mouse_move \
(xc, yc, self.clicked_item_xc_prev, self.clicked_item_yc_prev)
# update previous pos to present one (init for next drag call):
self.clicked_item_xc_prev = xc; self.clicked_item_yc_prev = yc The canvas coordinates of the planet’s current and previous positions are sent to the planet’s mouse_movemethod. Having these coordinates, it is easy to call the canvas widget’smovemethod. An important next step is to update thePlanetobject’s data structures, i.e., the center position of the planet:
# make a relative move when dragging the mouse:
def mouse_move(self, xc, yc, xc_prev, yc_prev):
self.canvas.move(self.id, xc-xc_prev, yc-yc_prev)
# update the planet’s physical coordinates:
c = self.canvas.coords(self.id) # grab new canvas coords corners = C.canvas2physical4(c) # to physical coords self.x, self.y = self.get_center(corners)
# compute center based on upper-left and lower-right corner
# coordinates in physical coordinate system:
def get_center(self, corners):
return (corners[0] + self.r/2.0, corners[1] - self.r/2.0) One should observe that thecoordsmethod in the canvas widget can be used for both specifying a new position of an item (seeabs_move in classPlanet) or extracting the coordinates of the current position of an item (like we do in themouse_move method).
To make the present demo application more user friendly, we add but- tons for starting and stopping the animation in the constructor of class PlanetarySystem:
button_row = Frame(self.frame, borderwidth=2) button_row.pack(side=’top’)
b = Button(button_row, text=’start animation’, command=self.animate)
b.pack(side=’left’)
b = Button(button_row, text=’stop animation’, command=self.stop)
b.pack(side=’right’)
A class variablestop_animation controls whether the animation is on or off:
def stop(self):
self.stop_animation = True def animate(self):
self.stop_animation = False ...
while self.model.time() < 5 and not self.stop_animation:
...
564 11. More Advanced GUI Programming
A slider for controlling the speed of the animation is also convenient:
self.speed = DoubleVar(); self.speed.set(1);
speed_scale = Scale(self.frame, orient=’horizontal’, from_=0, to=1, tickinterval=0.5, resolution=0.01, label=’animation speed’, length=300,
variable=self.speed) speed_scale.pack(side=’top’)
The self.speed attribute can now be used in the after call in the animate method:
self.canvas.after(int((1-self.speed.get())*1000)) # delay in ms The slowest speed corresponds to a delay of 1 second between each movement of the planet.
You are encouraged to test the application, called planet2.py and lo- cated insrc/py/examples/canvas. After having moved the sun and the planet around, and started and stopped the animation a few times, the widget might look like the one in Figure 11.10.