Search code examples
pythonwindowsctypessystem-tray

How to get system tray information in python?


How to traverse the system tray and get the icon information in it? Such as the icon, the process that created it...

I found a page that meets my requirements, I rewrote its code in Python using ctypes properly. The Original code is based on Windows 10.

import ctypes
from ctypes import wintypes

# 定义常量
PROCESS_VM_OPERATION = 0x0008
PROCESS_VM_READ = 0x0010
PROCESS_VM_WRITE = 0x0020
MEM_COMMIT = 0x00001000
PAGE_EXECUTE_READWRITE = 0x40
MEM_RELEASE = 0x00008000
TB_BUTTONCOUNT = 0x0400 + 24
TB_GETBUTTON = 0x0417

# 定义结构体
class SYSTEM_INFO(ctypes.Structure):
    _fields_ = [
        ('wProcessorArchitecture', wintypes.WORD),
        ('wReserved', wintypes.WORD),
        ('dwPageSize', wintypes.DWORD),
        ('lpMinimumApplicationAddress', wintypes.LPVOID),
        ('lpMaximumApplicationAddress', wintypes.LPVOID),
        ('dwActiveProcessorMask', wintypes.DWORD),
        ('dwNumberOfProcessors', wintypes.DWORD),
        ('dwProcessorType', wintypes.DWORD),
        ('dwAllocationGranularity', wintypes.DWORD),
        ('wProcessorLevel', wintypes.WORD),
        ('wProcessorRevision', wintypes.WORD)
    ]

class TBBUTTON(ctypes.Structure):
    _fields_ = [
        ('iBitmap', ctypes.c_int),
        ('idCommand', ctypes.c_int),
        ('fsState', ctypes.c_byte),
        ('fsStyle', ctypes.c_byte),
        ('bReserved', ctypes.c_byte * 6),
        ('dwData', ctypes.c_void_p),
        ('iString', ctypes.c_void_p)
    ]

# 定义函数
def Is64bitSystem():
    si = SYSTEM_INFO()
    ctypes.windll.kernel32.GetNativeSystemInfo(ctypes.byref(si))
    return si.wProcessorArchitecture in [ctypes.wintypes.WORD(9), ctypes.wintypes.WORD(6)]

def FindTrayWnd():
    hWnd = ctypes.windll.user32.FindWindowW("Shell_TrayWnd", None)
    hWnd = ctypes.windll.user32.FindWindowExW(hWnd, None, "TrayNotifyWnd", None)
    hWnd = ctypes.windll.user32.FindWindowExW(hWnd, None, "SysPager", None)
    hWnd = ctypes.windll.user32.FindWindowExW(hWnd, None, "ToolbarWindow32", None)
    return hWnd

def FindNotifyIconOverflowWindow():
    hWnd = ctypes.windll.user32.FindWindowW("NotifyIconOverflowWindow", None)
    hWnd = ctypes.windll.user32.FindWindowExW(hWnd, None, "ToolbarWindow32", None)
    return hWnd

def EnumNotifyWindow(hWnd):
    # 获取托盘进程ID
    dwProcessId = wintypes.DWORD()
    ctypes.windll.user32.GetWindowThreadProcessId(hWnd, ctypes.byref(dwProcessId))
    if dwProcessId.value == 0:
        print("GetWindowThreadProcessId failed:", ctypes.windll.kernel32.GetLastError())
        return False

    # 获取托盘进程句柄
    hProcess = ctypes.windll.kernel32.OpenProcess(PROCESS_VM_OPERATION | PROCESS_VM_READ | PROCESS_VM_WRITE, False, dwProcessId)
    if hProcess == 0:
        print("OpenProcess failed:", ctypes.windll.kernel32.GetLastError())
        return False

    # 在进程虚拟空间中分配内存,用来接收 TBBUTTON 结构体指针
    p_tbbutton = ctypes.windll.kernel32.VirtualAllocEx(hProcess, 0, 4096, MEM_COMMIT, PAGE_EXECUTE_READWRITE)
    if p_tbbutton == 0:
        print("VirtualAllocEx failed:", ctypes.windll.kernel32.GetLastError())
        return False

    # 初始化
    dw_addr_dwData = 0
    buff = ctypes.create_string_buffer(1024)
    h_mainWnd = None
    i_data_offset = 12
    i_str_offset = 18

    # 判断 x64
    if Is64bitSystem():
        i_data_offset += 4
        i_str_offset += 6

    # 获取托盘图标个数
    i_buttons = ctypes.windll.user32.SendMessageW(hWnd, TB_BUTTONCOUNT, 0, 0)
    if i_buttons != 0:
        print("TB_BUTTONCOUNT message failed:", ctypes.windll.kernel32.GetLastError())
        return False
    i_buttons = 2

    # 遍历托盘
    for i in range(i_buttons):
        # 获取 TBBUTTON 结构体指针
        if not ctypes.windll.user32.SendMessageW(hWnd, TB_GETBUTTON, i, p_tbbutton):
            print("TB_GETBUTTON message failed:", ctypes.windll.kernel32.GetLastError())
            return False

        # 读 TBBUTTON.dwData(附加信息)
        ctypes.windll.kernel32.ReadProcessMemory(hProcess, ctypes.c_void_p(p_tbbutton.value + i_data_offset), ctypes.byref(dw_addr_dwData), 4, None)
        if dw_addr_dwData:
            ctypes.windll.kernel32.ReadProcessMemory(hProcess, ctypes.c_void_p(dw_addr_dwData), buff, 1024, None)
            h_mainWnd = ctypes.cast(buff.raw[:4], ctypes.POINTER(wintypes.HWND)).contents
            ws_filePath = ctypes.c_wchar_p(buff.raw[i_str_offset:i_str_offset + ctypes.sizeof(wintypes.WCHAR) * ctypes.sizeof(wintypes.MAX_PATH)])
            ws_tile = ctypes.c_wchar_p(buff.raw[i_str_offset + ctypes.sizeof(wintypes.WCHAR) * ctypes.sizeof(wintypes.MAX_PATH):])
            print("hMainWnd =", hex(h_mainWnd.value))
            print("strFilePath =", ws_filePath.value)
            print("strTile =", ws_tile.value)

        # 清理
        dw_addr_dwData = 0
        h_mainWnd = None

        print()

    ctypes.windll.kernel32.VirtualFreeEx(hProcess, p_tbbutton, 0, MEM_RELEASE)
    ctypes.windll.kernel32.CloseHandle(hProcess)

    return True

