Reactos 第 7 章 视窗报文 — 7.7 鼠标器输入线程

第 7 章 视窗报文 --- 7.7 鼠标器输入线程

本节深入剖析 Windows/ReactOS 中"鼠标器输入"(Mouse Input)的内核态处理,包括 RIT 原始鼠标数据转换、鼠标位置计算、捕获机制、命中测试(Hit-Test)、消息合并(Coalesce)、双击/单击锁定、hover/leave 跟踪、WH_MOUSE_LL 钩子等。

概述

鼠标输入与键盘输入一样,是 Windows GUI 系统的核心 I/O 来源。但相比键盘,鼠标的复杂性更高:

  • 多种输入类型:移动、左/中/右键、XButton(4/5 键)、滚轮(垂直/水平)、触摸板手势;
  • 绝对/相对位置:不同设备使用不同坐标系(屏幕坐标 / 虚拟桌面坐标 / 相对位移);
  • 消息合并 :高频的 WM_MOUSEMOVE 需要合并,避免消息队列爆炸;
  • 捕获(Capture):鼠标按下后可以"捕获"到特定窗口,即使鼠标移出窗口也能收到事件;
  • 命中测试(Hit-Test):决定鼠标事件发送给哪个窗口(client / non-client / 系统区域);
  • 双击判定:基于时间间隔 + 位置距离的复杂逻辑;
  • Hover/Leave 跟踪:基于定时器的事件通知;
  • LL 钩子:内核态实现的全局鼠标钩子。

鼠标输入的本质是什么?

鼠标输入是一种"高频事件 + 空间敏感"的数据流。RIT 从硬件读取 MOUSE_INPUT_DATA,将其转换为 MOUSEINPUT/鼠标消息;win32k 在内核态根据当前光标位置、命中测试、捕获状态、消息队列状态等因素,将消息路由到合适的窗口;同时通过 co_MsqInsertMouseMessage 维护消息合并(Coalesce)、双击判定、单击锁定等机制。

想象一个"高灵敏度雷达跟踪"场景:

  • 鼠标硬件:雷达天线(不断发送位置数据);
  • MOUSE_INPUT_DATA:雷达回波(位置 + 按钮状态);
  • RIT(UserProcessMouseInput):雷达数据处理中心;
  • IntCoalesceMouseMove:目标跟踪合并器(避免对同一目标重复告警);
  • co_MsqInsertMouseMessage:目标识别与路由(决定告警发送给哪个操作员);
  • 捕获(Capture):把特定目标锁定在某个操作员屏幕上;
  • 命中测试:判断鼠标在窗口的哪个区域(客户区 / 标题栏 / 边框);
  • 双击/单击锁定:基于时间+位置的复杂事件识别;
  • WH_MOUSE_LL:监控所有目标活动的中央监控室。

本节内容概览

  1. 7.7.0 框架图:鼠标输入子系统的整体架构;
  2. 7.7.1 鼠标输入子系统架构(Mouclass → win32k → 窗口消息);
  3. 7.7.2 鼠标初始化与 gMouseHistoryOfMoves
  4. 7.7.3 UserProcessMouseInput:原始数据转换
  5. 7.7.4 UserSendMouseInput:合成输入入口
  6. 7.7.5 鼠标位置计算(绝对/相对/虚拟桌面)
  7. 7.7.6 按钮状态与左右键交换
  8. 7.7.7 鼠标消息生成(co_MsqInsertMouseMessage)
  9. 7.7.8 鼠标捕获机制
  10. 7.7.9 命中测试(WM_NCHITTEST)
  11. 7.7.10 鼠标移动消息合并(IntCoalesceMouseMove)
  12. 7.7.11 双击/单击锁定
  13. 7.7.12 鼠标 hover/leave 跟踪
  14. 7.7.13 设计哲学问答:5 个关键设计问题解答。

学习目标

读完本节后,读者应当能够:

  • 理解 RIT 鼠标处理的数据流;
  • 掌握 MOUSE_INPUT_DATAMOUSEINPUT 的转换;
  • 解释鼠标位置计算的绝对/相对/虚拟桌面三种模式;
  • 掌握鼠标捕获(Capture)机制及其用途;
  • 理解命中测试(WM_NCHITTEST)的判定流程;
  • 掌握 IntCoalesceMouseMove 的消息合并原理;
  • 解释双击判定与单击锁定的时间+位置逻辑;
  • 理解 IntTrackMouseMove 的 hover/leave 跟踪;
  • 掌握 WH_MOUSE_LL 钩子的实现。

涉及的内核子系统

子系统 职责
ntoskrnl / mouclass 鼠标类驱动:硬件数据 → MOUSE_INPUT_DATA
win32k.sys/ntuser/mouse.c 鼠标消息处理、UserProcessMouseInput、UserSendMouseInput
win32k.sys/ntuser/msgqueue.c co_MsqInsertMouseMessage、co_IntProcessMouseMessage、IntCoalesceMouseMove、IntTrackMouseMove
win32k.sys/ntuser/nonclient.c WM_NCHITTEST 命中测试
win32k.sys/ntuser/input.c UserProcessMouseInput 原始输入入口
user32.dll SetCapture、GetCapture、TrackMouseEvent 用户态 API

7.7.0 框架图

