wxPython: Introduction to Drag and Drop

Most computer users of this day and age use drag and drop (DnD) instinctively. You probably used it to transfer some files from one folder to another this week! The wxPython GUI toolkit provides drag and drop functionality baked in. In this tutorial, we'll see just how easy it is to implement!

Getting Started

wxPython provides several different kinds of drag and drop. You can have one of the following types:

  • wx.FileDropTarget
  • wx.TextDropTarget
  • wx.PyDropTarget

The first two are pretty self-explanatory. The last one, wx.PyDropTarget, is just a loose wrapper around wx.DropTarget itself. It adds a couple extra convenience methods that the plain wx.DropTarget doesn't have. We'll start with a wx.FileDropTarget example.

Creating a FileDropTarget

Drag-n-Dropping files

The wxPython toolkit makes the creation of a drop target pretty simple. You do have to override a method to make it work right, but other than that, it's pretty straight forward. Let's take a moment to look over this example code and then we'll spend some time explaining it.

import wx

########################################################################
class MyFileDropTarget(wx.FileDropTarget):
    """"""

    #----------------------------------------------------------------------
    def __init__(self, window):
        """Constructor"""
        wx.FileDropTarget.__init__(self)
        self.window = window
        
    #----------------------------------------------------------------------
    def OnDropFiles(self, x, y, filenames):
        """
        When files are dropped, write where they were dropped and then
        the file paths themselves
        """
        self.window.SetInsertionPointEnd()
        self.window.updateText("\n%d file(s) dropped at %d,%d:\n" %
                              (len(filenames), x, y))
        for filepath in filenames:
            self.window.updateText(filepath + '\n')    

########################################################################
class DnDPanel(wx.Panel):
    """"""

    #----------------------------------------------------------------------
    def __init__(self, parent):
        """Constructor"""
        wx.Panel.__init__(self, parent=parent)
        
        file_drop_target = MyFileDropTarget(self)
        lbl = wx.StaticText(self, label="Drag some files here:")
        self.fileTextCtrl = wx.TextCtrl(self,
                                        style=wx.TE_MULTILINE|wx.HSCROLL|wx.TE_READONLY)
        self.fileTextCtrl.SetDropTarget(file_drop_target)
        
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(lbl, 0, wx.ALL, 5)
        sizer.Add(self.fileTextCtrl, 1, wx.EXPAND|wx.ALL, 5)
        self.SetSizer(sizer)
        
    #----------------------------------------------------------------------
    def SetInsertionPointEnd(self):
        """
        Put insertion point at end of text control to prevent overwriting
        """
        self.fileTextCtrl.SetInsertionPointEnd()
        
    #----------------------------------------------------------------------
    def updateText(self, text):
        """
        Write text to the text control
        """
        self.fileTextCtrl.WriteText(text)
    
########################################################################
class DnDFrame(wx.Frame):
    """"""

    #----------------------------------------------------------------------
    def __init__(self):
        """Constructor"""
        wx.Frame.__init__(self, parent=None, title="DnD Tutorial")
        panel = DnDPanel(self)
        self.Show()
    
#----------------------------------------------------------------------
if __name__ == "__main__":
    app = wx.App(False)
    frame = DnDFrame()
    app.MainLoop()

That wasn't too bad, was it? The first thing to do is to subclass wx.FileDropTarget, which we do with our MyFileDropTarget class. Inside that we have one overridden method, OnDropFiles. It accepts the x/y position of the mouse and the file paths that are dropped, then it writes those out to the text control. To hook up the drop target to the text control, you'll want to look in the DnDPanel class where we call the text control's SetDropTarget method and set it to an instance of our drop target class. We have two more methods in our panel class that the drop target class calls to update the text control: SetInsertionPointEnd and updateText. Note that since we are passing the panel object as the drop target, we can call these methods whatever we want to. If the TextCtrl had been the drop target, we'd have to do it differently, which we will see in our next example!

Creating a TextDropTarget

Drag and drop text

The wx.TextDropTarget is used when you want to be able to drag and drop some selected text into a text control. Probably one of the most common examples is dragging a URL on a web page up to the address bar or some text up into the search box in Firefox. Let's spend some time learning how to create one of these kinds of drop targets in wxPython!

import wx

########################################################################
class MyTextDropTarget(wx.TextDropTarget):
    
    #----------------------------------------------------------------------
    def __init__(self, textctrl):
        wx.TextDropTarget.__init__(self)
        self.textctrl = textctrl
        
    #----------------------------------------------------------------------
    def OnDropText(self, x, y, text):
        self.textctrl.WriteText("(%d, %d)\n%s\n" % (x, y, text))

    #----------------------------------------------------------------------
    def OnDragOver(self, x, y, d):
        return wx.DragCopy