def main():
    # 解决控制台中文 '?'
    import locale
    locale.setlocale(locale.LC_ALL, "chs")

    # 获取托盘句柄
    h_tray = FindTrayWnd()
    h_tray_fold = FindNotifyIconOverflowWindow()

    # 遍历托盘窗口
    if not EnumNotifyWindow(h_tray) or not EnumNotifyWindow(h_tray_fold):
        print("EnumNotifyWindow false.")

    input()

if __name__ == "__main__":
    main()

However, it doesn't work on Windows 11... On the code, line 92, I found that the variable of "i_buttons" are always 0. I don't want to use ANY large library(such as PyQt) to complete my problem, thank you.


Solution

  • You can consider pywinauto it is not a large library like tk: Here is code to recursively traverse

    from pywinauto import Desktop
    
    
    def print_window_tree(window, level=0):
        indent = "  " * level 
        print(f"{indent}Level {level}: {window.window_text()}, Handle: {window.handle}, Class Name: {window.class_name()}")
        
    
        children = window.children()
        for child in children:
            print_window_tree(child, level + 1)
    
    
    window = Desktop(backend="uia").window(title="Taskbar")
    print_window_tree(window)
    

    here is my output:

    Level 0: Taskbar, Handle: 131226, Class Name: Shell_TrayWnd
      Level 1: , Handle: 131208, Class Name: TrayNotifyWnd
      Level 1: , Handle: 66140, Class Name: Windows.UI.Input.InputSite.WindowClass
        Level 2: , Handle: None, Class Name: Taskbar.TaskbarFrameAutomationPeer
          Level 3: Start, Handle: None, Class Name: ToggleButton
          Level 3: Search, Handle: None, Class Name: ToggleButton
            Level 4: Search, Handle: None, Class Name: TextBlock
            Level 4: Search - Winter travel planning, Handle: None, Class Name: Button
            Level 4: , Handle: None, Class Name: Image
          Level 3: Task View, Handle: None, Class Name: ToggleButton
          ------open applications------
        --the below is systray items--
        Level 2: Show Hidden Icons, Handle: None, Class Name: SystemTray.NormalButton
        Level 2:  OneDrive - Personal
    
    Your OneDrive is 92% full, Handle: None, Class Name: SystemTray.NormalButton
          Level 3: , Handle: None, Class Name: Image
        Level 2: Windows Security - Actions recommended. Windows Security - No actions needed., Handle: None, Class Name: SystemTray.NormalButton
          Level 3: , Handle: None, Class Name: Image
        Level 2: Tray Input Indicator English (United States)
    
    English (India)
    
    
    
    To switch input methods, press Windows key + space., Handle: None, Class Name: SystemTray.NormalButton
        Level 2: Network HOME 2.4GHz
    Internet access, Handle: None, Class Name: SystemTray.AccentButton
        Level 2: Volume Speakers (Realtek(R) Audio): 100%, Handle: None, Class Name: SystemTray.OmniButtonRight
        Level 2: Clock 08:37 PM
    ‎24.‎11.‎24, Handle: None, Class Name: SystemTray.OmniButtonLeft
        Level 2: Notifications 5 new notifications, Handle: None, Class Name: SystemTray.AccentButton
        Level 2: Show Desktop, Handle: None, Class Name: SystemTray.ShowDesktopButton
    

    (nothing personal)

    you can get required info in level 2

    The key is to use uia backend: https://learn.microsoft.com/en-us/windows/win32/winauto/entry-uiauto-win32 instead of win32