如何修改鼠标位置以触发已安装的WH_MOUSE_LL低级鼠标钩子
混合HiDPI双显示器下非感知应用的鼠标钩子与像素级移动解决方案
核心问题拆解
你遇到的两个核心问题:一是通过代码移动光标无法触发WH_MOUSE_LL钩子,二是非HiDPI感知应用与钩子坐标的映射偏差,这两个问题都需要针对Windows的输入机制和DPI虚拟化特性来解决。
一、让代码移动光标触发低级鼠标钩子
SetCursorPos和SetPhysicalCursorPos是直接修改光标位置的API,不会生成系统级的鼠标输入事件,因此无法被WH_MOUSE_LL钩子捕获。必须改用SendInput模拟真实的鼠标移动事件,才能触发钩子:
代码示例(C++)
void SimulateMouseMove(int physicalX, int physicalY) { // 获取系统物理屏幕尺寸 int screenWidth = GetSystemMetrics(SM_CXSCREEN); int screenHeight = GetSystemMetrics(SM_CYSCREEN); // 将物理坐标转换为SendInput需要的0-65535绝对坐标范围 INPUT input = {0}; input.type = INPUT_MOUSE; input.mi.dx = static_cast<LONG>(physicalX * (65535.0f / screenWidth)); input.mi.dy = static_cast<LONG>(physicalY * (65535.0f / screenHeight)); input.mi.dwFlags = MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE; SendInput(1, &input, sizeof(INPUT)); }
二、非HiDPI感知应用的坐标映射
非HiDPI感知应用会被Windows的DPI虚拟化机制包裹,应用内获取的是虚拟坐标(按主显示器缩放比例缩放后的坐标),而WH_MOUSE_LL钩子在非感知进程中收到的MSLLHOOKSTRUCT.pt也是虚拟坐标。要实现真实像素级操作,必须完成虚拟坐标与物理坐标的双向映射:
1. 获取当前显示器的DPI缩放因子
float GetMonitorScaleFactor(HMONITOR hMonitor) { UINT dpiX, dpiY; if (GetDpiForMonitor(hMonitor, MDT_EFFECTIVE_DPI, &dpiX, &dpiY) == S_OK) { return static_cast<float>(dpiX) / 96.0f; // 96DPI为100%缩放基准 } // 降级方案:获取系统全局DPI return static_cast<float>(GetDeviceCaps(GetDC(NULL), LOGPIXELSX)) / 96.0f; }
2. 坐标双向转换
// 虚拟坐标 → 物理坐标 POINT VirtualToPhysical(POINT virtualPt) { HMONITOR hMonitor = MonitorFromPoint(virtualPt, MONITOR_DEFAULTTONEAREST); float scale = GetMonitorScaleFactor(hMonitor); return { static_cast<LONG>(virtualPt.x * scale), static_cast<LONG>(virtualPt.y * scale) }; } // 物理坐标 → 虚拟坐标 POINT PhysicalToVirtual(POINT physicalPt) { HMONITOR hMonitor = MonitorFromPoint(physicalPt, MONITOR_DEFAULTTONEAREST); float scale = GetMonitorScaleFactor(hMonitor); return { static_cast<LONG>(physicalPt.x / scale), static_cast<LONG>(physicalPt.y / scale) }; }
三、方向键实现像素级光标移动
基于物理坐标调整,再通过SendInput发送,确保每个方向键按下都移动1个真实像素:
代码示例(窗口过程中处理方向键)
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch (uMsg) { case WM_KEYDOWN: { POINT virtualPt; GetCursorPos(&virtualPt); POINT physicalPt = VirtualToPhysical(virtualPt); // 按方向键调整物理坐标 switch (wParam) { case VK_LEFT: physicalPt.x -= 1; break; case VK_RIGHT: physicalPt.x += 1; break; case VK_UP: physicalPt.y -= 1; break; case VK_DOWN: physicalPt.y += 1; break; default: break; } // 模拟移动,触发钩子 SimulateMouseMove(physicalPt.x, physicalPt.y); return 0; } // 其他消息处理... } return DefWindowProc(hwnd, uMsg, wParam, lParam); }
四、准确取色的注意事项
非HiDPI感知应用的GDI默认使用虚拟坐标,要获取真实物理像素的颜色,必须针对光标所在显示器创建DC:
COLORREF GetPhysicalPixelColor(POINT physicalPt) { HMONITOR hMonitor = MonitorFromPoint(physicalPt, MONITOR_DEFAULTTONEAREST); MONITORINFOEX monitorInfo = {0}; monitorInfo.cbSize = sizeof(MONITORINFOEX); GetMonitorInfo(hMonitor, &monitorInfo); // 创建对应显示器的DC HDC hDc = CreateDC(L"DISPLAY", monitorInfo.szDevice, NULL, NULL); COLORREF color = GetPixel(hDc, physicalPt.x, physicalPt.y); DeleteDC(hDc); return color; }
内容的提问来源于stack exchange,提问作者AWirthK2
相关产品推荐
相关产品推荐