########################################################################
class DnDPanel(wx.Panel):
    """"""

    #----------------------------------------------------------------------
    def __init__(self, parent):
        """Constructor"""
        wx.Panel.__init__(self, parent=parent)
        
        
        lbl = wx.StaticText(self, label="Drag some text here:")
        self.myTextCtrl = wx.TextCtrl(self,
                                      style=wx.TE_MULTILINE|wx.HSCROLL|wx.TE_READONLY)
        text_dt = MyTextDropTarget(self.myTextCtrl)
        self.myTextCtrl.SetDropTarget(text_dt)
        
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.myTextCtrl, 1, wx.EXPAND)
        self.SetSizer(sizer)
        
    #----------------------------------------------------------------------
    def WriteText(self, text):
        self.text.WriteText(text)
        
########################################################################
class DnDFrame(wx.Frame):
    """"""

    #----------------------------------------------------------------------
    def __init__(self):
        """Constructor"""
        wx.Frame.__init__(self, parent=None, title="DnD Text Tutorial")
        panel = DnDPanel(self)
        self.Show()
    
#----------------------------------------------------------------------
if __name__ == "__main__":
    app = wx.App(False)
    frame = DnDFrame()
    app.MainLoop()

Once again we have to subclass our drop target class. In this case, we call it MyTextDropTarget. In that class, we have to override OnDropText and OnDragOver. I was unable to find satisfactory documentation on the latter, but I'm guessing it just returns a copy of the data dragged. The OnDropText method writes text out to the text control. Note that since we've bound the drop target directly to the text control (see the panel class) we HAVE to use a method named WriteText to update the text control. If you change it, you'll receive an error message.

Custom DnD with PyDropTarget

Drag and drop URLs with PyDropTarget

In case you haven't guessed yet, these examples have been slightly modified versions of the DnD demos from the official wxPython demo. We'll be using some code based on their URLDragAndDrop demo to explain PyDropTarget. The fun bit about this demo is that you not only get to create a widget that can accept dragged text but you also can drag some text from another widget back to your browser! Let's take a look:

import  wx

########################################################################
class MyURLDropTarget(wx.PyDropTarget):
    
    #----------------------------------------------------------------------
    def __init__(self, window):
        wx.PyDropTarget.__init__(self)
        self.window = window

        self.data = wx.URLDataObject();
        self.SetDataObject(self.data)

    #----------------------------------------------------------------------
    def OnDragOver(self, x, y, d):
        return wx.DragLink

    #----------------------------------------------------------------------
    def OnData(self, x, y, d):
        if not self.GetData():
            return wx.DragNone

        url = self.data.GetURL()
        self.window.AppendText(url + "\n")

        return d
    
#######################################################################
class DnDPanel(wx.Panel):
    """"""

    #----------------------------------------------------------------------
    def __init__(self, parent):
        """Constructor"""
        wx.Panel.__init__(self, parent=parent)
        font = wx.Font(12, wx.SWISS, wx.NORMAL, wx.BOLD, False)
                
        # create and setup first set of widgets
        lbl = wx.StaticText(self, label="Drag some URLS from your browser here:")
        lbl.SetFont(font)
        self.dropText = wx.TextCtrl(self, size=(200,200),
                                      style=wx.TE_MULTILINE|wx.HSCROLL|wx.TE_READONLY)
        dt = MyURLDropTarget(self.dropText)
        self.dropText.SetDropTarget(dt)
        firstSizer = self.addWidgetsToSizer([lbl, self.dropText])
        
        # create and setup second set of widgets
        lbl = wx.StaticText(self, label="Drag this URL to your browser:")
        lbl.SetFont(font)
        self.draggableURLText = wx.TextCtrl(self, value="http://www.mousevspython.com")
        self.draggableURLText.Bind(wx.EVT_MOTION, self.OnStartDrag)
        secondSizer = self.addWidgetsToSizer([lbl, self.draggableURLText])
        
        # Add sizers to main sizer
        mainSizer = wx.BoxSizer(wx.VERTICAL)
        mainSizer.Add(firstSizer, 0, wx.EXPAND)
        mainSizer.Add(secondSizer, 0, wx.EXPAND)
        self.SetSizer(mainSizer)
        
    #----------------------------------------------------------------------
    def addWidgetsToSizer(self, widgets):
        """
        Returns a sizer full of widgets
        """
        sizer = wx.BoxSizer(wx.HORIZONTAL)
        for widget in widgets:
            if isinstance(widget, wx.TextCtrl):
                sizer.Add(widget, 1, wx.EXPAND|wx.ALL, 5)
            else:
                sizer.Add(widget, 0, wx.ALL, 5)
        return sizer
    
    #----------------------------------------------------------------------
    def OnStartDrag(self, evt):
        """"""
        if evt.Dragging():
            url = self.draggableURLText.GetValue()
            data = wx.URLDataObject()
            data.SetURL(url)

            dropSource = wx.DropSource(self.draggableURLText)
            dropSource.SetData(data)
            result = dropSource.DoDragDrop()
    
########################################################################
class DnDFrame(wx.Frame):
    """"""

    #----------------------------------------------------------------------
    def __init__(self):
        """Constructor"""
        wx.Frame.__init__(self, parent=None, title="DnD URL Tutorial", size=(800,600))
        panel = DnDPanel(self)
        self.Show()
    
#----------------------------------------------------------------------
if __name__ == "__main__":
    app = wx.App(False)
    frame = DnDFrame()
    app.MainLoop()

