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.
The main problem is using WinEventProc(focus_changed)
as a parameter 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()
(andWINFUNCTYPE()
) 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 functions
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)