如何将附件从 Outlook 拖放到 WxPython 应用程序

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

我是运行以下的人:

  1. Python 3.7.9 64 位
  2. wxpython 4.1.1 msw(凤凰)wxWidgets 3.1.5

我正在尝试编写一个可以接收从 Outlook 拖动的附件的应用程序。这些东西似乎确实没有充分记录,但经过大量研究和苦恼之后,据我所知:

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()

现在我的应用程序接受拖拽的 Outlook 附件并且我可以解析它们的名称,但是我如何获得实际的文件内容?我似乎从未收到任何使用“FileContents”格式的 DataObject:s...

在旅行中我发现了以下内容:

这让我发疯,每次我认为我正在接近解决方案时它却回避了我...

不,不可能使用普通的 wxPython 实现这一点。问题是 DataObject 的 wx:s 概念不同于 WIN32:s。在 WX 中,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 在请求数据时采用一个结构,除了格式之外还采用 index。从 Outlook 中拖放文件需要您提供索引以遍历被拖拽的文件及其内容,而 WX 无法提供此索引。

因此,我不得不编写自己的拖放功能。此实现是 Windows 特定的。另外,由于每个window只能调用一次RegisterDragDrop,这意味着此代码不兼容WX:s拖放:

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()