第 7 章 视窗报文 --- 7.7 鼠标器输入线程
本节深入剖析 Windows/ReactOS 中"鼠标器输入"(Mouse Input)的内核态处理,包括 RIT 原始鼠标数据转换、鼠标位置计算、捕获机制、命中测试(Hit-Test)、消息合并(Coalesce)、双击/单击锁定、hover/leave 跟踪、WH_MOUSE_LL 钩子等。
概述
鼠标输入与键盘输入一样,是 Windows GUI 系统的核心 I/O 来源。但相比键盘,鼠标的复杂性更高:
- 多种输入类型:移动、左/中/右键、XButton(4/5 键)、滚轮(垂直/水平)、触摸板手势;
- 绝对/相对位置:不同设备使用不同坐标系(屏幕坐标 / 虚拟桌面坐标 / 相对位移);
- 消息合并 :高频的
WM_MOUSEMOVE需要合并,避免消息队列爆炸; - 捕获(Capture):鼠标按下后可以"捕获"到特定窗口,即使鼠标移出窗口也能收到事件;
- 命中测试(Hit-Test):决定鼠标事件发送给哪个窗口(client / non-client / 系统区域);
- 双击判定:基于时间间隔 + 位置距离的复杂逻辑;
- Hover/Leave 跟踪:基于定时器的事件通知;
- LL 钩子:内核态实现的全局鼠标钩子。
鼠标输入的本质是什么?
鼠标输入是一种"高频事件 + 空间敏感"的数据流。RIT 从硬件读取
MOUSE_INPUT_DATA,将其转换为MOUSEINPUT/鼠标消息;win32k 在内核态根据当前光标位置、命中测试、捕获状态、消息队列状态等因素,将消息路由到合适的窗口;同时通过co_MsqInsertMouseMessage维护消息合并(Coalesce)、双击判定、单击锁定等机制。
想象一个"高灵敏度雷达跟踪"场景:
- 鼠标硬件:雷达天线(不断发送位置数据);
- MOUSE_INPUT_DATA:雷达回波(位置 + 按钮状态);
- RIT(UserProcessMouseInput):雷达数据处理中心;
- IntCoalesceMouseMove:目标跟踪合并器(避免对同一目标重复告警);
- co_MsqInsertMouseMessage:目标识别与路由(决定告警发送给哪个操作员);
- 捕获(Capture):把特定目标锁定在某个操作员屏幕上;
- 命中测试:判断鼠标在窗口的哪个区域(客户区 / 标题栏 / 边框);
- 双击/单击锁定:基于时间+位置的复杂事件识别;
- WH_MOUSE_LL:监控所有目标活动的中央监控室。
本节内容概览
- 7.7.0 框架图:鼠标输入子系统的整体架构;
- 7.7.1 鼠标输入子系统架构(Mouclass → win32k → 窗口消息);
- 7.7.2 鼠标初始化与 gMouseHistoryOfMoves;
- 7.7.3 UserProcessMouseInput:原始数据转换;
- 7.7.4 UserSendMouseInput:合成输入入口;
- 7.7.5 鼠标位置计算(绝对/相对/虚拟桌面);
- 7.7.6 按钮状态与左右键交换;
- 7.7.7 鼠标消息生成(co_MsqInsertMouseMessage);
- 7.7.8 鼠标捕获机制;
- 7.7.9 命中测试(WM_NCHITTEST);
- 7.7.10 鼠标移动消息合并(IntCoalesceMouseMove);
- 7.7.11 双击/单击锁定;
- 7.7.12 鼠标 hover/leave 跟踪;
- 7.7.13 设计哲学问答:5 个关键设计问题解答。
学习目标
读完本节后,读者应当能够:
- 理解 RIT 鼠标处理的数据流;
- 掌握
MOUSE_INPUT_DATA→MOUSEINPUT的转换; - 解释鼠标位置计算的绝对/相对/虚拟桌面三种模式;
- 掌握鼠标捕获(Capture)机制及其用途;
- 理解命中测试(WM_NCHITTEST)的判定流程;
- 掌握
IntCoalesceMouseMove的消息合并原理; - 解释双击判定与单击锁定的时间+位置逻辑;
- 理解
IntTrackMouseMove的 hover/leave 跟踪; - 掌握 WH_MOUSE_LL 钩子的实现。
涉及的内核子系统
| 子系统 | 职责 |
|---|---|
| ntoskrnl / mouclass | 鼠标类驱动:硬件数据 → MOUSE_INPUT_DATA |
| win32k.sys/ntuser/mouse.c | 鼠标消息处理、UserProcessMouseInput、UserSendMouseInput |
| win32k.sys/ntuser/msgqueue.c | co_MsqInsertMouseMessage、co_IntProcessMouseMessage、IntCoalesceMouseMove、IntTrackMouseMove |
| win32k.sys/ntuser/nonclient.c | WM_NCHITTEST 命中测试 |
| win32k.sys/ntuser/input.c | UserProcessMouseInput 原始输入入口 |
| user32.dll | SetCapture、GetCapture、TrackMouseEvent 用户态 API |
7.7.0 框架图
┌──────────────────────────────────────────────────────────────────────────────────────┐
│ 鼠标输入子系统完整架构 │
├──────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ 硬件层 │
│ ┌────────────────────────────────────────────────────────────────────────────┐ │
│ │ PS/2 / USB / 蓝牙 鼠标硬件 │ │
│ │ 产生中断(IRQ12 / USB HID) │ │
│ │ 端口驱动(i8042prt / mouhid)读取数据包 │ │
│ └────────────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 内核类驱动层 │
│ ┌────────────────────────────────────────────────────────────────────────────┐ │
│ │ mouclass.sys (鼠标类驱动) │ │
│ │ └─► 包装为 MOUSE_INPUT_DATA 投递到 RIT │ │
│ └────────────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ (RIT: \Device\PointerClass0 的读 IRP 完成) │
│ win32k 用户子系统(RIT 在 Win32k 内核态) │
│ ┌────────────────────────────────────────────────────────────────────────────┐ │
│ │ input.c UserProcessMouseInput(MOUSE_INPUT_DATA) │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ mouse.c │ │
│ │ ├─► 原始数据 → MOUSEINPUT 转换 │ │
│ │ ├─► 按钮状态 (gpsi->ButtonsDown) │ │
│ │ ├─► 左右键交换 (gspv.bMouseBtnSwap) │ │
│ │ ├─► 鼠标位置计算(绝对/相对/虚拟桌面) │ │
│ │ ├─► UserSendMouseInput ─► 投递原始输入 │ │
│ │ └─► co_MsqInsertMouseMessage (鼠标消息插入核心) │ │
│ │ │ │ │
│ │ ├─► WH_MOUSE_LL 钩子 │ │
│ │ ├─► 鼠标捕获检查 (IntGetCaptureWindow) │ │
│ │ ├─► 命中测试 (IntTopLevelWindowFromPoint) │ │
│ │ ├─► IntTrackMouseMove (hover/leave 跟踪) │ │
│ │ ├─► 队列放置 (HardwareMessagesListHead) │ │
│ │ └─► ptiSysLock 抢占机制 │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ co_MsqPeekHardwareMessage → co_IntProcessMouseMessage │ │
│ │ ├─► 鼠标捕获窗口 (MessageQueue->spwndCapture) │ │
│ │ ├─► co_WinPosWindowFromPoint (命中测试) │ │
│ │ ├─► NC/Client 消息分类 (HTCLIENT vs HT*) │ │
│ │ ├─► 双击判定 (gspv.iDblClickTime + 系统度量) │ │
│ │ ├─► 单击锁定 (ClickLock) │ │
│ │ ├─► WH_MOUSE 钩子 │ │
│ │ ├─► 父窗口通知 (MsqSendParentNotify) │ │
│ │ ├─► WM_MOUSEACTIVATE / WM_SETCURSOR │ │
│ │ └─► IME 处理 │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ user32.dll GetMessage / DispatchMessage │ │
│ │ └─► WndProc 收到 WM_LBUTTONDOWN / WM_MOUSEMOVE / WM_MOUSEWHEEL 等 │ │
│ └────────────────────────────────────────────────────────────────────────────┘ │
│ │
│ 鼠标状态与历史 │
│ ┌────────────────────────────────────────────────────────────────────────────┐ │
│ │ gpsi->ptCursor : 当前光标位置(屏幕坐标) │ │
│ │ gpsi->ButtonsDown : 当前按下的按钮(MK_* 标志) │ │
│ │ gpsi->iDblClickTime : 双击时间间隔(毫秒) │ │
│ │ gMouseHistoryOfMoves[64] : 鼠标移动历史(最近 64 个位置) │ │
│ │ gcMouseHistoryOfMoves : 历史数组索引 │ │
│ │ gdwMouseMoveTimeStamp : 上次鼠标移动时间戳 │ │
│ │ ptiSysLock : 鼠标消息独占线程 │ │
│ │ MessageQueue->msgDblClk : 上次点击信息(用于双击判定) │ │
│ │ MessageQueue->spwndCapture : 鼠标捕获窗口 │ │
│ │ pDesk->spwndTrack : hover/leave 跟踪窗口 │ │
│ │ pDesk->dwDTFlags : DF_TME_HOVER / DF_TME_LEAVE │ │
│ └────────────────────────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────────────────┘
7.7.1 鼠标输入子系统架构
7.7.1.1 RIT 鼠标路径
鼠标硬件(PS/2 / USB / 蓝牙)
│ 中断(IRQ12 / USB HID)
▼
i8042prt / mouhid 端口驱动
│ 读取鼠标数据包
▼
mouclass 鼠标类驱动
│ 包装为 MOUSE_INPUT_DATA
│ 完成 IRP_MJ_READ
▼
RIT 线程(等待 IRP 完成)
│ UserProcessMouseInput(data)
▼
win32k 鼠标处理
│ ▼
│ UserSendMouseInput(MOUSEINPUT)
│ │
│ ├─► 计算光标位置
│ ├─► 更新 ButtonsDown
│ ├─► WH_MOUSE_LL 钩子
│ └─► co_MsqInsertMouseMessage
│ │
│ ├─► ptiSysLock 抢占
│ ├─► IntGetCaptureWindow
│ ├─► IntTopLevelWindowFromPoint
│ ├─► IntTrackMouseMove
│ └─► MsqPostMessage (硬件消息队列)
▼
co_MsqPeekHardwareMessage
│
▼
co_IntProcessMouseMessage
│
▼
WndProc (收到 WM_LBUTTONDOWN/WM_MOUSEMOVE/WM_MOUSEWHEEL 等)
7.7.1.2 MOUSE_INPUT_DATA 结构
win32ss/user/ntuser/mouse.c(file:///d:/reactos/win32ss/user/ntuser/mouse.c) 处理的输入数据:
c
typedef struct _MOUSE_INPUT_DATA {
USHORT UnitId; // 设备单元 ID
USHORT Flags; // MOUSE_MOVE_ABSOLUTE / MOUSE_VIRTUAL_DESKTOP
union {
ULONG Buttons; // 旧版:所有按钮位
USHORT ButtonFlags; // 新版:分立的按钮标志
};
union {
LONG LastX; // 相对/绝对 X
USHORT RawButtons; // 原始按钮(XButton 模式)
};
union {
LONG LastY; // 相对/绝对 Y
USHORT ButtonData; // 滚轮/XButton 数据
};
ULONG ExtraInformation; // 来自驱动
ULONG RawInputUnitId; // HID 设备 ID
} MOUSE_INPUT_DATA, *PMOUSE_INPUT_DATA;
MOUSE_INPUT_DATA.ButtonFlags 标志:
c
#define MOUSE_LEFT_BUTTON_DOWN 0x0001
#define MOUSE_LEFT_BUTTON_UP 0x0002
#define MOUSE_RIGHT_BUTTON_DOWN 0x0004
#define MOUSE_RIGHT_BUTTON_UP 0x0008
#define MOUSE_MIDDLE_BUTTON_DOWN 0x0010
#define MOUSE_MIDDLE_BUTTON_UP 0x0020
#define MOUSE_BUTTON_4_DOWN 0x0040
#define MOUSE_BUTTON_4_UP 0x0080
#define MOUSE_BUTTON_5_DOWN 0x0100
#define MOUSE_BUTTON_5_UP 0x0200
#define MOUSE_WHEEL 0x0400
#define MOUSE_MOVE_ABSOLUTE 0x0800
#define MOUSE_VIRTUAL_DESKTOP 0x4000
7.7.2 鼠标初始化与 gMouseHistoryOfMoves
7.7.2.1 全局鼠标状态
win32ss/user/ntuser/mouse.c:13-14(file:///d:/reactos/win32ss/user/ntuser/mouse.c#L13-L14):
c
MOUSEMOVEPOINT gMouseHistoryOfMoves[64];
INT gcMouseHistoryOfMoves = 0;
gMouseHistoryOfMoves :最近 64 个鼠标移动的位置记录,用于 GetMouseMovePoints API。
7.7.2.2 鼠标历史用途
GetMouseMovePoints API:
c
int GetMouseMovePoints(UINT cbSize, LPMOUSEMOVEPOINT lppt, LPMOUSEMOVEPOINT lpptBuf, int nBufPoints, DWORD resolution);
应用(如绘图软件、游戏)可以查询鼠标历史来:
- 实现"轨迹回放";
- 防止"瞬移"作弊(检测鼠标瞬间从 A 跳到 B 的异常);
- 实现"平滑移动"(在历史位置之间插值)。
7.7.2.3 SystemParametersInfo 鼠标设置
通过 SystemParametersInfo 可以查询/设置鼠标参数:
SPI_GETMOUSEHOVERTIME/SPI_SETMOUSEHOVERTIME:hover 时间SPI_GETMOUSEHOVERWIDTH/SPI_SETMOUSEHOVERWIDTH:hover 区域宽度SPI_GETMOUSECLICKLOCKTIME:单击锁定时间SPI_GETMOUSEDBLCLICKTIME/SPI_SETMOUSEDBLCLICKTIME:双击时间SPI_GETMOUSEDRAGPATHCOUNT:拖动路径数量
7.7.3 UserProcessMouseInput:原始数据转换
7.7.3.1 函数签名
win32ss/user/ntuser/mouse.c:39-130(file:///d:/reactos/win32ss/user/ntuser/mouse.c#L39-L130):
c
VOID NTAPI
UserProcessMouseInput(PMOUSE_INPUT_DATA mid)
{
MOUSEINPUT mi;
/* 初始化 MOUSEINPUT */
mi.dx = mid->LastX;
mi.dy = mid->LastY;
mi.mouseData = 0;
mi.dwFlags = 0;
mi.time = 0;
mi.dwExtraInfo = mid->ExtraInformation;
/* 鼠标位置标志 */
if (mi.dx != 0 || mi.dy != 0)
mi.dwFlags |= MOUSEEVENTF_MOVE;
if (mid->Flags & MOUSE_MOVE_ABSOLUTE)
mi.dwFlags |= MOUSEEVENTF_ABSOLUTE;
if (mid->Flags & MOUSE_VIRTUAL_DESKTOP)
mi.dwFlags |= MOUSEEVENTF_VIRTUALDESK;
/* 左键 */
if (mid->ButtonFlags & MOUSE_LEFT_BUTTON_DOWN) mi.dwFlags |= MOUSEEVENTF_LEFTDOWN;
if (mid->ButtonFlags & MOUSE_LEFT_BUTTON_UP) mi.dwFlags |= MOUSEEVENTF_LEFTUP;
/* 中键 */
if (mid->ButtonFlags & MOUSE_MIDDLE_BUTTON_DOWN) mi.dwFlags |= MOUSEEVENTF_MIDDLEDOWN;
if (mid->ButtonFlags & MOUSE_MIDDLE_BUTTON_UP) mi.dwFlags |= MOUSEEVENTF_MIDDLEUP;
/* 右键 */
if (mid->ButtonFlags & MOUSE_RIGHT_BUTTON_DOWN) mi.dwFlags |= MOUSEEVENTF_RIGHTDOWN;
if (mid->ButtonFlags & MOUSE_RIGHT_BUTTON_UP) mi.dwFlags |= MOUSEEVENTF_RIGHTUP;
/* Button 4 (XBUTTON1) */
if (mid->ButtonFlags & MOUSE_BUTTON_4_DOWN)
{
mi.dwFlags |= MOUSEEVENTF_XDOWN;
mi.mouseData |= XBUTTON1;
}
if (mid->ButtonFlags & MOUSE_BUTTON_4_UP)
{
mi.dwFlags |= MOUSEEVENTF_XUP;
mi.mouseData |= XBUTTON1;
}
/* Button 4 + Button 5 不能同包(mouseData 冲突),分两次发 */
if (mi.dwFlags & (MOUSE_BUTTON_4_DOWN | MOUSE_BUTTON_4_UP))
{
UserSendMouseInput(&mi, FALSE);
RtlZeroMemory(&mi, sizeof(mi));
}
/* Button 5 (XBUTTON2) */
// ... 类似处理 ...
/* 滚轮 */
if (mid->ButtonFlags & MOUSE_WHEEL)
{
mi.mouseData = mid->ButtonData;
mi.dwFlags |= MOUSEEVENTF_WHEEL;
}
if (mi.dwFlags)
UserSendMouseInput(&mi, FALSE);
}
7.7.3.2 转换要点
| 原始数据 | MOUSEINPUT 标志 | 说明 |
|---|---|---|
LastX / LastY |
MOUSEEVENTF_MOVE |
移动标志(位移非零时) |
MOUSE_MOVE_ABSOLUTE |
MOUSEEVENTF_ABSOLUTE |
绝对坐标 |
MOUSE_VIRTUAL_DESKTOP |
MOUSEEVENTF_VIRTUALDESK |
虚拟桌面坐标 |
MOUSE_LEFT_BUTTON_DOWN |
MOUSEEVENTF_LEFTDOWN |
左键按下 |
MOUSE_WHEEL |
MOUSEEVENTF_WHEEL |
滚轮(mouseData 存 delta) |
MOUSE_BUTTON_4_* |
MOUSEEVENTF_XDOWN/XUP |
XButton 1(mouseData = XBUTTON1) |
Button 4 + Button 5 分两次发送 :因为 mouseData 字段不能同时表示两个 XButton,所以拆分为两个 UserSendMouseInput 调用。
7.7.4 UserSendMouseInput:合成输入入口
7.7.4.1 函数签名
win32ss/user/ntuser/mouse.c:167-...(file:///d:/reactos/win32ss/user/ntuser/mouse.c#L167):
c
BOOL NTAPI
UserSendMouseInput(MOUSEINPUT *pmi, BOOL bInjected);
两个调用来源:
- RIT 原始输入(
UserProcessMouseInput调用,bInjected = FALSE); - 用户态 SendInput API(
bInjected = TRUE)。
7.7.4.2 完整流程
c
BOOL NTAPI
UserSendMouseInput(MOUSEINPUT *pmi, BOOL bInjected)
{
POINT ptCursor;
PSYSTEM_CURSORINFO pCurInfo;
MSG Msg;
DWORD dwFlags;
pCurInfo = IntGetSysCursorInfo();
ptCursor = gpsi->ptCursor;
dwFlags = IntFixMouseInputButtons(pmi->dwFlags); // 交换左右键
gppiInputProvider = ((PTHREADINFO)PsGetCurrentThreadWin32Thread())->ppi;
// 1. 计算新光标位置
if (pmi->dwFlags & MOUSEEVENTF_MOVE)
{
if (!(pmi->dwFlags & MOUSEEVENTF_ABSOLUTE))
{
// 相对位移
ptCursor.x += pmi->dx;
ptCursor.y += pmi->dy;
}
else if (pmi->dwFlags & MOUSEEVENTF_VIRTUALDESK)
{
// 虚拟桌面绝对坐标
ptCursor.x = pmi->dx * UserGetSystemMetrics(SM_CXVIRTUALSCREEN) >> 16;
ptCursor.y = pmi->dy * UserGetSystemMetrics(SM_CYVIRTUALSCREEN) >> 16;
}
else
{
// 主显示器绝对坐标
ptCursor.x = pmi->dx * UserGetSystemMetrics(SM_CXSCREEN) >> 16;
ptCursor.y = pmi->dy * UserGetSystemMetrics(SM_CYSCREEN) >> 16;
}
}
// 2. 裁剪到屏幕范围
// ...
// 3. 更新 gpsi->ptCursor
if (ptCursor.x != gpsi->ptCursor.x || ptCursor.y != gpsi->ptCursor.y)
{
gpsi->ptCursor = ptCursor;
// 更新鼠标历史
gcMouseHistoryOfMoves = (gcMouseHistoryOfMoves + 1) % 64;
gMouseHistoryOfMoves[gcMouseHistoryOfMoves].x = ptCursor.x;
gMouseHistoryOfMoves[gcMouseHistoryOfMoves].y = ptCursor.y;
}
// 4. 更新按钮状态
if (dwFlags & MOUSEEVENTF_LEFTDOWN) pCurInfo->ButtonsDown |= MK_LBUTTON;
if (dwFlags & MOUSEEVENTF_LEFTUP) pCurInfo->ButtonsDown &= ~MK_LBUTTON;
if (dwFlags & MOUSEEVENTF_RIGHTDOWN) pCurInfo->ButtonsDown |= MK_RBUTTON;
if (dwFlags & MOUSEEVENTF_RIGHTUP) pCurInfo->ButtonsDown &= ~MK_RBUTTON;
if (dwFlags & MOUSEEVENTF_MIDDLEDOWN) pCurInfo->ButtonsDown |= MK_MBUTTON;
if (dwFlags & MOUSEEVENTF_MIDDLEUP) pCurInfo->ButtonsDown &= ~MK_MBUTTON;
// 5. 调用 co_MsqInsertMouseMessage
// ... 构造 MSG 结构(hwnd, message, wParam, lParam)...
co_MsqInsertMouseMessage(&Msg, LLMHF_INJECTED, ExtraInfo, !bInjected);
return TRUE;
}
7.7.5 鼠标位置计算(绝对/相对/虚拟桌面)
7.7.5.1 三种位置模式
| 模式 | 输入范围 | 输出范围 | 用途 |
|---|---|---|---|
相对 (!MOUSEEVENTF_ABSOLUTE) |
dx, dy 位移 | ptCursor += (dx, dy) | 普通鼠标 |
绝对 (MOUSEEVENTF_ABSOLUTE,无 VIRTUALDESK) |
0...65535 | 0...SM_CXSCREEN | 触摸板、绝对设备 |
虚拟桌面绝对 (MOUSEEVENTF_VIRTUALDESK) |
0...65535 | 0...SM_CXVIRTUALSCREEN | 多显示器(跨越多个屏幕) |
7.7.5.2 绝对坐标的归一化
绝对坐标的"归一化"(0...65535 映射到屏幕像素):
c
ptCursor.x = pmi->dx * UserGetSystemMetrics(SM_CXSCREEN) >> 16;
ptCursor.y = pmi->dy * UserGetSystemMetrics(SM_CYSCREEN) >> 16;
>> 16 等价于除以 65536,将 0, 65535 映射到 [0, screen_size)。
为什么使用归一化坐标?
- 兼容不同分辨率的设备(同一驱动可以用于 800x600 和 1920x1080 屏幕);
- 避免浮点运算(纯整数运算高效)。
7.7.5.3 虚拟桌面(多显示器)
当 MOUSEEVENTF_VIRTUALDESK 设置时,使用 SM_CXVIRTUALSCREEN / SM_CYVIRTUALSCREEN(包含所有显示器的总尺寸)。
c
ptCursor.x = pmi->dx * UserGetSystemMetrics(SM_CXVIRTUALSCREEN) >> 16;
ptCursor.y = pmi->dy * UserGetSystemMetrics(SM_CYVIRTUALSCREEN) >> 16;
这样在多显示器配置中,鼠标可以"跨越"不同显示器。
7.7.6 按钮状态与左右键交换
7.7.6.1 UserGetMouseButtonsState
win32ss/user/ntuser/mouse.c:21-32(file:///d:/reactos/win32ss/user/ntuser/mouse.c#L21-L32):
c
WORD FASTCALL UserGetMouseButtonsState(VOID)
{
WORD wRet = 0;
wRet = IntGetSysCursorInfo()->ButtonsDown;
if (IS_KEY_DOWN(gafAsyncKeyState, VK_SHIFT)) wRet |= MK_SHIFT;
if (IS_KEY_DOWN(gafAsyncKeyState, VK_CONTROL)) wRet |= MK_CONTROL;
return wRet;
}
返回当前 WM_MOUSEMOVE 的 wParam 值:
MK_LBUTTON/MK_RBUTTON/MK_MBUTTON/MK_XBUTTON1/MK_XBUTTON2;MK_SHIFT/MK_CONTROL(修饰键状态)。
7.7.6.2 IntFixMouseInputButtons
win32ss/user/ntuser/mouse.c:137-160(file:///d:/reactos/win32ss/user/ntuser/mouse.c#L137-L160):
c
DWORD IntFixMouseInputButtons(DWORD dwFlags)
{
DWORD dwNewFlags;
if (!gspv.bMouseBtnSwap)
return dwFlags;
/* 左右键交换 */
dwNewFlags = dwFlags & ~(MOUSEEVENTF_LEFTDOWN | MOUSEEVENTF_LEFTUP |
MOUSEEVENTF_RIGHTDOWN | MOUSEEVENTF_RIGHTUP);
if (dwFlags & MOUSEEVENTF_LEFTDOWN) dwNewFlags |= MOUSEEVENTF_RIGHTDOWN;
if (dwFlags & MOUSEEVENTF_LEFTUP) dwNewFlags |= MOUSEEVENTF_RIGHTUP;
if (dwFlags & MOUSEEVENTF_RIGHTDOWN) dwNewFlags |= MOUSEEVENTF_LEFTDOWN;
if (dwFlags & MOUSEEVENTF_RIGHTUP) dwNewFlags |= MOUSEEVENTF_LEFTUP;
return dwNewFlags;
}
gspv.bMouseBtnSwap 是系统参数(SPI_SETMOUSEBUTTONSWAP)。当为 TRUE 时,所有鼠标消息中的"左键"和"右键"含义互换------专为左撇子设计。
7.7.7 鼠标消息生成(co_MsqInsertMouseMessage)
7.7.7.1 函数签名
win32ss/user/ntuser/msgqueue.c:579-660+(file:///d:/reactos/win32ss/user/ntuser/msgqueue.c#L579-L660):
c
VOID FASTCALL
co_MsqInsertMouseMessage(MSG* Msg, DWORD flags, ULONG_PTR dwExtraInfo, BOOL Hook);
参数:
Msg:消息结构(hwnd/message/wParam/lParam)flags:LLMHF_INJECTED(来自 SendInput)等dwExtraInfo:附加信息Hook:是否调用 WH_MOUSE_LL 钩子
7.7.7.2 完整流程
c
VOID FASTCALL
co_MsqInsertMouseMessage(MSG* Msg, DWORD flags, ULONG_PTR dwExtraInfo, BOOL Hook)
{
MSLLHOOKSTRUCT MouseHookData;
PWND pwnd, pwndDesktop;
HDC hdcScreen;
PTHREADINFO pti;
PUSER_MESSAGE_QUEUE MessageQueue;
PSYSTEM_CURSORINFO CurInfo;
Msg->time = EngGetTickCount32();
// 1. 构造 MSLLHOOKSTRUCT
MouseHookData.pt.x = LOWORD(Msg->lParam);
MouseHookData.pt.y = HIWORD(Msg->lParam);
switch (Msg->message)
{
case WM_MOUSEWHEEL:
MouseHookData.mouseData = MAKELONG(0, GET_WHEEL_DELTA_WPARAM(Msg->wParam));
break;
case WM_XBUTTONDOWN: case WM_XBUTTONUP: ...
MouseHookData.mouseData = MAKELONG(0, HIWORD(Msg->wParam));
break;
default:
MouseHookData.mouseData = 0;
}
MouseHookData.flags = flags;
MouseHookData.time = Msg->time;
MouseHookData.dwExtraInfo = dwExtraInfo;
// 2. WH_MOUSE_LL 钩子
if (Hook)
{
if (co_HOOK_CallHooks(WH_MOUSE_LL, HC_ACTION, Msg->message, (LPARAM)&MouseHookData))
return; // 钩子消费
}
// 3. 确定目标窗口
pwndDesktop = UserGetDesktopWindow();
if (!pwndDesktop) return;
Msg->hwnd = IntGetCaptureWindow(); // 优先使用捕获窗口
if (Msg->hwnd != NULL)
{
pwnd = UserGetWindowObject(Msg->hwnd);
}
else
{
// 命中测试:从屏幕坐标找到顶层窗口
pwnd = IntTopLevelWindowFromPoint(Msg->pt.x, Msg->pt.y);
if (pwnd) Msg->hwnd = UserHMGetHandle(pwnd);
}
// 4. 记录到鼠标历史
if (Msg->message == WM_MOUSEMOVE)
{
// 更新 gMouseHistoryOfMoves
}
// 5. 投递到硬件消息队列
pti = PsGetCurrentThreadWin32Thread();
MessageQueue = pti->MessageQueue;
CurInfo = IntGetSysCursorInfo();
if (Msg->message == WM_MOUSEMOVE)
{
// 合并:检查是否已存在未分派的 WM_MOUSEMOVE
PLIST_ENTRY Entry = MessageQueue->HardwareMessagesListHead.Flink;
while (Entry != &MessageQueue->HardwareMessagesListHead)
{
PUSER_MESSAGE Current = CONTAINING_RECORD(Entry, USER_MESSAGE, ListEntry);
if (Current->Msg.message == WM_MOUSEMOVE)
{
// 合并:更新现有消息的坐标
Current->Msg.lParam = Msg->lParam;
Current->Msg.wParam = Msg->wParam;
Current->Msg.time = Msg->time;
return; // 不插入新消息
}
Entry = Entry->Flink;
}
}
// 6. ptiSysLock 抢占
if (!MessageQueue->ptiSysLock)
{
MessageQueue->ptiSysLock = pti;
pti->pcti->CTI_flags |= CTI_THREADSYSLOCK;
}
if (MessageQueue->ptiSysLock != pti)
{
ERR("Thread Q is locked to ptiSysLock 0x%p pti 0x%p\n",...);
return;
}
// 7. 投递硬件消息
MsqPostMessage(pti, Msg, TRUE, QS_MOUSE, QF_MOUSEMOVED, dwExtraInfo);
}
7.7.7.3 鼠标消息的"合并"机制
高频的 WM_MOUSEMOVE(典型 1000 Hz 鼠标每秒 1000 次)如果不合并,消息队列会爆炸。ReactOS 采用"末尾合并":
c
if (Msg->message == WM_MOUSEMOVE)
{
PLIST_ENTRY Entry = MessageQueue->HardwareMessagesListHead.Flink;
while (Entry != &MessageQueue->HardwareMessagesListHead)
{
PUSER_MESSAGE Current = CONTAINING_RECORD(Entry, USER_MESSAGE, ListEntry);
if (Current->Msg.message == WM_MOUSEMOVE)
{
// 找到队列中已有的 WM_MOUSEMOVE
Current->Msg.lParam = Msg->lParam; // 更新坐标
Current->Msg.wParam = Msg->wParam; // 更新按钮状态
Current->Msg.time = Msg->time; // 更新时间
return; // 不插入新消息
}
Entry = Entry->Flink;
}
}
效果 :无论鼠标移动多快,硬件消息队列中最多只有一个 WM_MOUSEMOVE。当 WndProc 取出并处理后,下一个 WM_MOUSEMOVE 会进入队列并重复此过程。
其他消息 (点击、滚轮)不合并------必须每个都投递。
7.7.8 鼠标捕获机制
7.7.8.1 捕获的语义
鼠标捕获(Capture)允许特定窗口 接收所有鼠标事件,即使鼠标光标已经移出该窗口。
典型应用:
- 拖放(Drag & Drop) :按下左键后开始拖动,即使鼠标移出窗口也持续接收
WM_MOUSEMOVE; - 绘图应用:画线时持续接收鼠标位置;
- 自定义控件 :滚动条拖动、窗口调整大小(
SC_MOVE/SC_SIZE)。
7.7.8.2 SetCapture / GetCapture / ReleaseCapture
c
HWND SetCapture(HWND hWnd); // 设置捕获
HWND GetCapture(VOID); // 获取当前捕获窗口
BOOL ReleaseCapture(VOID); // 释放捕获
7.7.8.3 捕获的实现
SetCapture :win32ss/user/ntuser/ntstubs.c(file:///d:/reactos/win32ss/user/ntuser/ntstubs.c) NtUserSetCapture:
c
HWND NtUserSetCapture(HWND hWnd)
{
// 1. 验证窗口
// 2. 验证当前线程拥有该窗口
// 3. 记录到当前 MessageQueue
return MsqSetStateWindow(PsGetCurrentThreadWin32Thread(), MSQ_STATE_CAPTURE, hWnd);
}
IntGetCaptureWindow (在 co_MsqInsertMouseMessage 中调用):
c
Msg->hwnd = IntGetCaptureWindow();
if (Msg->hwnd != NULL)
{
pwnd = UserGetWindowObject(Msg->hwnd);
}
效果 :当捕获窗口存在时,所有鼠标消息都路由到该窗口,忽略实际光标位置。
7.7.8.4 捕获的自动释放
捕获在以下情况自动释放:
- 窗口被销毁;
- 线程被销毁;
- 鼠标按键全部释放(
WM_LBUTTONUP+WM_RBUTTONUP+WM_MBUTTONUP都已投递)。
7.7.9 命中测试(WM_NCHITTEST)
7.7.9.1 命中测试的作用
命中测试(Hit-Test)决定鼠标光标在窗口的哪个区域 ,并将该信息(HTCLIENT、HTBORDER、HTCAPTION 等)通过 WM_NCHITTEST 消息发送给窗口。
HT 标志*:
| 标志 | 值 | 区域 |
|---|---|---|
HTCLIENT |
1 | 客户区 |
HTCAPTION |
2 | 标题栏 |
HTBORDER |
5, 18-21 | 窗口边框 |
HTSYSMENU |
3 | 系统菜单 |
HTMINBUTTON |
8 | 最小化按钮 |
HTMAXBUTTON |
9 | 最大化按钮 |
HTCLOSE |
20 | 关闭按钮 |
HTHSCROLL / HTVSCROLL |
6, 7 | 滚动条 |
HTMENU |
5 | 菜单 |
HTERROR |
-2 | 错误区域(触发 WM_GETDLGCODE) |
HTNOWHERE |
0 | 不在任何窗口 |
7.7.9.2 co_WinPosWindowFromPoint
win32ss/user/ntuser/winpos.c(file:///d:/reactos/win32ss/user/ntuser/winpos.c) 中定义:
c
PWND co_WinPosWindowFromPoint(POINT pt, USHORT *pHitTest, BOOL bIgnorePopup);
流程:
- 从桌面窗口开始递归查找;
- 对每个窗口:测试点是否在窗口矩形内;
- 如果是子窗口,递归测试子窗口;
- 返回最顶层包含该点的窗口 + 命中测试结果。
7.7.9.3 WM_NCHITTEST 完整流程
在 co_IntProcessMouseMessage 中:
c
// 1. 命中测试
if (MessageQueue->spwndCapture)
{
hittest = HTCLIENT;
pwndMsg = MessageQueue->spwndCapture;
}
else
{
pwndMsg = co_WinPosWindowFromPoint(NULL, &msg->pt, &hittest, FALSE);
}
// 2. 跨线程检查
if (pwndMsg == NULL || pwndMsg->head.pti->MessageQueue != MessageQueue)
{
IntSystemSetCursor(SYSTEMCUR(ARROW));
*RemoveMessages = TRUE;
return FALSE;
}
// 3. NC/Client 消息分类
if (message != WM_MOUSEWHEEL) // 滚轮没有 NC 版本
{
if (hittest != HTCLIENT)
{
message += WM_NCMOUSEMOVE - WM_MOUSEMOVE; // WM_MOUSEMOVE → WM_NCMOUSEMOVE
msg->wParam = hittest;
}
else
{
// 转换坐标:屏幕 → 窗口客户区
if (!(MessageQueue->MenuOwner))
{
pt.x += pwndDesktop->rcClient.left - pwndMsg->rcClient.left;
pt.y += pwndDesktop->rcClient.top - pwndMsg->rcClient.top;
}
}
}
7.7.9.4 关键设计:WM_NCHITTEST 显式消息
WM_NCHITTEST 作为一个独立的窗口消息,让应用程序自定义命中测试:
c
case WM_NCHITTEST:
return HTCAPTION; // 把整个客户区当作标题栏(用于可拖动的窗口)
IntDefWindowProc 的默认处理见 7.8 节。
7.7.10 鼠标移动消息合并(IntCoalesceMouseMove)
7.7.10.1 函数签名
win32ss/user/ntuser/msgqueue.c:550-577(file:///d:/reactos/win32ss/user/ntuser/msgqueue.c#L550-L577):
c
VOID FASTCALL
IntCoalesceMouseMove(PTHREADINFO pti);
7.7.10.2 实现
c
VOID FASTCALL
IntCoalesceMouseMove(PTHREADINFO pti)
{
MSG Msg;
// 强制时间戳更新
if (gdwMouseMoveTimeStamp == 0)
gdwMouseMoveTimeStamp = EngGetTickCount32();
// 构造 WM_MOUSEMOVE 消息
Msg.hwnd = NULL;
Msg.message = WM_MOUSEMOVE;
Msg.wParam = 0;
Msg.lParam = MAKELONG(gpsi->ptCursor.x, gpsi->ptCursor.y);
Msg.time = gdwMouseMoveTimeStamp;
Msg.pt = gpsi->ptCursor;
// 投递(合并到队列末尾的 WM_MOUSEMOVE)
MsqPostMouseMove(pti, &Msg, gdwMouseMoveExtraInfo);
gdwMouseMoveTimeStamp = 0;
pti->MessageQueue->QF_flags &= ~QF_MOUSEMOVED;
}
7.7.10.3 调用时机
IntCoalesceMouseMove 在以下情况被调用:
- 线程空闲时(消息循环中
QS_MOUSEMOVE被设置但无待处理消息); - 当前消息队列被解锁(
ptiSysLock释放)时。
目的 :确保最终 会有一条 WM_MOUSEMOVE 反映光标的最新位置,即使期间有多次更新被合并。
7.7.11 双击/单击锁定
7.7.11.1 双击判定
win32ss/user/ntuser/msgqueue.c:1575-1615(file:///d:/reactos/win32ss/user/ntuser/msgqueue.c#L1575-L1615) co_IntProcessMouseMessage:
c
if ((msg->message == WM_LBUTTONDOWN) ||
(msg->message == WM_RBUTTONDOWN) ||
(msg->message == WM_MBUTTONDOWN) ||
(msg->message == WM_XBUTTONDOWN))
{
BOOL update = *RemoveMessages;
// 必须窗口有 CS_DBLCLKS 类样式
if ((MessageQueue->MenuOwner || MessageQueue->MoveSize) ||
hittest != HTCLIENT ||
(pwndMsg->pcls->style & CS_DBLCLKS))
{
if ((msg->message == clk_msg.message) &&
(msg->hwnd == clk_msg.hwnd) &&
(msg->message != WM_XBUTTONDOWN || GET_XBUTTON_WPARAM(msg->wParam) == GET_XBUTTON_WPARAM(clk_msg.wParam)) &&
((msg->time - clk_msg.time) < (ULONG)gspv.iDblClickTime) &&
(abs(msg->pt.x - clk_msg.pt.x) < UserGetSystemMetrics(SM_CXDOUBLECLK)/2) &&
(abs(msg->pt.y - clk_msg.pt.y) < UserGetSystemMetrics(SM_CYDOUBLECLK)/2))
{
// 转换为双击消息
message += (WM_LBUTTONDBLCLK - WM_LBUTTONDOWN);
if (update)
{
MessageQueue->msgDblClk.message = 0; // 清除双击条件
update = FALSE;
}
}
}
if (update) MessageQueue->msgDblClk = *msg; // 记录本次点击
}
7.7.11.2 双击判定的 4 个条件
| 条件 | 检查 | 备注 |
|---|---|---|
| 消息相同 | msg->message == clk_msg.message |
都是左键,或都是右键 |
| 窗口相同 | msg->hwnd == clk_msg.hwnd |
不能跨窗口 |
| 时间间隔 | (msg->time - clk_msg.time) < iDblClickTime |
典型 500ms |
| 位置接近 | abs(msg->pt - clk_msg.pt) < SM_CXDOUBLECLK/2 |
典型 4 像素 |
XButton 特殊 :需要比较 wParam 中的 XButton 编号(1 或 2)。
7.7.11.3 CS_DBLCLKS 类样式
只有注册窗口类时指定 CS_DBLCLKS,才会产生 WM_*BUTTONDBLCLK 消息:
c
WNDCLASS wc = { ... };
wc.style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS; // 启用双击
RegisterClass(&wc);
7.7.11.4 单击锁定(ClickLock)
单击锁定允许拖动时不需要持续按下按钮:
c
if (gspv.bMouseClickLock)
{
BOOL IsClkLck = FALSE;
if (msg->message == WM_LBUTTONUP)
{
// 检查是否已超过 ClickLock 时间
IsClkLck = ((msg->time - CurInfo->ClickLockTime) >= gspv.dwMouseClickLockTime);
if (IsClkLck && !CurInfo->ClickLockActive)
CurInfo->ClickLockActive = TRUE; // 激活 ClickLock
}
else if (msg->message == WM_LBUTTONDOWN)
{
if (CurInfo->ClickLockActive)
{
// 第二次按下时取消锁定
IsClkLck = TRUE;
CurInfo->ClickLockActive = FALSE;
}
CurInfo->ClickLockTime = msg->time;
}
if (IsClkLck)
{
*RemoveMessages = TRUE;
return FALSE; // 消费消息
}
}
使用场景:患有运动障碍的用户可以用"按下 + 等待 + 拖动 + 再次按下"代替"持续按住拖动"。
7.7.11.5 gspv 系统参数
c
gspv.iDblClickTime // 双击时间(毫秒)
gspv.bMouseClickLock // 单击锁定启用
gspv.dwMouseClickLockTime // 单击锁定时间
gspv.bMouseBtnSwap // 左右键交换
通过 SystemParametersInfo 设置。
7.7.12 鼠标 hover/leave 跟踪
7.7.12.1 IntTrackMouseMove
win32ss/user/ntuser/msgqueue.c:1419-1470(file:///d:/reactos/win32ss/user/ntuser/msgqueue.c#L1419-L1470):
c
VOID FASTCALL
IntTrackMouseMove(PWND pwndTrack, PDESKTOP pDesk, PMSG msg, USHORT hittest)
{
if (pDesk->spwndTrack != pwndTrack ||
msg->message != WM_MOUSEMOVE ||
pDesk->htEx != hittest)
{
// 1. 跨窗口/跨边界
if (pDesk->spwndTrack != pwndTrack ||
(pDesk->htEx == HTCLIENT) ^ (hittest == HTCLIENT))
{
if (pDesk->dwDTFlags & DF_TME_LEAVE)
UserPostMessage(UserHMGetHandle(pDesk->spwndTrack),
(pDesk->htEx != HTCLIENT) ? WM_NCMOUSELEAVE : WM_MOUSELEAVE,
0, 0);
if (pDesk->dwDTFlags & DF_TME_HOVER)
IntKillTimer(pDesk->spwndTrack, ID_EVENT_SYSTIMER_MOUSEHOVER, TRUE);
pDesk->dwDTFlags &= ~(DF_TME_LEAVE | DF_TME_HOVER);
}
pDesk->spwndTrack = pwndTrack;
pDesk->htEx = hittest;
}
// 2. 重新启动 hover 计时器
if (pDesk->spwndTrack == pwndTrack &&
(msg->message != WM_MOUSEMOVE || !RECTL_bPointInRect(&pDesk->rcMouseHover, msg->pt.x, msg->pt.y)) &&
pDesk->dwDTFlags & DF_TME_HOVER)
{
IntSetTimer(pDesk->spwndTrack, ID_EVENT_SYSTIMER_MOUSEHOVER,
pDesk->dwMouseHoverTime, SystemTimerProc, TMRF_SYSTEM);
RECTL_vSetRect(&pDesk->rcMouseHover, ...);
}
}
7.7.12.2 三个状态标志
c
#define DF_TME_HOVER 0x00000001 // TrackMouseEvent(TME_HOVER) 启用
#define DF_TME_LEAVE 0x00000002 // TrackMouseEvent(TME_LEAVE) 启用
7.7.12.3 跟踪流程
1. 应用调用 TrackMouseEvent(TME_HOVER | TME_LEAVE)
2. 系统记录请求到 pDesk->dwDTFlags
3. 鼠标首次进入窗口:
└─► IntTrackMouseMove 检测到 spwndTrack 变化
└─► 设置 hover 计时器
4. 鼠标在窗口内移动:
└─► 计时器重启
5. 鼠标静止超过 hover 时间:
└─► SystemTimerProc 触发 WM_MOUSEHOVER
6. 鼠标离开窗口:
└─► IntTrackMouseMove 检测到 spwndTrack 变化
└─► 投递 WM_MOUSELEAVE
└─► 清除 TME_HOVER | TME_LEAVE 标志
7.7.12.4 应用层 API
c
BOOL TrackMouseEvent(LPTRACKMOUSEEVENT lpEvent);
参数:
dwFlags = TME_HOVER:跟踪 hover(鼠标静止 N ms 触发 WM_MOUSEHOVER);dwFlags = TME_LEAVE:跟踪 leave(鼠标离开窗口触发 WM_MOUSELEAVE);dwFlags = TME_CANCEL:取消跟踪;dwHoverTime:hover 时间(默认SPI_GETMOUSEHOVERTIME)。
7.7.12.5 SystemTimerProc 回调
c
VOID CALLBACK SystemTimerProc(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime)
{
if (idEvent == ID_EVENT_SYSTIMER_MOUSEHOVER)
{
// 投递 WM_MOUSEHOVER
UserPostMessage(hwnd, WM_MOUSEHOVER, 0, 0);
}
}
7.7.13 设计哲学问答
Q1:为什么鼠标消息要进入"硬件消息队列"并由 ptiSysLock 独占?
A :避免高频 WM_MOUSEMOVE 在多线程间分配导致状态混乱。
- 场景 :用户拖动一个跨线程的窗口,鼠标快速移动。
WM_MOUSEMOVE持续产生。 - 如果不独占:消息可能被分配到不同线程,UI 更新顺序混乱(窗口 A 移动到位置 100,另一个线程又移动到位置 50,状态不一致)。
- 独占机制 :通过
ptiSysLock标识当前正在处理硬件消息的线程 ,其他线程不能同时读取硬件消息。这保证了消息处理的顺序一致性。 - 实现 :
co_MsqPeekHardwareMessage在MessageQueue->ptiSysLock被其他线程持有时立即返回 FALSE,调用方继续等待。
Q2:为什么高频 WM_MOUSEMOVE 需要合并(Coalesce)?
A :避免消息队列爆炸 + UI 跟不上。
- 场景 :1000 Hz 鼠标每秒产生 1000 个
WM_MOUSEMOVE。 - 不合并 :
- 消息队列被
WM_MOUSEMOVE占满; - 应用程序的 WndProc 处理速度跟不上输入速度(每秒 1000 次 Move 渲染);
- CPU 100% 占用。
- 消息队列被
- 合并后 :
- 队列中最多一个
WM_MOUSEMOVE; - 应用程序可以以自己的节奏处理(通常 60 FPS 即每 16ms 处理一次);
- 最新的鼠标位置总会反映(合并更新 lParam)。
- 队列中最多一个
类比:邮件合并------你不会为每个收件人单独寄信,而是把"今天的待办"合并成一份日报。
Q3:为什么命中测试(WM_NCHITTEST)要暴露给应用程序?
A :灵活的自定义交互。
c
case WM_NCHITTEST:
// 整个客户区都可拖动
if (PtInRect(&rcTitleBar, pt))
return HTCAPTION;
return HTCLIENT;
典型用途:
- 自绘标题栏 :现代应用常用
WM_NCHITTEST返回HTCAPTION让整个窗口可拖动; - 自定义窗口形状:异形窗口(如圆形、异形)需要自定义命中测试;
- Dock 区域:自定义"对接区"的命中行为;
- 热区:在客户区内自定义"可点击区域"。
如果命中测试是固定的(不可重写),这些高级交互就无法实现。
Q4:为什么 WM_NCMOUSEMOVE 和 WM_MOUSEMOVE 是两套消息?
A :客户区 vs 非客户区的语义完全不同。
| 维度 | WM_MOUSEMOVE | WM_NCMOUSEMOVE |
|---|---|---|
| 区域 | 客户区(应用内容) | 标题栏、边框、菜单等系统区域 |
| lParam | 客户区坐标 | 屏幕坐标 |
| wParam | 按钮状态(MK_*) | 命中测试结果(HT*) |
| 应用期望 | 应用可以处理(重绘、交互) | 应用通常不处理,让 DefWindowProc 处理(拖动、缩放) |
| 滚动条交互 | 不会发送 | 滚动条拖动时发送 |
两套消息让应用可以分别处理:
WM_MOUSEMOVE:绘画应用、拖放、游戏;WM_NCMOUSEMOVE:通常不处理,让系统处理"用户点击关闭按钮"等系统交互。
Q5:为什么捕获窗口(SetCapture)会自动释放?
A :防止应用"忘记"释放导致鼠标卡死。
场景 :应用 SetCapture(hWnd) 后崩溃 / 死循环 / 忘记 ReleaseCapture():
- 鼠标所有事件都被发送到该窗口;
- 窗口永远收不到
WM_LBUTTONUP(因为鼠标已经在它上面); - 整个系统看起来"鼠标卡住了"。
自动释放机制 (参见 MsgQueue::spwndCapture):
- 窗口销毁时释放;
- 线程销毁时释放;
- 所有按钮释放时释放 (投递
WM_LBUTTONUP后)。
自动释放通过 MsqReleaseCapture 实现,保证系统不会因为单个应用的 bug 而瘫痪。
总结
鼠标输入子系统是 Windows GUI 系统最复杂、消息量最大的 I/O 路径。本节介绍了:
- RIT 架构:原始输入线程统一处理所有鼠标设备;
MOUSE_INPUT_DATA转换 :原始数据 →MOUSEINPUT标志;- 位置计算:相对/绝对/虚拟桌面三种模式;
- 按钮状态:MK_* 标志 + 左右键交换;
- 消息生成 :
co_MsqInsertMouseMessage完整流程; - 消息合并 :
IntCoalesceMouseMove+ 队列末尾合并; - 捕获机制 :
SetCapture/IntGetCaptureWindow; - 命中测试 :
co_WinPosWindowFromPoint+WM_NCHITTEST; - 双击判定:4 条件(消息/窗口/时间/位置);
- 单击锁定:残障辅助功能;
- hover/leave 跟踪 :
IntTrackMouseMove+TrackMouseEvent; - LL 钩子:内核态实现,无需 DLL 注入。
核心要点回顾:
co_MsqInsertMouseMessage是鼠标消息的核心入口;WM_MOUSEMOVE通过"末尾合并"避免消息爆炸;- 鼠标捕获是拖放操作的基础;
- 命中测试(
WM_NCHITTEST)让应用可自定义窗口交互; - 双击判定需要窗口类支持
CS_DBLCLKS; - hover/leave 跟踪通过桌面对象的状态机实现;
ptiSysLock保证硬件消息的顺序处理。
本章代码索引
| 文件 | 内容 |
|---|---|
| win32ss/user/ntuser/mouse.c(file:///d:/reactos/win32ss/user/ntuser/mouse.c) | UserProcessMouseInput、UserGetMouseButtonsState、UserSendMouseInput、IntFixMouseInputButtons、IntFixMouseInputButtons、gMouseHistoryOfMoves、gcMouseHistoryOfMoves |
| win32ss/user/ntuser/msgqueue.c(file:///d:/reactos/win32ss/user/ntuser/msgqueue.c) | co_MsqInsertMouseMessage、co_IntProcessMouseMessage、co_MsqPeekHardwareMessage、IntCoalesceMouseMove、IntTrackMouseMove、MsqSendParentNotify、co_MsqPostMouseMove、ptiSysLock |
| win32ss/user/ntuser/input.c(file:///d:/reactos/win32ss/user/ntuser/input.c) | UserProcessMouseInput(RIT 入口) |
| win32ss/user/ntuser/nonclient.c(file:///d:/reactos/win32ss/user/ntuser/nonclient.c) | DefWndDoSizeMove、WM_NCHITTEST 默认处理 |
| win32ss/user/ntuser/winpos.c(file:///d:/reactos/win32ss/user/ntuser/winpos.c) | co_WinPosWindowFromPoint |
| win32ss/user/ntuser/ntstubs.c(file:///d:/reactos/win32ss/user/ntuser/ntstubs.c) | NtUserSetCapture、NtUserGetCapture、NtUserReleaseCapture、NtUserTrackMouseEvent |
| ntoskrnl/mouclass/mouclass.c(file:///d:/reactos/ntoskrnl/mouclass/mouclass.c) | mouclass 鼠标类驱动(IRP_MJ_READ 队列) |
| win32ss/user/user32/windows/input.c(file:///d:/reactos/win32ss/user/user32/windows/input.c) | user32 SendInput、mouse_event 用户态 API |