You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何修改鼠标位置以触发已安装的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 05:07:38