复制代码
┌──────────────────────────────────────────────────────────────────────────────────────┐
│                    鼠标输入子系统完整架构                                                │
├──────────────────────────────────────────────────────────────────────────────────────┤
│                                                                                      │
│   硬件层                                                                              │
│   ┌────────────────────────────────────────────────────────────────────────────┐     │
│   │  PS/2 / USB / 蓝牙 鼠标硬件                                                  │     │
│   │  产生中断(IRQ12 / USB HID)                                                 │     │
│   │  端口驱动(i8042prt / mouhid)读取数据包                                     │     │
│   └────────────────────────────────────────────────────────────────────────────┘     │
│                              │                                                       │
│                              ▼                                                       │
│   内核类驱动层                                                                          │
│   ┌────────────────────────────────────────────────────────────────────────────┐     │
│   │  mouclass.sys  (鼠标类驱动)                                                   │     │
│   │  └─► 包装为 MOUSE_INPUT_DATA 投递到 RIT                                    │     │
│   └────────────────────────────────────────────────────────────────────────────┘     │
│                              │                                                       │
│                              ▼  (RIT: \Device\PointerClass0 的读 IRP 完成)            │
│   win32k 用户子系统(RIT 在 Win32k 内核态)                                              │
│   ┌────────────────────────────────────────────────────────────────────────────┐     │
│   │  input.c  UserProcessMouseInput(MOUSE_INPUT_DATA)                          │     │
│   │      │                                                                     │     │
│   │      ▼                                                                     │     │
│   │  mouse.c                                                                    │     │
│   │  ├─► 原始数据 → MOUSEINPUT 转换                                             │     │
│   │  ├─► 按钮状态 (gpsi->ButtonsDown)                                          │     │
│   │  ├─► 左右键交换 (gspv.bMouseBtnSwap)                                       │     │
│   │  ├─► 鼠标位置计算(绝对/相对/虚拟桌面)                                       │     │
│   │  ├─► UserSendMouseInput ─► 投递原始输入                                    │     │
│   │  └─► co_MsqInsertMouseMessage (鼠标消息插入核心)                            │     │
│   │      │                                                                     │     │
│   │      ├─► WH_MOUSE_LL 钩子                                                  │     │
│   │      ├─► 鼠标捕获检查 (IntGetCaptureWindow)                                │     │
│   │      ├─► 命中测试 (IntTopLevelWindowFromPoint)                             │     │
│   │      ├─► IntTrackMouseMove (hover/leave 跟踪)                              │     │
│   │      ├─► 队列放置 (HardwareMessagesListHead)                               │     │
│   │      └─► ptiSysLock 抢占机制                                               │     │
│   │      │                                                                     │     │
│   │      ▼                                                                     │     │
│   │  co_MsqPeekHardwareMessage → co_IntProcessMouseMessage                   │     │
│   │  ├─► 鼠标捕获窗口 (MessageQueue->spwndCapture)                             │     │
│   │  ├─► co_WinPosWindowFromPoint (命中测试)                                   │     │
│   │  ├─► NC/Client 消息分类 (HTCLIENT vs HT*)                                  │     │
│   │  ├─► 双击判定 (gspv.iDblClickTime + 系统度量)                              │     │
│   │  ├─► 单击锁定 (ClickLock)                                                  │     │
│   │  ├─► WH_MOUSE 钩子                                                        │     │
│   │  ├─► 父窗口通知 (MsqSendParentNotify)                                      │     │
│   │  ├─► WM_MOUSEACTIVATE / WM_SETCURSOR                                      │     │
│   │  └─► IME 处理                                                              │     │
│   │      │                                                                     │     │
│   │      ▼                                                                     │     │
│   │  user32.dll  GetMessage / DispatchMessage                                  │     │
│   │  └─► WndProc 收到 WM_LBUTTONDOWN / WM_MOUSEMOVE / WM_MOUSEWHEEL 等        │     │
│   └────────────────────────────────────────────────────────────────────────────┘     │
│                                                                                      │
│   鼠标状态与历史                                                                         │
│   ┌────────────────────────────────────────────────────────────────────────────┐     │
│   │  gpsi->ptCursor               : 当前光标位置(屏幕坐标)                     │     │
│   │  gpsi->ButtonsDown            : 当前按下的按钮(MK_* 标志)                  │     │
│   │  gpsi->iDblClickTime          : 双击时间间隔(毫秒)                        │     │
│   │  gMouseHistoryOfMoves[64]     : 鼠标移动历史(最近 64 个位置)              │     │
│   │  gcMouseHistoryOfMoves        : 历史数组索引                                │     │
│   │  gdwMouseMoveTimeStamp        : 上次鼠标移动时间戳                          │     │
│   │  ptiSysLock                   : 鼠标消息独占线程                            │     │
│   │  MessageQueue->msgDblClk      : 上次点击信息(用于双击判定)                │     │
│   │  MessageQueue->spwndCapture   : 鼠标捕获窗口                               │     │
│   │  pDesk->spwndTrack            : hover/leave 跟踪窗口                       │     │
│   │  pDesk->dwDTFlags             : DF_TME_HOVER / DF_TME_LEAVE                │     │
│   └────────────────────────────────────────────────────────────────────────────┘     │
│                                                                                      │
└──────────────────────────────────────────────────────────────────────────────────────┘

7.7.1 鼠标输入子系统架构

7.7.1.1 RIT 鼠标路径

复制代码
鼠标硬件(PS/2 / USB / 蓝牙)
  │  中断(IRQ12 / USB HID)
  ▼
i8042prt / mouhid 端口驱动
  │  读取鼠标数据包
  ▼
mouclass 鼠标类驱动
  │  包装为 MOUSE_INPUT_DATA
  │  完成 IRP_MJ_READ
  ▼
RIT 线程(等待 IRP 完成)
  │  UserProcessMouseInput(data)
  ▼
win32k 鼠标处理
  │  ▼
  │  UserSendMouseInput(MOUSEINPUT)
  │     │
  │     ├─► 计算光标位置
  │     ├─► 更新 ButtonsDown
  │     ├─► WH_MOUSE_LL 钩子
  │     └─► co_MsqInsertMouseMessage
  │            │
  │            ├─► ptiSysLock 抢占
  │            ├─► IntGetCaptureWindow
  │            ├─► IntTopLevelWindowFromPoint
  │            ├─► IntTrackMouseMove
  │            └─► MsqPostMessage (硬件消息队列)
  ▼
co_MsqPeekHardwareMessage
  │
  ▼
co_IntProcessMouseMessage
  │
  ▼
WndProc (收到 WM_LBUTTONDOWN/WM_MOUSEMOVE/WM_MOUSEWHEEL 等)

7.7.1.2 MOUSE_INPUT_DATA 结构

win32ss/user/ntuser/mouse.c(file:///d:/reactos/win32ss/user/ntuser/mouse.c) 处理的输入数据:

c 复制代码
typedef struct _MOUSE_INPUT_DATA {
    USHORT UnitId;        // 设备单元 ID
    USHORT Flags;         // MOUSE_MOVE_ABSOLUTE / MOUSE_VIRTUAL_DESKTOP
    union {
        ULONG Buttons;    // 旧版:所有按钮位
        USHORT ButtonFlags; // 新版:分立的按钮标志
    };
    union {
        LONG  LastX;      // 相对/绝对 X
        USHORT RawButtons; // 原始按钮(XButton 模式)
    };
    union {
        LONG  LastY;      // 相对/绝对 Y
        USHORT ButtonData; // 滚轮/XButton 数据
    };
    ULONG  ExtraInformation; // 来自驱动
    ULONG  RawInputUnitId;  // HID 设备 ID
} MOUSE_INPUT_DATA, *PMOUSE_INPUT_DATA;

MOUSE_INPUT_DATA.ButtonFlags 标志:

c 复制代码
#define MOUSE_LEFT_BUTTON_DOWN   0x0001
#define MOUSE_LEFT_BUTTON_UP     0x0002
#define MOUSE_RIGHT_BUTTON_DOWN  0x0004
#define MOUSE_RIGHT_BUTTON_UP    0x0008
#define MOUSE_MIDDLE_BUTTON_DOWN 0x0010
#define MOUSE_MIDDLE_BUTTON_UP   0x0020
#define MOUSE_BUTTON_4_DOWN      0x0040
#define MOUSE_BUTTON_4_UP        0x0080
#define MOUSE_BUTTON_5_DOWN      0x0100
#define MOUSE_BUTTON_5_UP        0x0200
#define MOUSE_WHEEL              0x0400
#define MOUSE_MOVE_ABSOLUTE      0x0800
#define MOUSE_VIRTUAL_DESKTOP    0x4000

7.7.2 鼠标初始化与 gMouseHistoryOfMoves

7.7.2.1 全局鼠标状态

win32ss/user/ntuser/mouse.c:13-14(file:///d:/reactos/win32ss/user/ntuser/mouse.c#L13-L14):

c 复制代码
MOUSEMOVEPOINT gMouseHistoryOfMoves[64];
INT gcMouseHistoryOfMoves = 0;

gMouseHistoryOfMoves :最近 64 个鼠标移动的位置记录,用于 GetMouseMovePoints API。

7.7.2.2 鼠标历史用途

GetMouseMovePoints API:

c 复制代码
int GetMouseMovePoints(UINT cbSize, LPMOUSEMOVEPOINT lppt, LPMOUSEMOVEPOINT lpptBuf, int nBufPoints, DWORD resolution);

应用(如绘图软件、游戏)可以查询鼠标历史来:

  • 实现"轨迹回放";
  • 防止"瞬移"作弊(检测鼠标瞬间从 A 跳到 B 的异常);
  • 实现"平滑移动"(在历史位置之间插值)。

7.7.2.3 SystemParametersInfo 鼠标设置

通过 SystemParametersInfo 可以查询/设置鼠标参数:

  • SPI_GETMOUSEHOVERTIME / SPI_SETMOUSEHOVERTIME:hover 时间
  • SPI_GETMOUSEHOVERWIDTH / SPI_SETMOUSEHOVERWIDTH:hover 区域宽度
  • SPI_GETMOUSECLICKLOCKTIME:单击锁定时间
  • SPI_GETMOUSEDBLCLICKTIME / SPI_SETMOUSEDBLCLICKTIME:双击时间
  • SPI_GETMOUSEDRAGPATHCOUNT:拖动路径数量

7.7.3 UserProcessMouseInput:原始数据转换

7.7.3.1 函数签名

win32ss/user/ntuser/mouse.c:39-130(file:///d:/reactos/win32ss/user/ntuser/mouse.c#L39-L130):

c 复制代码
VOID NTAPI
UserProcessMouseInput(PMOUSE_INPUT_DATA mid)
{
    MOUSEINPUT mi;

    /* 初始化 MOUSEINPUT */
    mi.dx = mid->LastX;
    mi.dy = mid->LastY;
    mi.mouseData = 0;
    mi.dwFlags = 0;
    mi.time = 0;
    mi.dwExtraInfo = mid->ExtraInformation;

    /* 鼠标位置标志 */
    if (mi.dx != 0 || mi.dy != 0)
        mi.dwFlags |= MOUSEEVENTF_MOVE;
    if (mid->Flags & MOUSE_MOVE_ABSOLUTE)
        mi.dwFlags |= MOUSEEVENTF_ABSOLUTE;
    if (mid->Flags & MOUSE_VIRTUAL_DESKTOP)
        mi.dwFlags |= MOUSEEVENTF_VIRTUALDESK;

    /* 左键 */
    if (mid->ButtonFlags & MOUSE_LEFT_BUTTON_DOWN)  mi.dwFlags |= MOUSEEVENTF_LEFTDOWN;
    if (mid->ButtonFlags & MOUSE_LEFT_BUTTON_UP)    mi.dwFlags |= MOUSEEVENTF_LEFTUP;

    /* 中键 */
    if (mid->ButtonFlags & MOUSE_MIDDLE_BUTTON_DOWN) mi.dwFlags |= MOUSEEVENTF_MIDDLEDOWN;
    if (mid->ButtonFlags & MOUSE_MIDDLE_BUTTON_UP)   mi.dwFlags |= MOUSEEVENTF_MIDDLEUP;

    /* 右键 */
    if (mid->ButtonFlags & MOUSE_RIGHT_BUTTON_DOWN) mi.dwFlags |= MOUSEEVENTF_RIGHTDOWN;
    if (mid->ButtonFlags & MOUSE_RIGHT_BUTTON_UP)   mi.dwFlags |= MOUSEEVENTF_RIGHTUP;

    /* Button 4 (XBUTTON1) */
    if (mid->ButtonFlags & MOUSE_BUTTON_4_DOWN)
    {
        mi.dwFlags |= MOUSEEVENTF_XDOWN;
        mi.mouseData |= XBUTTON1;
    }
    if (mid->ButtonFlags & MOUSE_BUTTON_4_UP)
    {
        mi.dwFlags |= MOUSEEVENTF_XUP;
        mi.mouseData |= XBUTTON1;
    }

    /* Button 4 + Button 5 不能同包(mouseData 冲突),分两次发 */
    if (mi.dwFlags & (MOUSE_BUTTON_4_DOWN | MOUSE_BUTTON_4_UP))
    {
        UserSendMouseInput(&mi, FALSE);
        RtlZeroMemory(&mi, sizeof(mi));
    }

    /* Button 5 (XBUTTON2) */
    // ... 类似处理 ...

    /* 滚轮 */
    if (mid->ButtonFlags & MOUSE_WHEEL)
    {
        mi.mouseData = mid->ButtonData;
        mi.dwFlags |= MOUSEEVENTF_WHEEL;
    }

    if (mi.dwFlags)
        UserSendMouseInput(&mi, FALSE);
}

7.7.3.2 转换要点

原始数据 MOUSEINPUT 标志 说明
LastX / LastY MOUSEEVENTF_MOVE 移动标志(位移非零时)
MOUSE_MOVE_ABSOLUTE MOUSEEVENTF_ABSOLUTE 绝对坐标
MOUSE_VIRTUAL_DESKTOP MOUSEEVENTF_VIRTUALDESK 虚拟桌面坐标
MOUSE_LEFT_BUTTON_DOWN MOUSEEVENTF_LEFTDOWN 左键按下
MOUSE_WHEEL MOUSEEVENTF_WHEEL 滚轮(mouseData 存 delta)
MOUSE_BUTTON_4_* MOUSEEVENTF_XDOWN/XUP XButton 1(mouseData = XBUTTON1)

Button 4 + Button 5 分两次发送 :因为 mouseData 字段不能同时表示两个 XButton,所以拆分为两个 UserSendMouseInput 调用。


7.7.4 UserSendMouseInput:合成输入入口

7.7.4.1 函数签名

win32ss/user/ntuser/mouse.c:167-...(file:///d:/reactos/win32ss/user/ntuser/mouse.c#L167):

c 复制代码
BOOL NTAPI
UserSendMouseInput(MOUSEINPUT *pmi, BOOL bInjected);

两个调用来源

  • RIT 原始输入(UserProcessMouseInput 调用,bInjected = FALSE);
  • 用户态 SendInput API(bInjected = TRUE)。

7.7.4.2 完整流程

c 复制代码
BOOL NTAPI
UserSendMouseInput(MOUSEINPUT *pmi, BOOL bInjected)
{
    POINT ptCursor;
    PSYSTEM_CURSORINFO pCurInfo;
    MSG Msg;
    DWORD dwFlags;

    pCurInfo = IntGetSysCursorInfo();
    ptCursor = gpsi->ptCursor;
    dwFlags = IntFixMouseInputButtons(pmi->dwFlags);  // 交换左右键

    gppiInputProvider = ((PTHREADINFO)PsGetCurrentThreadWin32Thread())->ppi;

    // 1. 计算新光标位置
    if (pmi->dwFlags & MOUSEEVENTF_MOVE)
    {
        if (!(pmi->dwFlags & MOUSEEVENTF_ABSOLUTE))
        {
            // 相对位移
            ptCursor.x += pmi->dx;
            ptCursor.y += pmi->dy;
        }
        else if (pmi->dwFlags & MOUSEEVENTF_VIRTUALDESK)
        {
            // 虚拟桌面绝对坐标
            ptCursor.x = pmi->dx * UserGetSystemMetrics(SM_CXVIRTUALSCREEN) >> 16;
            ptCursor.y = pmi->dy * UserGetSystemMetrics(SM_CYVIRTUALSCREEN) >> 16;
        }
        else
        {
            // 主显示器绝对坐标
            ptCursor.x = pmi->dx * UserGetSystemMetrics(SM_CXSCREEN) >> 16;
            ptCursor.y = pmi->dy * UserGetSystemMetrics(SM_CYSCREEN) >> 16;
        }
    }

    // 2. 裁剪到屏幕范围
    // ...

    // 3. 更新 gpsi->ptCursor
    if (ptCursor.x != gpsi->ptCursor.x || ptCursor.y != gpsi->ptCursor.y)
    {
        gpsi->ptCursor = ptCursor;
        // 更新鼠标历史
        gcMouseHistoryOfMoves = (gcMouseHistoryOfMoves + 1) % 64;
        gMouseHistoryOfMoves[gcMouseHistoryOfMoves].x = ptCursor.x;
        gMouseHistoryOfMoves[gcMouseHistoryOfMoves].y = ptCursor.y;
    }

    // 4. 更新按钮状态
    if (dwFlags & MOUSEEVENTF_LEFTDOWN)   pCurInfo->ButtonsDown |= MK_LBUTTON;
    if (dwFlags & MOUSEEVENTF_LEFTUP)     pCurInfo->ButtonsDown &= ~MK_LBUTTON;
    if (dwFlags & MOUSEEVENTF_RIGHTDOWN)  pCurInfo->ButtonsDown |= MK_RBUTTON;
    if (dwFlags & MOUSEEVENTF_RIGHTUP)    pCurInfo->ButtonsDown &= ~MK_RBUTTON;
    if (dwFlags & MOUSEEVENTF_MIDDLEDOWN) pCurInfo->ButtonsDown |= MK_MBUTTON;
    if (dwFlags & MOUSEEVENTF_MIDDLEUP)   pCurInfo->ButtonsDown &= ~MK_MBUTTON;

    // 5. 调用 co_MsqInsertMouseMessage
    // ... 构造 MSG 结构(hwnd, message, wParam, lParam)...

    co_MsqInsertMouseMessage(&Msg, LLMHF_INJECTED, ExtraInfo, !bInjected);
    return TRUE;
}

7.7.5 鼠标位置计算(绝对/相对/虚拟桌面)

7.7.5.1 三种位置模式

模式 输入范围 输出范围 用途
相对!MOUSEEVENTF_ABSOLUTE dx, dy 位移 ptCursor += (dx, dy) 普通鼠标
绝对MOUSEEVENTF_ABSOLUTE,无 VIRTUALDESK) 0...65535 0...SM_CXSCREEN 触摸板、绝对设备
虚拟桌面绝对MOUSEEVENTF_VIRTUALDESK 0...65535 0...SM_CXVIRTUALSCREEN 多显示器(跨越多个屏幕)

7.7.5.2 绝对坐标的归一化

绝对坐标的"归一化"(0...65535 映射到屏幕像素):

c 复制代码
ptCursor.x = pmi->dx * UserGetSystemMetrics(SM_CXSCREEN) >> 16;
ptCursor.y = pmi->dy * UserGetSystemMetrics(SM_CYSCREEN) >> 16;

>> 16 等价于除以 65536,将 0, 65535 映射到 [0, screen_size)。

为什么使用归一化坐标?

  • 兼容不同分辨率的设备(同一驱动可以用于 800x600 和 1920x1080 屏幕);
  • 避免浮点运算(纯整数运算高效)。

7.7.5.3 虚拟桌面(多显示器)

MOUSEEVENTF_VIRTUALDESK 设置时,使用 SM_CXVIRTUALSCREEN / SM_CYVIRTUALSCREEN(包含所有显示器的总尺寸)。

c 复制代码
ptCursor.x = pmi->dx * UserGetSystemMetrics(SM_CXVIRTUALSCREEN) >> 16;
ptCursor.y = pmi->dy * UserGetSystemMetrics(SM_CYVIRTUALSCREEN) >> 16;

这样在多显示器配置中,鼠标可以"跨越"不同显示器。


7.7.6 按钮状态与左右键交换

7.7.6.1 UserGetMouseButtonsState

win32ss/user/ntuser/mouse.c:21-32(file:///d:/reactos/win32ss/user/ntuser/mouse.c#L21-L32):

c 复制代码
WORD FASTCALL UserGetMouseButtonsState(VOID)
{
    WORD wRet = 0;

    wRet = IntGetSysCursorInfo()->ButtonsDown;

    if (IS_KEY_DOWN(gafAsyncKeyState, VK_SHIFT))   wRet |= MK_SHIFT;
    if (IS_KEY_DOWN(gafAsyncKeyState, VK_CONTROL)) wRet |= MK_CONTROL;

    return wRet;
}

返回当前 WM_MOUSEMOVEwParam 值:

  • MK_LBUTTON / MK_RBUTTON / MK_MBUTTON / MK_XBUTTON1 / MK_XBUTTON2
  • MK_SHIFT / MK_CONTROL(修饰键状态)。

7.7.6.2 IntFixMouseInputButtons

win32ss/user/ntuser/mouse.c:137-160(file:///d:/reactos/win32ss/user/ntuser/mouse.c#L137-L160):

c 复制代码
DWORD IntFixMouseInputButtons(DWORD dwFlags)
{
    DWORD dwNewFlags;

    if (!gspv.bMouseBtnSwap)
        return dwFlags;

    /* 左右键交换 */
    dwNewFlags = dwFlags & ~(MOUSEEVENTF_LEFTDOWN | MOUSEEVENTF_LEFTUP |
                             MOUSEEVENTF_RIGHTDOWN | MOUSEEVENTF_RIGHTUP);

    if (dwFlags & MOUSEEVENTF_LEFTDOWN)  dwNewFlags |= MOUSEEVENTF_RIGHTDOWN;
    if (dwFlags & MOUSEEVENTF_LEFTUP)    dwNewFlags |= MOUSEEVENTF_RIGHTUP;
    if (dwFlags & MOUSEEVENTF_RIGHTDOWN) dwNewFlags |= MOUSEEVENTF_LEFTDOWN;
    if (dwFlags & MOUSEEVENTF_RIGHTUP)   dwNewFlags |= MOUSEEVENTF_LEFTUP;

    return dwNewFlags;
}

gspv.bMouseBtnSwap 是系统参数(SPI_SETMOUSEBUTTONSWAP)。当为 TRUE 时,所有鼠标消息中的"左键"和"右键"含义互换------专为左撇子设计。


7.7.7 鼠标消息生成(co_MsqInsertMouseMessage)

7.7.7.1 函数签名

win32ss/user/ntuser/msgqueue.c:579-660+(file:///d:/reactos/win32ss/user/ntuser/msgqueue.c#L579-L660):

c 复制代码
VOID FASTCALL
co_MsqInsertMouseMessage(MSG* Msg, DWORD flags, ULONG_PTR dwExtraInfo, BOOL Hook);

参数

  • Msg:消息结构(hwnd/message/wParam/lParam)
  • flags:LLMHF_INJECTED(来自 SendInput)等
  • dwExtraInfo:附加信息
  • Hook:是否调用 WH_MOUSE_LL 钩子

7.7.7.2 完整流程

c 复制代码
VOID FASTCALL
co_MsqInsertMouseMessage(MSG* Msg, DWORD flags, ULONG_PTR dwExtraInfo, BOOL Hook)
{
    MSLLHOOKSTRUCT MouseHookData;
    PWND pwnd, pwndDesktop;
    HDC hdcScreen;
    PTHREADINFO pti;
    PUSER_MESSAGE_QUEUE MessageQueue;
    PSYSTEM_CURSORINFO CurInfo;

    Msg->time = EngGetTickCount32();

    // 1. 构造 MSLLHOOKSTRUCT
    MouseHookData.pt.x = LOWORD(Msg->lParam);
    MouseHookData.pt.y = HIWORD(Msg->lParam);
    switch (Msg->message)
    {
        case WM_MOUSEWHEEL:
            MouseHookData.mouseData = MAKELONG(0, GET_WHEEL_DELTA_WPARAM(Msg->wParam));
            break;
        case WM_XBUTTONDOWN: case WM_XBUTTONUP: ...
            MouseHookData.mouseData = MAKELONG(0, HIWORD(Msg->wParam));
            break;
        default:
            MouseHookData.mouseData = 0;
    }
    MouseHookData.flags = flags;
    MouseHookData.time = Msg->time;
    MouseHookData.dwExtraInfo = dwExtraInfo;

    // 2. WH_MOUSE_LL 钩子
    if (Hook)
    {
        if (co_HOOK_CallHooks(WH_MOUSE_LL, HC_ACTION, Msg->message, (LPARAM)&MouseHookData))
            return;  // 钩子消费
    }

    // 3. 确定目标窗口
    pwndDesktop = UserGetDesktopWindow();
    if (!pwndDesktop) return;

    Msg->hwnd = IntGetCaptureWindow();  // 优先使用捕获窗口
    if (Msg->hwnd != NULL)
    {
        pwnd = UserGetWindowObject(Msg->hwnd);
    }
    else
    {
        // 命中测试:从屏幕坐标找到顶层窗口
        pwnd = IntTopLevelWindowFromPoint(Msg->pt.x, Msg->pt.y);
        if (pwnd) Msg->hwnd = UserHMGetHandle(pwnd);
    }

    // 4. 记录到鼠标历史
    if (Msg->message == WM_MOUSEMOVE)
    {
        // 更新 gMouseHistoryOfMoves
    }

    // 5. 投递到硬件消息队列
    pti = PsGetCurrentThreadWin32Thread();
    MessageQueue = pti->MessageQueue;
    CurInfo = IntGetSysCursorInfo();

    if (Msg->message == WM_MOUSEMOVE)
    {
        // 合并:检查是否已存在未分派的 WM_MOUSEMOVE
        PLIST_ENTRY Entry = MessageQueue->HardwareMessagesListHead.Flink;
        while (Entry != &MessageQueue->HardwareMessagesListHead)
        {
            PUSER_MESSAGE Current = CONTAINING_RECORD(Entry, USER_MESSAGE, ListEntry);
            if (Current->Msg.message == WM_MOUSEMOVE)
            {
                // 合并:更新现有消息的坐标
                Current->Msg.lParam = Msg->lParam;
                Current->Msg.wParam = Msg->wParam;
                Current->Msg.time = Msg->time;
                return;  // 不插入新消息
            }
            Entry = Entry->Flink;
        }
    }

    // 6. ptiSysLock 抢占
    if (!MessageQueue->ptiSysLock)
    {
        MessageQueue->ptiSysLock = pti;
        pti->pcti->CTI_flags |= CTI_THREADSYSLOCK;
    }
    if (MessageQueue->ptiSysLock != pti)
    {
        ERR("Thread Q is locked to ptiSysLock 0x%p pti 0x%p\n",...);
        return;
    }

    // 7. 投递硬件消息
    MsqPostMessage(pti, Msg, TRUE, QS_MOUSE, QF_MOUSEMOVED, dwExtraInfo);
}

7.7.7.3 鼠标消息的"合并"机制

高频的 WM_MOUSEMOVE(典型 1000 Hz 鼠标每秒 1000 次)如果不合并,消息队列会爆炸。ReactOS 采用"末尾合并":

c 复制代码
if (Msg->message == WM_MOUSEMOVE)
{
    PLIST_ENTRY Entry = MessageQueue->HardwareMessagesListHead.Flink;
    while (Entry != &MessageQueue->HardwareMessagesListHead)
    {
        PUSER_MESSAGE Current = CONTAINING_RECORD(Entry, USER_MESSAGE, ListEntry);
        if (Current->Msg.message == WM_MOUSEMOVE)
        {
            // 找到队列中已有的 WM_MOUSEMOVE
            Current->Msg.lParam = Msg->lParam;  // 更新坐标
            Current->Msg.wParam = Msg->wParam;  // 更新按钮状态
            Current->Msg.time = Msg->time;      // 更新时间
            return;  // 不插入新消息
        }
        Entry = Entry->Flink;
    }
}

效果 :无论鼠标移动多快,硬件消息队列中最多只有一个 WM_MOUSEMOVE。当 WndProc 取出并处理后,下一个 WM_MOUSEMOVE 会进入队列并重复此过程。

其他消息 (点击、滚轮)不合并------必须每个都投递。


7.7.8 鼠标捕获机制

7.7.8.1 捕获的语义

鼠标捕获(Capture)允许特定窗口 接收所有鼠标事件,即使鼠标光标已经移出该窗口

典型应用

  • 拖放(Drag & Drop) :按下左键后开始拖动,即使鼠标移出窗口也持续接收 WM_MOUSEMOVE
  • 绘图应用:画线时持续接收鼠标位置;
  • 自定义控件 :滚动条拖动、窗口调整大小(SC_MOVE / SC_SIZE)。

7.7.8.2 SetCapture / GetCapture / ReleaseCapture

c 复制代码
HWND SetCapture(HWND hWnd);     // 设置捕获
HWND GetCapture(VOID);          // 获取当前捕获窗口
BOOL ReleaseCapture(VOID);      // 释放捕获

7.7.8.3 捕获的实现

SetCapturewin32ss/user/ntuser/ntstubs.c(file:///d:/reactos/win32ss/user/ntuser/ntstubs.c) NtUserSetCapture

c 复制代码
HWND NtUserSetCapture(HWND hWnd)
{
    // 1. 验证窗口
    // 2. 验证当前线程拥有该窗口
    // 3. 记录到当前 MessageQueue
    return MsqSetStateWindow(PsGetCurrentThreadWin32Thread(), MSQ_STATE_CAPTURE, hWnd);
}

IntGetCaptureWindow (在 co_MsqInsertMouseMessage 中调用):

c 复制代码
Msg->hwnd = IntGetCaptureWindow();
if (Msg->hwnd != NULL)
{
    pwnd = UserGetWindowObject(Msg->hwnd);
}

效果 :当捕获窗口存在时,所有鼠标消息都路由到该窗口,忽略实际光标位置。

7.7.8.4 捕获的自动释放

捕获在以下情况自动释放

  • 窗口被销毁;
  • 线程被销毁;
  • 鼠标按键全部释放(WM_LBUTTONUP + WM_RBUTTONUP + WM_MBUTTONUP 都已投递)。

7.7.9 命中测试(WM_NCHITTEST)

7.7.9.1 命中测试的作用

命中测试(Hit-Test)决定鼠标光标在窗口的哪个区域 ,并将该信息(HTCLIENT、HTBORDER、HTCAPTION 等)通过 WM_NCHITTEST 消息发送给窗口。

HT 标志*:

标志 区域
HTCLIENT 1 客户区
HTCAPTION 2 标题栏
HTBORDER 5, 18-21 窗口边框
HTSYSMENU 3 系统菜单
HTMINBUTTON 8 最小化按钮
HTMAXBUTTON 9 最大化按钮
HTCLOSE 20 关闭按钮
HTHSCROLL / HTVSCROLL 6, 7 滚动条
HTMENU 5 菜单
HTERROR -2 错误区域(触发 WM_GETDLGCODE
HTNOWHERE 0 不在任何窗口

7.7.9.2 co_WinPosWindowFromPoint

win32ss/user/ntuser/winpos.c(file:///d:/reactos/win32ss/user/ntuser/winpos.c) 中定义:

c 复制代码
PWND co_WinPosWindowFromPoint(POINT pt, USHORT *pHitTest, BOOL bIgnorePopup);

流程

  1. 从桌面窗口开始递归查找;
  2. 对每个窗口:测试点是否在窗口矩形内;
  3. 如果是子窗口,递归测试子窗口;
  4. 返回最顶层包含该点的窗口 + 命中测试结果。

7.7.9.3 WM_NCHITTEST 完整流程

co_IntProcessMouseMessage 中:

c 复制代码
// 1. 命中测试
if (MessageQueue->spwndCapture)
{
    hittest = HTCLIENT;
    pwndMsg = MessageQueue->spwndCapture;
}
else
{
    pwndMsg = co_WinPosWindowFromPoint(NULL, &msg->pt, &hittest, FALSE);
}

// 2. 跨线程检查
if (pwndMsg == NULL || pwndMsg->head.pti->MessageQueue != MessageQueue)
{
    IntSystemSetCursor(SYSTEMCUR(ARROW));
    *RemoveMessages = TRUE;
    return FALSE;
}

// 3. NC/Client 消息分类
if (message != WM_MOUSEWHEEL)  // 滚轮没有 NC 版本
{
    if (hittest != HTCLIENT)
    {
        message += WM_NCMOUSEMOVE - WM_MOUSEMOVE;  // WM_MOUSEMOVE → WM_NCMOUSEMOVE
        msg->wParam = hittest;
    }
    else
    {
        // 转换坐标:屏幕 → 窗口客户区
        if (!(MessageQueue->MenuOwner))
        {
            pt.x += pwndDesktop->rcClient.left - pwndMsg->rcClient.left;
            pt.y += pwndDesktop->rcClient.top - pwndMsg->rcClient.top;
        }
    }
}

7.7.9.4 关键设计:WM_NCHITTEST 显式消息

WM_NCHITTEST 作为一个独立的窗口消息,让应用程序自定义命中测试:

c 复制代码
case WM_NCHITTEST:
    return HTCAPTION;  // 把整个客户区当作标题栏(用于可拖动的窗口)

IntDefWindowProc 的默认处理见 7.8 节。


7.7.10 鼠标移动消息合并(IntCoalesceMouseMove)

7.7.10.1 函数签名

win32ss/user/ntuser/msgqueue.c:550-577(file:///d:/reactos/win32ss/user/ntuser/msgqueue.c#L550-L577):

c 复制代码
VOID FASTCALL
IntCoalesceMouseMove(PTHREADINFO pti);

7.7.10.2 实现

c 复制代码
VOID FASTCALL
IntCoalesceMouseMove(PTHREADINFO pti)
{
    MSG Msg;

    // 强制时间戳更新
    if (gdwMouseMoveTimeStamp == 0)
        gdwMouseMoveTimeStamp = EngGetTickCount32();

    // 构造 WM_MOUSEMOVE 消息
    Msg.hwnd    = NULL;
    Msg.message = WM_MOUSEMOVE;
    Msg.wParam  = 0;
    Msg.lParam  = MAKELONG(gpsi->ptCursor.x, gpsi->ptCursor.y);
    Msg.time    = gdwMouseMoveTimeStamp;
    Msg.pt      = gpsi->ptCursor;

    // 投递(合并到队列末尾的 WM_MOUSEMOVE)
    MsqPostMouseMove(pti, &Msg, gdwMouseMoveExtraInfo);

    gdwMouseMoveTimeStamp = 0;
    pti->MessageQueue->QF_flags &= ~QF_MOUSEMOVED;
}

7.7.10.3 调用时机

IntCoalesceMouseMove 在以下情况被调用:

  • 线程空闲时(消息循环中 QS_MOUSEMOVE 被设置但无待处理消息);
  • 当前消息队列被解锁(ptiSysLock 释放)时。

目的 :确保最终 会有一条 WM_MOUSEMOVE 反映光标的最新位置,即使期间有多次更新被合并。


7.7.11 双击/单击锁定

7.7.11.1 双击判定

win32ss/user/ntuser/msgqueue.c:1575-1615(file:///d:/reactos/win32ss/user/ntuser/msgqueue.c#L1575-L1615) co_IntProcessMouseMessage

c 复制代码
if ((msg->message == WM_LBUTTONDOWN) ||
    (msg->message == WM_RBUTTONDOWN) ||
    (msg->message == WM_MBUTTONDOWN) ||
    (msg->message == WM_XBUTTONDOWN))
{
    BOOL update = *RemoveMessages;

    // 必须窗口有 CS_DBLCLKS 类样式
    if ((MessageQueue->MenuOwner || MessageQueue->MoveSize) ||
        hittest != HTCLIENT ||
        (pwndMsg->pcls->style & CS_DBLCLKS))
    {
        if ((msg->message == clk_msg.message) &&
            (msg->hwnd == clk_msg.hwnd) &&
            (msg->message != WM_XBUTTONDOWN || GET_XBUTTON_WPARAM(msg->wParam) == GET_XBUTTON_WPARAM(clk_msg.wParam)) &&
            ((msg->time - clk_msg.time) < (ULONG)gspv.iDblClickTime) &&
            (abs(msg->pt.x - clk_msg.pt.x) < UserGetSystemMetrics(SM_CXDOUBLECLK)/2) &&
            (abs(msg->pt.y - clk_msg.pt.y) < UserGetSystemMetrics(SM_CYDOUBLECLK)/2))
        {
            // 转换为双击消息
            message += (WM_LBUTTONDBLCLK - WM_LBUTTONDOWN);
            if (update)
            {
                MessageQueue->msgDblClk.message = 0;  // 清除双击条件
                update = FALSE;
            }
        }
    }

    if (update) MessageQueue->msgDblClk = *msg;  // 记录本次点击
}

7.7.11.2 双击判定的 4 个条件

条件 检查 备注
消息相同 msg->message == clk_msg.message 都是左键,或都是右键
窗口相同 msg->hwnd == clk_msg.hwnd 不能跨窗口
时间间隔 (msg->time - clk_msg.time) < iDblClickTime 典型 500ms
位置接近 abs(msg->pt - clk_msg.pt) < SM_CXDOUBLECLK/2 典型 4 像素

XButton 特殊 :需要比较 wParam 中的 XButton 编号(1 或 2)。

7.7.11.3 CS_DBLCLKS 类样式

只有注册窗口类时指定 CS_DBLCLKS,才会产生 WM_*BUTTONDBLCLK 消息:

c 复制代码
WNDCLASS wc = { ... };
wc.style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS;  // 启用双击
RegisterClass(&wc);

7.7.11.4 单击锁定(ClickLock)

单击锁定允许拖动时不需要持续按下按钮

c 复制代码
if (gspv.bMouseClickLock)
{
    BOOL IsClkLck = FALSE;

    if (msg->message == WM_LBUTTONUP)
    {
        // 检查是否已超过 ClickLock 时间
        IsClkLck = ((msg->time - CurInfo->ClickLockTime) >= gspv.dwMouseClickLockTime);
        if (IsClkLck && !CurInfo->ClickLockActive)
            CurInfo->ClickLockActive = TRUE;  // 激活 ClickLock
    }
    else if (msg->message == WM_LBUTTONDOWN)
    {
        if (CurInfo->ClickLockActive)
        {
            // 第二次按下时取消锁定
            IsClkLck = TRUE;
            CurInfo->ClickLockActive = FALSE;
        }
        CurInfo->ClickLockTime = msg->time;
    }

    if (IsClkLck)
    {
        *RemoveMessages = TRUE;
        return FALSE;  // 消费消息
    }
}

使用场景:患有运动障碍的用户可以用"按下 + 等待 + 拖动 + 再次按下"代替"持续按住拖动"。

7.7.11.5 gspv 系统参数

c 复制代码
gspv.iDblClickTime            // 双击时间(毫秒)
gspv.bMouseClickLock          // 单击锁定启用
gspv.dwMouseClickLockTime     // 单击锁定时间
gspv.bMouseBtnSwap            // 左右键交换

通过 SystemParametersInfo 设置。


7.7.12 鼠标 hover/leave 跟踪

7.7.12.1 IntTrackMouseMove

win32ss/user/ntuser/msgqueue.c:1419-1470(file:///d:/reactos/win32ss/user/ntuser/msgqueue.c#L1419-L1470):

c 复制代码
VOID FASTCALL
IntTrackMouseMove(PWND pwndTrack, PDESKTOP pDesk, PMSG msg, USHORT hittest)
{
    if (pDesk->spwndTrack != pwndTrack ||
        msg->message != WM_MOUSEMOVE ||
        pDesk->htEx != hittest)
    {
        // 1. 跨窗口/跨边界
        if (pDesk->spwndTrack != pwndTrack ||
            (pDesk->htEx == HTCLIENT) ^ (hittest == HTCLIENT))
        {
            if (pDesk->dwDTFlags & DF_TME_LEAVE)
                UserPostMessage(UserHMGetHandle(pDesk->spwndTrack),
                                (pDesk->htEx != HTCLIENT) ? WM_NCMOUSELEAVE : WM_MOUSELEAVE,
                                0, 0);
            if (pDesk->dwDTFlags & DF_TME_HOVER)
                IntKillTimer(pDesk->spwndTrack, ID_EVENT_SYSTIMER_MOUSEHOVER, TRUE);
            pDesk->dwDTFlags &= ~(DF_TME_LEAVE | DF_TME_HOVER);
        }
        pDesk->spwndTrack = pwndTrack;
        pDesk->htEx = hittest;
    }

    // 2. 重新启动 hover 计时器
    if (pDesk->spwndTrack == pwndTrack &&
        (msg->message != WM_MOUSEMOVE || !RECTL_bPointInRect(&pDesk->rcMouseHover, msg->pt.x, msg->pt.y)) &&
        pDesk->dwDTFlags & DF_TME_HOVER)
    {
        IntSetTimer(pDesk->spwndTrack, ID_EVENT_SYSTIMER_MOUSEHOVER,
                    pDesk->dwMouseHoverTime, SystemTimerProc, TMRF_SYSTEM);
        RECTL_vSetRect(&pDesk->rcMouseHover, ...);
    }
}

7.7.12.2 三个状态标志

c 复制代码
#define DF_TME_HOVER     0x00000001   // TrackMouseEvent(TME_HOVER) 启用
#define DF_TME_LEAVE     0x00000002   // TrackMouseEvent(TME_LEAVE) 启用

7.7.12.3 跟踪流程

复制代码
1. 应用调用 TrackMouseEvent(TME_HOVER | TME_LEAVE)
2. 系统记录请求到 pDesk->dwDTFlags
3. 鼠标首次进入窗口:
   └─► IntTrackMouseMove 检测到 spwndTrack 变化
       └─► 设置 hover 计时器
4. 鼠标在窗口内移动:
   └─► 计时器重启
5. 鼠标静止超过 hover 时间:
   └─► SystemTimerProc 触发 WM_MOUSEHOVER
6. 鼠标离开窗口:
   └─► IntTrackMouseMove 检测到 spwndTrack 变化
       └─► 投递 WM_MOUSELEAVE
       └─► 清除 TME_HOVER | TME_LEAVE 标志

7.7.12.4 应用层 API

c 复制代码
BOOL TrackMouseEvent(LPTRACKMOUSEEVENT lpEvent);

参数:

  • dwFlags = TME_HOVER:跟踪 hover(鼠标静止 N ms 触发 WM_MOUSEHOVER);
  • dwFlags = TME_LEAVE:跟踪 leave(鼠标离开窗口触发 WM_MOUSELEAVE);
  • dwFlags = TME_CANCEL:取消跟踪;
  • dwHoverTime:hover 时间(默认 SPI_GETMOUSEHOVERTIME)。

7.7.12.5 SystemTimerProc 回调

c 复制代码
VOID CALLBACK SystemTimerProc(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime)
{
    if (idEvent == ID_EVENT_SYSTIMER_MOUSEHOVER)
    {
        // 投递 WM_MOUSEHOVER
        UserPostMessage(hwnd, WM_MOUSEHOVER, 0, 0);
    }
}

7.7.13 设计哲学问答

Q1:为什么鼠标消息要进入"硬件消息队列"并由 ptiSysLock 独占?

A避免高频 WM_MOUSEMOVE 在多线程间分配导致状态混乱

  • 场景 :用户拖动一个跨线程的窗口,鼠标快速移动。WM_MOUSEMOVE 持续产生。
  • 如果不独占:消息可能被分配到不同线程,UI 更新顺序混乱(窗口 A 移动到位置 100,另一个线程又移动到位置 50,状态不一致)。
  • 独占机制 :通过 ptiSysLock 标识当前正在处理硬件消息的线程 ,其他线程不能同时读取硬件消息。这保证了消息处理的顺序一致性
  • 实现co_MsqPeekHardwareMessageMessageQueue->ptiSysLock 被其他线程持有时立即返回 FALSE,调用方继续等待。

Q2:为什么高频 WM_MOUSEMOVE 需要合并(Coalesce)?

A避免消息队列爆炸 + UI 跟不上

  • 场景 :1000 Hz 鼠标每秒产生 1000 个 WM_MOUSEMOVE
  • 不合并
    • 消息队列被 WM_MOUSEMOVE 占满;
    • 应用程序的 WndProc 处理速度跟不上输入速度(每秒 1000 次 Move 渲染);
    • CPU 100% 占用。
  • 合并后
    • 队列中最多一个 WM_MOUSEMOVE
    • 应用程序可以以自己的节奏处理(通常 60 FPS 即每 16ms 处理一次);
    • 最新的鼠标位置总会反映(合并更新 lParam)。

类比:邮件合并------你不会为每个收件人单独寄信,而是把"今天的待办"合并成一份日报。

Q3:为什么命中测试(WM_NCHITTEST)要暴露给应用程序?

A灵活的自定义交互

c 复制代码
case WM_NCHITTEST:
    // 整个客户区都可拖动
    if (PtInRect(&rcTitleBar, pt))
        return HTCAPTION;
    return HTCLIENT;

典型用途

  • 自绘标题栏 :现代应用常用 WM_NCHITTEST 返回 HTCAPTION 让整个窗口可拖动;
  • 自定义窗口形状:异形窗口(如圆形、异形)需要自定义命中测试;
  • Dock 区域:自定义"对接区"的命中行为;
  • 热区:在客户区内自定义"可点击区域"。

如果命中测试是固定的(不可重写),这些高级交互就无法实现。

Q4:为什么 WM_NCMOUSEMOVE 和 WM_MOUSEMOVE 是两套消息?

A客户区 vs 非客户区的语义完全不同

维度 WM_MOUSEMOVE WM_NCMOUSEMOVE
区域 客户区(应用内容) 标题栏、边框、菜单等系统区域
lParam 客户区坐标 屏幕坐标
wParam 按钮状态(MK_*) 命中测试结果(HT*)
应用期望 应用可以处理(重绘、交互) 应用通常不处理,让 DefWindowProc 处理(拖动、缩放)
滚动条交互 不会发送 滚动条拖动时发送

两套消息让应用可以分别处理

  • WM_MOUSEMOVE:绘画应用、拖放、游戏;
  • WM_NCMOUSEMOVE:通常不处理,让系统处理"用户点击关闭按钮"等系统交互。

Q5:为什么捕获窗口(SetCapture)会自动释放?

A防止应用"忘记"释放导致鼠标卡死

场景 :应用 SetCapture(hWnd) 后崩溃 / 死循环 / 忘记 ReleaseCapture()

  • 鼠标所有事件都被发送到该窗口;
  • 窗口永远收不到 WM_LBUTTONUP(因为鼠标已经在它上面);
  • 整个系统看起来"鼠标卡住了"。

自动释放机制 (参见 MsgQueue::spwndCapture):

  • 窗口销毁时释放
  • 线程销毁时释放
  • 所有按钮释放时释放 (投递 WM_LBUTTONUP 后)。

自动释放通过 MsqReleaseCapture 实现,保证系统不会因为单个应用的 bug 而瘫痪。


总结

鼠标输入子系统是 Windows GUI 系统最复杂、消息量最大的 I/O 路径。本节介绍了:

  1. RIT 架构:原始输入线程统一处理所有鼠标设备;
  2. MOUSE_INPUT_DATA 转换 :原始数据 → MOUSEINPUT 标志;
  3. 位置计算:相对/绝对/虚拟桌面三种模式;
  4. 按钮状态:MK_* 标志 + 左右键交换;
  5. 消息生成co_MsqInsertMouseMessage 完整流程;
  6. 消息合并IntCoalesceMouseMove + 队列末尾合并;
  7. 捕获机制SetCapture / IntGetCaptureWindow
  8. 命中测试co_WinPosWindowFromPoint + WM_NCHITTEST
  9. 双击判定:4 条件(消息/窗口/时间/位置);
  10. 单击锁定:残障辅助功能;
  11. hover/leave 跟踪IntTrackMouseMove + TrackMouseEvent
  12. LL 钩子:内核态实现,无需 DLL 注入。

核心要点回顾

  1. co_MsqInsertMouseMessage 是鼠标消息的核心入口;
  2. WM_MOUSEMOVE 通过"末尾合并"避免消息爆炸;
  3. 鼠标捕获是拖放操作的基础;
  4. 命中测试(WM_NCHITTEST)让应用可自定义窗口交互;
  5. 双击判定需要窗口类支持 CS_DBLCLKS
  6. hover/leave 跟踪通过桌面对象的状态机实现;
  7. ptiSysLock 保证硬件消息的顺序处理。

本章代码索引

文件 内容
win32ss/user/ntuser/mouse.c(file:///d:/reactos/win32ss/user/ntuser/mouse.c) UserProcessMouseInput、UserGetMouseButtonsState、UserSendMouseInput、IntFixMouseInputButtons、IntFixMouseInputButtons、gMouseHistoryOfMoves、gcMouseHistoryOfMoves
win32ss/user/ntuser/msgqueue.c(file:///d:/reactos/win32ss/user/ntuser/msgqueue.c) co_MsqInsertMouseMessage、co_IntProcessMouseMessage、co_MsqPeekHardwareMessage、IntCoalesceMouseMove、IntTrackMouseMove、MsqSendParentNotify、co_MsqPostMouseMove、ptiSysLock
win32ss/user/ntuser/input.c(file:///d:/reactos/win32ss/user/ntuser/input.c) UserProcessMouseInput(RIT 入口)
win32ss/user/ntuser/nonclient.c(file:///d:/reactos/win32ss/user/ntuser/nonclient.c) DefWndDoSizeMove、WM_NCHITTEST 默认处理
win32ss/user/ntuser/winpos.c(file:///d:/reactos/win32ss/user/ntuser/winpos.c) co_WinPosWindowFromPoint
win32ss/user/ntuser/ntstubs.c(file:///d:/reactos/win32ss/user/ntuser/ntstubs.c) NtUserSetCapture、NtUserGetCapture、NtUserReleaseCapture、NtUserTrackMouseEvent
ntoskrnl/mouclass/mouclass.c(file:///d:/reactos/ntoskrnl/mouclass/mouclass.c) mouclass 鼠标类驱动(IRP_MJ_READ 队列)
win32ss/user/user32/windows/input.c(file:///d:/reactos/win32ss/user/user32/windows/input.c) user32 SendInput、mouse_event 用户态 API
相关推荐
caimouse2 小时前
Reactos 第 7 章 视窗报文 — 7.2 视窗报文的接收
windows
caimouse2 小时前
Reactos 第 8 章 结构化异常处理 — 8.3 用户空间的结构化异常处理
windows
caimouse2 小时前
Reactos 第 9 章 设备驱动 — 9.6 中断处理
网络·windows
caimouse2 小时前
Reactos 第 7 章 视窗报文 — 7.6 键盘输入线程
windows
yinhunzw3 小时前
Claude code windows 安装
windows
七仔啊3 小时前
windows server 2022 部署前后端项目
windows
caimouse4 小时前
Reactos 第 7 章 视窗报文 — 7.4 用户空间的外挂函数
windows
辣香牛肉面4 小时前
Windows发票工具大全
windows·发票助手
caimouse4 小时前
Reactos 第 9 章 设备驱动 — 9.3 DPC函数及其执行
windows