An example of using WxPython to create a node interface. Part 4: Implementing Drag & Drop

    In a short series of articles we will describe the use of WxPython to solve a very specific task of developing a user interface, and how to make this solution universal. This tutorial is intended for those who have already begun to study this library and want to see something more complex and holistic than the simplest examples (although it starts with relatively simple things).



    In this part, we will add Drag & Drop support to our application and teach him how to create new nodes in this way.

    Part 1: Learning to draw
    Part 2: Handling mouse events
    Part 3: Continuing to add features + keyboard processing
    Part 4: Implementing Drag & Drop
    Part 5: Connecting nodes

    Who cares, welcome to the ...


    9. Add support for Drag & Drop


    Drag & Drop support is a useful and popular thing, but here we will use it not for its intended purpose. In this way we will create new nodes. This business works quite simply. It is required to create an object of class "wx.TextDropTarget" and pass it to the "SetDropTarget" method of class "wx.Window" from which our canvas is inherited. Accordingly, at the time of the drop, the "wx.TextDropTarget.OnDropText" method will be called, which we will need to implement. For the test, the implementation of this class will look like this:
    class TextDropTarget(wx.TextDropTarget):
        def __init__(self):
            wx.TextDropTarget.__init__(self)
        def OnDropText(self, x, y, data):
            print x, y, data
    

    Now, when throwing text in a window, a message with the position and the text itself will be printed in the console. As you might have guessed, for accepting files, there is a similar class “wx.FileDropTarget”, which works in the same way, well, or a class “wx.PyDropTarget”, which can accept everything and everything.

    10. Creating Nodes with Drag & Drop


    Just typing text is not very useful, so we use the resulting text to create nodes with it. But first you need to change the architecture a bit and add a node factory (this will come in handy for the future). At the moment - it will be a very simple factory:
    class NodesFactory(object):
        def __init__(self):
            pass
        def CreateNodeFromDescription(self, nodeDescription):
            return SimpleTextBoxNode(text=nodeDescription)
    

    which simply creates an instance of "SimpleTextBoxNode", which is always an advanced descendant of "SimpleBoxNode":
    class SimpleTextBoxNode(SimpleBoxNode):
        def __init__(self, **kwargs):
            super(SimpleTextBoxNode, self).__init__(**kwargs)
            self.text = kwargs.get("text", "No text")
        def Render(self, gc):
            super(SimpleTextBoxNode, self).Render(gc)
            gc.DrawText(self.text, self.position[0]+10, self.position[1]+10)
    

    which in turn renders the given text on top of the rectangular node.
    It remains to add a method to the canvas, allowing you to add new nodes from the description:
        def CreateNodeFromDescriptionAtPosition(self, nodeDescription, pos):
            node = self._nodesFactory.CreateNodeFromDescription(nodeDescription)
            if node:
                node.position = pos
                self._canvasObjects.append(node)
                self.Render()
    

    And slightly modernize TextDropTarget so that it calls this method when the text arrives:
    class TextDropTarget(wx.TextDropTarget):
        def __init__(self, canvas):
            wx.TextDropTarget.__init__(self)
            self._canvas = canvas
        def OnDropText(self, x, y, data):
            print x, y, data
            self._canvas.CreateNodeFromDescriptionAtPosition(data, [x, y])
    

    And now we can create new text nodes simply by throwing text fragments onto the canvas.
    It looks like this:

    The code, as always, can be found in the corresponding commit on GitHub .

    11. Clone nodes with Drag & Drop


    But in addition to creating nodes, using Drag & Drop, you can also organize copying nodes, and it’s very easy. If the user at the moment you start dragging the node holds Ctrl, we just need to initiate the beginning of Drag & Drop and give a description of the node. And the node creation code will do the rest of the work for us. To initiate Drag & Drop, we will add the following code to the left-click handler:
            if evt.ControlDown() and self._objectUnderCursor.clonable:
                text = self._objectUnderCursor.GetCloningNodeDescription()
                data = wx.TextDataObject(text)
                dropSource = wx.DropSource(self)
                dropSource.SetData(data)
                dropSource.DoDragDrop(wx.Drag_AllowMove)
    

    Here we create the source of Drag & Drop and give it the description that we received from the node. It remains to implement the GetCloningNodeDescription method on the node and everything will be ready. But first, we implement the interface:
    class ClonableObject(CanvasObject):
        def __init__(self, **kwargs):
            super(ClonableObject, self).__init__(**kwargs)
            self.clonable = True
        def GetCloningNodeDescription(self):
            """
            GetNodeDescription should return a dictionary that contains 
            all information required for cloning this node at another position
            """
            raise NotImplementedError()
    

    And now the implementation of the method for the node:
        def GetCloningNodeDescription(self):
            return self.text
    

    which just gives her text.
    The current version of the code lives here .

    12. Scalable nodes


    Well, before completing the fourth part, add the last little thing. Let's make the nodes scale to fit the text. To do this, we will slightly change the method of rendering a text node:
        def Render(self, gc):
            textDimensions = gc.GetTextExtent(self.text)
            self.boundingBoxDimensions = [textDimensions[0]+20, textDimensions[1]+20] 
            super(SimpleTextBoxNode, self).Render(gc)
            gc.DrawText(self.text, self.position[0]+10, self.position[1]+10)
    

    The GetTextExtent method in this case returns the size of the rectangle that the text occupies. Accordingly, before rendering a node, we update its dimensions so that it is 10 pixels larger than the text on each side. This is how this whole process now looks:


    The code lives in this commit on GitHub .

    PS: About typos write in a personal.

    Also popular now: