Search code examples
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).

    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:

    • I don't know how Win divides the screen into tiles, so I don't know how to make the icon move at any iteration (but that's beyond the question scope, and it was similar in the old approach). I'm incrementing the X / Y coordinates by 16 for no particular reason

    • An item can be specified by its data (that is used in the underlying WinAPI implementation). But since that data (that gibberish string in the above output) makes no sense to the user, I added functionality to also use its index. If you want to use it by name, check this code (code02.py, "stolen" from [GitHub]: mhammond/pywin32 - (main) com/win32comext/shell/test/testShellFolder.py) out:

      #!/usr/bin/env python
      
      import sys
      
      from win32com.shell import shell, shellcon
      
      
      def main(*argv):
          sf = shell.SHGetDesktopFolder()
          print("Shell Folder is", sf, dir(sf))
          for i, e in enumerate(sf):
              print(f"Item: {i}\n  Name: {sf.GetDisplayNameOf(e, shellcon.SHGDN_NORMAL)}\n  Data: {e}")
      
          # And get the enumerator manually
          objs = sf.EnumObjects(0, shellcon.SHCONTF_FOLDERS | shellcon.SHCONTF_NONFOLDERS | shellcon.SHCONTF_INCLUDEHIDDEN)
          print(objs, dir(objs))
      
      
      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)
      
    • Check [SO]: How to change username of job in print queue using python & win32print (@CristiFati's answer) (at the end) for ways of moving forward with the above patch

    • Didn't try it on new Win 11 builds as I don't have it



    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)