I'm running the following:
- Python 3.7.9 64-bit
- 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 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
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()