ReactOS 窗口系统分析(22):鼠标 — mouse.c

ReactOS 窗口系统分析(22):鼠标 --- mouse.c

本文是"窗口系统三大主线之鼠标控制"系列的第 22 册,核心回答:窗口怎么样实现鼠标控制

即:鼠标硬件输入如何变成发给特定窗口的 WM_MOUSE* 消息,如何命中窗口、如何判定双击、

如何捕获、如何跟踪悬停/离开、如何合并移动消息。

核心源码:win32ss/user/ntuser/mouse.c(643 行,鼠标输入主文件)、

win32ss/user/ntuser/msgqueue.c(鼠标消息落地/入队/合并)、

win32ss/user/ntuser/winpos.c(命中测试)、win32ss/user/ntuser/focus.c(捕获/激活联动)、

win32ss/user/user32/windows/input.c(用户态鼠标 API 封装)。

关联分册:_21(input.c/event.c 输入子系统)、_20(cursoricon.c 光标图标)、_8(msgqueue.c 消息队列)、_16(focus.c 焦点激活)。


目录

  1. 概述
  2. 设计动机
  3. 核心数据结构
  4. [鼠标输入主处理 UserProcessMouseInput](#鼠标输入主处理 UserProcessMouseInput)
  5. [SendInput 注入 UserSendMouseInput](#SendInput 注入 UserSendMouseInput)
  6. 键状态与按钮交换
  7. 命中测试
  8. [消息落地 co_IntProcessMouseMessage](#消息落地 co_IntProcessMouseMessage)
  9. [消息入队 co_MsqInsertMouseMessage](#消息入队 co_MsqInsertMouseMessage)
  10. [鼠标移动合并 IntCoalesceMouseMove](#鼠标移动合并 IntCoalesceMouseMove)
  11. 双击判定
  12. 鼠标捕获
  13. [TrackMouseEvent 跟踪](#TrackMouseEvent 跟踪)
  14. [鼠标移动轨迹 GetMouseMovePointsEx](#鼠标移动轨迹 GetMouseMovePointsEx)
  15. 鼠标输入处理全流程
  16. 用户态封装与系统参数
  17. 与其他模块的关系
  18. 调用链总览
  19. 源码索引

1. 概述

"鼠标控制"这条主线的目标是回答:窗口怎么样实现鼠标控制 。鼠标控制的核心是把物理世界

的鼠标动作(移动、按键、滚轮)翻译成 Win32 窗口消息体系中的 WM_MOUSE* 消息,并把它

准确地投递给"应该接收"的那个窗口。

从物理设备到应用窗口过程,鼠标输入经过如下流水线(本册的讨论范围是加粗部分):

复制代码
鼠标硬件中断(i8042prt / mouclass / HID)
  → 输入驱动排队原始数据 MOUSE_INPUT_DATA(ntddmou.h)
  → RawInput 线程(input.c: RawInputThreadMain,LOW_REALTIME 优先级)
      → UserProcessMouseInput(mouse.c L39)【本册】
          ├── 数据转换:MOUSE_INPUT_DATA → MOUSEINPUT(MOUSEEVENTF_* 标志)
          ├── UserSendMouseInput(mouse.c L167)【本册】
          │     ├── 光标位置计算(相对/绝对/虚拟桌面)
          │     ├── 维护 GetMouseMovePointsEx FIFO 历史
          │     ├── UserSetCursorPos 更新光标(cursoricon.c)
          │     ├── 更新异步键状态 gafAsyncKeyState
          │     ├── 构造 MSG(WM_MOUSEMOVE / WM_LBUTTONDOWN / ...)
          │     └── co_MsqInsertMouseMessage 入队(msgqueue.c L580)【本册】
          │           ├── WH_MOUSE_LL 低级钩子
          │           ├── 命中测试:捕获窗口优先,否则窗口树遍历
          │           ├── 光标形状切换(GreSetPointerShape/GreMovePointer)
          │           └── 队列置 QS_MOUSEMOVE / QS_MOUSEBUTTON 并唤醒
  → 应用 GetMessage / PeekMessage
      → co_MsqPeekHardwareMessage(msgqueue.c L1949)
          → co_IntProcessMouseMessage(msgqueue.c L1483)【本册】
                ├── 二次命中测试(co_WinPosWindowFromPoint)
                ├── 双击判定(msgDblClk + iDblClickTime + SM_CX/YDOUBLECLK)
                ├── 客户区/非客户区坐标与消息换算(+WM_NCMOUSEMOVE-WM_MOUSEMOVE)
                ├── WM_PARENTNOTIFY、WM_MOUSEACTIVATE、WM_SETCURSOR 联动
                └── WH_JOURNALRECORD / WH_MOUSE 钩子
  → 应用窗口过程收到 WM_MOUSE* 消息(lParam 坐标、wParam 键状态)

一次鼠标输入的"可见结果"有两个:一是消息 (进入某个窗口的消息队列,最终到达窗口过程),

二是光标 (屏幕上的指针形状与位置)。这两者都在 UserSendMouseInput /

co_MsqInsertMouseMessage 中同步完成------这也是鼠标与键盘处理的最大区别:键盘输入只跟

"焦点窗口"绑定,而鼠标输入要随光标所在位置在窗口之间"漂移",因此每一帧都要重新做命中测试。


2. 设计动机

2.1 从"硬件事件"到"窗口消息"的映射

鼠标硬件只产生坐标增量与按键位(MOUSE_INPUT_DATA:LastX/LastY/ButtonFlags/ButtonData),

它不知道"窗口"为何物。窗口系统需要把这种原始的、无目标的输入翻译成面向窗口 的、

带丰富语义的消息:

硬件事件 生成的消息 说明
位移 WM_MOUSEMOVE 鼠标在窗口内移动,wParam 带键状态
左键按下/抬起 WM_LBUTTONDOWN / WM_LBUTTONUP 双击时按下变 WM_LBUTTONDBLCLK
右键按下/抬起 WM_RBUTTONDOWN / WM_RBUTTONUP 同上
中键按下/抬起 WM_MBUTTONDOWN / WM_MBUTTONUP 同上
侧键按下/抬起 WM_XBUTTONDOWN / WM_XBUTTONUP XBUTTON1/2 在 wParam 高字
滚轮 WM_MOUSEWHEEL 滚动量在 wParam 高字(WHEEL_DELTA=120 的倍数)
命中非客户区 WM_NCLBUTTONDOWN wParam 变为 HT* 命中码,坐标为屏幕坐标
悬停/离开 WM_MOUSEHOVER / WM_MOUSELEAVE 由 TrackMouseEvent 按需开启

2.2 为什么需要"命中测试"

键盘消息发给"焦点窗口",语义确定;鼠标消息则必须回答"这个点(屏幕坐标)在哪个窗口上"。

由于窗口可以嵌套(子窗口)、重叠(Z 序)、镂空(hrgnClip 裁剪区域)、穿透(WS_EX_TRANSPARENT)、

禁用(WS_DISABLED)、最小化(WS_MINIMIZE),"点在哪"是一个需要遍历窗口树、综合多种样式

判定后才能回答的问题。命中测试的结果还附带一个命中码 (HTCLIENT/HTMAXBUTTON/HTSYSMENU...),

它决定消息走客户区还是非客户区、wParam 携带什么。

2.3 为什么需要"捕获"

鼠标按下后拖动(选择文本、拖拽文件、拖动滚动条 thumb、移动窗口标题栏)时,光标可能迅速

移出窗口客户区。如果消息仍然按"光标位置"定向,拖动过程会瞬间丢失。因此 Win32 提供

SetCapture:捕获期间所有 鼠标消息无条件发往捕获窗口,直到 ReleaseCapture

捕获存储在消息队列的 spwndCapture 字段,命中测试时优先于窗口树遍历。

2.4 为什么需要"双击判定"

双击(Double Click)在硬件上就是"两次快速点击",但窗口需要把它与"两次独立单击"区分开,

否则按钮控件无法实现"双击打开"语义。判定依据三要素:时间 (小于系统双击时间)、

距离 (两点均落在双击方框内)、目标 (同一窗口、同一消息、同一 X 键)。

双击消息本身并不单独产生------它是"第二次按下"被重新解释为 WM_*BUTTONDBLCLK 的结果。

2.5 为什么需要"移动合并"

鼠标移动事件产生频率极高(尤其是 USB 高采样率鼠标),若每个原始输入都入队一个

WM_MOUSEMOVE,队列会被淹没,应用来不及处理,反而造成"迟滞"。Windows 的做法是

合并 :队列尾部若已有一个未消费的 WM_MOUSEMOVE,就用新位置覆盖它------应用永远只

看到"最新的位置",中间的中间位置被丢弃。这就是 IntCoalesceMouseMove /

MsqPostMouseMove 的意义。

2.6 为什么需要"注入通道"

SendInput/mouse_event(用户态)与 NtUserSendInput(系统调用)允许应用程序、自动化

脚本、远程桌面产生与硬件同等的鼠标输入。这条通道必须与硬件通道共用同一套 处理代码

UserSendMouseInput),但要把来源标记为"注入"(bInjected = TRUE,低级钩子

LLMHF_INJECTED),以便应用与安全软件区分真实用户操作与模拟输入。


3. 核心数据结构

3.1 MOUSE_INPUT_DATA(硬件原始数据)--- sdk/include/psdk/ntddmou.h L76

鼠标驱动(mouclass)通过 ZwReadFile 提供给 win32k 的原始数据块:

c 复制代码
typedef struct _MOUSE_INPUT_DATA {
  USHORT UnitId;          // 设备单元号
  USHORT Flags;           // MOUSE_MOVE_ABSOLUTE / MOUSE_VIRTUAL_DESKTOP / ...
  union {
    ULONG Buttons;        // 合并视图
    struct {
      USHORT ButtonFlags; // 按键位标志(见下)
      USHORT ButtonData;  // 滚轮增量(MOUSE_WHEEL 时)
    } DUMMYSTRUCTNAME;
  } DUMMYUNIONNAME;
  ULONG RawButtons;       // 原始按键位
  LONG LastX;             // X 位移(相对)或绝对坐标
  LONG LastY;             // Y 位移(相对)或绝对坐标
  ULONG ExtraInformation; // 附加信息(应用可经 dwExtraInfo 传递)
} MOUSE_INPUT_DATA, *PMOUSE_INPUT_DATA;

ButtonFlags 各位(ntddmou.h L46-L57):

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   // XButton1
#define MOUSE_BUTTON_4_UP                 0x0080
#define MOUSE_BUTTON_5_DOWN               0x0100   // XButton2
#define MOUSE_BUTTON_5_UP                 0x0200
#define MOUSE_WHEEL                       0x0400   // ButtonData 为滚轮增量
#define MOUSE_HWHEEL                      0x0800   // 水平滚轮(本册未用)

Flags 各位(ntddmou.h L67-L74):MOUSE_MOVE_RELATIVE(0,相对)、MOUSE_MOVE_ABSOLUTE(1,

绝对坐标,LastX/LastY 为 0-65535 归一化值)、MOUSE_VIRTUAL_DESKTOP(0x02,虚拟桌面坐标系)、

MOUSE_ATTRIBUTES_CHANGED(0x04)、MOUSE_MOVE_NOCOALESCE(0x08,Vista+ 要求不合并)。

注意:ButtonData(滚轮增量)与 X 键数据共用 mouseData 通道,因此 UserProcessMouseInput

必须把 X 键与滚轮拆成两次 UserSendMouseInput 调用------一次装不进一个 MOUSEINPUT

3.2 MOUSEINPUT(注入/统一输入结构)

UserProcessMouseInput 先把 MOUSE_INPUT_DATA 转换成标准的 MOUSEINPUT

win32ss/include/win32k.h,由 winuser.h 的 INPUT 联合体成员):

c 复制代码
typedef struct tagMOUSEINPUT {
  LONG dx;                  // 相对位移或绝对坐标
  LONG dy;
  DWORD mouseData;          // 滚轮增量(MOUSEEVENTF_WHEEL)或 XBUTTON1/2
  DWORD dwFlags;            // MOUSEEVENTF_* 标志
  DWORD time;               // 时间戳(0 表示取当前)
  ULONG_PTR dwExtraInfo;    // 附加信息
} MOUSEINPUT;

dwFlags 对应关系:MOUSEEVENTF_MOVE(位移)、MOUSEEVENTF_LEFTDOWN/LEFTUP

MOUSEEVENTF_RIGHTDOWN/RIGHTUPMOUSEEVENTF_MIDDLEDOWN/MIDDLEUP

MOUSEEVENTF_XDOWN/XUPMOUSEEVENTF_WHEELMOUSEEVENTF_ABSOLUTE

MOUSEEVENTF_VIRTUALDESK。这样一个统一结构让"硬件输入"与"SendInput 注入"走同一条

处理函数 UserSendMouseInput

3.3 消息的 wParam:MK_* 键状态位

鼠标消息的 wParam 高字/低字携带"按下时哪些键处于按下状态"。基础位(winuser.h):

c 复制代码
#define MK_LBUTTON          0x0001   // 左键按下
#define MK_RBUTTON          0x0002   // 右键按下
#define MK_SHIFT            0x0004   // Shift 按下
#define MK_CONTROL          0x0008   // Ctrl 按下
#define MK_MBUTTON          0x0010   // 中键按下
#define MK_XBUTTON1         0x0020   // 侧键1
#define MK_XBUTTON2         0x0040   // 侧键2

WM_XBUTTONDOWN/UP/DBLCLK 的 wParam 布局特殊:低字是键状态位,高字是 XBUTTON1/XBUTTON2

GET_XBUTTON_WPARAM 宏取高字);WM_MOUSEWHEEL 的 wParam 低字是键状态位,高字是滚轮

增量(GET_WHEEL_DELTA_WPARAM,WHEEL_DELTA=120)。这两处特殊布局在

co_MsqInsertMouseMessage 的钩子数据装配(L595-L611)与双击判定(L1604)中都有专门处理。

3.4 双击相关的系统参数 --- sysparams.h / sysparams.c

gspv(全局 SPIVALUES)中的鼠标字段(sysparams.h L74-L95):

c 复制代码
    /* Mouse */
    CURSORACCELERATION_INFO caiMouse;   // 指针加速
    MOUSEKEYS mousekeys;                // 鼠标键辅助
    BOOL bMouseClickLock;               // 单击锁定(ClickLock)
    BOOL bMouseCursorShadow;            // 指针阴影
    DWORD dwMouseClickLockTime;         // 单击锁定时间
    BOOL bMouseSonar;
    BOOL bMouseVanish;
    BOOL bMouseBtnSwap;                 // 左右键交换
    BOOL bSmoothScrolling;              // 平滑滚动
    INT iMouseSpeed;
    INT iMouseHoverWidth;               // 悬停方框宽
    INT iMouseHoverHeight;              // 悬停方框高
    INT iMouseHoverTime;                // 悬停判定时间(HOVER_DEFAULT)
    INT iDblClickWidth;                 // 双击方框宽(对应 SM_CXDOUBLECLK)
    INT iDblClickHeight;                // 双击方框高(对应 SM_CYDOUBLECLK)
    INT iDblClickTime;                  // 双击时间(毫秒,默认 500)
    INT iDragWidth;  INT iDragHeight;   // 拖动判定方框
    INT iMouseTrails;                   // 指针轨迹
    INT iWheelScrollLines;              // 滚轮滚动行数

初始化(sysparams.c):

  • L214:if (gspv.iDblClickTime == 0) gspv.iDblClickTime = 500;(系统默认 500ms)
  • L249:gspv.iDblClickTime = SpiLoadMouse(VAL_DBLCLKTIME, 500);(从注册表加载)
  • L942:case SPI_SETDOUBLECLICKTIME: return SpiSetInt(&gspv.iDblClickTime, uiParam, ...);
    (用户 SetDoubleClickTime 修改)

3.5 队列与桌面的鼠标状态

USER_MESSAGE_QUEUE(msgqueue.h,见 _8 册)中与鼠标相关的字段:

c 复制代码
    PTHREADINFO ptiMouse;        // 队列鼠标归属线程(attach 组共享)
    LIST_ENTRY HardwareMessagesListHead; // 硬件消息链表
    MSG msgDblClk;               // 上次"潜在双击"消息(时间/坐标/消息号/hwnd)
    PWND spwndCapture;           // 捕获窗口(捕获期间鼠标消息定向于此)
    BYTE afKeyState[256*2/8];    // 每键 2 位键状态(down + lock)
    INT iCursorLevel;            // 光标显隐计数(ShowCursor)
    PCURICON_OBJECT CursorObject;// 当前光标对象
    DWORD QF_flags;              // 队列标志(QF_MOUSEMOVED 等)

DESKTOP(desktop.h L12-L50)中与鼠标跟踪相关的字段:

c 复制代码
    DWORD dwDTFlags;             // DF_TME_HOVER(0x400) / DF_TME_LEAVE(0x800)
    PWND spwndTrack;             // 当前被跟踪窗口(TrackMouseEvent)
    DWORD htEx;                  // 当前命中码(用于客户/非客户判断)
    RECT rcMouseHover;           // 悬停方框(当前光标位置 ± iMouseHoverWidth/2)
    DWORD dwMouseHoverTime;      // 悬停超时

注意:跟踪状态是桌面级(每桌面一份)而不是队列级的 ------因为 TrackMouseEvent 的语义

是"光标离开/悬停在某窗口",而光标移动跨越窗口边界时内核需要知道"上一个被跟踪窗口是谁"

才能发出 WM_MOUSELEAVE,这个信息天然属于桌面全局。

3.6 全局异步键状态 gafAsyncKeyState --- input.h L97

c 复制代码
extern BYTE gafAsyncKeyState[256 * 2 / 8]; // 2 bits per key

每键 2 位:低位(GET_KS_DOWN_BIT)表示按下状态,高位(GET_KS_LOCK_BIT)表示锁定状态。

宏(input.h L99-L109):

c 复制代码
#define IS_KEY_DOWN(ks, vk) (((ks)[GET_KS_BYTE(vk)] & GET_KS_DOWN_BIT(vk)) ? TRUE : FALSE)
#define SET_KEY_DOWN(ks, vk, down) (ks)[GET_KS_BYTE(vk)] = ((down) ? \
                            ((ks)[GET_KS_BYTE(vk)] | GET_KS_DOWN_BIT(vk)) : \
                            ((ks)[GET_KS_BYTE(vk)] & ~GET_KS_DOWN_BIT(vk)))

鼠标按键的虚键是 VK_LBUTTON/VK_RBUTTON/VK_MBUTTON/VK_XBUTTON1/VK_XBUTTON2,与键盘共用

同一张状态表。NtUserGetAsyncKeyState(keyboard.c)读取的就是它------所以

GetAsyncKeyState(VK_LBUTTON) 能实时反映鼠标按键。

3.7 SYSTEM_CURSORINFO(系统光标全局状态)--- cursoricon.h

c 复制代码
typedef struct _SYSTEM_CURSORINFO {
    PCURICON_OBJECT CurrentCursorObject; // 当前屏幕光标对象
    ULONG ButtonsDown;                   // 当前按下的 MK_* 组合(给 wParam 用)
    INT ShowingCursor;                   // 光标显隐计数
    ULONG ClickLockTime;                 // 单击锁定计时
    BOOL ClickLockActive;                // 单击锁定激活
} SYSTEM_CURSORINFO;

IntGetSysCursorInfo()(cursoricon.c L187)返回该全局结构。其中 ButtonsDown 是"当前

按下键的 MK_* 位组合",由 UserSendMouseInput 随每个按键事件维护,并被

UserGetMouseButtonsState 用来生成消息的 wParam。注意区分三个键状态来源

状态来源 位置 语义 使用者
gafAsyncKeyState 全局(input.h L97) 任意时刻异步键状态 UserSendMouseInput、GetAsyncKeyState
CurInfo->ButtonsDown SYSTEM_CURSORINFO 鼠标键 MK_* 组合 wParam 构造
MessageQueue->afKeyState 每队列 该队列已处理消息的键状态 MsqGetDownKeyState(WM_MOUSEMOVE 的 wParam)

3.8 鼠标消息编号与区间 --- msgqueue.h L229-L247

c 复制代码
#define IS_BTN_MESSAGE(message,code) \
  ((message) == WM_LBUTTON##code || (message) == WM_MBUTTON##code || \
   (message) == WM_RBUTTON##code || (message) == WM_XBUTTON##code || \
   (message) == WM_NCLBUTTON##code || (message) == WM_NCMBUTTON##code || \
   (message) == WM_NCRBUTTON##code || (message) == WM_NCXBUTTON##code )

#define WM_NCMOUSEFIRST WM_NCMOUSEMOVE
#define WM_NCMOUSELAST  (WM_NCMOUSEFIRST+(WM_MOUSELAST-WM_MOUSEFIRST))

#define IS_MOUSE_MESSAGE(message) \
    ((message >= WM_NCMOUSEFIRST && message <= WM_NCMOUSELAST) || \
            (message >= WM_MOUSEFIRST && message <= WM_MOUSELAST))

客户区鼠标消息编号(winuser.h)连续排列:WM_MOUSEMOVE=0x0200

WM_LBUTTONDOWN=0x0201WM_LBUTTONUP=0x0202WM_LBUTTONDBLCLK=0x0203

WM_RBUTTONDOWN=0x0204... WM_MBUTTONDBLCLK=0x0209。非客户区版本从

WM_NCMOUSEMOVE=0x00A0 起,且与客户区一一对应、差值恒定:

WM_NCMOUSEMOVE - WM_MOUSEMOVE。这个编号恒差 特性被 co_IntProcessMouseMessage

利用(L1568):message += WM_NCMOUSEMOVE - WM_MOUSEMOVE; 一条语句完成

客户区消息到非客户区消息的换算。


4. 鼠标输入主处理 UserProcessMouseInput

位置 :mouse.c L39-L130;声明 :input.h L81。

调用者RawInputThreadMain(input.c L324,硬件)------在 UserEnterExclusive() 临界区中调用。

4.1 职责

UserProcessMouseInput硬件鼠标数据的唯一入口 。它只做一件事:把

MOUSE_INPUT_DATA 精确地翻译成一次或多次 MOUSEINPUT 交给 UserSendMouseInput

不更新任何窗口状态,不做命中测试------那些都在下游。

4.2 转换逻辑

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

    /* Convert MOUSE_INPUT_DATA to MOUSEINPUT. First init all fields. */
    mi.dx = mid->LastX;
    mi.dy = mid->LastY;
    mi.mouseData = 0;
    mi.dwFlags = 0;
    mi.time = 0;
    mi.dwExtraInfo = mid->ExtraInformation;

    /* Mouse position */
    if (mi.dx != 0 || mi.dy != 0)
        mi.dwFlags |= MOUSEEVENTF_MOVE;

    /* Flags for absolute move */
    if (mid->Flags & MOUSE_MOVE_ABSOLUTE)
        mi.dwFlags |= MOUSEEVENTF_ABSOLUTE;
    if (mid->Flags & MOUSE_VIRTUAL_DESKTOP)
        mi.dwFlags |= MOUSEEVENTF_VIRTUALDESK;
    ...

转换要点:

  1. 位移标志dx/dy 任一非零即设 MOUSEEVENTF_MOVE。注意"移动"与"按键"是
    可叠加 的------一次硬件事件既可能移动又按键,dwFlags 会同时包含 MOVE 与按键位。
  2. 绝对坐标MOUSE_MOVE_ABSOLUTE/MOUSE_VIRTUAL_DESKTOP 原样透传为
    MOUSEEVENTF_ABSOLUTE/MOUSEEVENTF_VIRTUALDESK(触摸屏/平板/远程桌面使用)。
  3. 按键位 :左/中/右/侧键的 DOWN/UP 逐一映射为 MOUSEEVENTF_*DOWN/*UP
  4. X 键与滚轮的"装不下"问题MOUSEEVENTF_XDOWN/XUPMOUSEEVENTF_WHEEL 都要
    占用 mouseData 字段,一个 MOUSEINPUT 无法同时表达两者。因此源码采用"拆分发送":
c 复制代码
    /* Button 4 */
    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;
    }
    /* If mouseData is used by button 4, send input and clear mi */
    if (mi.dwFlags & (MOUSE_BUTTON_4_DOWN | MOUSE_BUTTON_4_UP))
    {
        UserSendMouseInput(&mi, FALSE);
        RtlZeroMemory(&mi, sizeof(mi));
    }
    /* Button 5 */ ...(同 Button 4)
    /* Mouse wheel */
    if (mid->ButtonFlags & MOUSE_WHEEL)
    {
        mi.mouseData = mid->ButtonData;
        mi.dwFlags |= MOUSEEVENTF_WHEEL;
    }
    /* If something has changed, send input to user */
    if (mi.dwFlags)
        UserSendMouseInput(&mi, FALSE);
  1. 收尾mi.dwFlags 仍非零(即还有移动/滚轮等未发送部分)时再发一次。
    这样一次硬件事件最多拆成 3 次 UserSendMouseInput(X1 键、X2 键、其余)。

4.3 硬件侧的调用环境

RawInputThreadMain(input.c L139)是系统输入线程(TIF_SYSTEMTHREAD,优先级

LOW_REALTIME_PRIORITY + 3)。它对鼠标设备 \Device\PointerClass0 发起异步

ZwReadFile(L238-L247),读到 MOUSE_INPUT_DATA 后:

c 复制代码
        if (NT_SUCCESS(MouStatus) && MouStatus != STATUS_PENDING)
        {
            TRACE("MouseEvent\n");
            /* Set LastInputTick */
            IntLastInputTick(TRUE);
            /* Process data */
            UserEnterExclusive();
            UserProcessMouseInput(&MouseInput);
            UserLeave();
        }

IntLastInputTick(TRUE) 更新 gpsi->dwLastRITEventTickCount(GetLastInputInfo 的数据源)。

整个处理在 UserEnterExclusive()(全局 USER 锁)内执行------鼠标输入是全局串行化的,

避免多线程并发命中测试/入队造成队列错乱。


5. SendInput 注入 UserSendMouseInput

位置 :mouse.c L167-L351;声明 :input.h L82。

调用者UserProcessMouseInput(硬件,bInjected=FALSE)、NtUserSendInput

(input.c L773,注入,bInjected=TRUE)。

这是鼠标输入的核心处理函数:位置计算、历史记录、键状态、消息构造、入队全部在此完成。

5.1 参数与准备

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

    ASSERT(pmi);

    pCurInfo = IntGetSysCursorInfo();
    ptCursor = gpsi->ptCursor;
    dwFlags = IntFixMouseInputButtons(pmi->dwFlags);

    gppiInputProvider = ((PTHREADINFO)PsGetCurrentThreadWin32Thread())->ppi;
  • ptCursor = gpsi->ptCursor:当前光标屏幕坐标(全局,gpsi)。
  • IntFixMouseInputButtons:若系统启用"交换鼠标左右键"(gspv.bMouseBtnSwap),把
    LEFT/RIGHT 的 DOWN/UP 标志互换(见第 6 节)。
  • gppiInputProvider:记录输入来源进程(注入者的 PROCESSINFO),供上层审计。

5.2 光标位置计算

c 复制代码
    if (pmi->dwFlags & MOUSEEVENTF_MOVE)
    {
        if (!(pmi->dwFlags & MOUSEEVENTF_ABSOLUTE))
        {
            /* Relative move */
            ptCursor.x += pmi->dx;
            ptCursor.y += pmi->dy;
        }
        else if (pmi->dwFlags & MOUSEEVENTF_VIRTUALDESK)
        {
            /* Absolute move in virtual screen units */
            ptCursor.x = pmi->dx * UserGetSystemMetrics(SM_CXVIRTUALSCREEN) >> 16;
            ptCursor.y = pmi->dy * UserGetSystemMetrics(SM_CYVIRTUALSCREEN) >> 16;
        }
        else
        {
            /* Absolute move in primary monitor units */
            ptCursor.x = pmi->dx * UserGetSystemMetrics(SM_CXSCREEN) >> 16;
            ptCursor.y = pmi->dy * UserGetSystemMetrics(SM_CYSCREEN) >> 16;
        }
    }

三种模式:

  • 相对(普通鼠标):当前坐标 + 增量;
  • 绝对-虚拟桌面dx(0-65535 归一化)× 虚拟屏幕宽 >> 16 得到像素坐标;
  • 绝对-主屏 :同理用 SM_CXSCREEN/SM_CYSCREEN

5.3 消息基本字段与移动历史

c 复制代码
    /* Init message fields */
    Msg.wParam = UserGetMouseButtonsState();
    Msg.lParam = MAKELPARAM(ptCursor.x, ptCursor.y);
    Msg.pt = ptCursor;
    Msg.time = pmi->time;
    if (!Msg.time)
    {
        Msg.time = EngGetTickCount32();
    }

    /* Do GetMouseMovePointsEx FIFO. */
    gMouseHistoryOfMoves[gcMouseHistoryOfMoves].x = ptCursor.x;
    gMouseHistoryOfMoves[gcMouseHistoryOfMoves].y = ptCursor.y;
    gMouseHistoryOfMoves[gcMouseHistoryOfMoves].time = Msg.time;
    gMouseHistoryOfMoves[gcMouseHistoryOfMoves].dwExtraInfo = pmi->dwExtraInfo;
    if (++gcMouseHistoryOfMoves == ARRAYSIZE(gMouseHistoryOfMoves))
       gcMouseHistoryOfMoves = 0; // 0 - 63 is 64, FIFO forwards.
  • Msg.wParam 初始为 UserGetMouseButtonsState() 的当前键组合(后续按键事件再叠加/清除该
    键的位,见 5.5)。
  • Msg.lParam = MAKELPARAM(x, y)屏幕坐标 (内核阶段一律屏幕坐标,客户区换算在
    co_IntProcessMouseMessage 完成)。
  • Msg.pt 是消息附带点(GetMessagePos 数据源)。
  • 移动历史 :每次输入都写入 64 项环形 FIFO gMouseHistoryOfMoves[64]
    (mouse.c L13),供 NtUserGetMouseMovePointsEx 查询(见第 14 节)。

5.4 光标更新

c 复制代码
    /* Update cursor position */
    if (dwFlags & MOUSEEVENTF_MOVE)
    {
        UserSetCursorPos(ptCursor.x, ptCursor.y, bInjected, pmi->dwExtraInfo, TRUE);
    }

UserSetCursorPos(cursoricon.c L238)执行真正的位置更新:移动屏幕指针、

触发 WM_MOUSEMOVE 相关的光标形状刷新等。bInjecteddwExtraInfo 透传给钩子。

5.5 键状态维护与消息构造

UserSendMouseInput 按按键类型分别处理。以左键为例:

c 复制代码
    /* Left button */
    if (dwFlags & MOUSEEVENTF_LEFTDOWN)
    {
        SET_KEY_DOWN(gafAsyncKeyState, VK_LBUTTON, TRUE);
        Msg.message = WM_LBUTTONDOWN;
        pCurInfo->ButtonsDown |= MK_LBUTTON;
        Msg.wParam |= MK_LBUTTON;
        co_MsqInsertMouseMessage(&Msg, bInjected, pmi->dwExtraInfo, TRUE);
    }
    else if (dwFlags & MOUSEEVENTF_LEFTUP)
    {
        SET_KEY_DOWN(gafAsyncKeyState, VK_LBUTTON, FALSE);
        Msg.message = WM_LBUTTONUP;
        pCurInfo->ButtonsDown &= ~MK_LBUTTON;
        Msg.wParam &= ~MK_LBUTTON;
        co_MsqInsertMouseMessage(&Msg, bInjected, pmi->dwExtraInfo, TRUE);
    }

每个按键的处理都是四元组:

  1. SET_KEY_DOWN(gafAsyncKeyState, VK_x, down) ------ 更新全局异步键状态;
  2. Msg.message = WM_xBUTTONDOWN/UP ------ 确定消息号;
  3. pCurInfo->ButtonsDown |= / &= ~ MK_x 且同步改 Msg.wParam ------ 维护键组合;
  4. co_MsqInsertMouseMessage(&Msg, ...) ------ 命中测试 + 入队。

WM_MOUSEMOVE 本身不在这里入队------它由 co_MsqInsertMouseMessage 内检测到

Msg->message == WM_MOUSEMOVE 时以特殊方式处理(置 QF_MOUSEMOVED 标志,见第 9 节)。

X 键 (dwFlags 含 XDOWN/XUP 时)需要按 pmi->mouseData 区分 XBUTTON1/2,且 wParam

MAKEWPARAM(MK_XBUTTONx, XBUTTONx) 同时携带键状态位与 X 键标识:

c 复制代码
    if (dwFlags & MOUSEEVENTF_XDOWN)
    {
        Msg.message = WM_XBUTTONDOWN;
        if (pmi->mouseData & XBUTTON1)
        {
            SET_KEY_DOWN(gafAsyncKeyState, VK_XBUTTON1, TRUE);
            pCurInfo->ButtonsDown |= MK_XBUTTON1;
            Msg.wParam |= MAKEWPARAM(MK_XBUTTON1, XBUTTON1);
            co_MsqInsertMouseMessage(&Msg, bInjected, pmi->dwExtraInfo, TRUE);
        }
        if (pmi->mouseData & XBUTTON2) { ...同构... }
    }

滚轮dwFlags & MOUSEEVENTF_WHEEL):

c 复制代码
    if (dwFlags & MOUSEEVENTF_WHEEL)
    {
        Msg.message = WM_MOUSEWHEEL;
        Msg.wParam = MAKEWPARAM(pCurInfo->ButtonsDown, pmi->mouseData);
        co_MsqInsertMouseMessage(&Msg, bInjected, pmi->dwExtraInfo, TRUE);
    }

注意滚轮消息的 wParam 完全覆盖:低字 = 当前键组合,高字 = 滚轮增量(pmi->mouseData)。

滚轮消息的目标窗口co_MsqInsertMouseMessage 的命中测试决定(WM_MOUSEWHEEL 有

焦点优先规则,见第 9 节)。

非法组合检查 :若同一事件同时含 XDOWN/XUP 与 WHEEL(二者都要 mouseData),

UserSendMouseInput 直接失败:

c 复制代码
    if((dwFlags & (MOUSEEVENTF_XDOWN | MOUSEEVENTF_XUP)) &&
       (dwFlags & MOUSEEVENTF_WHEEL))
    {
        /* Fail because both types of events use the mouseData field */
        WARN("Invalid flags!\n");
        return FALSE;
    }

5.6 注入来源

NtUserSendInput(input.c L720-L793)是用户态 SendInput/mouse_event 的系统调用入口:

c 复制代码
            case INPUT_MOUSE:
                if (UserSendMouseInput(&SafeInput.mi, TRUE))
                    uRet++;
                break;

bInjected = TRUE 会经 co_MsqInsertMouseMessage 传入 MouseHookData.flags

LLMHF_INJECTED),低级鼠标钩子可据此识别注入事件。此外 NtUserSendInput

ThreadHasInputAccess(pti)IntIsActiveDesktop(pti->rpdesk) 安全检查。


6. 键状态与按钮交换

6.1 UserGetMouseButtonsState --- mouse.c L21

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;
}

返回当前"按下键"的 MK_* 位组合:基础组合来自 SYSTEM_CURSORINFO.ButtonsDown(由

UserSendMouseInput 逐键维护),再叠加 Shift/Ctrl(从全局 gafAsyncKeyState 读)。

它被 UserSendMouseInput 用作每条消息 wParam 的初值 ------因此 WM_MOUSEMOVE 的

wParam 会反映"移动时还按着哪些键"。

6.2 IntFixMouseInputButtons --- mouse.c L137

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

    if (!gspv.bMouseBtnSwap)
        return dwFlags;

    /* Buttons other than left and right are not affected */
    dwNewFlags = dwFlags & ~(MOUSEEVENTF_LEFTDOWN | MOUSEEVENTF_LEFTUP |
                             MOUSEEVENTF_RIGHTDOWN | MOUSEEVENTF_RIGHTUP);

    /* Swap buttons */
    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;
}

"交换鼠标左右键"(SwapMouseButton API / 控制面板设置)在输入管线的源头 实现:

硬件左键事件被改写成右键事件。这样整个下游(键状态、消息号、双击、捕获)无需任何修改。

中键、X 键不受影响。用户态 SwapMouseButton(user32/windows/input.c L942)通过

NtUserxSwapMouseButton 设置 gspv.bMouseBtnSwap

