'Setting up a WindowsHook in Python (ctypes, Windows API)

I am trying to globally track the mouse with a Python (3.4.3) background app (in Windows 7/8). This involves setting up a WindowsHook which should return me a valid handle to that specific hook - but my handle is always 0.

Tracking only the mouse position is very easy with GetCursorPos (as an alternative GetCursorInfo works as well):

from ctypes.wintypes import *
ppoint = ctypes.pointer(POINT())
ctypes.windll.user32.GetCursorPos(ppoint)
print('({}, {})'.format(ppoint[0].x, ppoint[0].y))

Also convenient to track only the position is GetMouseMovePointsEx, which tracks the last 64 mouse positions:

from ctypes.wintypes import *

# some additional types and structs
ULONG_PTR = ctypes.c_ulong
class MOUSEMOVEPOINT(ctypes.Structure):
    _fields_ = [
        ("x", ctypes.c_int),
        ("y", ctypes.c_int),
        ("time", DWORD),
        ("dwExtraInfo", ULONG_PTR)
    ]
GMMP_USE_DISPLAY_POINTS = 1

# get initial tracking point
ppoint = ctypes.pointer(POINT())
ctypes.windll.user32.GetCursorPos(ppoint)
point = MOUSEMOVEPOINT(ppoint[0].x,ppoint[0].y)

# track last X points
number_mouse_points = 64
points = (MOUSEMOVEPOINT * number_mouse_points)()
ctypes.windll.user32.GetMouseMovePointsEx(ctypes.sizeof(MOUSEMOVEPOINT), 
    ctypes.pointer(point), ctypes.pointer(points), number_mouse_points, 
    GMMP_USE_DISPLAY_POINTS)

# print results
for point in points:
    print('({}, {})'.format(point.x, point.y))

However I want to be able to also track clicks, drags, etc. A good solution seems to be the LowLevelMouseProc. (There might be another way yet to be explored: Raw Input)

