Home > Net >  How to drag and drop attachments from Outlook to a WxPython application
How to drag and drop attachments from Outlook to a WxPython application

Time:10-27

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://docs.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...

CodePudding user response:

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://docs.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://docs.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()
  • Related