6.3 MsqGetDownKeyState --- msgqueue.c L338

c 复制代码
WPARAM FASTCALL
MsqGetDownKeyState(PUSER_MESSAGE_QUEUE MessageQueue)
{
    WPARAM ret = 0;

    if (gspv.bMouseBtnSwap)
    {
       if (IS_KEY_DOWN(MessageQueue->afKeyState, VK_RBUTTON)) ret |= MK_LBUTTON;
       if (IS_KEY_DOWN(MessageQueue->afKeyState, VK_LBUTTON)) ret |= MK_RBUTTON;
    }
    else
    {
       if (IS_KEY_DOWN(MessageQueue->afKeyState, VK_LBUTTON)) ret |= MK_LBUTTON;
       if (IS_KEY_DOWN(MessageQueue->afKeyState, VK_RBUTTON)) ret |= MK_RBUTTON;
    }
    if (IS_KEY_DOWN(MessageQueue->afKeyState, VK_MBUTTON))  ret |= MK_MBUTTON;
    if (IS_KEY_DOWN(MessageQueue->afKeyState, VK_SHIFT))    ret |= MK_SHIFT;
    if (IS_KEY_DOWN(MessageQueue->afKeyState, VK_CONTROL))  ret |= MK_CONTROL;
    if (IS_KEY_DOWN(MessageQueue->afKeyState, VK_XBUTTON1)) ret |= MK_XBUTTON1;
    if (IS_KEY_DOWN(MessageQueue->afKeyState, VK_XBUTTON2)) ret |= MK_XBUTTON2;
    return ret;
}

UserGetMouseButtonsState 的区别:它从队列的 afKeyState (该队列已处理消息后的

键状态)读取,且是"交换感知"的(按钮交换后 MK_LBUTTON 语义跟随物理右键)。

它用于 WM_MOUSEMOVE 的 wParam 补全(co_IntProcessMouseMessage L1636-L1639)与

WM_MOUSEHOVER 的 wParam(timer.c L328)。源码注释指出这修复了"左键在桌面拖动后释放

时轮廓卡住"的问题------因为 WM_MOUSEMOVE 的 wParam 若用全局状态生成,可能与队列实际

处理过的按键序列脱节。

6.4 UpdateKeyStateFromMsg --- msgqueue.c L266

co_MsqPeekHardwareMessage 取出硬件消息后、调用 co_IntProcessMouseMessage 之前,

先用 UpdateKeyStateFromMsg(MessageQueue, &msg) 同步队列 afKeyState:

c 复制代码
    case WM_LBUTTONDOWN:
        down = TRUE;
        /* fall through */
    case WM_LBUTTONUP:
        UpdateKeyState(MessageQueue, VK_LBUTTON, down);
        break;
    case WM_XBUTTONDOWN: ... case WM_XBUTTONUP:
        if (msg->wParam == XBUTTON1)
            UpdateKeyState(MessageQueue, VK_XBUTTON1, down);
        else if (msg->wParam == XBUTTON2)
            UpdateKeyState(MessageQueue, VK_XBUTTON2, down);
        break;

这使得 afKeyState 与该队列实际处理的鼠标消息流保持一致------同一桌面下多线程 attach

cThreads > 1)时尤为重要。


7. 命中测试

命中测试回答:"屏幕坐标 (x,y) 属于哪个窗口?命中码是什么?"它有两处使用:

  • co_MsqInsertMouseMessage 入队前的"粗"定向(顶层窗口级,L637);
  • co_IntProcessMouseMessage 落地时的"精"定向(完整窗口树 + WM_NCHITTEST,L1524)。

7.1 IntPtInWindow --- winpos.h L30

最基本的"点是否在窗口内"判定(FORCEINLINE 内联):

c 复制代码
FORCEINLINE BOOL IntPtInWindow(PWND pwnd, INT x, INT y)
{
    if(!RECTL_bPointInRect(&pwnd->rcWindow, x, y))
    {
        return FALSE;
    }

    if(!pwnd->hrgnClip || pwnd->style & WS_MINIMIZE)
    {
        return TRUE;
    }

    return NtGdiPtInRegion(pwnd->hrgnClip,
                           x - pwnd->rcWindow.left,
                           y - pwnd->rcWindow.top);
}

三步:

  1. 点必须在 rcWindow(窗口矩形,屏幕坐标)内;
  2. 无裁剪区域(hrgnClip == NULL)或窗口最小化 → 直接命中(最小化窗口矩形只有图标那么大,
    本就在矩形内);
  3. hrgnClip → 调用 GDI NtGdiPtInRegion区域级 命中(窗口可能被
    SetWindowRgn 抠成圆角/异形),注意坐标要换算成相对窗口原点的区域坐标。

7.2 IntTopLevelWindowFromPoint --- msgqueue.c L60

入队前的快速定向:只遍历顶层窗口 链(桌面的 spwndChild 兄弟链),取 Z 序最上(链首)

命中者:

c 复制代码
PWND FASTCALL
IntTopLevelWindowFromPoint(INT x, INT y)
{
    PWND pWnd, pwndDesktop;

    pwndDesktop = UserGetDesktopWindow();
    if (!pwndDesktop) return NULL;

    /* Loop all top level windows */
    for (pWnd = pwndDesktop->spwndChild;
         pWnd != NULL;
         pWnd = pWnd->spwndNext)
    {
        if (pWnd->state2 & WNDS2_INDESTROY || pWnd->state & WNDS_DESTROYED)
        {
            continue;
        }

        if ((pWnd->style & WS_VISIBLE) &&
            (pWnd->ExStyle & (WS_EX_LAYERED|WS_EX_TRANSPARENT)) != (WS_EX_LAYERED|WS_EX_TRANSPARENT) &&
            IntPtInWindow(pWnd, x, y))
            return pWnd;
    }
    /* Window has not been found */
    return pwndDesktop;
}

要点:

  • 兄弟链按 Z 序排列(spwndChildspwndNext 是"最上 → 最下"),第一个命中即最上层窗口;
  • 跳过销毁中的窗口;
  • WS_EX_LAYERED|WS_EX_TRANSPARENT 组合的窗口穿透(不拦截命中,事件落到下层);
  • 找不到则返回桌面窗口。

7.3 co_WinPosWindowFromPoint --- winpos.c L2999

完整命中测试入口。任务书要点:co_WinPosWindowFromPoint(IN PWND ScopeWin, IN POINT*, IN OUT USHORT* HitTest, IN BOOL Ignore)

c 复制代码
PWND APIENTRY
co_WinPosWindowFromPoint(
   IN PWND ScopeWin,
   IN POINT *WinPoint,
   IN OUT USHORT* HitTest,
   IN BOOL Ignore)
{
   PWND Window;
   POINT Point = *WinPoint;
   USER_REFERENCE_ENTRY Ref;

   if( ScopeWin == NULL )
   {
       ScopeWin = UserGetDesktopWindow();
       if(ScopeWin == NULL)
           return NULL;
   }

   *HitTest = HTNOWHERE;

   ASSERT_REFS_CO(ScopeWin);
   UserRefObjectCo(ScopeWin, &Ref);

   Window = co_WinPosSearchChildren(ScopeWin, &Point, HitTest, Ignore);

   UserDerefObjectCo(ScopeWin);
   ...
   return Window;
}

ScopeWin 通常为桌面窗口(从桌面窗口开始全树搜索);HitTest 初始化为 HTNOWHERE

由搜索过程填充(命中禁用窗口时变 HTERROR,跨线程命中时由调用方补 HTCLIENT)。

Ignore 参数为 TRUE 时忽略禁用(WS_DISABLED)检查。

7.4 co_WinPosSearchChildren --- winpos.c L2914

深度优先的窗口树遍历核心:

c 复制代码
static PWND
co_WinPosSearchChildren(
   IN PWND ScopeWin,
   IN POINT *Point,
   IN OUT USHORT *HitTest,
   IN BOOL Ignore
   )
{
    HWND *List, *phWnd;
    PWND pwndChild = NULL;

    /* not visible */
    if (!(ScopeWin->style & WS_VISIBLE))
    {
        return NULL;
    }

    /* not in window or in window region */
    if (!IntPtInWindow(ScopeWin, Point->x, Point->y))
    {
        return NULL;
    }

    /* transparent */
    if ((ScopeWin->ExStyle & (WS_EX_LAYERED|WS_EX_TRANSPARENT)) == (WS_EX_LAYERED|WS_EX_TRANSPARENT))
    {
        return NULL;
    }

    if (!Ignore && (ScopeWin->style & WS_DISABLED))
    {   /* disabled child */
        if ((ScopeWin->style & (WS_POPUP|WS_CHILD)) == WS_CHILD) return NULL;
        /* process the hit error */
        *HitTest = HTERROR;
        return ScopeWin;
    }
    ...

判定顺序(每层窗口):

  1. 不可见 → 返回 NULL(不命中,向上层回溯);
  2. 点不在窗口矩形/区域内 → NULL;
  3. 分层+透明 → NULL(穿透);
  4. 禁用:子窗口(WS_CHILD)直接跳过;顶层禁用窗口命中但标记 HTERROR
    (这样点击禁用窗口时系统知道"点到东西了但不给消息");
  5. 非最小化且点在客户区内 → 递归子窗口(IntWinListChildren 取子窗口句柄数组,
    co_WinPosSearchChildren 逐个深入),子窗口优先 ------任何一个子窗口命中即返回,
    不再发 WM_NCHITTEST 给父窗口;
  6. 若该窗口属于当前线程,向窗口发 WM_NCHITTEST(同步 SendMessage)以得到精确命中码:
c 复制代码
    if (ScopeWin->head.pti == PsGetCurrentThreadWin32Thread())
    {
       *HitTest = (USHORT)co_IntSendMessage(UserHMGetHandle(ScopeWin), WM_NCHITTEST, 0, MAKELONG(Point->x, Point->y));

       if ((*HitTest) == (USHORT)HTTRANSPARENT)
       {
           return NULL;   // 窗口声明"此处透明",穿透到下层
       }
    }
    else
    {
       if (*HitTest == HTNOWHERE && pwndChild == NULL) *HitTest = HTCLIENT;
    }
    return ScopeWin;
  • WM_NCHITTEST 的返回值(HTCLIENT/HTMAXBUTTON/HTSYSMENU/HTBORDER...)就是最终的
    htEx 命中码;DefWindowProc 对 WM_NCHITTEST 的实现(defwnd.c L1183 → GetNCHitEx)
    按标题栏/边框/按钮等非客户区几何判定。
  • 返回 HTTRANSPARENT 表示窗口自己声明"该点不属于我",搜索放弃此窗口继续向上层回溯;
  • 跨线程时不能同步发送 WM_NCHITTEST(会死锁/阻塞),直接给 HTCLIENT。

7.5 IntRealChildWindowFromPoint / IntChildWindowFromPointEx --- winpos.c L3033/L3075

WindowFromPoint/ChildWindowFromPointEx API 使用的子窗口查询:

  • IntRealChildWindowFromPoint:遍历子窗口取第一个命中者;跳过按钮类
    BS_GROUPBOX
    (组框不拦截命中,点组框落回父窗口)------这是为 Win32 兼容的细节;
  • IntChildWindowFromPointEx:支持 CWP_SKIPINVISIBLE/CWP_SKIPDISABLED/CWP_SKIPTRANSPARENT
    过滤,支持 WS_EX_LAYOUTRTL 镜像坐标。

这两者在鼠标消息管线中不直接使用(管线用 co_WinPosWindowFromPoint),但共享

IntPtInWindow 基础判定,故在此一并说明。

7.6 命中测试的优先级

co_IntProcessMouseMessage L1513-L1525 明确给出捕获优先原则:

c 复制代码
    /* find the window to dispatch this mouse message to */
    if (MessageQueue->spwndCapture)
    {
        hittest = HTCLIENT;
        pwndMsg = MessageQueue->spwndCapture;
    }
    else
    {
        pwndMsg = co_WinPosWindowFromPoint( NULL, &msg->pt, &hittest, FALSE);
    }

捕获存在时:目标窗口=捕获窗口、命中码强制 HTCLIENT(消息总是当客户区消息处理),

不做任何窗口树遍历。捕获为 NULL 时才做全树命中测试。注意这里 msg->hwnd(入队时的

粗定向结果)被完全忽略------落地阶段以当时的 命中测试为准,因为从入队到取出消费

之间窗口可能已移动/销毁。


8. 消息落地 co_IntProcessMouseMessage

位置:msgqueue.c L1483-L1781。这是"鼠标消息落地处理"的总装车间,任务书指定重点。

调用链co_MsqPeekHardwareMessage(L2020)→ co_IntProcessHardwareMessage

(L1908,IS_MOUSE_MESSAGE 判定)→ co_IntProcessMouseMessage

8.1 签名与初始状态

c 复制代码
BOOL co_IntProcessMouseMessage(MSG* msg, BOOL* RemoveMessages, BOOL* NotForUs,
                               LONG_PTR ExtraInfo, UINT first, UINT last)
  • *RemoveMessages:调用方(PeekMessage)是否移除消息(PM_REMOVE);
  • *NotForUs:置 TRUE 表示这条消息属于 attach 组的其他线程,本线程必须放行;
  • first/last:消息过滤器区间(GetMessage/PeekMessage 的 wMsgFilterMin/Max);
  • 返回值 TRUE = 接受消息(返回给应用),FALSE = 丢弃/跳过。

8.2 命中定向与跨线程

c 复制代码
    pti = PsGetCurrentThreadWin32Thread();
    pwndDesktop = UserGetDesktopWindow();
    ...
    MessageQueue = pti->MessageQueue;
    CurInfo = IntGetSysCursorInfo();
    pwndMsg = ValidateHwndNoErr(msg->hwnd);
    clk_msg = MessageQueue->msgDblClk;
    pDesk = pwndDesktop->head.rpdesk;

    /* find the window to dispatch this mouse message to */
    if (MessageQueue->spwndCapture)
    {
        hittest = HTCLIENT;
        pwndMsg = MessageQueue->spwndCapture;
    }
    else
    {
        pwndMsg = co_WinPosWindowFromPoint( NULL, &msg->pt, &hittest, FALSE);
    }

然后三种分流:

c 复制代码
    // Null window or not the same "Hardware" message queue.
    if (pwndMsg == NULL || pwndMsg->head.pti->MessageQueue != MessageQueue)
    {
        // Crossing a boundary, so set cursor. See default message queue cursor.
        IntSystemSetCursor(SYSTEMCUR(ARROW));
        *RemoveMessages = TRUE;
        return FALSE;
    }

    // Check to see if this is attached,
    if ( pwndMsg->head.pti != pti &&  // window thread is not current,
         MessageQueue->cThreads > 1 ) // and is attached...
    {
        *NotForUs = TRUE;
        *RemoveMessages = FALSE;
        return FALSE;
    }
  1. 命中为空或不属于本队列 :光标移到了别的线程的窗口/桌面背景。此时把光标重置为系统
    箭头(IntSystemSetCursor(SYSTEMCUR(ARROW)),cursoricon.c L230),并丢弃消息
    *RemoveMessages = TRUE)。这条消息是从别的线程队列"借"出来的?不是------它本来
    就在本队列里,但命中测试发现光标下的窗口属于其他线程。注意这里的丢弃语义:消息
    是从"光标所落窗口所属线程"的队列取出的,若窗口属别的线程(attach 组除外),直接删。
  2. attach 组内的其他线程cThreads > 1 且目标窗口线程≠当前线程):置
    *NotForUs = TRUE 返回 FALSE。上层 co_MsqPeekHardwareMessage 见 NotForUs 即
    break,让 attach 组中真正拥有该窗口的线程来消费。

8.3 鼠标跟踪联动 IntTrackMouseMove

c 复制代码
    if ( MessageQueue == gpqCursor ) // Cursor must use the same Queue!
    {
       IntTrackMouseMove(pwndMsg, pDesk, msg, hittest);
    }
    else
    {
       WARN("Not the same cursor!\n");
    }

只有"拥有当前光标的队列"才更新跟踪状态(否则跟踪会被光标移动误触发)。IntTrackMouseMove