To be able to use the LowLevelMouseProc the documentation tells us to use SetWindowsHookEx(W/A), which is also covered in various (C++) tutorials (C#), as well as some interesting projects (also C#).

The documentation defines it in C++ as follows:

HHOOK WINAPI SetWindowsHookEx(
  _In_ int       idHook,
  _In_ HOOKPROC  lpfn,
  _In_ HINSTANCE hMod,
  _In_ DWORD     dwThreadId
);

Where the following should be the correct values for me in python:

  • idHook: WH_MOUSE_LL = 14
  • hMod: HINSTANCE(0) (basically a null pointer)
  • dwThreadId: ctypes.windll.kernel32.GetCurrentThreadId()

And for the lpfn I need some callback implementing the LowLevelMouseProc, here LLMouseProc:

def _LLMouseProc (nCode, wParam, lParam):
    return ctypes.windll.user32.CallNextHookEx(None, nCode, wParam, lParam)
LLMouseProcCB = ctypes.CFUNCTYPE(LRESULT, ctypes.c_int, WPARAM, LPARAM)
LLMouseProc = LLMouseProcCB(_LLMouseProc)

Putting it all together I expected this to work:

from ctypes.wintypes import *

LONG_PTR = ctypes.c_long
LRESULT = LONG_PTR
WH_MOUSE_LL = 14

def _LLMouseProc(nCode, wParam, lParam):
    print("_LLMouseProc({!s}, {!s}, {!s})".format(nCode, wParam, lParam))
    return ctypes.windll.user32.CallNextHookEx(None, nCode, wParam, lParam)
LLMouseProcCB = ctypes.CFUNCTYPE(LRESULT, ctypes.c_int, WPARAM, LPARAM)
LLMouseProc = LLMouseProcCB(_LLMouseProc)

threadId = ctypes.windll.kernel32.GetCurrentThreadId()

# register callback as hook
print('hook = SetWindowsHookExW({!s}, {!s}, {!s}, {!s})'.format(WH_MOUSE_LL, LLMouseProc,
    HINSTANCE(0), threadId))
hook = ctypes.windll.user32.SetWindowsHookExW(WH_MOUSE_LL, LLMouseProc, 
    HINSTANCE(0), threadId)
print('Hook: {}'.format(hook))

import time
try:
    while True:
        time.sleep(0.2)
except KeyboardInterrupt:
    pass

But the output reveals that hook == 0:

hook = SetWindowsHookExW(14, <CFunctionType object at 0x026183F0>, c_void_p(None), 5700)
Hook: 0

I think that maybe the last parameter of the callback function, name lParam is not really correct as LPARAM (which is ctypes.c_long), since what I assume is really expected is a pointer to this struct:

class MSLLHOOKSTRUCT(ctypes.Structure):
    _fields_ = [
        ("pt", POINT),
        ("mouseData", DWORD),
        ("flags", DWORD),
        ("time", DWORD),
        ("dwExtraInfo", ULONG_PTR)
    ]

But changing the signature to LLMouseProcCB = ctypes.CFUNCTYPE(LRESULT, ctypes.c_int, WPARAM, ctypes.POINTER(MSLLHOOKSTRUCT)) does not solve the problem, I still have a hook of 0.

Is this the right approach of tracking the mouse? What do I need to change to be able to correctly register hooks with Windows?



Solution 1:[1]

A simplified version of the accepted answer

Note: pip install pywin32 first.

# Created by [email protected] at 2022/2/10 22:27

from ctypes import WINFUNCTYPE, c_int, Structure, cast, POINTER, windll
from ctypes.wintypes import LPARAM, WPARAM, DWORD, PULONG, LONG

import win32con
import win32gui


def genStruct(name="Structure", **kwargs):
    return type(name, (Structure,), dict(
        _fields_=list(kwargs.items()),
        __str__=lambda self: "%s(%s)" % (name, ",".join("%s=%s" % (k, getattr(self, k)) for k in kwargs))
    ))


@WINFUNCTYPE(LPARAM, c_int, WPARAM, LPARAM)
def hookProc(nCode, wParam, lParam):
    msg = cast(lParam, POINTER(HookStruct))[0]
    print(msgDict[wParam], msg)
    return windll.user32.CallNextHookEx(None, nCode, WPARAM(wParam), LPARAM(lParam))


HookStruct = genStruct(
    "Hook", pt=genStruct("Point", x=LONG, y=LONG), mouseData=DWORD, flags=DWORD, time=DWORD, dwExtraInfo=PULONG)
msgDict = {v: k for k, v in win32con.__dict__.items() if k.startswith("WM_")}
windll.user32.SetWindowsHookExW(win32con.WH_MOUSE_LL, hookProc, None, 0)
win32gui.PumpMessages()

Sample Output

WM_MOUSEMOVE Hook(pt=Point(x=50,y=702),mouseData=0,flags=0,time=343134468,dwExtraInfo=<ctypes.wintypes.LP_c_ulong object at 0x000001A466CDF8C8>)
WM_MOUSEMOVE Hook(pt=Point(x=49,y=704),mouseData=0,flags=0,time=343134484,dwExtraInfo=<ctypes.wintypes.LP_c_ulong object at 0x000001A466CDF8C8>)
WM_MOUSEMOVE Hook(pt=Point(x=49,y=705),mouseData=0,flags=0,time=343134484,dwExtraInfo=<ctypes.wintypes.LP_c_ulong object at 0x000001A466CDF8C8>)
WM_MOUSEMOVE Hook(pt=Point(x=49,y=705),mouseData=0,flags=0,time=343134500,dwExtraInfo=<ctypes.wintypes.LP_c_ulong object at 0x000001A466CDF8C8>)
WM_MOUSEMOVE Hook(pt=Point(x=49,y=706),mouseData=0,flags=0,time=343134500,dwExtraInfo=<ctypes.wintypes.LP_c_ulong object at 0x000001A466CDF8C8>)
WM_MOUSEMOVE Hook(pt=Point(x=48,y=707),mouseData=0,flags=0,time=343134515,dwExtraInfo=<ctypes.wintypes.LP_c_ulong object at 0x000001A466CDF8C8>)
WM_LBUTTONDOWN Hook(pt=Point(x=48,y=707),mouseData=0,flags=0,time=343134593,dwExtraInfo=<ctypes.wintypes.LP_c_ulong object at 0x000001A466CDF8C8>)
WM_LBUTTONUP Hook(pt=Point(x=48,y=707),mouseData=0,flags=0,time=343134671,dwExtraInfo=<ctypes.wintypes.LP_c_ulong object at 0x000001A466CDF8C8>)

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1