pythonwinapictypes

Setting window properties via SHGetPropertyStoreForWindow using only ctypes


Python developers need to set certain window properties for their applications to work correctly with the Windows taskbar (correct icon and grouping, and the ability to pin). In my case a PyQt5 application.

Its possible to use pywin32 to access the property store like so:

from win32com.propsys import propsys, pscon
import pythoncom
def setWindowProperties(hwnd, app_id, display_name, relaunch_path):
    propStore = propsys.SHGetPropertyStoreForWindow(hwnd, propsys.IID_IPropertyStore)
    propStore.SetValue(pscon.PKEY_AppUserModel_ID, propsys.PROPVARIANTType(app_id, pythoncom.VT_ILLEGAL))
    propStore.SetValue(pscon.PKEY_AppUserModel_RelaunchDisplayNameResource, propsys.PROPVARIANTType(display_name, pythoncom.VT_ILLEGAL))
    propStore.SetValue(pscon.PKEY_AppUserModel_RelaunchCommand, propsys.PROPVARIANTType(relaunch_path, pythoncom.VT_ILLEGAL))
    propStore.Commit()

hwnd = ... # Window handle (for Qt see QWindow::winId() -> int(engine.rootObjects()[0].winId()))
app_id = "CompanyName.ProductName.Version"
display_name = "ProductName"
relaunch_path = "C:\\path\\to\\exe"

setWindowProperties(hwnd, app_id, display_name, relaunch_path)

This theoretically can be done with only ctypes to avoid a dependency on pywin32, but information about implementing this is scarce.


Solution

  • import ctypes
    from ctypes import wintypes
    
    GUID = ctypes.c_ubyte * 16
    
    class PROPERTYKEY(ctypes.Structure):
        _fields_ = [("fmtid", GUID),
                    ("pid", wintypes.DWORD)]
    
    class PROPVARIANT(ctypes.Structure):
        _fields_ = [("vt", wintypes.USHORT),
                    ("wReserved1", wintypes.USHORT),
                    ("wReserved2", wintypes.USHORT),
                    ("wReserved3", wintypes.USHORT),
                    ("pszVal", wintypes.LPWSTR)]
    
    class IPropertyStoreVtbl(ctypes.Structure):
        _fields_ = [
            ('QueryInterface', ctypes.CFUNCTYPE(ctypes.HRESULT, ctypes.c_void_p, ctypes.POINTER(GUID), ctypes.POINTER(ctypes.c_void_p))),
            ('AddRef', ctypes.CFUNCTYPE(ctypes.c_ulong, ctypes.c_void_p)),
            ('Release', ctypes.CFUNCTYPE(ctypes.c_ulong, ctypes.c_void_p)),
            ('GetCount', ctypes.CFUNCTYPE(ctypes.HRESULT, ctypes.c_void_p, ctypes.POINTER(ctypes.c_ulong))),
            ('GetAt', ctypes.CFUNCTYPE(ctypes.HRESULT, ctypes.c_void_p, ctypes.c_ulong, ctypes.POINTER(PROPERTYKEY))),
            ('GetValue', ctypes.CFUNCTYPE(ctypes.HRESULT, ctypes.c_void_p, ctypes.POINTER(PROPERTYKEY), ctypes.POINTER(PROPVARIANT))),
            ('SetValue', ctypes.CFUNCTYPE(ctypes.HRESULT, ctypes.c_void_p, ctypes.POINTER(PROPERTYKEY), ctypes.POINTER(PROPVARIANT))),
            ('Commit', ctypes.CFUNCTYPE(ctypes.HRESULT, ctypes.c_void_p))
        ]
    
    class IPropertyStore(ctypes.Structure):
        _fields_ = [('lpVtbl', ctypes.POINTER(IPropertyStoreVtbl))]
    
    # {886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99}
    IID_IPropertyStore = (GUID)(*bytearray.fromhex("eb8e6d88f28c46448d02cdba1dbdcf99"))
    # {9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3}
    PKEY_AppUserModel = (GUID)(*bytearray.fromhex("55284c9f799f394ba8d0e1d42de1d5f3"))
    
    ctypes.windll.ole32.CoInitialize.restype = ctypes.HRESULT
    ctypes.windll.ole32.CoInitialize.argtypes = [ctypes.c_void_p]
    ctypes.windll.ole32.CoUninitialize.restype = None
    ctypes.windll.ole32.CoUninitialize.argtypes = None
    ctypes.windll.shell32.SHGetPropertyStoreForWindow.restype = ctypes.HRESULT
    ctypes.windll.shell32.SHGetPropertyStoreForWindow.argtypes = [wintypes.HWND, ctypes.POINTER(GUID), ctypes.POINTER(ctypes.POINTER(IPropertyStore))]
    
    def setWindowProperties(hwnd, app_id, display_name, relaunch_path):
        ctypes.windll.ole32.CoInitialize(None)
    
        prop_store = ctypes.POINTER(IPropertyStore)()
        result = ctypes.windll.shell32.SHGetPropertyStoreForWindow(int(hwnd), IID_IPropertyStore, ctypes.pointer(prop_store))
        if result != 0:
            return False
        functions = prop_store.contents.lpVtbl.contents
        
        success = False
        # PID of PKEY_AppUserModel_ID is 5, etc
        values = (5, app_id), (4, display_name), (2, relaunch_path)
        for pid, value in values:
            prop_key = PROPERTYKEY()
            prop_key.fmtid = PKEY_AppUserModel
            prop_key.pid = pid
    
            prop_variant = PROPVARIANT()
            prop_variant.vt = 31 # VT_LPWSTR
            prop_variant.pszVal = value
    
            result = functions.SetValue(prop_store, prop_key, prop_variant)
            if result != 0:
                break
        else:
            success = True
        
        if success:
            functions.Commit(prop_store)
            
        functions.Release(prop_store)
        ctypes.windll.ole32.CoUninitialize()
        return success
    
    hwnd = ...
    app_id = "CompanyName.ProductName.Version"
    display_name = "ProductName"
    relaunch_path = "C:\\path\\to\\exe"
    
    setWindowProperties(hwnd, app_id, display_name, relaunch_path)