(msgqueue.c L1432,详见第 13 节)负责在窗口/命中码变化时生成 WM_(NC)MOUSELEAVE 并重置

悬停计时器。

8.4 客户区/非客户区换算

c 复制代码
    msg->hwnd = UserHMGetHandle(pwndMsg);

    pt = msg->pt;
    message = msg->message;

    /* Note: windows has no concept of a non-client wheel message */
    if (message != WM_MOUSEWHEEL)
    {
        if (hittest != HTCLIENT)
        {
            message += WM_NCMOUSEMOVE - WM_MOUSEMOVE;
            msg->wParam = hittest; // Caution! This might break wParam check in DblClk.
        }
        else
        {
            /* coordinates don't get translated while tracking a menu */
            /* FIXME: should differentiate popups and top-level menus */
            if (!(MessageQueue->MenuOwner))
            {
                pt.x += pwndDesktop->rcClient.left - pwndMsg->rcClient.left;
                pt.y += pwndDesktop->rcClient.top - pwndMsg->rcClient.top;
            }
        }
    }
    msg->lParam = MAKELONG( pt.x, pt.y );
  • 非客户区 :消息号整体平移(+WM_NCMOUSEMOVE-WM_NCMOUSEMOVE),wParam 换成
    命中码 hittest(HT* 码,例如 WM_NCLBUTTONDOWN 的 wParam=HTMAXBUTTON 表示点在
    最大化按钮上);坐标为屏幕坐标 (非客户区消息的 lParam 就是屏幕坐标,Windows 约定)。
    滚轮消息没有非客户区版本,跳过平移。
  • 客户区 :坐标从屏幕坐标换算成客户区坐标 (减去窗口客户区原点),桌面原点
    (rcClient.left/top,即屏幕工作区原点)参与换算。菜单跟踪期间(MenuOwner 非空)不换算
    ------菜单循环用自己的坐标体系。

8.5 双击判定与消息过滤

见第 11 节详解。此处先给流程:对 *BUTTONDOWN 消息做"是否构成双击"判定,命中则

message += (WM_LBUTTONDBLCLK - WM_LBUTTONDOWN) 变 DBLCLK 消息;无论是否双击,

都会用过滤器检查 message 是否在 first,last 区间内,不在则丢弃。非按键消息

(含 WM_MOUSEMOVE)走 else 分支补全 wParam:

c 复制代码
        // Update mouse move down keys.
        if (message == WM_MOUSEMOVE)
        {
           msg->wParam = MsqGetDownKeyState(MessageQueue);
        }

8.6 单击锁定(ClickLock)

gspv.bMouseClickLock 开启"单击锁定"(按下左键后保持拖动而不必一直按住)时:

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

        if(msg->message == WM_LBUTTONUP)
        {
            IsClkLck = ((msg->time - CurInfo->ClickLockTime) >= gspv.dwMouseClickLockTime);
            if (IsClkLck && (!CurInfo->ClickLockActive))
            {
                CurInfo->ClickLockActive = TRUE;   // 进入锁定:按下不放也算拖动
            }
        }
        else if (msg->message == WM_LBUTTONDOWN)
        {
            if (CurInfo->ClickLockActive)
            {
                IsClkLck = TRUE;
                CurInfo->ClickLockActive = FALSE;  // 单击即解锁
            }
            CurInfo->ClickLockTime = msg->time;
        }

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

即:若两次点击间隔达到锁定时间,第二次的 WM_LBUTTONUP 被"吃掉"(保持按下语义);

直到用户再单击一次才解除。这是 Windows 的"单击锁定"辅助功能。

8.7 可访问性事件

c 复制代码
    if (pti->TIF_flags & TIF_MSGPOSCHANGED)
    {
        pti->TIF_flags &= ~TIF_MSGPOSCHANGED;
        IntNotifyWinEvent(EVENT_OBJECT_LOCATIONCHANGE, NULL, OBJID_CLIENT, CHILDID_SELF, 0);
    }

鼠标位置变化(co_MsqPeekHardwareMessage L2054 检测 ptLast 改变时置位

TIF_MSGPOSCHANGED)触发屏幕阅读器关心的位置变更事件。

8.8 钩子:WH_JOURNALRECORD 与 WH_MOUSE

c 复制代码
    event.message = msg->message;
    event.time    = msg->time;
    event.hwnd    = msg->hwnd;
    event.paramL  = msg->pt.x;
    event.paramH  = msg->pt.y;
    co_HOOK_CallHooks( WH_JOURNALRECORD, HC_ACTION, 0, (LPARAM)&event );

    hook.pt           = msg->pt;
    hook.hwnd         = msg->hwnd;
    hook.wHitTestCode = hittest;
    hook.dwExtraInfo  = ExtraInfo;
    if (co_HOOK_CallHooks( WH_MOUSE, *RemoveMessages ? HC_ACTION : HC_NOREMOVE,
                        message, (LPARAM)&hook ))
    {
        hook.pt           = msg->pt;
        hook.hwnd         = msg->hwnd;
        hook.wHitTestCode = hittest;
        hook.dwExtraInfo  = ExtraInfo;
        co_HOOK_CallHooks( WH_CBT, HCBT_CLICKSKIPPED, message, (LPARAM)&hook );
        *RemoveMessages = TRUE;
        return FALSE;
    }
  • WH_JOURNALRECORD:宏录制(journal record)系统在消息被接受前记录 EVENTMSG;
  • WH_MOUSE :鼠标钩子。钩子返回非零 → 吃掉消息并通知 WH_CBT 的
    HCBT_CLICKSKIPPED(点击被跳过,供 CBT 应用得知)。

8.9 HTERROR/HTNOWHERE 处理

c 复制代码
    if ((hittest == (USHORT)HTERROR) || (hittest == (USHORT)HTNOWHERE))
    {
        co_IntSendMessage( msg->hwnd, WM_SETCURSOR, (WPARAM)msg->hwnd, MAKELONG( hittest, msg->message ));
        *RemoveMessages = TRUE;
        return FALSE;
    }

点到禁用窗口(HTERROR)或桌面空白(HTNOWHERE):只发 WM_SETCURSOR(让窗口有机会换

光标形状),丢弃鼠标消息本身。

8.10 接受判定与 WM_PARENTNOTIFY / WM_MOUSEACTIVATE / WM_SETCURSOR

c 复制代码
    if ((*RemoveMessages == FALSE) || MessageQueue->spwndCapture)
    {
        /* Accept the message */
        msg->message = message;
        return TRUE;
    }
  • PeekMessage(PM_NOREMOVE) 时(*RemoveMessages==FALSE)或捕获期间:直接接受,
    跳过下面的副作用(PARENTNOTIFY/激活/光标)。
c 复制代码
    if ((msg->message == WM_LBUTTONDOWN) ||
        (msg->message == WM_RBUTTONDOWN) ||
        (msg->message == WM_MBUTTONDOWN) ||
        (msg->message == WM_XBUTTONDOWN))
    {
        /* Send the WM_PARENTNOTIFY,
         * note that even for double/nonclient clicks
         * notification message is still WM_L/M/RBUTTONDOWN. */
        MsqSendParentNotify(pwndMsg, msg->message, 0, msg->pt );

        /* Activate the window if needed */
        if (pwndMsg != MessageQueue->spwndActive)
        {
            PWND pwndTop = pwndMsg;
            pwndTop = IntGetNonChildAncestor(pwndTop);
            ...
            if (pwndTop && pwndTop != pwndDesktop)
            {
                LONG ret = co_IntSendMessage( msg->hwnd, WM_MOUSEACTIVATE,
                                              (WPARAM)UserHMGetHandle(pwndTop),
                                              MAKELONG( hittest, msg->message));
                switch(ret)
                {
                case MA_NOACTIVATEANDEAT: eatMsg = TRUE; /* fall through */
                case MA_NOACTIVATE: break;
                case MA_ACTIVATEANDEAT: eatMsg = TRUE; /* fall through */
                case MA_ACTIVATE:
                case 0:
                    if (!co_IntMouseActivateWindow( pwndTop )) eatMsg = TRUE;
                    break;
                default:
                    ERR( "unknown WM_MOUSEACTIVATE code %d\n", ret );
                    break;
                }
            }
        }
    }

    /* send the WM_SETCURSOR message */
    co_IntSendMessage( msg->hwnd, WM_SETCURSOR, (WPARAM)msg->hwnd, MAKELONG( hittest, msg->message ));

    msg->message = message;
    return !eatMsg;

按键按下(注意:非双击化之前 的原始消息号,所以 DBLCLK 也发 BUTTONDOWN 的

PARENTNOTIFY)时依次:

  1. WM_PARENTNOTIFYMsqSendParentNotify(msgqueue.c L1405)沿父链上溯,把
    MAKEWPARAM(event, idChild) 发给所有祖先(跳过 WS_EX_NOPARENTNOTIFY 窗口),坐标
    逐级换算成各父窗口客户区坐标;
  2. WM_MOUSEACTIVATE :若点击窗口不是当前活动窗口,向其发送 WM_MOUSEACTIVATE
    (wParam=点击的顶层窗口,lParam=MAKELONG(hittest, message))。返回值决定:
    • MA_ACTIVATE/0 → co_IntMouseActivateWindow(pwndTop) 激活;
    • MA_ACTIVATEANDEAT → 激活但吃掉消息;
    • MA_NOACTIVATE → 不激活;
    • MA_NOACTIVATEANDEAT → 不激活且吃掉消息。
      co_IntMouseActivateWindow(focus.c L1248):WS_EX_NOACTIVATE 窗口不激活
      (点击不抢焦点),否则 co_IntSetForegroundAndFocusWindow(Wnd, TRUE, TRUE)
      (bMouse=TRUE,见第 12 节)。
  3. WM_SETCURSORMAKELONG(hittest, msg->message) ------注意消息参数始终用
    原始的客户区消息号 (Windows 约定),窗口过程据此按命中码设置光标
    (DefWindowProc 的 WM_SETCURSOR 实现 defwnd.c L309-L358 按 HTSIZE/HTCAPTION 等换
    SIZEWE/SIZENS/ARROW 等系统光标)。

返回值 !eatMsg:MA_*ANDEAT 时消息被吃掉,不返回给应用。


9. 消息入队 co_MsqInsertMouseMessage

位置 :msgqueue.c L580-L729;声明 :msgqueue.h L206。

调用者UserSendMouseInput(对每条鼠标消息)。

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();

9.1 低级钩子数据装配

c 复制代码
   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:
      case WM_XBUTTONDBLCLK:
      case WM_NCXBUTTONDOWN:
      case WM_NCXBUTTONUP:
      case WM_NCXBUTTONDBLCLK:
         MouseHookData.mouseData = MAKELONG(0, HIWORD(Msg->wParam));
         break;
      default:
         MouseHookData.mouseData = 0;
         break;
   }

   MouseHookData.flags = flags; // LLMHF_INJECTED
   MouseHookData.time = Msg->time;
   MouseHookData.dwExtraInfo = dwExtraInfo;

   /* If the hook procedure returned non zero, dont send the message */
   if (Hook)
   {
      if (co_HOOK_CallHooks(WH_MOUSE_LL, HC_ACTION, Msg->message, (LPARAM) &MouseHookData))
         return;
   }
  • flags 参数即注入标志(bInjected ? LLMHF_INJECTED : 0),低级钩子 MSLLHOOKSTRUCT.flags
    据此区分真实/注入;
  • mouseData 按消息类型提取:滚轮取 wParam 高字(滚轮增量),X 键取 wParam 高字(XBUTTONx);
  • WH_MOUSE_LL 是低级鼠标钩子(全局、在消息真正入队前执行),返回非零则整个消息
    被丢弃(不入队)。

9.2 捕获优先与粗定向

c 复制代码
   /* Get the desktop window */
   pwndDesktop = UserGetDesktopWindow();
   if (!pwndDesktop) return;

   /* Check if the mouse is captured */
   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);
   }

与落地阶段一致:捕获优先IntGetCaptureWindow,focus.c L34 从前景队列取

spwndCapture),否则 IntTopLevelWindowFromPoint 粗定向到顶层窗口(不递归子窗口、

不发 WM_NCHITTEST------精确判定留给落地阶段,避免每次移动都做昂贵的全树查询)。

9.3 队列选择与 WM_MOUSEMOVE 特殊路径

c 复制代码
   hdcScreen = IntGetScreenDC();
   CurInfo = IntGetSysCursorInfo();

   /* Check if we found a window */
   if (Msg->hwnd != NULL && pwnd != NULL)
   {
       pti = pwnd->head.pti;
       MessageQueue = pti->MessageQueue;

       if (MessageQueue->QF_flags & QF_INDESTROY)
       {
          ERR("Mouse is over a Window with a Dead Message Queue!\n");
          return;
       }

       // Check to see if this is attached.
       if ( pti != MessageQueue->ptiMouse &&
            MessageQueue->cThreads > 1 )
       {
          // Set the send pti to the message queue mouse pti.
          pti = MessageQueue->ptiMouse;
       }

       if (Msg->message == WM_MOUSEMOVE)
       {
          /* Check if cursor should be visible */
           if(hdcScreen &&
              MessageQueue->CursorObject &&
              MessageQueue->iCursorLevel >= 0)
           {
               /* Check if shape has changed */
               if(CurInfo->CurrentCursorObject != MessageQueue->CursorObject)
               {
                   /* Call GDI to set the new screen cursor */
                    GreSetPointerShape(hdcScreen, ...);
               } else if (hdcScreen)
                   GreMovePointer(hdcScreen, Msg->pt.x, Msg->pt.y);
           }
           /* Check if we have to hide cursor */
           else if (hdcScreen && CurInfo->ShowingCursor >= 0)
               GreMovePointer(hdcScreen, -1, -1);

           /* Update global cursor info */
           CurInfo->ShowingCursor = MessageQueue->iCursorLevel;
           CurInfo->CurrentCursorObject = MessageQueue->CursorObject;
           gpqCursor = MessageQueue;

           /* Mouse move is a special case */
           MessageQueue->QF_flags |= QF_MOUSEMOVED;
           gdwMouseMoveExtraInfo = dwExtraInfo;
           gdwMouseMoveTimeStamp = Msg->time;
           MsqWakeQueue(pti, QS_MOUSEMOVE, TRUE);
       }
       else
       {
           /* Post mouse move before posting mouse buttons, keep it in sync. */
           if (pti->MessageQueue->QF_flags & QF_MOUSEMOVED)
           {
              IntCoalesceMouseMove(pti);
           }
           MsqPostMessage(pti, Msg, TRUE, QS_MOUSEBUTTON, 0, dwExtraInfo);
       }
   }
   else if (hdcScreen)
   {
       /* always show cursor on background; FIXME: set default pointer */
       GreMovePointer(hdcScreen, Msg->pt.x, Msg->pt.y);
       CurInfo->ShowingCursor = 0;
   }

WM_MOUSEMOVE 特殊处理(L664-L703):

  • 光标形状切换:目标队列的 CursorObject 与当前屏幕光标不同 → GreSetPointerShape
    换形状(含 alpha 光标与热点);相同 → GreMovePointer 移动位置;
  • 光标隐藏:iCursorLevel < 0(应用调用 ShowCursor(FALSE))→ GreMovePointer(-1,-1) 隐藏;
  • 更新全局光标归属(gpqCursor = MessageQueue);
  • 不真正入队 :置 QF_MOUSEMOVED 标志 + 记录时间戳/ExtraInfo + MsqWakeQueue(QS_MOUSEMOVE)
    唤醒。真正的 WM_MOUSEMOVE 消息延迟到"下一个按键消息入队前"或"应用取消息前"由
    IntCoalesceMouseMove 合并生成(见第 10 节)。

非移动消息 :若队列还有未合并的移动(QF_MOUSEMOVED), IntCoalesceMouseMove

把移动落地(保证"按下前先看到最新位置"------源码注释 "Post mouse move before posting mouse

buttons, keep it in sync."),再 MsqPostMessage 把按键/滚轮消息真正入队

(QS_MOUSEBUTTON)。

桌面背景 :未命中任何窗口(hdcScreen 有效时)只移动光标不隐藏,ShowingCursor = 0

9.4 MsqPostMessage 入队

MsqPostMessage(msgqueue.h L198 声明)把 MSG 封装成 USER_MESSAGE

MsqCreateMessage,msgqueue.c L731,从分页 Lookaside 池分配),插入

HardwareMessagesListHead 尾部,MsqWakeQueue 置 QS 位并 KeSetEvent 唤醒目标线程。

USER_MESSAGE 额外携带 ExtraInfo(GetMessageExtraInfo 数据源)与 QS_Flags

(消息消费后据此清理队列位)。


10. 鼠标移动合并 IntCoalesceMouseMove

位置 :msgqueue.c L551-L578;声明:msgqueue.h L203。

c 复制代码
/*
    Bring together the mouse move message.
    Named "Coalesce" from Amine email ;^) (jt).
 */
VOID FASTCALL
IntCoalesceMouseMove(PTHREADINFO pti)
{
    MSG Msg;

    // Force time stamp to update, keeping message time in sync.
    if (gdwMouseMoveTimeStamp == 0)
    {
        gdwMouseMoveTimeStamp = EngGetTickCount32();
    }

    // Build mouse move message.
    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;

    // Post the move.
    MsqPostMouseMove(pti, &Msg, gdwMouseMoveExtraInfo);

    // Zero the time stamp.
    gdwMouseMoveTimeStamp = 0;

    // Clear flag since the move was posted.
    pti->MessageQueue->QF_flags &= ~QF_MOUSEMOVED;
}
  • gpsi->ptCursor(当前光标位置)生成 WM_MOUSEMOVE------总是反映最新位置
  • Msg.hwnd = NULL:hwnd 由落地阶段重新命中测试填充;
  • 时间戳取自 gdwMouseMoveTimeStampco_MsqInsertMouseMessage 记录),保证消息时间
    与光标到达时间一致;
  • 交给 MsqPostMouseMove(msgqueue.c L518)入队:
