Search code examples
pythonwindowseventshookctypes

How do I make windll.user32.SetWinEventHook work?


I am trying to hook a focus change event using the following program in windows 11, python 3.11.

import win32con
import pythoncom
from ctypes import wintypes, windll, WINFUNCTYPE

# WinEventProc and GUITHREADINFO
WinEventProc = WINFUNCTYPE(
    None,
    wintypes.HANDLE,
    wintypes.DWORD,
    wintypes.HWND,
    wintypes.LONG,
    wintypes.LONG,
    wintypes.DWORD,
    wintypes.DWORD
)

# focus_changed function
def focus_changed(hWinEventHook, event, hwnd, idObject, idChild, dwEventThread, dwmsEventTime):
    print("Focus changed event detected")

# Main code
hook = windll.user32.SetWinEventHook(
    win32con.EVENT_SYSTEM_FOREGROUND,
    win32con.EVENT_SYSTEM_FOREGROUND,
    0,
    WinEventProc(focus_changed),
    0,
    0,
    win32con.WINEVENT_OUTOFCONTEXT
)

if not hook:
    print(f"SetWinEventHook failed")

print("Script is running...")

while True:
    try:
        pythoncom.PumpWaitingMessages()
    except KeyboardInterrupt:
        print("Exiting...")
        break

# Cleanup: Unhook events and release resources
windll.user32.UnhookWinEvent(hook)

I was expecting to see "Focus changed event detected" printed on the console when I start Notepad.

When I start the program above, it prints "Script is running...".

When I start notepad, the program above terminates silently without printing the focus changed event message or any other message.


Solution

  • The main problem is using WinEventProc(focus_changed) as a paramter to SetWinEventHook. This is an object whose reference count goes to zero and is freed immediately after the call. From the ctypes documentation:

    Note: Make sure you keep references to CFUNCTYPE() (and WINFUNCTYPE()) objects as long as they are used from C code. ctypes doesn’t, and if you don’t, they may be garbage collected, crashing your program when a callback is made.

    One way to make a permanent reference is to simply decorate the callback function with the prototype.

    Working code below. Note also it is good practice to declare parameter types explicitly on ctypes functions for better type and error checking. for example, ctypes assumes 32-bit integers for non-pointer parameters and all return values. Handles are integers, but are 64-bit. If the handle is large enough the value will be truncated unless the argument or return value is declared as a handle.

    import win32con
    import pythoncom
    import ctypes as ct
    import ctypes.wintypes as w
    
    WinEventProc = ct.WINFUNCTYPE(None, w.HANDLE, w.DWORD, w.HWND, w.LONG, w.LONG, w.DWORD, w.DWORD)
    
    @WinEventProc  # Decorate the callback for a permanent reference
    def focus_changed(hWinEventHook, event, hwnd, idObject, idChild, dwEventThread, dwmsEventTime):
        print('Focus changed event detected')
    
    # Failure check helper for ctypes functinos
    def fail_check(result, func, args):
        if not result:
            raise ct.WinError(ct.get_last_error())
        return result
    
    # Good habit to explicitly declare arguments and result type of all functions
    # used by ctypes for better type/error checking.
    # "errcheck" suport will throw an exception with Win32 failure info if the function fails.
    u32 = ct.WinDLL('user32', use_last_error=True)
    u32.SetWinEventHook.argtypes = w.DWORD, w.DWORD, w.HMODULE, WinEventProc, w.DWORD, w.DWORD, w.DWORD
    u32.SetWinEventHook.restype = w.HANDLE
    u32.SetWinEventHook.errcheck = fail_check
    u32.UnhookWinEvent.argtypes = w.HANDLE,
    u32.UnhookWinEvent.restype = w.BOOL
    u32.UnhookWinEvent.errcheck = fail_check
    
    hook = u32.SetWinEventHook(
        win32con.EVENT_SYSTEM_FOREGROUND, win32con.EVENT_SYSTEM_FOREGROUND, 0,
        focus_changed, # changed from an object that immediately goes out of scope after hook call
        0, 0, win32con.WINEVENT_OUTOFCONTEXT)
    
    print('Script is running...')
    try:
        while True:
            pythoncom.PumpWaitingMessages()
    except KeyboardInterrupt:
        print('Exiting...')
    u32.UnhookWinEvent(hook)