wxPython: How to drag and drop a file from your app to the OS

Today on StackOverflow I saw someone who wanted to know how to drag a file from a wx.ListCtrl onto their Desktop or somewhere else in the file system. They were using the file manager skeleton from zetcode, but couldn’t figure out how to add the DnD portion. After a bit of searching and hacking, I came up with this based on something Robin Dunn mentioned in a forum.

import wx
import os
import time

########################################################################
class MyListCtrl(wx.ListCtrl):
    
    #----------------------------------------------------------------------
    def __init__(self, parent, id):
        wx.ListCtrl.__init__(self, parent, id, style=wx.LC_REPORT)

        files = os.listdir('.')
        
        self.InsertColumn(0, 'Name')
        self.InsertColumn(1, 'Ext')
        self.InsertColumn(2, 'Size', wx.LIST_FORMAT_RIGHT)
        self.InsertColumn(3, 'Modified')

        self.SetColumnWidth(0, 220)
        self.SetColumnWidth(1, 70)
        self.SetColumnWidth(2, 100)
        self.SetColumnWidth(3, 420)

        j = 0
        for i in files:
            (name, ext) = os.path.splitext(i)
            ex = ext[1:]
            size = os.path.getsize(i)
            sec = os.path.getmtime(i)
            self.InsertStringItem(j, "%s%s" % (name, ext))
            self.SetStringItem(j, 1, ex)
            self.SetStringItem(j, 2, str(size) + ' B')
            self.SetStringItem(j, 3, time.strftime('%Y-%m-%d %H:%M', 
                                                   time.localtime(sec)))

            if os.path.isdir(i):
                self.SetItemImage(j, 1)
            elif ex == 'py':
                self.SetItemImage(j, 2)
            elif ex == 'jpg':
                self.SetItemImage(j, 3)
            elif ex == 'pdf':
                self.SetItemImage(j, 4)
            else:
                self.SetItemImage(j, 0)

            if (j % 2) == 0:
                self.SetItemBackgroundColour(j, '#e6f1f5')
            j = j + 1

########################################################################
class FileHunter(wx.Frame):
    #----------------------------------------------------------------------
    def __init__(self, parent, id, title):
        wx.Frame.__init__(self, parent, -1, title)
        panel = wx.Panel(self)

        p1 = MyListCtrl(panel, -1)
        p1.Bind(wx.EVT_LIST_BEGIN_DRAG, self.onDrag)
        sizer = wx.BoxSizer()
        sizer.Add(p1, 1, wx.EXPAND)
        panel.SetSizer(sizer)
        
        self.Center()
        self.Show(True)
        
    #----------------------------------------------------------------------
    def onDrag(self, event):
        """"""
        data = wx.FileDataObject()
        obj = event.GetEventObject()
        id = event.GetIndex()
        filename = obj.GetItem(id).GetText()
        dirname = os.path.dirname(os.path.abspath(os.listdir(".")[0]))
        fullpath = str(os.path.join(dirname, filename))
        
        data.AddFile(fullpath)
        
        dropSource = wx.DropSource(obj)
        dropSource.SetData(data)
        result = dropSource.DoDragDrop()
        print fullpath
        
#----------------------------------------------------------------------
app = wx.App(False)
FileHunter(None, -1, 'File Hunter')
app.MainLoop()

There are a couple of important points here. First off, you need to bind to EVT_LIST_BEGIN_DRAG to catch the appropriate event. Then in your handler you need to create a wx.FileDataObject object and use its AddFile method to append a full path to its internal file list. According to the wxPython documentation, AddFile is Windows-only, however since Robin Dunn (creator of wxPython) recommends this method, I went with it. It may be that the documentation is wrong. Anyway, we also need to define the DropSource and call its DoDragDrop method and you’re done. This code worked for me as-is on Windows 7, Python 2.6.6 and wxPython 2.8.12.1.