c 复制代码
VOID FASTCALL
MsqPostMouseMove(PTHREADINFO pti, MSG* Msg, LONG_PTR ExtraInfo)
{
    PUSER_MESSAGE Message;
    PLIST_ENTRY ListHead;
    PUSER_MESSAGE_QUEUE MessageQueue = pti->MessageQueue;

    ListHead = &MessageQueue->HardwareMessagesListHead;

    // Do nothing if empty.
    if (!IsListEmpty(ListHead->Flink))
    {
       // Look at the end of the list,
       Message = CONTAINING_RECORD(ListHead->Blink, USER_MESSAGE, ListEntry);

       // If the mouse move message is existing on the list,
       if (Message->Msg.message == WM_MOUSEMOVE)
       {
          // Overwrite the message with updated data!
          Message->Msg = *Msg;

          MsqWakeQueue(pti, QS_MOUSEMOVE, TRUE);
          return;
       }
    }

    MsqPostMessage(pti, Msg, TRUE, QS_MOUSEMOVE, 0, ExtraInfo);
}

两级合并

  1. 队列尾若已有未消费的 WM_MOUSEMOVE → 原地覆盖 (只保留最新位置,中间位置全部
    丢弃),这是合并的主力;
  2. 否则追加一条新消息。

合并的收益:高频移动(1000Hz 鼠标)下队列里最多 1 条 WM_MOUSEMOVE,应用取到的是

"当前帧"位置,不会因队列积压产生迟滞。代价是丢失中间轨迹------需要完整轨迹的应用应

使用 GetMouseMovePointsEx(见第 14 节)或原始输入。

触发点汇总IntCoalesceMouseMove 的全部调用处):

  • co_MsqInsertMouseMessage L716:按键消息入队前,把待决移动落地;
  • co_MsqWaitForNewMessages L2138:线程即将睡眠等待消息前,把待决移动落地
    (保证等待期间移动不丢);
  • ClearMsgBitsMask 对 QS_MOUSEMOVE 的清零采用"节流"策略(L456-L461):一次
    nCntsQBits[QSRosMouseMove] 直接归零,注释 "Up to > 3:1 entries are ignored"------
    移动位计数被刻意压低,避免高频移动反复唤醒。

11. 双击判定

11.1 判定数据结构

双击判定的唯一状态是队列字段 USER_MESSAGE_QUEUE.msgDblClk(MSG):保存"上一次

潜在的第一次点击"的完整消息(message/hwnd/time/pt/wParam)。每次 *BUTTONDOWN

处理后更新它;命中双击时清空它。

11.2 判定流程 --- msgqueue.c L1586-L1626

c 复制代码
    /* translate double-clicks */

    if ((msg->message == WM_LBUTTONDOWN) ||
        (msg->message == WM_RBUTTONDOWN) ||
        (msg->message == WM_MBUTTONDOWN) ||
        (msg->message == WM_XBUTTONDOWN))
    {
        BOOL update = *RemoveMessages;

        /* translate double-clicks -
         * note that ...MOUSEMOVEs can slip in between
         * ...BUTTONDOWN and ...BUTTONDBLCLK messages */

        if ((MessageQueue->MenuOwner || MessageQueue->MoveSize) ||
            hittest != HTCLIENT ||
            (pwndMsg->pcls->style & CS_DBLCLKS))
        {
           if ((msg->message == clk_msg.message) &&
               (msg->hwnd == clk_msg.hwnd) &&
               // Only worry about XButton wParam.
               (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;  /* clear the double-click conditions */
                   update = FALSE;
               }
           }
        }

        if (!((first ==  0 && last == 0) || (message >= first || message <= last)))
        {
            TRACE("Message out of range!!!\n");
            return FALSE;
        }

        /* update static double-click conditions */
        if (update) MessageQueue->msgDblClk = *msg;
    }

外闸门(决定是否进入双击判定):三者任一成立即可------

  1. 菜单跟踪中(MenuOwner)或窗口移动/缩放中(MoveSize):任何点击都当双击候选
    (菜单项连击、标题栏拖动双击最大化都依赖它);
  2. 命中码非 HTCLIENT(非客户区双击,如双击标题栏);
  3. 窗口类有 CS_DBLCLKS 样式(只有注册了 CS_DBLCLKS 的类才接收双击消息 ------
    Win32 的著名约定;没这个样式的类,快速连击只会得到两次 BUTTONDOWN/UP)。

内判定(六条件全部满足才是双击):

  1. 消息号相同(都是 LBUTTONDOWN 等);
  2. 目标窗口相同(msg->hwnd == clk_msg.hwnd);
  3. X 键消息需 XBUTTON1/2 相同(GET_XBUTTON_WPARAM);
  4. 时间差 < gspv.iDblClickTime(系统双击时间,默认 500ms);
  5. 水平距离 < SM_CXDOUBLECLK/2(即两次点击都在"双击方框"内);
  6. 垂直距离 < SM_CYDOUBLECLK/2

命中后 message += (WM_LBUTTONDBLCLK - WM_LBUTTONDOWN)(数值差为 2),把消息变

WM_LBUTTONDBLCLK;并清空 msgDblClk(这次点击作为"已用掉的第一次",不再构成下次

双击的基准)。未命中时 msgDblClk = *msg 记录本次点击作为新的基准。

注意:SM_CXDOUBLECLK/SM_CYDOUBLECLK 是系统度量(对应 gspv.iDblClickWidth/Height),

除以 2 是因为"方框"以第一次点击点为中心向四周各扩展一半。

11.3 过滤区间检查

判定(及非按键消息)都检查 message 是否落在 [first, last](GetMessage/PeekMessage

过滤器)内;first==0 && last==0 表示不过滤。注意这里用的是 message >= first || message <= last

(源码原样)------该写法依赖调用方 co_MsqPeekHardwareMessage 已用

filter_contains_hw_range 做过硬件消息区间预判,且 first/last 恒为鼠标消息区间。

11.4 双击时间/阈值的存取

操作 API 实现
读双击时间 GetDoubleClickTimeNtUserGetDoubleClickTime misc.c L387:返回 gspv.iDblClickTime
写双击时间 SetDoubleClickTimeNtUserSystemParametersInfo(SPI_SETDOUBLECLICKTIME,...) user32/windows/input.c L930;sysparams.c L942 SpiSetInt(&gspv.iDblClickTime,...)
读双击方框 GetSystemMetrics(SM_CXDOUBLECLK/SM_CYDOUBLECLK) metric.c 按 gspv.iDblClickWidth/Height 返回
判定消费 co_IntProcessMouseMessage L1605-1607 直接用 gspv.iDblClickTime + 系统度量

任务书提到的 IntGetDoubleClickTime 在 ReactOS 中的实际对应物是

NtUserGetDoubleClickTime(misc.c L385-L400):

c 复制代码
UINT
APIENTRY
NtUserGetDoubleClickTime(VOID)
{
   UINT Result;
   TRACE("Enter NtUserGetDoubleClickTime\n");
   UserEnterShared();

   // FIXME: Check if this works on non-interactive winsta
   Result = gspv.iDblClickTime;

   TRACE("Leave NtUserGetDoubleClickTime, ret=%u\n", Result);
   UserLeave();
   return Result;
}

修改双击时间没有独立的 NtUserSetDoubleClickTime,而是统一走

SystemParametersInfo(SPI_SETDOUBLECLICKTIME)(第 16 节详述)。

11.5 双击的时序示例

复制代码
t=0    左键按下 → WM_LBUTTONDOWN 入队(msgDblClk 记录该消息)
t=30   左键抬起 → WM_LBUTTONUP
t=80   左键按下 → 命中测试:时间差 80<500,同窗口同键 → WM_LBUTTONDBLCLK
        (msgDblClk 清空)
t=110  左键抬起 → WM_LBUTTONUP

若第二次按下发生在 600ms 后或移出方框,则仍得 WM_LBUTTONDOWN,且 msgDblClk 被更新为

第二次按下的消息------为"三次点击=双击+单击"语义服务(第二次按下后抬起又按下仍可能构成

双击)。


12. 鼠标捕获

12.1 捕获的存储与查询

捕获窗口存储在前台消息队列spwndCapture 字段。查询统一走

IntGetCaptureWindow(focus.c L33-L40):

c 复制代码
HWND FASTCALL
IntGetCaptureWindow(VOID)
{
    PUSER_MESSAGE_QUEUE ForegroundQueue = IntGetFocusMessageQueue();
    if (!ForegroundQueue)
        return NULL;
    return (ForegroundQueue->spwndCapture ? UserHMGetHandle(ForegroundQueue->spwndCapture) : NULL);
}

12.2 捕获期间的消息定向

捕获的作用在两条路径上体现:

入队阶段co_MsqInsertMouseMessage L630-L639):Msg->hwnd = IntGetCaptureWindow()

------捕获窗口非空则消息直接定向到捕获窗口,不再做顶层命中。注意捕获窗口可能属于

其他线程pwnd->head.pti 决定消息进入哪个队列,因此跨线程捕获(如菜单循环捕获、

拖动窗口时)时消息会进捕获窗口线程的队列------这是 Windows 允许的(SetCapture 可捕获

其他线程的窗口,典型用于菜单/滚动条拖动)。

落地阶段co_IntProcessMouseMessage L1513-L1518):再次检查本队列

MessageQueue->spwndCapture(注意这里看的是队列字段而非全局函数),捕获存在时:

目标=捕获窗口、命中码强制 HTCLIENT、*RemoveMessages==FALSE 时也直接接受(L1718),

捕获期间连 PeekMessage 探测也不触发 PARENTNOTIFY/激活副作用

12.3 捕获与激活联动

点击非活动窗口时,co_IntProcessMouseMessage 发送 WM_MOUSEACTIVATE 并按返回值决定

是否激活(8.10 节)。激活动作落到 focus.c 的 co_IntMouseActivateWindow

(focus.c L1248-L1255):

c 复制代码
BOOL FASTCALL
co_IntMouseActivateWindow(PWND Wnd)
{
   TRACE("Mouse Active\n");
   if (Wnd && (Wnd->ExStyle & WS_EX_NOACTIVATE))
      return TRUE;
   return co_IntSetForegroundAndFocusWindow(Wnd, TRUE, TRUE);
}
  • WS_EX_NOACTIVATE(工具窗口/通知窗口)点击不激活,直接返回 TRUE;
  • 否则 co_IntSetForegroundAndFocusWindow(Wnd, bMouse=TRUE, bFocus=TRUE):把该窗口置为
    前台 + 焦点窗口(focus.c 内部经 IntUserSetActiveWindow 的规则检查:仅当本线程是前台
    线程、或窗口线程被允许前台激活时才真正切换前台,防止后台窗口抢焦点------这正是 Windows
    "前台锁定"(foreground lock)规则的体现)。bMouse=TRUE 表示本次激活由鼠标引发,
    焦点切换会遵循"点击激活"语义(如对 WS_POPUP 父链的判定)。

12.4 捕获的典型场景

场景 捕获者 释放时机
文本选择/拖拽 应用 SetCapture WM_LBUTTONUP / ReleaseCapture
菜单循环 内核 menu.c(TrackPopupMenu) 菜单关闭
标题栏移动窗口 内核 nonclient.c(HTCAPTION 拖动) 鼠标抬起
滚动条拖动 thumb 内核 scrollbar.c 鼠标抬起
双击跟踪 捕获(配合 CS_DBLCLKS) 第二次抬起

捕获期间 WM_MOUSEMOVE 仍按"合并"路径处理,但目标始终是捕获窗口,因此拖拽过程即使

光标飞出窗口也能持续收到移动消息------这就是"捕获"解决的核心问题。


13. TrackMouseEvent 跟踪

TrackMouseEvent 让窗口在光标进入/离开/悬停时收到 WM_MOUSELEAVE/WM_MOUSEHOVER

(及非客户区版本)。跟踪状态是桌面级 的(每桌面一份 spwndTrack/dwDTFlags/

rcMouseHover/dwMouseHoverTime,desktop.h L12-L50),因为光标跨越窗口边界时,

内核必须在"上一个窗口"离开时发出 WM_MOUSELEAVE------这个"上一个"必须全局记得。

13.1 系统调用入口 NtUserTrackMouseEvent --- mouse.c L480

c 复制代码
BOOL
APIENTRY
NtUserTrackMouseEvent(
    LPTRACKMOUSEEVENT lpEventTrack)
{
    TRACKMOUSEEVENT SafeTME;
    BOOL bRet = FALSE;

    TRACE("Enter NtUserTrackMouseEvent\n");

    _SEH2_TRY
    {
        ProbeForRead(lpEventTrack, sizeof(TRACKMOUSEEVENT), 1);
        RtlCopyMemory(&SafeTME, lpEventTrack, sizeof(TRACKMOUSEEVENT));
    }
    _SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
    {
        SetLastNtError(_SEH2_GetExceptionCode());
        _SEH2_YIELD(return FALSE);
    }
    _SEH2_END;

    if (SafeTME.cbSize != sizeof(TRACKMOUSEEVENT))
    {
        EngSetLastError(ERROR_INVALID_PARAMETER);
        return FALSE;
    }

    if (SafeTME.dwFlags & ~(TME_CANCEL | TME_QUERY | TME_NONCLIENT | TME_LEAVE | TME_HOVER) )
    {
        EngSetLastError(ERROR_INVALID_FLAGS);
        return FALSE;
    }

    UserEnterExclusive();

    if (SafeTME.dwFlags & TME_QUERY)
    {
        bRet = IntQueryTrackMouseEvent(&SafeTME);
        _SEH2_TRY
        {
            ProbeForWrite(lpEventTrack, sizeof(TRACKMOUSEEVENT), 1);
            RtlCopyMemory(lpEventTrack, &SafeTME, sizeof(TRACKMOUSEEVENT));
        }
        _SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
        {
            SetLastNtError(_SEH2_GetExceptionCode());
            bRet = FALSE;
        }
        _SEH2_END;
    }
    else
    {
        bRet = IntTrackMouseEvent(&SafeTME);
    }

    TRACE("Leave NtUserTrackMouseEvent, ret=%i\n", bRet);
    UserLeave();
    return bRet;
}

流程:SEH 保护的用户指针拷贝(ProbeForRead)→ cbSize 校验(结构版本检查)→

dwFlags 合法性校验(只允许 TME_CANCEL/TME_QUERY/TME_NONCLIENT/TME_LEAVE/TME_HOVER)

→ 全局锁 → TME_QUERY 走 IntQueryTrackMouseEvent(查询当前状态写回用户缓冲,带

ProbeForWrite),否则走 IntTrackMouseEvent

13.2 IntTrackMouseEvent --- mouse.c L408

设置/取消跟踪:

c 复制代码
BOOL
FASTCALL
IntTrackMouseEvent(
    LPTRACKMOUSEEVENT lpEventTrack)
{
    PDESKTOP pDesk;
    PTHREADINFO pti;
    PWND pWnd;
    POINT point;

    pti = PsGetCurrentThreadWin32Thread();
    pDesk = pti->rpdesk;

    if (!(pWnd = UserGetWindowObject(lpEventTrack->hwndTrack)))
        return FALSE;

    if ( pDesk->spwndTrack != pWnd ||
            (pDesk->htEx != HTCLIENT) ^ !!(lpEventTrack->dwFlags & TME_NONCLIENT) )
    {
        if ( lpEventTrack->dwFlags & TME_LEAVE && !(lpEventTrack->dwFlags & TME_CANCEL) )
        {
            UserPostMessage( lpEventTrack->hwndTrack,
                             lpEventTrack->dwFlags & TME_NONCLIENT ? WM_NCMOUSELEAVE : WM_MOUSELEAVE,
                             0, 0);
        }
        TRACE("IntTrackMouseEvent spwndTrack %p pwnd %p\n", pDesk->spwndTrack, pWnd);
        return TRUE;
    }
    ...

分支一:跟踪窗口已变化spwndTrack != pWnd,或客户/非客户属性与 TME_NONCLIENT

标志不一致)------若请求含 TME_LEAVE 且非 TME_CANCEL,立即补发一次

WM_MOUSELEAVE/WM_NCMOUSELEAVE(光标其实不在该窗口上,语义上"已经离开了"),

然后返回。这保证"重新跟踪一个光标已离开的窗口"时应用仍收到离开事件。

c 复制代码
    /* Tracking spwndTrack same as pWnd */
    if (lpEventTrack->dwFlags & TME_CANCEL) // Canceled mode.
    {
        if (lpEventTrack->dwFlags & TME_LEAVE)
            pDesk->dwDTFlags &= ~DF_TME_LEAVE;

        if (lpEventTrack->dwFlags & TME_HOVER)
        {
            if (pDesk->dwDTFlags & DF_TME_HOVER)
            {   // Kill hover timer.
                IntKillTimer(pWnd, ID_EVENT_SYSTIMER_MOUSEHOVER, TRUE);
                pDesk->dwDTFlags &= ~DF_TME_HOVER;
            }
        }
    }
    else // Not Canceled.
    {
        if (lpEventTrack->dwFlags & TME_LEAVE)
            pDesk->dwDTFlags |= DF_TME_LEAVE;

        if (lpEventTrack->dwFlags & TME_HOVER)
        {
            pDesk->dwDTFlags |= DF_TME_HOVER;

            if (!lpEventTrack->dwHoverTime || lpEventTrack->dwHoverTime == HOVER_DEFAULT)
                pDesk->dwMouseHoverTime = gspv.iMouseHoverTime; // use the system default hover time-out.
            else
                pDesk->dwMouseHoverTime = lpEventTrack->dwHoverTime;
            // Start timer for the hover period.
            IntSetTimer(pWnd, ID_EVENT_SYSTIMER_MOUSEHOVER, pDesk->dwMouseHoverTime, SystemTimerProc, TMRF_SYSTEM);
            // Get windows thread message points.
            point = pWnd->head.pti->ptLast;
            // Set desktop mouse hover from the system default hover rectangle.
            RECTL_vSetRect(&pDesk->rcMouseHover,
                           point.x - gspv.iMouseHoverWidth  / 2,
                           point.y - gspv.iMouseHoverHeight / 2,
                           point.x + gspv.iMouseHoverWidth  / 2,
                           point.y + gspv.iMouseHoverHeight / 2);
        }
    }
    return TRUE;
}

分支二:正常设置/取消

  • TME_CANCEL:清除 DF_TME_LEAVE;若曾开 HOVER,IntKillTimer 杀掉悬停计时器并清
    DF_TME_HOVER;
  • 设置:置 DF_TME_LEAVE / DF_TME_HOVER;HOVER 时用系统默认
    gspv.iMouseHoverTime(HOVER_DEFAULT=0xFFFFFFFF)或调用者指定的悬停时间启动
    系统计时器 ID_EVENT_SYSTIMER_MOUSEHOVERIntSetTimer(..., SystemTimerProc, TMRF_SYSTEM),见 _14 册),并以当前光标位置为中心、iMouseHoverWidth/Height 为边长
    构建悬停方框 rcMouseHover------光标在方框内停留超过悬停时间即触发 WM_MOUSEHOVER。

13.3 IntRemoveTrackMouseEvent --- mouse.c L355

c 复制代码
VOID
FASTCALL
IntRemoveTrackMouseEvent(
    PDESKTOP pDesk)
{
    /* Generate a leave message */
    if (pDesk->dwDTFlags & DF_TME_LEAVE)
    {
        UINT uMsg = (pDesk->htEx != HTCLIENT) ? WM_NCMOUSELEAVE : WM_MOUSELEAVE;
        UserPostMessage(UserHMGetHandle(pDesk->spwndTrack), uMsg, 0, 0);
    }
    /* Kill the timer */
    if (pDesk->dwDTFlags & DF_TME_HOVER)
        IntKillTimer(pDesk->spwndTrack, ID_EVENT_SYSTIMER_MOUSEHOVER, TRUE);

    /* Reset state */
    pDesk->dwDTFlags &= ~(DF_TME_LEAVE|DF_TME_HOVER);
    pDesk->spwndTrack = NULL;
}

桌面销毁/切换时清理:补发最后一次离开消息(按当前 htEx 选客户/非客户版本)、杀悬停

计时器、复位跟踪状态。

13.4 IntQueryTrackMouseEvent --- mouse.c L373

TME_QUERY 实现:清空输出缓冲(固定 cbSize),若当前桌面有跟踪且跟踪窗口与调用线程

同队列,则回填 TME_LEAVE/TME_HOVER/TME_NONCLIENT 标志、dwHoverTime、hwndTrack:

c 复制代码
    /* Always cleared with size set and return true. */
    RtlZeroMemory(lpEventTrack , sizeof(TRACKMOUSEEVENT));
    lpEventTrack->cbSize = sizeof(TRACKMOUSEEVENT);

    if (pDesk->dwDTFlags & (DF_TME_LEAVE | DF_TME_HOVER) &&
        pDesk->spwndTrack &&
        pti->MessageQueue == pDesk->spwndTrack->head.pti->MessageQueue)
    {
        if (pDesk->htEx != HTCLIENT)
            lpEventTrack->dwFlags |= TME_NONCLIENT;
        if (pDesk->dwDTFlags & DF_TME_LEAVE)
            lpEventTrack->dwFlags |= TME_LEAVE;
        if (pDesk->dwDTFlags & DF_TME_HOVER)
        {
            lpEventTrack->dwFlags |= TME_HOVER;
            lpEventTrack->dwHoverTime = pDesk->dwMouseHoverTime;
        }
        lpEventTrack->hwndTrack = UserHMGetHandle(pDesk->spwndTrack);
    }
    return TRUE;

注意 TME_NONCLIENT 不是独立跟踪位,而是从当前 htEx != HTCLIENT 推导出来的"当前

是否处于非客户区"状态。

13.5 IntTrackMouseMove --- msgqueue.c L1432

跟踪的运行时驱动 :每次鼠标消息落地(co_IntProcessMouseMessage L1551)时调用,

负责"光标换了窗口/区域"时发出 WM_MOUSELEAVE 并重启悬停计时器:

c 复制代码
VOID FASTCALL
IntTrackMouseMove(PWND pwndTrack, PDESKTOP pDesk, PMSG msg, USHORT hittest)
{
   if ( pDesk->spwndTrack != pwndTrack || // Change with tracking window or
        msg->message != WM_MOUSEMOVE   || // Mouse click changes or
        pDesk->htEx != hittest)           // Change in current hit test states.
   {
      TRACE("ITMM: Track Mouse Move!\n");

      /* Handle only the changing window track and mouse move across a border. */
      if ( pDesk->spwndTrack != pwndTrack ||
          (pDesk->htEx == HTCLIENT) ^ (hittest == HTCLIENT) )
      {
         TRACE("ITMM: Another Wnd %d or Across Border %d\n", ...);

         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);

         /* Clear the flags to sign a change. */
         pDesk->dwDTFlags &= ~(DF_TME_LEAVE|DF_TME_HOVER);
      }
      /* Set the Track window and hit test. */
      pDesk->spwndTrack = pwndTrack;
      pDesk->htEx = hittest;
   }

   /* Reset, Same Track window, Hover set and Mouse Clicks or Clobbered Hover box. */
   if ( pDesk->spwndTrack == pwndTrack &&
       ( msg->message != WM_MOUSEMOVE || !RECTL_bPointInRect(&pDesk->rcMouseHover, msg->pt.x, msg->pt.y)) &&
        pDesk->dwDTFlags & DF_TME_HOVER )
   {
      TRACE("ITMM: Reset Hover points!\n");
      // Restart timer for the hover period.
      IntSetTimer(pDesk->spwndTrack, ID_EVENT_SYSTIMER_MOUSEHOVER, pDesk->dwMouseHoverTime, SystemTimerProc, TMRF_SYSTEM);
      // Reset desktop mouse hover from the system default hover rectangle.
      RECTL_vSetRect(&pDesk->rcMouseHover,
                      msg->pt.x - gspv.iMouseHoverWidth  / 2,
                      msg->pt.y - gspv.iMouseHoverHeight / 2,
                      msg->pt.x + gspv.iMouseHoverWidth  / 2,
                      msg->pt.y + gspv.iMouseHoverHeight / 2);
   }
}

触发"离开"的条件是窗口变化客户/非客户边界穿越(htEx==HTCLIENT) ^ (hittest==HTCLIENT) 异或为真即一侧是客户区另一侧不是)------只在这两种情况下发

WM_MOUSELEAVE;命中码在客户区内变化(如从客户区 A 点到客户区 B 点)不构成离开。

悬停方框被"踩出"(移动出 rcMouseHover)或收到非移动消息(点击)时重启悬停计时器并

把方框挪到新位置。

13.6 悬停触发 WM_MOUSEHOVER --- timer.c L313

ID_EVENT_SYSTIMER_MOUSEHOVER 计时器到期回调 SystemTimerProc(timer.c L283):

c 复制代码
     case ID_EVENT_SYSTIMER_MOUSEHOVER:
       {
          POINT Point;
          UINT Msg;
          WPARAM wParam;

          pDesk = pWnd->head.rpdesk;
          if ( pDesk->dwDTFlags & DF_TME_HOVER &&
               pWnd == pDesk->spwndTrack )
          {
             Point = gpsi->ptCursor;
             if ( RECTL_bPointInRect(&pDesk->rcMouseHover, Point.x, Point.y) )
             {
                if (pDesk->htEx == HTCLIENT) // In a client area.
                {
                   wParam = MsqGetDownKeyState(pWnd->head.pti->MessageQueue);
                   Msg = WM_MOUSEHOVER;
                   if (pWnd->ExStyle & WS_EX_LAYOUTRTL)
                      Point.x = pWnd->rcClient.right - Point.x - 1;
                   else
                      Point.x -= pWnd->rcClient.left;
                   Point.y -= pWnd->rcClient.top;
                }
                else
                {
                   wParam = pDesk->htEx; // Need to support all HTXYZ hits.
                   Msg = WM_NCMOUSEHOVER;
                }
                UserPostMessage(hwnd, Msg, wParam, MAKELPARAM(Point.x, Point.y));
                pDesk->dwDTFlags &= ~DF_TME_HOVER;
                break; // Kill this timer.
             }
          }
       }
       return; // Not this window so just return.

到期时检查:HOVER 跟踪仍有效、光标仍在悬停方框内 → 生成 WM_MOUSEHOVER

(客户区,wParam=当前键状态,坐标为客户区坐标 ,含 WS_EX_LAYOUTRTL 镜像)或

WM_NCMOUSEHOVER(非客户区,wParam=htEx 命中码);发送后清 DF_TME_HOVER(悬停

只触发一次,需要重新 TrackMouseEvent 才能再次触发),并杀掉计时器

IntKillTimer(pWnd, idEvent, TRUE) 在 SystemTimerProc 末尾统一执行)。

WM_MOUSELEAVE/WM_MOUSEHOVER 的生命周期总结

复制代码
应用 TrackMouseEvent(TME_LEAVE|TME_HOVER)
  → 桌面置 DF_TME_LEAVE|DF_TME_HOVER,spwndTrack=窗口
  → 启动悬停计时器,构建 rcMouseHover
光标移出窗口/跨越客户-非客户边界
  → IntTrackMouseMove 发 WM_(NC)MOUSELEAVE,清标志,杀计时器
光标在方框内停留 > 悬停时间
  → SystemTimerProc 发 WM_(NC)MOUSEHOVER,清 DF_TME_HOVER
光标离开后再次进入 → 需要应用重新 TrackMouseEvent(Win32 语义)

14. 鼠标移动轨迹 GetMouseMovePointsEx

14.1 数据源:移动历史 FIFO --- mouse.c L13

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

UserSendMouseInput 每次处理输入时把 (x, y, time, dwExtraInfo) 写入 64 项环形缓冲区

(L217-L222,见 5.3 节),gcMouseHistoryOfMoves 始终指向"下一个写入槽",即历史按

时间顺序环形推进。

14.2 NtUserGetMouseMovePointsEx --- mouse.c L541

用户态 GetMouseMovePointsEx(user32.spec L345 直通该系统调用)查询历史:

c 复制代码
DWORD
APIENTRY
NtUserGetMouseMovePointsEx(
    UINT cbSize,
    LPMOUSEMOVEPOINT lpptIn,
    LPMOUSEMOVEPOINT lpptOut,
    int nBufPoints,
    DWORD resolution)
{
    MOUSEMOVEPOINT Safeppt;
    INT iRet = -1;

    if ((cbSize != sizeof(MOUSEMOVEPOINT)) || (nBufPoints < 0) || (nBufPoints > 64))
    {
        EngSetLastError(ERROR_INVALID_PARAMETER);
        return (DWORD)-1;
    }

    if (!lpptIn || (!lpptOut && nBufPoints))
    {
        EngSetLastError(ERROR_NOACCESS);
        return (DWORD)-1;
    }

    _SEH2_TRY
    {
        ProbeForRead(lpptIn, cbSize, 1);
        RtlCopyMemory(&Safeppt, lpptIn, cbSize);
    }
    _SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
    { ... }
    _SEH2_END;

    UserEnterShared();

    iRet = gcMouseHistoryOfMoves; // FIFO is forward so retrieve backward.
    do
    {
        if (Safeppt.x == 0 && Safeppt.y == 0)
            break; // No test.
        // Finds the point, it returns the last nBufPoints prior to and including the supplied point.
        if (gMouseHistoryOfMoves[iRet].x == Safeppt.x && gMouseHistoryOfMoves[iRet].y == Safeppt.y)
        {
            if (Safeppt.time) // Now test time and it seems to be absolute.
            {
                if (Safeppt.time == gMouseHistoryOfMoves[iRet].time)
                {
                    break;   // 找到匹配点
                }
                else
                {
                    if (--iRet < 0) iRet = 63;
                    continue; // 坐标相同但时间不同,继续回溯
                }
            }
            break;
        }
        if (--iRet < 0) iRet = 63;
    }
    while (iRet != gcMouseHistoryOfMoves);
    ...

查找算法:从"最新写入槽"开始向后 (时间倒序)扫描历史;若调用者给出参考点

(x/y/time),找历史中坐标(和可选时间)匹配的条目,返回包含该点在内、它之前的
nBufPoints 条
;参考点为 (0,0) 时不做匹配(直接返回最近的 n 条)。

resolution 参数处理(GMMP_USE_DISPLAY_POINTS=1 / GMMP_USE_HIGH_RESOLUTION_POINTS=2):

ReactOS 当前只实现 DISPLAY_POINTS(校验输出缓冲可写后返回 nBufPoints 计数),

HIGH_RESOLUTION 无实际转换,其他值报 ERROR_POINT_NOT_FOUND

c 复制代码
    switch(resolution)
    {
        case GMMP_USE_DISPLAY_POINTS:
            if (nBufPoints)
            {
                _SEH2_TRY { ProbeForWrite(lpptOut, cbSize, 1); }
                _SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER) { ... iRet = -1; goto cleanup; }
                _SEH2_END;
            }
            iRet = nBufPoints;
            break;
        case GMMP_USE_HIGH_RESOLUTION_POINTS:
            break;   // FIXME: 未实现高分辨率坐标转换
        default:
            EngSetLastError(ERROR_POINT_NOT_FOUND);
            iRet = -1;
    }

该 API 的典型使用者是手写笔/绘图 应用与鼠标手势 识别:给定"当前点",取最近

若干历史点绘制平滑轨迹。


15. 鼠标输入处理全流程

15.1 移动(MOUSEMOVE)的完整路径

复制代码
鼠标移动(相对增量 dx=3, dy=-2)
  → mouclass 组装 MOUSE_INPUT_DATA{LastX=3, LastY=-2, ButtonFlags=0}
  → RawInputThreadMain:ZwReadFile 完成 → IntLastInputTick → UserEnterExclusive
  → UserProcessMouseInput:dx/dy≠0 → MOUSEEVENTF_MOVE
  → UserSendMouseInput(&mi, bInjected=FALSE)
      ├── ptCursor = gpsi->ptCursor + (3,-2)
      ├── 写 gMouseHistoryOfMoves FIFO
      ├── UserSetCursorPos(新坐标) → GreMovePointer 屏幕指针移动
      ├── Msg = {message 待定, wParam=UserGetMouseButtonsState(), lParam=屏幕坐标, pt, time}
      └── co_MsqInsertMouseMessage(&Msg, ...)
            ├── WH_MOUSE_LL(可拦截)
            ├── 目标 = 捕获窗口 ?: IntTopLevelWindowFromPoint(屏幕坐标)
            ├── 光标形状同步(GreSetPointerShape / GreMovePointer)
            ├── 置 QF_MOUSEMOVED、gdwMouseMoveTimeStamp/ExtraInfo
            └── MsqWakeQueue(QS_MOUSEMOVE)(消息未入队!)
  应用 GetMessage(WM_MOUSEMOVE)
  → co_MsqPeekHardwareMessage → 更新 afKeyState → co_IntProcessMouseMessage
      ├── 无捕获 → co_WinPosWindowFromPoint(桌面, pt, &htEx)
      │     → co_WinPosSearchChildren 深度遍历 + WM_NCHITTEST → 目标窗口 + htEx
      ├── IntTrackMouseMove(跨窗口/边界 → WM_MOUSELEAVE)
      ├── 客户区 → 坐标换算为窗口客户区坐标
      ├── WM_MOUSEMOVE → wParam = MsqGetDownKeyState(afKeyState)
      └── 接受 → 返回 MSG{WM_MOUSEMOVE, hwnd, wParam, lParam=客户区坐标}
  → co_IntGetPeekMessage 记录 ptLast/timeLast
  → DispatchMessage → 窗口过程(DefWindowProc 依 WM_SETCURSOR 逻辑设光标形状)

关键点:移动消息的入队是延迟的 (QF_MOUSEMOVED 标志 + 消费时合并),

IntCoalesceMouseMove 在按键/睡眠两个触发点把它落成一条 WM_MOUSEMOVE。

15.2 按下(LBUTTONDOWN / DBLCLK)的完整路径

复制代码
鼠标左键按下
  → MOUSE_INPUT_DATA{ButtonFlags=MOUSE_LEFT_BUTTON_DOWN}
  → UserProcessMouseInput:dwFlags |= MOUSEEVENTF_LEFTDOWN(无位移则无 MOVE)
  → UserSendMouseInput
      ├── SET_KEY_DOWN(gafAsyncKeyState, VK_LBUTTON, TRUE)
      ├── Msg.message = WM_LBUTTONDOWN;ButtonsDown |= MK_LBUTTON;wParam |= MK_LBUTTON
      └── co_MsqInsertMouseMessage
            ├── 队列有未消费移动?→ IntCoalesceMouseMove 先入队 WM_MOUSEMOVE
            ├── MsqPostMessage(QS_MOUSEBUTTON) 按键消息入队
  应用 GetMessage
  → co_MsqPeekHardwareMessage → UpdateKeyStateFromMsg(VK_LBUTTON down)
  → co_IntProcessMouseMessage
      ├── 无捕获 → co_WinPosWindowFromPoint → 窗口 + htEx
      ├── 双击判定:与 msgDblClk 比较(同窗/同键/时间<500ms/距离<方框)
      │     ├── 命中 → message = WM_LBUTTONDBLCLK,清 msgDblClk
      │     └── 未中 → msgDblClk = 本次消息(作为下次基准)
      ├── MsqSendParentNotify(沿父链发 WM_PARENTNOTIFY)
      ├── 非活动窗口 → WM_MOUSEACTIVATE → MA_ACTIVATE → co_IntMouseActivateWindow
      │     → co_IntSetForegroundAndFocusWindow(前台锁定规则检查)
      ├── WM_SETCURSOR(htEx + 消息号)→ DefWindowProc 按 htEx 换光标
      └── 接受 → 返回给应用(未被 MA_*ANDEAT 吃掉时)

15.3 抬起(LBUTTONUP)与捕获结束

复制代码
左键抬起
  → ... UserSendMouseInput
      ├── SET_KEY_DOWN(VK_LBUTTON, FALSE)
      ├── Msg.message = WM_LBUTTONUP;ButtonsDown &= ~MK_LBUTTON
      └── 入队 QS_MOUSEBUTTON
  应用 GetMessage → co_IntProcessMouseMessage
      ├── 目标仍是捕获窗口(拖拽期间)→ 命中测试直接返回捕获窗口
      ├── 无双击判定(UP 消息不参与)
      └── 应用收到 WM_LBUTTONUP → ReleaseCapture → spwndCapture=NULL

15.4 滚轮(MOUSEWHEEL)

复制代码
滚轮滚动 +120
  → MOUSE_INPUT_DATA{ButtonFlags=MOUSE_WHEEL, ButtonData=120}
  → UserProcessMouseInput:dwFlags |= MOUSEEVENTF_WHEEL;mouseData=120
  → UserSendMouseInput
      ├── Msg.message = WM_MOUSEWHEEL
      ├── Msg.wParam = MAKEWPARAM(ButtonsDown, 120)   // 高字=滚动量
      └── co_MsqInsertMouseMessage → 入队 QS_MOUSEBUTTON
  应用 GetMessage → co_IntProcessMouseMessage
      ├── WM_MOUSEWHEEL 不换算非客户区(无 NC 版本)
      ├── 目标由命中测试给出(焦点窗口语义:默认滚动到光标下窗口)
      └── wParam 高字 120 = WHEEL_DELTA(一次"格"),应用按 iWheelScrollLines 换算行数

15.5 非客户区(NCLBUTTONDOWN)

复制代码
点击标题栏(命中码 HTCAPTION)
  → 命中测试 WM_NCHITTEST 返回 HTCAPTION(DefWindowProc → GetNCHitEx 几何判定)
  → co_IntProcessMouseMessage:hittest != HTCLIENT
      ├── message += WM_NCMOUSEMOVE - WM_MOUSEMOVE → WM_NCLBUTTONDOWN
      ├── msg->wParam = HTCAPTION(命中码)
      ├── 坐标为屏幕坐标(不换算)
      └── WM_SETCURSOR 按 HTCAPTION → 默认箭头
  应用 DefWindowProc 处理 WM_NCLBUTTONDOWN(HT CAPTION)
  → nonclient.c 开始标题栏拖动循环(捕获 + 移动窗口 + 双击最大化)

16. 用户态封装与系统参数

16.1 user32/windows/input.c 中的鼠标 API

API 实现 说明
GetAsyncKeyState L530-L538 → NtUserGetAsyncKeyState 读 gafAsyncKeyState
GetKeyState L603-L609 → NtUserGetKeyState 读队列 afKeyState
SetDoubleClickTime L929-L936 → NtUserSystemParametersInfo(SPI_SETDOUBLECLICKTIME, uInterval, ...) 修改 gspv.iDblClickTime
SwapMouseButton L942-L948 → NtUserxSwapMouseButton 设置 gspv.bMouseBtnSwap
mouse_event L1102-L1122 → NtUserSendInput 组装 INPUT.mi 注入
keybd_event L1078-L1096 → NtUserSendInput 键盘注入(对照)
GetLastInputInfo L655-L668 → gpsi->dwLastRITEventTickCount 最后一次 RIT 输入时间
DragDetect L460-L516 → NtUserDragDetect 拖动判定(用户态参考实现被 #if 0 保留)
GetMouseMovePointsEx user32.spec L345 → NtUserGetMouseMovePointsEx 移动轨迹查询

mouse_event 的组装(L1104-L1122):

c 复制代码
VOID
WINAPI
mouse_event(
    DWORD dwFlags,
    DWORD dx,
    DWORD dy,
    DWORD dwData,
    ULONG_PTR dwExtraInfo)
{
    INPUT Input;

    Input.type = INPUT_MOUSE;
    Input.mi.dx = dx;
    Input.mi.dy = dy;
    Input.mi.mouseData = dwData;
    Input.mi.dwFlags = dwFlags;
    Input.mi.time = 0;
    Input.mi.dwExtraInfo = dwExtraInfo;

    NtUserSendInput(1, &Input, sizeof(INPUT));
}

16.2 SPI_SETDOUBLECLICKTIME --- sysparams.c L942

c 复制代码
            case SPI_SETDOUBLECLICKTIME:
                return SpiSetInt(&gspv.iDblClickTime, uiParam, KEY_MOUSE, VAL_DBLCLKTIME, fl);

SpiSetInt 更新 gspv.iDblClickTime 并写注册表(KEY_MOUSE 键下 VAL_DBLCLKTIME),

后续 co_IntProcessMouseMessage 双击判定直接读该字段生效。

16.3 鼠标系统参数的加载 --- sysparams.c

  • L214:if (gspv.iDblClickTime == 0) gspv.iDblClickTime = 500;
  • L249:gspv.iDblClickTime = SpiLoadMouse(VAL_DBLCLKTIME, 500);(启动时从注册表恢复)
  • 同批加载的还有 bMouseBtnSwapiMouseHoverTime/Width/HeightbMouseClickLock
    dwMouseClickLockTimeiDblClickWidth/HeightiWheelScrollLines 等(sysparams.h
    "Mouse" 字段组)。

17. 与其他模块的关系

复制代码
                    ┌─────────────────────────────────────────────┐
                    │  user32.dll(用户态)                        │
                    │  input.c:mouse_event/SendInput/SetDoubleClickTime │
                    └───────────────┬─────────────────────────────┘
                                    │ NtUserSendInput / NtUserSystemParametersInfo
                                    ▼
   ┌──────────────┐   ┌──────────────────────────────────────────────┐
   │ 硬件驱动      │   │ win32k.sys  ntuser/                          │
   │ mouclass      │   │                                              │
   │ MOUSE_INPUT_  │──►│ input.c:RawInputThreadMain(读设备、RIT 线程)│
   │ DATA          │   │      │ UserProcessMouseInput                 │
   └──────────────┘   │      ▼                                       │
                      │ mouse.c:UserSendMouseInput(核心)           │
                      │      ├── cursoricon.c:UserSetCursorPos/       │
                      │      │     IntGetSysCursorInfo/GreSetPointerShape│
                      │      │     (光标位置/形状)                    │
                      │      ├── IntFixMouseInputButtons(左右交换)    │
                      │      └──► msgqueue.c:co_MsqInsertMouseMessage │
                      │             ├── focus.c:IntGetCaptureWindow(捕获)│
                      │             ├── winpos.c:IntTopLevelWindowFromPoint│
                      │             ├── 光标同步/合并标志/QF_MOUSEMOVED  │
                      │             └── MsqPostMessage → 硬件消息链表    │
                      │                                                  │
                      │ 应用 GetMessage → message.c → co_MsqPeekHardwareMessage│
                      │        → co_IntProcessHardwareMessage           │
                      │        → co_IntProcessMouseMessage(msgqueue.c)│
                      │              ├── winpos.c:co_WinPosWindowFromPoint│
                      │              │     (完整命中 + WM_NCHITTEST)    │
                      │              ├── 双击判定(gspv.iDblClickTime +   │
                      │              │      SM_CX/YDOUBLECLK,msgDblClk) │
                      │              ├── focus.c:co_IntMouseActivateWindow│
                      │              ├── 钩子:WH_MOUSE/WH_CBT/           │
                      │              │      WH_JOURNALRECORD(hook.c)    │
                      │              └── timer.c:SystemTimerProc         │
                      │                    (WM_MOUSEHOVER)              │
                      └──────────────────────────────────────────────┘
模块 交互点 方向
input.c RawInputThreadMain 读设备并调 UserProcessMouseInput;NtUserSendInput 注入 上游
cursoricon.c UserSetCursorPos/IntGetSysCursorInfo/UserSetCursor/IntSystemSetCursor/GreSetPointerShape 光标
winpos.c IntTopLevelWindowFromPoint(粗命中)/co_WinPosWindowFromPoint(精命中) 命中
focus.c IntGetCaptureWindow(捕获)/co_IntMouseActivateWindow(点击激活) 定向/激活
msgqueue.c co_MsqInsertMouseMessage/co_IntProcessMouseMessage/IntCoalesceMouseMove/IntTrackMouseMove 本册主体
message.c co_IntGetPeekMessage 消费硬件消息 → co_MsqPeekHardwareMessage 消费
hook.c WH_MOUSE_LL/WH_MOUSE/WH_CBT/WH_JOURNALRECORD 钩子
timer.c ID_EVENT_SYSTIMER_MOUSEHOVER → WM_MOUSEHOVER 悬停
defwnd.c WM_NCHITTEST(GetNCHitEx)/WM_SETCURSOR(换光标) 非客户区
misc.c NtUserGetDoubleClickTime(gspv.iDblClickTime) 双击时间
sysparams.c SPI_SETDOUBLECLICKTIME 等鼠标参数 配置
metric.c SM_CXDOUBLECLK/SM_CYDOUBLECLK/SM_CXSCREEN 等 度量
event.c IntNotifyWinEvent(EVENT_OBJECT_LOCATIONCHANGE) 可访问性

18. 调用链总览

18.1 硬件鼠标事件(mermaid)

消息队列 (HardwareMessagesListHead) UserSetCursorPos (cursoricon.c) 命中测试 (winpos.c/msgqueue.c) co_MsqInsertMouseMessage (msgqueue.c) UserSendMouseInput (mouse.c) UserProcessMouseInput (mouse.c) RawInputThreadMain (input.c) 鼠标硬件/驱动 消息队列 (HardwareMessagesListHead) UserSetCursorPos (cursoricon.c) 命中测试 (winpos.c/msgqueue.c) co_MsqInsertMouseMessage (msgqueue.c) UserSendMouseInput (mouse.c) UserProcessMouseInput (mouse.c) RawInputThreadMain (input.c) 鼠标硬件/驱动 #mermaid-svg-RXa3PxYlrqBNVTdh{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-RXa3PxYlrqBNVTdh .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-RXa3PxYlrqBNVTdh .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-RXa3PxYlrqBNVTdh .error-icon{fill:#552222;}#mermaid-svg-RXa3PxYlrqBNVTdh .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-RXa3PxYlrqBNVTdh .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-RXa3PxYlrqBNVTdh .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-RXa3PxYlrqBNVTdh .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-RXa3PxYlrqBNVTdh .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-RXa3PxYlrqBNVTdh .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-RXa3PxYlrqBNVTdh .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-RXa3PxYlrqBNVTdh .marker{fill:#333333;stroke:#333333;}#mermaid-svg-RXa3PxYlrqBNVTdh .marker.cross{stroke:#333333;}#mermaid-svg-RXa3PxYlrqBNVTdh svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-RXa3PxYlrqBNVTdh p{margin:0;}#mermaid-svg-RXa3PxYlrqBNVTdh .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-RXa3PxYlrqBNVTdh text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-RXa3PxYlrqBNVTdh .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-RXa3PxYlrqBNVTdh .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-RXa3PxYlrqBNVTdh .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-RXa3PxYlrqBNVTdh .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-RXa3PxYlrqBNVTdh #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-RXa3PxYlrqBNVTdh .sequenceNumber{fill:white;}#mermaid-svg-RXa3PxYlrqBNVTdh #sequencenumber{fill:#333;}#mermaid-svg-RXa3PxYlrqBNVTdh #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-RXa3PxYlrqBNVTdh .messageText{fill:#333;stroke:none;}#mermaid-svg-RXa3PxYlrqBNVTdh .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-RXa3PxYlrqBNVTdh .labelText,#mermaid-svg-RXa3PxYlrqBNVTdh .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-RXa3PxYlrqBNVTdh .loopText,#mermaid-svg-RXa3PxYlrqBNVTdh .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-RXa3PxYlrqBNVTdh .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-RXa3PxYlrqBNVTdh .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-RXa3PxYlrqBNVTdh .noteText,#mermaid-svg-RXa3PxYlrqBNVTdh .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-RXa3PxYlrqBNVTdh .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-RXa3PxYlrqBNVTdh .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-RXa3PxYlrqBNVTdh .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-RXa3PxYlrqBNVTdh .actorPopupMenu{position:absolute;}#mermaid-svg-RXa3PxYlrqBNVTdh .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-RXa3PxYlrqBNVTdh .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-RXa3PxYlrqBNVTdh .actor-man circle,#mermaid-svg-RXa3PxYlrqBNVTdh line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-RXa3PxYlrqBNVTdh :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ZwReadFile 完成 (MOUSE_INPUT_DATA) UserProcessMouseInput(&MouseInput) UserSendMouseInput(&mi, FALSE)(可能多次) IntFixMouseInputButtons(左右交换) 位置计算 → gMouseHistoryOfMoves FIFO UserSetCursorPos(更新屏幕指针) co_MsqInsertMouseMessage(按键)或置 QF_MOUSEMOVED(移动) IntGetCaptureWindow ?: IntTopLevelWindowFromPoint MsqPostMessage / 合并标志 + MsqWakeQueue

18.2 消息消费与落地(mermaid)

DefWindowProc (defwnd.c) co_IntMouseActivateWindow (focus.c) IntTrackMouseMove (msgqueue.c) co_WinPosWindowFromPoint (winpos.c) co_IntProcessMouseMessage (msgqueue.c) co_MsqPeekHardwareMessage (msgqueue.c) co_IntGetPeekMessage (message.c) 应用 GetMessage/PeekMessage DefWindowProc (defwnd.c) co_IntMouseActivateWindow (focus.c) IntTrackMouseMove (msgqueue.c) co_WinPosWindowFromPoint (winpos.c) co_IntProcessMouseMessage (msgqueue.c) co_MsqPeekHardwareMessage (msgqueue.c) co_IntGetPeekMessage (message.c) 应用 GetMessage/PeekMessage #mermaid-svg-2VyoL3tw5EDYfSsX{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-2VyoL3tw5EDYfSsX .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-2VyoL3tw5EDYfSsX .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-2VyoL3tw5EDYfSsX .error-icon{fill:#552222;}#mermaid-svg-2VyoL3tw5EDYfSsX .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-2VyoL3tw5EDYfSsX .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-2VyoL3tw5EDYfSsX .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-2VyoL3tw5EDYfSsX .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-2VyoL3tw5EDYfSsX .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-2VyoL3tw5EDYfSsX .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-2VyoL3tw5EDYfSsX .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-2VyoL3tw5EDYfSsX .marker{fill:#333333;stroke:#333333;}#mermaid-svg-2VyoL3tw5EDYfSsX .marker.cross{stroke:#333333;}#mermaid-svg-2VyoL3tw5EDYfSsX svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-2VyoL3tw5EDYfSsX p{margin:0;}#mermaid-svg-2VyoL3tw5EDYfSsX .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-2VyoL3tw5EDYfSsX text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-2VyoL3tw5EDYfSsX .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-2VyoL3tw5EDYfSsX .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-2VyoL3tw5EDYfSsX .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-2VyoL3tw5EDYfSsX .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-2VyoL3tw5EDYfSsX #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-2VyoL3tw5EDYfSsX .sequenceNumber{fill:white;}#mermaid-svg-2VyoL3tw5EDYfSsX #sequencenumber{fill:#333;}#mermaid-svg-2VyoL3tw5EDYfSsX #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-2VyoL3tw5EDYfSsX .messageText{fill:#333;stroke:none;}#mermaid-svg-2VyoL3tw5EDYfSsX .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-2VyoL3tw5EDYfSsX .labelText,#mermaid-svg-2VyoL3tw5EDYfSsX .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-2VyoL3tw5EDYfSsX .loopText,#mermaid-svg-2VyoL3tw5EDYfSsX .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-2VyoL3tw5EDYfSsX .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-2VyoL3tw5EDYfSsX .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-2VyoL3tw5EDYfSsX .noteText,#mermaid-svg-2VyoL3tw5EDYfSsX .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-2VyoL3tw5EDYfSsX .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-2VyoL3tw5EDYfSsX .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-2VyoL3tw5EDYfSsX .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-2VyoL3tw5EDYfSsX .actorPopupMenu{position:absolute;}#mermaid-svg-2VyoL3tw5EDYfSsX .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-2VyoL3tw5EDYfSsX .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-2VyoL3tw5EDYfSsX .actor-man circle,#mermaid-svg-2VyoL3tw5EDYfSsX line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-2VyoL3tw5EDYfSsX :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} GetMessage(WM_MOUSEFIRST..WM_MOUSELAST) 取硬件消息 UpdateKeyStateFromMsg co_IntProcessMouseMessage 无捕获 → 完整命中测试(WM_NCHITTEST) 目标窗口 + htEx 跟踪状态更新(跨窗口→WM_MOUSELEAVE) 双击判定(msgDblClk + 时间/距离) 按下非活动窗口 → WM_MOUSEACTIVATE → 激活 WM_SETCURSOR(htEx → 光标形状) 接受 → 返回 MSG 窗口过程收到 WM_MOUSE*(客户区坐标)

18.3 函数调用层级图(mermaid)

#mermaid-svg-rRbLex0IFMqJN59H{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-rRbLex0IFMqJN59H .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-rRbLex0IFMqJN59H .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-rRbLex0IFMqJN59H .error-icon{fill:#552222;}#mermaid-svg-rRbLex0IFMqJN59H .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-rRbLex0IFMqJN59H .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-rRbLex0IFMqJN59H .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-rRbLex0IFMqJN59H .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-rRbLex0IFMqJN59H .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-rRbLex0IFMqJN59H .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-rRbLex0IFMqJN59H .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-rRbLex0IFMqJN59H .marker{fill:#333333;stroke:#333333;}#mermaid-svg-rRbLex0IFMqJN59H .marker.cross{stroke:#333333;}#mermaid-svg-rRbLex0IFMqJN59H svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-rRbLex0IFMqJN59H p{margin:0;}#mermaid-svg-rRbLex0IFMqJN59H .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-rRbLex0IFMqJN59H .cluster-label text{fill:#333;}#mermaid-svg-rRbLex0IFMqJN59H .cluster-label span{color:#333;}#mermaid-svg-rRbLex0IFMqJN59H .cluster-label span p{background-color:transparent;}#mermaid-svg-rRbLex0IFMqJN59H .label text,#mermaid-svg-rRbLex0IFMqJN59H span{fill:#333;color:#333;}#mermaid-svg-rRbLex0IFMqJN59H .node rect,#mermaid-svg-rRbLex0IFMqJN59H .node circle,#mermaid-svg-rRbLex0IFMqJN59H .node ellipse,#mermaid-svg-rRbLex0IFMqJN59H .node polygon,#mermaid-svg-rRbLex0IFMqJN59H .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-rRbLex0IFMqJN59H .rough-node .label text,#mermaid-svg-rRbLex0IFMqJN59H .node .label text,#mermaid-svg-rRbLex0IFMqJN59H .image-shape .label,#mermaid-svg-rRbLex0IFMqJN59H .icon-shape .label{text-anchor:middle;}#mermaid-svg-rRbLex0IFMqJN59H .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-rRbLex0IFMqJN59H .rough-node .label,#mermaid-svg-rRbLex0IFMqJN59H .node .label,#mermaid-svg-rRbLex0IFMqJN59H .image-shape .label,#mermaid-svg-rRbLex0IFMqJN59H .icon-shape .label{text-align:center;}#mermaid-svg-rRbLex0IFMqJN59H .node.clickable{cursor:pointer;}#mermaid-svg-rRbLex0IFMqJN59H .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-rRbLex0IFMqJN59H .arrowheadPath{fill:#333333;}#mermaid-svg-rRbLex0IFMqJN59H .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-rRbLex0IFMqJN59H .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-rRbLex0IFMqJN59H .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rRbLex0IFMqJN59H .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-rRbLex0IFMqJN59H .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rRbLex0IFMqJN59H .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-rRbLex0IFMqJN59H .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-rRbLex0IFMqJN59H .cluster text{fill:#333;}#mermaid-svg-rRbLex0IFMqJN59H .cluster span{color:#333;}#mermaid-svg-rRbLex0IFMqJN59H div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-rRbLex0IFMqJN59H .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-rRbLex0IFMqJN59H rect.text{fill:none;stroke-width:0;}#mermaid-svg-rRbLex0IFMqJN59H .icon-shape,#mermaid-svg-rRbLex0IFMqJN59H .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rRbLex0IFMqJN59H .icon-shape p,#mermaid-svg-rRbLex0IFMqJN59H .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-rRbLex0IFMqJN59H .icon-shape .label rect,#mermaid-svg-rRbLex0IFMqJN59H .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rRbLex0IFMqJN59H .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-rRbLex0IFMqJN59H .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-rRbLex0IFMqJN59H :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} RawInputThreadMain
UserProcessMouseInput
UserSendMouseInput
IntFixMouseInputButtons
UserGetMouseButtonsState
UserSetCursorPos
co_MsqInsertMouseMessage
IntGetCaptureWindow
IntTopLevelWindowFromPoint
IntCoalesceMouseMove
MsqPostMouseMove
MsqPostMessage
NtUserSendInput
GetMessage/PeekMessage
co_MsqPeekHardwareMessage
co_IntProcessMouseMessage
co_WinPosWindowFromPoint
co_WinPosSearchChildren
IntTrackMouseMove
MsqSendParentNotify
co_IntMouseActivateWindow
SystemTimerProc --> WM_MOUSEHOVER
NtUserTrackMouseEvent
IntTrackMouseEvent
IntQueryTrackMouseEvent
IntSetTimer ID_EVENT_SYSTIMER_MOUSEHOVER
NtUserGetMouseMovePointsEx
gMouseHistoryOfMoves FIFO


19. 源码索引

19.1 本册核心文件

文件 关键函数(行号)
mouse.c(file:///d:/reactos/win32ss/user/ntuser/mouse.c) gMouseHistoryOfMoves(L13)、UserGetMouseButtonsState(L21)、UserProcessMouseInput(L39)、IntFixMouseInputButtons(L137)、UserSendMouseInput(L167)、IntRemoveTrackMouseEvent(L355)、IntQueryTrackMouseEvent(L373)、IntTrackMouseEvent(L408)、NtUserTrackMouseEvent(L480)、NtUserGetMouseMovePointsEx(L541)
input.h(file:///d:/reactos/win32ss/user/ntuser/input.h) UserGetMouseButtonsState(L80)、UserProcessMouseInput(L81)、UserSendMouseInput(L82)、gafAsyncKeyState(L97)、IS_KEY_DOWN/SET_KEY_DOWN(L102-L109)
msgqueue.c(file:///d:/reactos/win32ss/user/ntuser/msgqueue.c) IntTopLevelWindowFromPoint(L60)、UserSetCursor(L93)、UpdateKeyStateFromMsg(L266)、MsqGetDownKeyState(L339)、MsqWakeQueue(L413)、ClearMsgBitsMask(L446)、MsqPostMouseMove(L519)、IntCoalesceMouseMove(L552)、co_MsqInsertMouseMessage(L581)、MsqSendParentNotify(L1405)、IntTrackMouseMove(L1432)、co_IntProcessMouseMessage(L1483)、co_IntProcessHardwareMessage(L1908)、is_mouse_message(L1940)、co_MsqPeekHardwareMessage(L1950)、co_MsqWaitForNewMessages(L2130)
msgqueue.h(file:///d:/reactos/win32ss/user/ntuser/msgqueue.h) IS_BTN_MESSAGE(L229)、WM_NCMOUSEFIRST/LAST(L239-L240)、IS_MOUSE_MESSAGE(L242)、IS_KBD_MESSAGE(L246)
winpos.h(file:///d:/reactos/win32ss/user/ntuser/winpos.h) IntPtInWindow(L30)、co_WinPosWindowFromPoint 声明(L66)
winpos.c(file:///d:/reactos/win32ss/user/ntuser/winpos.c) co_WinPosSearchChildren(L2914)、co_WinPosWindowFromPoint(L2999)、IntRealChildWindowFromPoint(L3033)、IntChildWindowFromPointEx(L3075)
focus.c(file:///d:/reactos/win32ss/user/ntuser/focus.c) IntGetCaptureWindow(L33)、co_IntMouseActivateWindow(L1248)、IntUserSetActiveWindow(L~1150)
focus.h(file:///d:/reactos/win32ss/user/ntuser/focus.h) IntGetCaptureWindow(L10)
misc.c(file:///d:/reactos/win32ss/user/ntuser/misc.c) NtUserGetDoubleClickTime(L385-L400,返回 gspv.iDblClickTime)
sysparams.c(file:///d:/reactos/win32ss/user/ntuser/sysparams.c) 双击时间默认(L214)/加载(L249)/SPI_SETDOUBLECLICKTIME(L942)、悬停参数、bMouseBtnSwap 等鼠标系统参数
sysparams.h(file:///d:/reactos/win32ss/user/ntuser/sysparams.h) SPIVALUES 鼠标字段组(L74-L95):iDblClickTime/iDblClickWidth/iDblClickHeight/iMouseHoverTime/iMouseHoverWidth/iMouseHoverHeight/bMouseBtnSwap/bMouseClickLock/dwMouseClickLockTime/iWheelScrollLines
timer.c(file:///d:/reactos/win32ss/user/ntuser/timer.c) SystemTimerProc(L283-L351,ID_EVENT_SYSTIMER_MOUSEHOVER→WM_MOUSEHOVER L313-L350)
timer.h(file:///d:/reactos/win32ss/user/ntuser/timer.h) ID_EVENT_SYSTIMER_MOUSEHOVER(L27)、SystemTimerProc 声明(L42)
input.c(file:///d:/reactos/win32ss/user/ntuser/input.c) RawInputThreadMain(L139,读鼠标设备 + UserProcessMouseInput L324)、NtUserSendInput(L720-L793,INPUT_MOUSEUserSendMouseInput L773)
desktop.h(file:///d:/reactos/win32ss/user/ntuser/desktop.h) 鼠标跟踪字段(L12-L34):dwDTFlags/spwndTrack/htEx/rcMouseHover/dwMouseHoverTimeDF_TME_HOVER(L49)/DF_TME_LEAVE(L50)
ntddmou.h(file:///d:/reactos/sdk/include/psdk/ntddmou.h) MOUSE_INPUT_DATA(L76-L90)、ButtonFlags 常量(L46-L57)、Flags 常量(L67-L74)
cursoricon.c(file:///d:/reactos/win32ss/user/ntuser/cursoricon.c) IntGetSysCursorInfo(L187)、IntSystemSetCursor(L230)、UserSetCursorPos(L238)
cursoricon.h(file:///d:/reactos/win32ss/user/ntuser/cursoricon.h) SYSTEMCUR(L131)、UserSetCursorPos(L142)、IntSystemSetCursor(L145)、IntGetSysCursorInfo(L144)
user32/windows/input.c(file:///d:/reactos/win32ss/user/user32/windows/input.c) SetDoubleClickTime(L929-L936)、SwapMouseButton(L942-L948)、mouse_event(L1102-L1122)、keybd_event(L1078-L1096)、GetAsyncKeyState(L530-L538)、GetKeyState(L603-L609)、GetLastInputInfo(L655-L668)、DragDetect(L460-L516)
user32.spec(file:///d:/reactos/win32ss/user/user32/user32.spec) GetMouseMovePointsEx(L345) → NtUserGetMouseMovePointsEx
defwnd.c(file:///d:/reactos/win32ss/user/ntuser/defwnd.c) WM_NCHITTEST 默认实现(L1183→GetNCHitEx)、WM_SETCURSOR 默认光标切换(L309-L358)
focus.c(file:///d:/reactos/win32ss/user/ntuser/focus.c) IntGetCaptureWindow(L33-L40)、co_IntMouseActivateWindow(L1248-L1255)

19.2 关键常量与宏索引

常量/宏 值/位置 用途
MOUSE_LEFT_BUTTON_DOWN/UP ntddmou.h L46-L57 硬件按键位
MOUSE_WHEEL ntddmou.h L56 滚轮事件标志
MOUSE_MOVE_ABSOLUTE / MOUSE_VIRTUAL_DESKTOP ntddmou.h L68-L69 绝对坐标/虚拟桌面
MOUSEEVENTF_MOVE/LEFTDOWN/.../WHEEL winuser.h MOUSEINPUT 标志
MK_LBUTTON/MK_RBUTTON/MK_SHIFT/MK_CONTROL/MK_MBUTTON/MK_XBUTTON1/MK_XBUTTON2 winuser.h wParam 键状态位
XBUTTON1/XBUTTON2 winuser.h X 键标识(wParam 高字)
WHEEL_DELTA (120) winuser.h 滚轮一格
GET_WHEEL_DELTA_WPARAM / GET_XBUTTON_WPARAM winuser.h wParam 高字提取
CS_DBLCLKS winuser.h 类样式:允许双击消息
HTCLIENT/HTCAPTION/HTERROR/HTNOWHERE/HTTRANSPARENT winuser.h 命中码
MA_NOACTIVATE/MA_ACTIVATE/MA_NOACTIVATEANDEAT/MA_ACTIVATEANDEAT winuser.h WM_MOUSEACTIVATE 返回值
TME_LEAVE/TME_HOVER/TME_QUERY/TME_CANCEL/TME_NONCLIENT / HOVER_DEFAULT winuser.h TrackMouseEvent 标志
DF_TME_HOVER(0x400)/DF_TME_LEAVE(0x800) desktop.h L49-L50 桌面跟踪标志
QF_MOUSEMOVED msgqueue.h 队列"待合并移动"标志
ID_EVENT_SYSTIMER_MOUSEHOVER timer.h L27 悬停系统计时器 ID
WM_NCMOUSEFIRST/WM_NCMOUSELAST msgqueue.h L239-L240 非客户区鼠标消息区间
IS_MOUSE_MESSAGE/IS_KBD_MESSAGE/IS_BTN_MESSAGE msgqueue.h L229-L247 消息分类宏
SM_CXDOUBLECLK/SM_CYDOUBLECLK/SM_CXSCREEN/SM_CYSCREEN/SM_CXVIRTUALSCREEN/SM_CYVIRTUALSCREEN winuser.h 系统度量
LLMHF_INJECTED winuser.h 低级钩子注入标志

19.3 本册覆盖函数清单

函数 文件 行号 类别
UserProcessMouseInput mouse.c L39 输入主处理(硬件)
UserSendMouseInput mouse.c L167 输入主处理(统一)
UserGetMouseButtonsState mouse.c L21 键状态
IntFixMouseInputButtons mouse.c L137 左右键交换
NtUserTrackMouseEvent mouse.c L480 TrackMouseEvent 系统调用
IntTrackMouseEvent mouse.c L408 跟踪设置/取消
IntQueryTrackMouseEvent mouse.c L373 TME_QUERY
IntRemoveTrackMouseEvent mouse.c L355 跟踪清理
NtUserGetMouseMovePointsEx mouse.c L541 移动轨迹
co_MsqInsertMouseMessage msgqueue.c L581 消息入队
co_IntProcessMouseMessage msgqueue.c L1483 消息落地
IntCoalesceMouseMove msgqueue.c L552 移动合并
MsqPostMouseMove msgqueue.c L519 移动入队(覆盖)
IntTrackMouseMove msgqueue.c L1432 跟踪运行时驱动
MsqGetDownKeyState msgqueue.c L339 队列键状态
MsqSendParentNotify msgqueue.c L1405 WM_PARENTNOTIFY
IntTopLevelWindowFromPoint msgqueue.c L60 顶层命中
co_WinPosWindowFromPoint winpos.c L2999 完整命中
co_WinPosSearchChildren winpos.c L2914 窗口树遍历
IntRealChildWindowFromPoint winpos.c L3033 子窗口命中
IntChildWindowFromPointEx winpos.c L3075 子窗口命中(过滤)
IntPtInWindow winpos.h L30 点-窗口判定
IntGetCaptureWindow focus.c L33 捕获查询
co_IntMouseActivateWindow focus.c L1248 点击激活
NtUserGetDoubleClickTime misc.c L385 双击时间
SystemTimerProc timer.c L283 悬停触发

本文基于 ReactOS 源码(截至 2026-08)撰写,覆盖 mouse.c 全部 10 个核心函数及

msgqueue.c / winpos.c / focus.c 中的协同函数。源码行号以当前分支为准。

配套分册:_21 输入子系统、_23 键盘、_24 输入法;_8 消息队列、_16 焦点与激活、_20 光标与图标。

相关推荐
互联网中的一颗神经元2 小时前
06. Small 对象分配:mcache 无锁路径
开发语言·golang
PPPPickup2 小时前
金证秋招笔试
java·开发语言
kkkkkkkkkk_Z2 小时前
学嵌入式和C语言编程数据结构|学习日记Day21:内核链表与队列学习
c语言·数据结构·学习
不正经学生2 小时前
C语言结构体:自定义数据类型,让变量打包出行
java·c语言·开发语言·数据结构·算法
老一岁2 小时前
c问题总结(1)
c语言·开发语言
一木 之林3 小时前
三、内存管理、指针与堆栈
c语言·jvm·c++
笨笨饿3 小时前
#110_SCH3000 平台图传系统配置指南:从 DVP 摄像头到 UVC/WiFi 双通道传输
c语言·stm32·单片机·嵌入式硬件·物联网·vim
林森lsjs3 小时前
队列 FIFO 原理 & 手撕两种队列实现(链表 + 循环数组)—数据结构陆
java·开发语言·数据结构·队列
何以解忧,唯有..4 小时前
Python time 模块详解:时间处理与日期操作指南
开发语言·python
caimouse4 小时前
ReactOS 窗口系统分析(20):光标与图标 — cursoricon.c
c语言·开发语言