The first class is our drop target class. Here we create a wx.URLDataObject that stores our URL information. Then in the OnData method we extract the URL and append it to the bound text control. In our panel class, we hook up the drop target in the same way that we did in the other two examples, so we'll skip that and go on to the new stuff. The second text control is where we need to pay attention. Here we find it to mouse movement via EVT_MOTION. In the mouse movement event handler (OnStartDrag), we check to make sure that the user is dragging. If so, then we grab the value from the text box and add it to a newly created URLDataObject. Next we create an instance of a DropSource and pass it our second text control since it IS the source. We set the source's data to the URLDataObject. Finally we call DoDragDrop on our drop source (the text control) which will respond by moving, copying, canceling or failing. If you dragged the URL to your browser's address bar, it will copy. Otherwise it probably won't work. Now let's take what we've learned and create something original!

Creating A Custom Drag-and-Drop App

Drag and Drop with ObjectListView

I thought it would be fun to take the file drop target demo and make it into something with an ObjectListView widget (a ListCtrl wrapper) that can tell us some information about the files we're dropping into it. We'll be showing the following information: file name, creation date, modified date and file size. Here's the code:

import os
import stat
import time
import wx
from ObjectListView import ObjectListView, ColumnDefn

########################################################################
class MyFileDropTarget(wx.FileDropTarget):
    """"""

    #----------------------------------------------------------------------
    def __init__(self, window):
        """Constructor"""
        wx.FileDropTarget.__init__(self)
        self.window = window
        
    #----------------------------------------------------------------------
    def OnDropFiles(self, x, y, filenames):
        """
        When files are dropped, update the display
        """
        self.window.updateDisplay(filenames)

########################################################################
class FileInfo(object):
    """"""

    #----------------------------------------------------------------------
    def __init__(self, path, date_created, date_modified, size):
        """Constructor"""
        self.name = os.path.basename(path)
        self.path = path
        self.date_created = date_created
        self.date_modified = date_modified
        self.size = size
       
########################################################################
class MainPanel(wx.Panel):
    """"""

    #----------------------------------------------------------------------
    def __init__(self, parent):
        """Constructor"""
        wx.Panel.__init__(self, parent=parent)
        self.file_list = []
        
        file_drop_target = MyFileDropTarget(self)
        self.olv = ObjectListView(self, style=wx.LC_REPORT|wx.SUNKEN_BORDER)
        self.olv.SetDropTarget(file_drop_target)
        self.setFiles()
        
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.olv, 1, wx.EXPAND)
        self.SetSizer(sizer)
        
    #----------------------------------------------------------------------
    def updateDisplay(self, file_list):
        """"""
        for path in file_list:
            file_stats = os.stat(path)
            creation_time = time.strftime("%m/%d/%Y %I:%M %p",
                                          time.localtime(file_stats[stat.ST_CTIME]))
            modified_time = time.strftime("%m/%d/%Y %I:%M %p",
                                          time.localtime(file_stats[stat.ST_MTIME]))
            file_size = file_stats[stat.ST_SIZE]
            if file_size > 1024:
                file_size = file_size / 1024.0
                file_size = "%.2f KB" % file_size
            
            self.file_list.append(FileInfo(path,
                                           creation_time,
                                           modified_time,
                                           file_size))
        
        self.olv.SetObjects(self.file_list)
    
    #----------------------------------------------------------------------
    def setFiles(self):
        """"""
        self.olv.SetColumns([
            ColumnDefn("Name", "left", 220, "name"),
            ColumnDefn("Date created", "left", 150, "date_created"),
            ColumnDefn("Date modified", "left", 150, "date_modified"),
            ColumnDefn("Size", "left", 100, "size")
            ])
        self.olv.SetObjects(self.file_list)
        
########################################################################
class MainFrame(wx.Frame):
    """"""

    #----------------------------------------------------------------------
    def __init__(self):
        """Constructor"""
        wx.Frame.__init__(self, None, title="OLV DnD Tutorial", size=(800,600))
        panel = MainPanel(self)
        self.Show()
        
#----------------------------------------------------------------------
def main():
    """"""
    app = wx.App(False)
    frame = MainFrame()
    app.MainLoop()
    
if __name__ == "__main__":
    main()    

Most of this stuff you've seen before. We have our FileDropTarget subclass, we connect the panel to it and then the ObjectListView widget to the drop target instance. We also have a generic class for holding our file-related data. If you run this program and drop folders into it, you won't receive the correct file size. You would probably need to walk the folder and add up the sizes of the files therein to get that to work. Feel free to fix that on your own. Anyway, the meat of the program is in the updateDisplay method. Here we grab the file's vital statistics and convert them into more readable formats as most people don't understand dates that are in seconds since the epoch. Once we've massaged the data a bit, we display it. Now wasn't that pretty cool?

Wrapping Up

At this point, you should now know how to do at least 3 different types of drag and drop in wxPython. Hopefully you will use this new information responsibly and create some fresh open source applications in the near future. Good luck!

Further Reading

Source Code

Copyright © 2024 Mouse Vs Python | Powered by Pythonlibrary