如何将附件从 Outlook 拖放到 WxPython 应用程序
How to drag and drop attachments from Outlook to a WxPython application
我是运行以下的人:
- Python 3.7.9 64 位
- 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...
在旅行中我发现了以下内容:
- This site describes the types of Windows Clipboard formats and their corresponding names
- This questions which discusses how to accomplish this in C++
- This MSDN site which descries the layout of the FILEGROUPDESCRIPTORA struct
这让我发疯,每次我认为我正在接近解决方案时它却回避了我...
不,不可能使用普通的 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()
我是运行以下的人:
- Python 3.7.9 64 位
- 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...
在旅行中我发现了以下内容:
- This site describes the types of Windows Clipboard formats and their corresponding names
- This questions which discusses how to accomplish this in C++
- This MSDN site which descries the layout of the FILEGROUPDESCRIPTORA struct
这让我发疯,每次我认为我正在接近解决方案时它却回避了我...
不,不可能使用普通的 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()