pythonwinapicompywin32

How to move desktop icons with python?


I am making a python program that moves the desktop icons using LVM_SETITEMPOSITION from winapi but I have a problem with commctrl.LVM_SETITEMPOSITION and it gives me an error 'int' object is not callable. Here is my code:

import win32gui
import commctrl
from time import sleep
from ctypes import wintypes

hd = wintypes.HWND

hd = win32gui.FindWindow("Progman", None)
hd = win32gui.FindWindowEx(hd, 0, "SHELLDLL_DefView", None)
hd = win32gui.FindWindowEx(hd, 0, "SysListView32", None)

i = 0
while i < 1000:
  commctrl.LVM_SETITEMPOSITION(hd, 0, i, i)
  i+100
  sleep(1)

Solution

  • The proper way (using COM) is way more painful than the shortcut I took (still at the end of the answer).

    Resources:

    I started "translating" code from the 1st URL. Everything went fine til the point of getting IFolderView, which is not wrapped by PyWin32's COM extensions, hence yielding:
    TypeError: There is no interface object registered that supports this IID.

    I tried to work around it by [SO]: Implementing a COM interface in Python (@SimonMourier's answer), but no success.

    So, I was only left with the option of adding IFolderView (partial) support myself. PR is [GitHub]: mhammond/pywin32 - IFolderView COM client (merged to main on 240220, and present in official builds since v307).

    After building the shell.pyd locally (containing the above changes), I was able to get the same behavior using the following script (which works fine).

    code01.py:

    #!/usr/bin/env python
    
    import msvcrt
    import sys
    import time
    
    import pythoncom
    import win32com.client as wcomcli
    from win32com.shell import shell, shellcon
    
    
    SWC_DESKTOP = 0x08
    SWFO_NEEDDISPATCH = 0x01
    
    CLSID_ShellWindows = "{9BA05972-F6A8-11CF-A442-00A0C90A8F39}"
    IID_IFolderView = "{CDE725B0-CCC9-4519-917E-325D72FAB4CE}"
    IID_IShellView = "{000214E3-0000-0000-C000-000000000046}"
    
    
    def main(*argv):
        shell_windows = wcomcli.Dispatch(CLSID_ShellWindows)
        hwnd = 0
        dispatch = shell_windows.FindWindowSW(
            wcomcli.VARIANT(pythoncom.VT_I4, shellcon.CSIDL_DESKTOP),
            wcomcli.VARIANT(pythoncom.VT_EMPTY, None),
            SWC_DESKTOP, hwnd, SWFO_NEEDDISPATCH,
        )
        service_provider = dispatch._oleobj_.QueryInterface(pythoncom.IID_IServiceProvider)
        browser = service_provider.QueryService(shell.SID_STopLevelBrowser, shell.IID_IShellBrowser)
        shell_view = browser.QueryActiveShellView()
        print(shell_view.GetCurrentInfo())
        folder_view = shell_view.QueryInterface(IID_IFolderView)
        #print(folder_view.QueryInterface(shell.IID_IShellView))
        items_len = folder_view.ItemCount(shellcon.SVGIO_ALLVIEW)
        print(f"ItemCount: {items_len}")
        for i in range(items_len):
            item = folder_view.Item(i)
            print(f"Item {i:2d}\n  {item}")
    
        print(f"Spacings: {folder_view.GetSpacing(300, 300)}, {folder_view.GetDefaultSpacing()}")
    
        item = b"\x1fx@\xf0_d\x81P\x1b\x10\x9f\x08\x00\xaa\x00/\x95N"  # "Recycle Bin" equivalent (in my case)
        #item = 0  # May also be the index (still "Recicle Bin" equivalent)
        #item = 1
    
        print(f"Item {item} position: {folder_view.GetItemPosition(item)}")
        for i in range(0, 1080, 16):
            print(i)
            folder_view.SelectAndPositionItem(item, (i, i), shellcon.SVSI_POSITIONITEM)
            time.sleep(0.5)
            if msvcrt.kbhit():
                break
    
    
    if __name__ == "__main__":
        print(
            "Python {:s} {:03d}bit on {:s}\n".format(
                " ".join(elem.strip() for elem in sys.version.split("\n")),
                64 if sys.maxsize > 0x100000000 else 32,
                sys.platform,
            )
        )
        rc = main(*sys.argv[1:])
        print("\nDone.\n")
        sys.exit(rc)
    

    Output:

    [cfati@CFATI-W10PC064:e:\Work\Dev\StackExchange\StackOverflow\q071905594]> "c:\Work\Dev\VEnvs\py_pc064_03.10_test0\Scripts\python.exe" ./code01.py
    Python 3.10.11 (tags/v3.10.11:7d4cc5a, Apr  5 2023, 00:38:17) [MSC v.1929 64 bit (AMD64)] 064bit on win32
    
    (1, 1075839524)
    ItemCount: 8
    Item  0
      b'\x1fx@\xf0_d\x81P\x1b\x10\x9f\x08\x00\xaa\x00/\x95N'
    Item  1
      b':\x00\r\x08\x00\x00NX\x95\xa4 \x00ADOBEA~2.LNK\x00\x00T\x00\t\x00\x04\x00\xef\xbemV\xa2\xbcRX\xe1h.\x00\x00\x00\xd8\x96\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00k\xc7(\x00A\x00d\x00o\x00b\x00e\x00 \x00A\x00c\x00r\x00o\x00b\x00a\x00t\x00.\x00l\x00n\x00k\x00\x00\x00\x1c\x00'
    Item  2
      b':\x00\xae\x00\x00\x00\x87O\x96I&\x00desktop.ini\x00H\x00\t\x00\x04\x00\xef\xbe\x87O\xdcITX.m.\x00\x00\x00{\xb2\x03\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x86)\xbf\x00d\x00e\x00s\x00k\x00t\x00o\x00p\x00.\x00i\x00n\x00i\x00\x00\x00\x1a\x00'
    Item  3
      b':\x00\xe4\x08\x00\x00TXtm \x00MICROS~1.LNK\x00\x00V\x00\t\x00\x04\x00\xef\xbesQ\xd1=TXtm.\x00\x00\x00s\xb8\x04\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00h\xf0Q\x00M\x00i\x00c\x00r\x00o\x00s\x00o\x00f\x00t\x00 \x00E\x00d\x00g\x00e\x00.\x00l\x00n\x00k\x00\x00\x00\x1c\x00'
    Item  4
      b'2\x00\x19\x04\x00\x00wSiN \x00CONNEC~1.LNK\x00\x00\x86\x00\t\x00\x04\x00\xef\xbewSiNRX\xe1h.\x00\x00\x00\xa7\x8d\x00\x00\x00\x00\x11\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00X\xe5>\x00C\x00o\x00n\x00n\x00e\x00c\x00t\x00i\x00o\x00n\x00 \x00t\x00o\x00 \x00Q\x00C\x00_\x00A\x00I\x00C\x001\x000\x000\x00_\x006\x004\x00.\x001\x008\x007\x00.\x002\x001\x003\x00.\x001\x009\x004\x00.\x00l\x00n\x00k\x00\x00\x00\x1c\x00'
    Item  5
      b'2\x00\x1a\x01\x00\x00\xafR+\xae&\x00desktop.ini\x00H\x00\t\x00\x04\x00\xef\xbe\xaeR\xeb\x01TX.m.\x00\x00\x00\xd9k\x01\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x95\xa35\x00d\x00e\x00s\x00k\x00t\x00o\x00p\x00.\x00i\x00n\x00i\x00\x00\x00\x1a\x00'
    Item  6
      b'2\x00\x9c\x04\x00\x00mV\x94\xb0 \x00TOTALC~1.LNK\x00\x00f\x00\t\x00\x04\x00\xef\xbemV\x94\xb0RX\xe1h.\x00\x00\x00\x9bo\x00\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x001\xbf\x01\x01T\x00o\x00t\x00a\x00l\x00 \x00C\x00o\x00m\x00m\x00a\x00n\x00d\x00e\x00r\x00 \x006\x004\x00 \x00b\x00i\x00t\x00.\x00l\x00n\x00k\x00\x00\x00\x1c\x00'
    Item  7
      b'2\x00\x8e\x04\x00\x00mV\x94\xb0 \x00TOTALC~2.LNK\x00\x00X\x00\t\x00\x04\x00\xef\xbemV\x94\xb0RX\xe1h.\x00\x00\x00\x9co\x00\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x001\xbf\x01\x01T\x00o\x00t\x00a\x00l\x00 \x00C\x00o\x00m\x00m\x00a\x00n\x00d\x00e\x00r\x00.\x00l\x00n\x00k\x00\x00\x00\x1c\x00'
    Spacings: (75, 101), (96, 16)
    Item b'\x1fx@\xf0_d\x81P\x1b\x10\x9f\x08\x00\xaa\x00/\x95N' position: (13, 2)
    0
    16
    32
    48
    
    Done.
    

    Notes:



    Original answer

    Sorry, I misread WORD (as DWORD) in [MS.Docs]: LVM_SETITEMPOSITION message, so I shifted too much to the left.
    Here's a working example (which should move the item on the leading diagonal on the desktop).

    code00.py:

    #!/usr/bin/env python
    
    import msvcrt
    import sys
    import time
    
    import commctrl as cc
    import win32gui as wgui
    
    
    def main(*argv):
        search_criteria = (
            (0, "Progman", None),
            (0, "SHELLDLL_DefView", None),
            (0, "SysListView32", None),
        )
        wnd = 0
        for crit in search_criteria:
            wnd = wgui.FindWindowEx(wnd, *crit)
            if wnd == 0:
                print("Could not find child matching criteria: {:}".format(crit))
                return
        idx = 0
        for i in range(0, 1000, 16):
            lparam = (i << 16) | i
            print("{:d} - 0x{:08X}".format(i, lparam))
            wgui.SendMessage(wnd, cc.LVM_SETITEMPOSITION, idx, lparam)
            time.sleep(0.5)
            if msvcrt.kbhit():
                break
    
    
    if __name__ == "__main__":
        print("Python {:s} {:03d}bit on {:s}\n".format(" ".join(elem.strip() for elem in sys.version.split("\n")),
                                                       64 if sys.maxsize > 0x100000000 else 32, sys.platform))
        rc = main(*sys.argv[1:])
        print("\nDone.")
        sys.exit(rc)