An example of using WxPython to create a node interface. Part 2: Handling Mouse Events
In a small series of articles will be described the use of WxPython to solve a very specific task of developing a user interface, and even how to make this solution universal. This tutorial is designed for those who have already begun to study this library and want to see something more complex and holistic than the simplest examples (although everything will start from relatively simple things).
In the last part, I talked about the task and began to describe the process of implementation, or rather the rendering of objects. Now it's time to implement user interaction.
Let me remind you that the last time we got a simple program that draws simple notes on the canvas (for now, rectangles with text). It's time to make the nodes moveable.
4. Highlighting objects when you hover over them
But before realizing the movement of the nodes, we will make one useful feature: highlighting the object when the cursor is moved over it. Parts of this feature will later come in handy when implementing the rest of the functionality. To implement, we need to perform 3 steps: 1) Track cursor movement 2) Find and save the topmost object under the cursor 3) Render the selection of this object
To track the cursor movement, we need to add a handler for the corresponding event to the canvas class:
self.Bind(wx.EVT_MOTION, self.OnMouseMotion)
Now when you move the cursor, the method will be called:
Three actions take place here: since the cursor coordinates are relative to the window, we first need to translate them into the coordinates of the canvas (since we have scrolled), then we need to find the corresponding object and update the image so that the object lights up on which the user has moved the cursor . Objects under the cursor are searched by the method:
defFindObjectUnderPoint(self, pos):#Check all objects on a canvas. Some objects may have multiple components and connections. for obj in reversed(self._canvasObjects): objUnderCursor = obj.ReturnObjectUnderCursor(pos) if objUnderCursor: return objUnderCursor return None
Everything is trivial and not very. On the one hand, we simply go through all the objects and look for the one that is under the cursor. And we do this in reverse order, because we want to get the top one, i.e. last added object. On the other hand, we use the “ReturnObjectUnderCursor” method, which returns an object to us, although we seem to know which object we are checking. This is done with a margin for the future, so that you can make nodes that contain other objects in themselves (for example: connections with other nodes or angles to change the size of the node). So far this method at our node just checks if the cursor is in a rectangle:
Here we use a transparent brush, so that when rendering, do not overwrite what was previously rendered (i.e., the node itself). The result is this picture: Cursor had to finish after the fact, so it’s not traditional :) I will not give here all the code, who cares, this one here on GitHub contains it.
5. A little refactoring and adding interfaces
And again, we will not postpone for a long time the implementation of the movement of our objects, this time for a small refactoring. If this framework has to be universal, it means that the nodes here can be all sorts of different, including unmovable ones (for example, connections between objects that are defined by the objects themselves or some kind of node components, and even if it doesn’t matter what people will think of). So we need some kind of universal way of describing what can and cannot be done with nodes. And in general, I would like to introduce some kind of universal interface for nodes. But now we will not use abc, zope.interface or something like that, but just make the base class for the objects on the canvas:
classCanvasObject(object):def__init__(self):#Supported operations self.clonable = False self.movable = False self.connectable = False self.deletable = False self.selectable = False def Render(self, gc): """ Rendering method should draw an object. gc: GraphicsContext object that should be used for drawing. """ raise NotImplementedError() def RenderHighlighting(self, gc): """ RenderHighlighting method should draw an object with a highlighting border around it. gc: GraphicsContext object that should be used for drawing. """ raise NotImplementedError() def ReturnObjectUnderCursor(self, pos): """ ReturnObjectUnderCursor method returns a top component of this object at a given position or None if position is outside of all objects. pos: tested position as a list of x, y coordinates such as [100, 200] """ raise NotImplementedError()
As you can see, we have a number of standard actions that are not supported by default. But there are 3 methods that should be in any object on the canvas. What is logical, why do we need such objects on the canvas that we cannot see (Render), and how we will see, so poke them with the cursor (ReturnObjectUnderCursor, RenderHighlighting). And here we remember that we want to move our nodes, i.e. they must be relocatable, and for this there is a special class:
from MoveMe.Canvas.Objects.Base.CanvasObject import CanvasObject classMovableObject(CanvasObject):def__init__(self, position): super(MovableObject, self).__init__() self.position = position self.movable = True
Everything is simple, this class allows movement and also adds such a useful property as a position, so moving something from one position to another without having this position itself is difficult. Now the definition of our node has become a bit more complicated, since it has become the heir of our new classes, although, in general, it’s still the same good old node:
from MoveMe.Canvas.Objects.Base.CanvasObject import CanvasObject from MoveMe.Canvas.Objects.Base.MovableObject import MovableObject classSimpleBoxNode(MovableObject, CanvasObject): ...........
6. Move Nodes
Here we come to the long-awaited implementation of moving nodes. To do this, we need to do 2 basic steps: remember which object the user started dragging (i.e. what object was under the cursor at the moment of pressing the left mouse button) and update the position of the object when moving the cursor until the user releases the mouse button. The first action is performed in:
We simply remember the position of the cursor and the current object under the cursor as moved, if it supports the movement. Unless there is still a check for the presence of an object under the cursor, since there is no point in moving the void. The second part is a bit more interesting.
defOnMouseMotion(self, evt): pos = self.CalcUnscrolledPosition(evt.GetPosition()).Get() self._objectUnderCursor = self.FindObjectUnderPoint(pos) ifnot evt.LeftIsDown(): self._draggingObject = Noneif evt.LeftIsDown() and evt.Dragging() and self._draggingObject: dx = pos[0]-self._lastDraggingPosition[0] dy = pos[1]-self._lastDraggingPosition[1] newX = self._draggingObject.position[0]+dx newY = self._draggingObject.position[1]+dy #Check canvas boundaries newX = min(newX, self.canvasDimensions[0]-self._draggingObject.boundingBoxDimensions[0]) newY = min(newY, self.canvasDimensions[1]-self._draggingObject.boundingBoxDimensions[1]) newX = max(newX, 0) newY = max(newY, 0) self._draggingObject.position = [newX, newY] #Cursor will be at a border of a node if it goes out of canvas self._lastDraggingPosition = [min(pos[0], self.canvasDimensions[0]), min(pos[1], self.canvasDimensions[1])] self.Render()
The first check ensures that if the user at some point drives the mouse with the left button released, it means that he is already moving nothing. This is better than stopping the movement on a button release event, since the cursor may be outside the window and then we will not receive this event. Then we check that we are really carrying something and begin to consider the relative movement of our object. At the moment we do not think about what is happening with the keyboard (whether Ctrl is pressed or something else, it will be later). There is still a check for going beyond the limits of canvas. With this test, everything is not entirely simple and clear. On the one hand, if the size of the canvas is fixed, then everything should be so, but on the other hand, it would be nice to stretch the canvass along the way (although this is not the ideal solution). In general, at the moment, the size of the canvas will be fixed and the nodes will rest against the borders of the canvas. That's all, now we can move objects along the canvas. The code lives in this commit on GitHub . And it looks like this: