5

I have an application with a ListView ('SysListView32') control, from which I would like to extract data. The control has 4 columns, only textual data.

I have been playing around the following lines (found online somewhere):

VALUE_LENGTH = 256
bufferlength_int=struct.pack('i', VALUE_LENGTH)
count = win32gui.SendMessage(TargetHwnd, commctrl.LVM_GETITEMCOUNT, 0, 0)
for ItemIndex in range(count):
    valuebuffer = array.array('c',bufferlength_int + " " * (VALUE_LENGTH - len(bufferlength_int)))
    ListItems = win32gui.SendMessage(TargetHwnd, commctrl.LVM_GETITEMTEXT, ItemIndex, valuebuffer)

[The above code may not be entirely executable, as I stripped it from irrelevant stuff. but the gist is certainly here.]

This seems to run ok but I must be doing something wrong - I get all sorts of mostly-zeroed data buffers in return, and none of the actual text contents I was looking for.

Any suggestions?

Thanks,
Yonatan

Yonatan
  • 1,187
  • 15
  • 33

2 Answers2

7

Well, it turns out I was wrong on several points there. However it is possible to do by allocating memory inside the target process, constructing the required struct (LVITEM) there, sending the message and reading back the result from the buffer allocated in said process.

For the sake of completeness, I attach a code example for reading SysListView32 items from a foreign process, given a window handle of the control.

from win32con import PAGE_READWRITE, MEM_COMMIT, MEM_RESERVE, MEM_RELEASE,\
    PROCESS_ALL_ACCESS
from commctrl import LVM_GETITEMTEXT, LVM_GETITEMCOUNT

import struct
import ctypes
import win32api
import win32gui

GetWindowThreadProcessId = ctypes.windll.user32.GetWindowThreadProcessId
VirtualAllocEx = ctypes.windll.kernel32.VirtualAllocEx
VirtualFreeEx = ctypes.windll.kernel32.VirtualFreeEx
OpenProcess = ctypes.windll.kernel32.OpenProcess
WriteProcessMemory = ctypes.windll.kernel32.WriteProcessMemory
ReadProcessMemory = ctypes.windll.kernel32.ReadProcessMemory
memcpy = ctypes.cdll.msvcrt.memcpy


def readListViewItems(hwnd, column_index=0):

    # Allocate virtual memory inside target process
    pid = ctypes.create_string_buffer(4)
    p_pid = ctypes.addressof(pid)
    GetWindowThreadProcessId(hwnd, p_pid) # process owning the given hwnd
    hProcHnd = OpenProcess(PROCESS_ALL_ACCESS, False, struct.unpack("i",pid)[0])
    pLVI = VirtualAllocEx(hProcHnd, 0, 4096, MEM_RESERVE|MEM_COMMIT, PAGE_READWRITE)
    pBuffer = VirtualAllocEx(hProcHnd, 0, 4096, MEM_RESERVE|MEM_COMMIT, PAGE_READWRITE)

    # Prepare an LVITEM record and write it to target process memory
    lvitem_str = struct.pack('iiiiiiiii', *[0,0,column_index,0,0,pBuffer,4096,0,0])
    lvitem_buffer = ctypes.create_string_buffer(lvitem_str)
    copied = ctypes.create_string_buffer(4)
    p_copied = ctypes.addressof(copied)
    WriteProcessMemory(hProcHnd, pLVI, ctypes.addressof(lvitem_buffer), ctypes.sizeof(lvitem_buffer), p_copied)

    # iterate items in the SysListView32 control
    num_items = win32gui.SendMessage(hwnd, LVM_GETITEMCOUNT)
    item_texts = []
    for item_index in range(num_items):
        win32gui.SendMessage(hwnd, LVM_GETITEMTEXT, item_index, pLVI)
        target_buff = ctypes.create_string_buffer(4096)
        ReadProcessMemory(hProcHnd, pBuffer, ctypes.addressof(target_buff), 4096, p_copied)
        item_texts.append(target_buff.value)

    VirtualFreeEx(hProcHnd, pBuffer, 0, MEM_RELEASE)
    VirtualFreeEx(hProcHnd, pLVI, 0, MEM_RELEASE)
    win32api.CloseHandle(hProcHnd)
    return item_texts
Yonatan
  • 1,187
  • 15
  • 33
  • 2
    Note: this only works on 32 bit Python, and only for 32 bit 'other' processes. – David Heffernan Dec 24 '10 at 22:19
  • 1
    @DavidHeffernan how to make it x64 compatible? – clumpter May 07 '13 at 14:49
  • How can this be made 64 bits compatible? I'm trying to get the selected items from desktop on 32 bits python on 64 bits windows. Or if it can't be done, why? – Edw590 Nov 01 '18 at 02:54
  • 1
    On 64-bit systems the `struct` is a different size (88 bytes vs. 60 on 32-bit systems), due to pointers being larger and empty space being added to ensure that those pointers start at 8-byte boundaries. You just need to tweak the `struct.pack` call accordingly. – Alex Peters Jun 15 '21 at 13:05
1

If the control is in the same process as your code, it should work. If it's in a different process (as "another application" suggests), then this doesn't work (or at least it shouldn't). Check the error codes, you should get something along the lines of "permission denied": Applications can't see into each others memory.

Aaron Digulla
  • 321,842
  • 108
  • 597
  • 820
  • I don't seem to be getting error codes, at least I don't think so. I understand that when supplying a 'global' pointer through SendMessage, the receiving application (running the ListView) should be able to copy there the requested information. I am unsure however if I'm supplying such a 'global' pointer, or if (and how) this can be done in Python. – Yonatan Dec 09 '09 at 09:38