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 焦点激活)。
目录
- 概述
- 设计动机
- 核心数据结构
- [鼠标输入主处理 UserProcessMouseInput](#鼠标输入主处理 UserProcessMouseInput)
- [SendInput 注入 UserSendMouseInput](#SendInput 注入 UserSendMouseInput)
- 键状态与按钮交换
- 命中测试
- [消息落地 co_IntProcessMouseMessage](#消息落地 co_IntProcessMouseMessage)
- [消息入队 co_MsqInsertMouseMessage](#消息入队 co_MsqInsertMouseMessage)
- [鼠标移动合并 IntCoalesceMouseMove](#鼠标移动合并 IntCoalesceMouseMove)
- 双击判定
- 鼠标捕获
- [TrackMouseEvent 跟踪](#TrackMouseEvent 跟踪)
- [鼠标移动轨迹 GetMouseMovePointsEx](#鼠标移动轨迹 GetMouseMovePointsEx)
- 鼠标输入处理全流程
- 用户态封装与系统参数
- 与其他模块的关系
- 调用链总览
- 源码索引
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/RIGHTUP、MOUSEEVENTF_MIDDLEDOWN/MIDDLEUP、
MOUSEEVENTF_XDOWN/XUP、MOUSEEVENTF_WHEEL、MOUSEEVENTF_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=0x0201、WM_LBUTTONUP=0x0202、WM_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;
...
转换要点:
- 位移标志 :
dx/dy任一非零即设MOUSEEVENTF_MOVE。注意"移动"与"按键"是
可叠加 的------一次硬件事件既可能移动又按键,dwFlags会同时包含 MOVE 与按键位。 - 绝对坐标 :
MOUSE_MOVE_ABSOLUTE/MOUSE_VIRTUAL_DESKTOP原样透传为
MOUSEEVENTF_ABSOLUTE/MOUSEEVENTF_VIRTUALDESK(触摸屏/平板/远程桌面使用)。 - 按键位 :左/中/右/侧键的 DOWN/UP 逐一映射为
MOUSEEVENTF_*DOWN/*UP。 - X 键与滚轮的"装不下"问题 :
MOUSEEVENTF_XDOWN/XUP与MOUSEEVENTF_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);
- 收尾 :
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 相关的光标形状刷新等。bInjected 与 dwExtraInfo 透传给钩子。
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);
}
每个按键的处理都是四元组:
SET_KEY_DOWN(gafAsyncKeyState, VK_x, down)------ 更新全局异步键状态;Msg.message = WM_xBUTTONDOWN/UP------ 确定消息号;pCurInfo->ButtonsDown |= / &= ~ MK_x且同步改Msg.wParam------ 维护键组合;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);
}
三步:
- 点必须在
rcWindow(窗口矩形,屏幕坐标)内; - 无裁剪区域(
hrgnClip == NULL)或窗口最小化 → 直接命中(最小化窗口矩形只有图标那么大,
本就在矩形内); - 有
hrgnClip→ 调用 GDINtGdiPtInRegion做区域级 命中(窗口可能被
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 序排列(
spwndChild→spwndNext是"最上 → 最下"),第一个命中即最上层窗口; - 跳过销毁中的窗口;
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;
}
...
判定顺序(每层窗口):
- 不可见 → 返回 NULL(不命中,向上层回溯);
- 点不在窗口矩形/区域内 → NULL;
- 分层+透明 → NULL(穿透);
- 禁用:子窗口(WS_CHILD)直接跳过;顶层禁用窗口命中但标记 HTERROR
(这样点击禁用窗口时系统知道"点到东西了但不给消息"); - 非最小化且点在客户区内 → 递归子窗口(
IntWinListChildren取子窗口句柄数组,
co_WinPosSearchChildren逐个深入),子窗口优先 ------任何一个子窗口命中即返回,
不再发 WM_NCHITTEST 给父窗口; - 若该窗口属于当前线程,向窗口发 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;
}
- 命中为空或不属于本队列 :光标移到了别的线程的窗口/桌面背景。此时把光标重置为系统
箭头(IntSystemSetCursor(SYSTEMCUR(ARROW)),cursoricon.c L230),并丢弃消息
(*RemoveMessages = TRUE)。这条消息是从别的线程队列"借"出来的?不是------它本来
就在本队列里,但命中测试发现光标下的窗口属于其他线程。注意这里的丢弃语义:消息
是从"光标所落窗口所属线程"的队列取出的,若窗口属别的线程(attach 组除外),直接删。 - 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)时依次:
- WM_PARENTNOTIFY :
MsqSendParentNotify(msgqueue.c L1405)沿父链上溯,把
MAKEWPARAM(event, idChild)发给所有祖先(跳过 WS_EX_NOPARENTNOTIFY 窗口),坐标
逐级换算成各父窗口客户区坐标; - 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 节)。
- WM_SETCURSOR :
MAKELONG(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 由落地阶段重新命中测试填充;- 时间戳取自
gdwMouseMoveTimeStamp(co_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);
}
两级合并:
- 队列尾若已有未消费的 WM_MOUSEMOVE → 原地覆盖 (只保留最新位置,中间位置全部
丢弃),这是合并的主力; - 否则追加一条新消息。
合并的收益:高频移动(1000Hz 鼠标)下队列里最多 1 条 WM_MOUSEMOVE,应用取到的是
"当前帧"位置,不会因队列积压产生迟滞。代价是丢失中间轨迹------需要完整轨迹的应用应
使用 GetMouseMovePointsEx(见第 14 节)或原始输入。
触发点汇总 (IntCoalesceMouseMove 的全部调用处):
co_MsqInsertMouseMessageL716:按键消息入队前,把待决移动落地;co_MsqWaitForNewMessagesL2138:线程即将睡眠等待消息前,把待决移动落地
(保证等待期间移动不丢);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;
}
外闸门(决定是否进入双击判定):三者任一成立即可------
- 菜单跟踪中(
MenuOwner)或窗口移动/缩放中(MoveSize):任何点击都当双击候选
(菜单项连击、标题栏拖动双击最大化都依赖它); - 命中码非 HTCLIENT(非客户区双击,如双击标题栏);
- 窗口类有
CS_DBLCLKS样式(只有注册了 CS_DBLCLKS 的类才接收双击消息 ------
Win32 的著名约定;没这个样式的类,快速连击只会得到两次 BUTTONDOWN/UP)。
内判定(六条件全部满足才是双击):
- 消息号相同(都是 LBUTTONDOWN 等);
- 目标窗口相同(
msg->hwnd == clk_msg.hwnd); - X 键消息需 XBUTTON1/2 相同(
GET_XBUTTON_WPARAM); - 时间差
< gspv.iDblClickTime(系统双击时间,默认 500ms); - 水平距离
< SM_CXDOUBLECLK/2(即两次点击都在"双击方框"内); - 垂直距离
< 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 | 实现 |
|---|---|---|
| 读双击时间 | GetDoubleClickTime → NtUserGetDoubleClickTime |
misc.c L387:返回 gspv.iDblClickTime |
| 写双击时间 | SetDoubleClickTime → NtUserSystemParametersInfo(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_MOUSEHOVER(IntSetTimer(..., 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);(启动时从注册表恢复) - 同批加载的还有
bMouseBtnSwap、iMouseHoverTime/Width/Height、bMouseClickLock、
dwMouseClickLockTime、iDblClickWidth/Height、iWheelScrollLines等(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_MOUSE→UserSendMouseInput L773) |
| desktop.h(file:///d:/reactos/win32ss/user/ntuser/desktop.h) | 鼠标跟踪字段(L12-L34):dwDTFlags/spwndTrack/htEx/rcMouseHover/dwMouseHoverTime、DF_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 光标与图标。