Search code examples
pythonwinapiwxpythonpython-3.7

How to drag and drop attachments from Outlook to a WxPython application


I'm running the following:

  1. Python 3.7.9 64-bit
  2. wxpython 4.1.1 msw (phoenix) wxWidgets 3.1.5

I'm trying to write an app which can receive attachments dragged from Outlook. This stuff seems to be really underdocumented, but after much research and anguish, this is as far as I got:

import struct
import wx

class MainFrame(wx.Frame):
    def __init__(self, *args, **kwargs):
        wx.Frame.__init__(self, *args, **kwargs)

        self.drop_target = MyDropTarget()

        self.SetSize((800, 600))
        self.SetDropTarget(self.drop_target)

class MyDropTarget(wx.DropTarget):
    def __init__(self):
        wx.DropTarget.__init__(self)

        self.fileContentsDataFormat = wx.DataFormat("FileContents")
        self.fileGroupDataFormat = wx.DataFormat("FileGroupDescriptor")
        self.fileGroupWDataFormat = wx.DataFormat("FileGroupDescriptorW")

        self.composite = wx.DataObjectComposite()
        self.fileContentsDropData = wx.CustomDataObject(format=self.fileContentsDataFormat)
        self.fileGroupDropData = wx.CustomDataObject(format=self.fileGroupDataFormat)
        self.fileGroupWDropData = wx.CustomDataObject(format=self.fileGroupWDataFormat)

        self.composite.Add(self.fileContentsDropData, preferred=True)
        self.composite.Add(self.fileGroupDropData)
        self.composite.Add(self.fileGroupWDropData)

        self.SetDataObject(self.composite)

    def OnDrop(self, x, y):
        return True

    def OnData(self, x, y, result):
        self.GetData()

        format = self.composite.GetReceivedFormat()
        data_object = self.composite.GetObject(format, wx.DataObject.Get)

        if format in [self.fileGroupDataFormat, self.fileGroupWDataFormat]:
            # See:
            #   https://learn.microsoft.com/en-us/windows/win32/api/shlobj_core/ns-shlobj_core-filedescriptora
            filenames = []
            data = data_object.GetData()
            count = struct.unpack("i", data[:4])
            fmt = "i16s8s8si8s8s8sii260s"
            for unpacked in struct.iter_unpack(fmt, data[4:]):
                filename = ""
                for b in unpacked[10]:
                    if b:
                        filename += chr(b)
                    else:
                        break
                filenames.append(filename)
                print(filenames)
        return result

app = wx.App(redirect=False)
frame = MainFrame(None)
frame.Show()
app.MainLoop()

So now my application accepts dragged Outlook attachments and I can parse their names, but how do I get at the actual file contents? I never seem to receive any DataObject:s using the "FileContents" format...

During my travels I found the following:

This is driving me insane, everytime I think I'm closing in on a solution it evades me...


