4

Background: I'm coding in Python and trying to use the win32 IFileOperation to perform shell file operations. In particular, moving/copying a file or directory to a non-existant location requires you to get a IShellItem (or IShellItem2) item referring to the target directory, but SHCreateItemFromParsingName fails if the target doesn't exist yet. Following advice from these related questions:

I've handled creating the WIN32_FIND_DATAW structure, created the IBindCtx object via pywin32's pythoncom.CreateBindCtx(). The step I'm stuck on is implementing a IFileSysBindData class in Python.

Here's what I've got so far:

class FileSysBindData:
    _public_methods_ = ['SetFindData', 'GetFindData']
    _reg_clsid_ = '{01E18D10-4D8B-11d2-855D-006008059367}'

    def SetFindData(self, win32_find_data):
        self.win32_find_data = win32_find_data
        return 0

    def GetFindData(self, p_win32_find_data):
        p_win32_find_data.contents = self.win32_find_data
        return 0

bind_data = FileSysBindData()
ibindctx = pythoncom.CreateBindCtx()
# TODO: set the bind data
ibindctx.RegisterObjectParam('File System Bind Data', bind_data)

The last line gives:

ValueError: argument is not a COM object (got type=FileSysBindData)

So obviously there's more to do to get pywin32 to create this in a COM-compatible way.

But I'm stuck on how to use pywin32 (or another solution) to actually create it as a COM object, so I can pass it to the IBindCtx instance.

I'm not super familiar with COM in general, so reading the docs in pywin32 haven't helped me much. I don't even know if I'm supposed to register my class as a server, or use it as a client somehow.

2
  • You don't have to register anything. Note the two methods use a pointer, not just the getter. Do you have a more complete python code with all functions involved to easily reproduce what you're trying to do? Commented Jan 19, 2023 at 6:56
  • Here's the minimal version of the code I'm working on at the moment: gist I can work out the call args later (it'll just be a matter of passing by reference in most cases), really the SetFindData is only there to make sure the VTable comes out correct on the class. The real problem is I need to create this class in a COM-compatible way, which pywin32 purports to be able to do, I just don't know how to do that. Commented Jan 19, 2023 at 20:05

1 Answer 1

4

The difficulty is you have to use two python packages:

  • comtypes to declare and implement custom COM objects
  • pywin32 because it has lots of already baked interop types, interfaces, classes, etc.

Here is the code, and you'll see the trick to pass a comtypes's custom COM object to pywin32:

import pythoncom
import ctypes
from comtypes.hresult import *
from comtypes import IUnknown, GUID, COMMETHOD, COMObject, HRESULT
from ctypes.wintypes import *
from ctypes import *
from win32com.shell import shell
from os import fspath

# ripped from
# <python install path>\Lib\site-packages\comtypes\test\test_win32com_interop.py
# We use the PyCom_PyObjectFromIUnknown function in pythoncomxxx.dll to
# convert a comtypes COM pointer into a pythoncom COM pointer.
# This is the C prototype; we must pass 'True' as third argument:
# PyObject *PyCom_PyObjectFromIUnknown(IUnknown *punk, REFIID riid, BOOL bAddRef)
_PyCom_PyObjectFromIUnknown = PyDLL(
    pythoncom.__file__).PyCom_PyObjectFromIUnknown
_PyCom_PyObjectFromIUnknown.restype = py_object
_PyCom_PyObjectFromIUnknown.argtypes = (POINTER(IUnknown), c_void_p, BOOL)


def comtypes2pywin(ptr, interface=None):
    """Convert a comtypes pointer 'ptr' into a pythoncom PyI<interface> object.
    'interface' specifies the interface we want; it must be a comtypes
    interface class.  The interface must be implemented by the object and
    the interface must be known to pythoncom.
    If 'interface' is not specified, comtypes.IUnknown is used.
    """
    if interface is None:
        interface = IUnknown
    return _PyCom_PyObjectFromIUnknown(ptr, byref(interface._iid_), True)


class IFileSystemBindData(IUnknown):
    """The IFileSystemBindData interface
     https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nn-shobjidl_core-ifilesystembinddata"""
    _iid_ = GUID('{01e18d10-4d8b-11d2-855d-006008059367}')
    _methods_ = [
        COMMETHOD([], HRESULT, 'SetFindData',
                  (['in'], POINTER(WIN32_FIND_DATAW), 'pfd')),
        COMMETHOD([], HRESULT, 'GetFindData',
                  (['out'], POINTER(WIN32_FIND_DATAW), 'pfd'))
    ]


class FileSystemBindData(COMObject):
    """Implements the IFileSystemBindData interface:
     https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nn-shobjidl_core-ifilesystembinddata"""
    _com_interfaces_ = [IFileSystemBindData]

    def IFileSystemBindData_SetFindData(self, this, pfd):
        self.pfd = pfd
        return S_OK

    def IFileSystemBindData_GetFindData(self, this, pfd):
        pfd = self.pfd
        return S_OK


find_data = WIN32_FIND_DATAW()  # from wintypes
bind_data = FileSystemBindData()

# to avoid this long thing, we could declare a shorter helper on
# FileSystemBindData
bind_data.IFileSystemBindData_SetFindData(bind_data, ctypes.byref(find_data))
ctx = pythoncom.CreateBindCtx()

# we need this conversion to avoid
# "ValueError: argument is not a COM object (got type=FileSystemBindData)"
# error from pythoncom
pydata = comtypes2pywin(bind_data)
ctx.RegisterObjectParam('File System Bind Data', pydata)

item = shell.SHCreateItemFromParsingName(
    fspath("z:\\blah\\blah"), ctx, shell.IID_IShellItem2)

SIGDN_DESKTOPABSOLUTEPARSING = 0x80028000
print(item.GetDisplayName(SIGDN_DESKTOPABSOLUTEPARSING))  # prints Z:\blah\blah
Sign up to request clarification or add additional context in comments.

1 Comment

Thank you! That is indeed a bit of info that is burried deep in there on how to do this. I've adapted this to my needs, but the specifics of how to create my COM object is exactly what I needed. Unfortunate that I have to use both python COM libraries, but much better than trying to roll my own in ctypes.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.