Solution

  • No, it is not possible to achieve this using plain wxPython. The problem is that wx:s concept of a DataObject differs from WIN32:s. In WX, a DataObject has a list of all the formats it supports. Each format is assumed to correspond to a single piece of data. In WIN32, a DataObject takes a struct when requesting data which, in addition to the format also takes an index. Dragging and dropping files from Outlook requires you to provide the index to iterate over the dragged files and their contents and there is no way to provide this index to WX.

    Thus, I had to write my own drag and drop functionality. This implementation is Windows-specific. Also, since RegisterDragDrop can only be called once for each window, it means that this code is not compatible with WX:s drag and drop:

    import struct
    
    import pythoncom
    import winerror
    import win32con
    import win32com
    import win32api
    import win32clipboard
    import win32com.server.policy
    from win32com.shell import shell, shellcon
    
    import wx
    
    # See:
    #   http://timgolden.me.uk/pywin32-docs/PyFORMATETC.html
    fmt_filegroupdescriptor = win32clipboard.RegisterClipboardFormat("FileGroupDescriptorW")
    fmt_filegroupdescriptorw = win32clipboard.RegisterClipboardFormat("FileGroupDescriptorW")
    fmt_filecontents = win32clipboard.RegisterClipboardFormat("FileContents")
    
    fmts = [
        fmt_filegroupdescriptorw,
        fmt_filegroupdescriptor,
    ]
    
    class MainFrame(wx.Frame):
    
        def __init__(self, *args, **kwargs):
            wx.Frame.__init__(self, *args, **kwargs)
    
            self.SetSize((800, 600))
    
            self.hwnd = self.GetHandle()
            self.drop_target = DropTarget(self.hwnd)
    
            wx.CallAfter(self.After)
    
        def After(self):
            pass
    
    # For info on setting up COM objects in Python, see:
    #   https://mail.python.org/pipermail/python-win32/2008-April/007410.html
    #
    #   http://www.catch22.net/tuts/win32/drag-and-drop-introduction
    #   https://learn.microsoft.com/en-us/windows/win32/shell/datascenarios#copying-the-contents-of-a-dropped-file-into-an-application
    #
    # For clipboard format names under WIN32, see:
    #   https://www.codeproject.com/Reference/1091137/Windows-Clipboard-Formats
    #
    # Dragging and dropping from Outlook is a "Shell Clipboard" DataObject. The formats
    # and instructions on how to query are here:
    #   https://learn.microsoft.com/en-us/windows/win32/shell/clipboard
    class DropTarget(win32com.server.policy.DesignatedWrapPolicy):
        _reg_clsid_ = '{495E9ABE-5337-4AD5-8948-DF3B17D97FBC}'
        _reg_progid_ = "Test.DropTarget"
        _reg_desc_ = "Test for DropTarget"
        _public_methods_ = ["DragEnter", "DragLeave", "DragOver", "Drop"]
        _com_interfaces_ = [pythoncom.IID_IDropTarget]
        
        def __init__(self, hwnd):
            self._wrap_(self)
            self.hWnd = hwnd
    
            pythoncom.RegisterDragDrop(
                hwnd,
                pythoncom.WrapObject(
                    self,
                    pythoncom.IID_IDropTarget,
                    pythoncom.IID_IDropTarget
                )
            )
    
        def DragEnter(self, data_object, key_state, point, effect):
            # print(data_object, key_state, point, effect)
            return shellcon.DROPEFFECT_COPY
    
        def DragOver(self, key_state, point, effect):
            # print(key_state, point, effect)
            return shellcon.DROPEFFECT_COPY
    
        def DragLeave(self):
            pass
    
        def Drop(self, data_object, key_state, point, effect):
            print(data_object)
    
            self.EnumFormats(data_object)
            print("")
    
            fmts = [
                (win32con.CF_HDROP,        self.OnDropFileNames),
                (fmt_filegroupdescriptorw, self.OnDropFileGroupDescriptor),
                (fmt_filegroupdescriptor,  self.OnDropFileGroupDescriptor),
            ]
    
            for fmt, callback in fmts:
                try:
                    formatetc = (fmt, None, 1, -1, pythoncom.TYMED_HGLOBAL)
                    ret = data_object.QueryGetData(formatetc)
                    if not ret:
                        callback(data_object, fmt)
                        break
                except Exception as e:
                    pass
    
            return effect
    
        def EnumFormats(self, data_object):
            for enum in data_object.EnumFormatEtc(pythoncom.DATADIR_GET):
                try:
                    fmt = enum[0]
                    name = win32clipboard.GetClipboardFormatName(fmt)
                    print("GET", name, enum)
                except Exception as e:
                    print(e, enum)
    
        def OnDropFileNames(self, data_object, fmt):
            formatetc = (win32con.CF_HDROP, None, 1, -1, pythoncom.TYMED_HGLOBAL)
            stgmedium = data_object.GetData(formatetc)
    
            data = stgmedium.data
    
            dropfiles_fmt = "I2lii"
            dropfiles_fmt_size = struct.calcsize(dropfiles_fmt)
            (offset, px, py, area_flag, is_unicode) = struct.unpack(dropfiles_fmt, data[0:dropfiles_fmt_size])
    
            charsize = 2 if is_unicode else 1
    
            data = data[dropfiles_fmt_size:]
            index = 0
            while True:
                data = data[index:]
                index, string = self.UnpackString(data, charsize)
                print(f"string: {string}")
                if not string:
                    break
    
        def UnpackString(self, data, charsize):
            i = 0
            while True:
                if any(data[i*charsize:i*charsize + charsize]):
                    i += 1
                else:
                    break
    
            text = ""
            if i:
                if charsize == 1:
                    text = data[:i*charsize].decode("ascii")
                elif charsize == 2:
                    text = data[:i*charsize].decode("utf-16")
    
            return (i+1)*charsize, text
    
        def OnDropFileGroupDescriptor(self, data_object, fmt):
            filenames = self.UnpackGroupFileDescriptor(data_object, fmt)
            for index, filename in enumerate(filenames):
                # See:
                #   http://timgolden.me.uk/pywin32-docs/PyIStream.html
                formatetc_contents = (fmt_filecontents,  None, 1, index, pythoncom.TYMED_ISTREAM)
                stgmedium_stream = data_object.GetData(formatetc_contents)
                stream = stgmedium_stream.data
    
                stat = stream.Stat()
                data_size = stat[2]
                data = stream.Read(data_size)
    
                print(index, filename, len(data))
    
        def UnpackGroupFileDescriptor(self, data_object, fmt):
            formatetc = (fmt, None, 1, -1, pythoncom.TYMED_HGLOBAL)
            stgmedium = data_object.GetData(formatetc)
            data = stgmedium.data
            filenames = []
            count = struct.unpack("i", data[:4])
            if fmt == fmt_filegroupdescriptorw:
                charsize = 2 
                struct_fmt = "i16s8s8si8s8s8sii520s"
            else:
                charsize = 1
                struct_fmt = "i16s8s8si8s8s8sii260s"
    
            for unpacked in struct.iter_unpack(struct_fmt, data[4:]):
                filename = self.UnpackString(unpacked[10], charsize)
                filenames.append(filename)
    
            return filenames
    
    if __name__ == "__main__":
    
        pythoncom.OleInitialize()
    
        app = wx.App(redirect=False)
        frame = MainFrame(None)
        frame.Show()
        app.MainLoop()