ReactOS 窗口系统分析(14):计时器/属性/加速键/热键 --- timer.c + prop.c + accelerator.c + hotkey.c
本文是"窗口系统三大主线之窗口管理"系列第 14 册(对应总览文档第 5.6 节)。
主题:四类"窗口附属设施"------窗口计时器(timer.c)、窗口属性(prop.c)、加速键表
(accelerator.c)与全局热键(hotkey.c)。它们共同回答一个问题:除了消息本身,
窗口管理子系统还提供哪些"粘性"服务来把时间事件、数据附件、键盘快捷方式与
窗口/线程关联起来。本文全部内容基于 ReactOS 实际源码分析,行号以 2026 年 8 月代码为准,禁止编造。
1. 概述
1.1 本册定位
窗口管理主线已经讲过窗口的创建/销毁(window.c)、消息队列与派发(msgqueue.c、
message.c)、默认窗口过程(defwnd.c)、菜单(menu.c,见 _11/_12 册)与滚动条
(scrollbar.c,见 _13 册)。本册补全四条相对独立的支线:
- 窗口计时器(timer.c,720 行) :
SetTimer/KillTimer的内核实现。它以
一个内核 KTIMER(MasterTimer)驱动所有 窗口计时器,到期后把WM_TIMER
消息投递到线程消息队列;同时提供WM_SYSTIMER内部系统计时器机制(光标闪烁、
窗口闪烁、鼠标悬停等)。 - 窗口属性(prop.c,274 行) :
SetProp/GetProp/RemoveProp的内核实现。
以原子 (ATOM)为键,把应用自定义数据附加到任意窗口上,属性项分配在
桌面堆中。 - 加速键表(accelerator.c,446 行) :
CreateAcceleratorTable/
TranslateAccelerator的内核实现。把WM_KEYDOWN/WM_CHAR翻译成
WM_COMMAND/WM_SYSCOMMAND,并负责查窗口菜单确认命令合法。 - 全局热键(hotkey.c,604 行) :
RegisterHotKey/UnregisterHotKey的内核
实现。维护一个全局热键链表 ,在原始输入线程(RIT)处理按键时匹配,
命中后向注册线程/窗口投递WM_HOTKEY(或WM_SYSCOMMAND)。
四条支线形态各异:计时器是"时间驱动",属性是"存储驱动",加速键是"翻译驱动",
热键是"输入抢占驱动"。但它们共享同一批基础设施------USER 对象句柄表
(UserCreateObject/UserGetObject)、桌面堆(UserHeapAlloc)、消息队列
(MsqPostMessage/MsqWakeQueue)、原子表(GlobalAddAtom)与 win32k 的
UserEnterExclusive/UserEnterShared 锁协议。
1.2 四个文件的职责划分
| 文件 | 行数 | 职责 |
|---|---|---|
win32ss/user/ntuser/timer.c |
720 | 窗口计时器:NtUserSetTimer/NtUserKillTimer/NtUserSetSystemTimer/NtUserValidateTimerCallback;核心例程 IntSetTimer/IntKillTimer/ProcessTimers/PostTimerMessages/SystemTimerProc;窗口无关计时器 ID 位图分配 |
win32ss/user/ntuser/prop.c |
274 | 窗口属性:NtUserSetProp/NtUserRemoveProp/NtUserBuildPropList;内部 IntGetProp/UserSetProp/UserGetProp/UserRemoveProp/UserRemoveWindowProps |
win32ss/user/ntuser/accelerator.c |
446 | 加速键表:NtUserCreateAcceleratorTable/NtUserCopyAcceleratorTable/NtUserDestroyAcceleratorTable/NtUserTranslateAccelerator;核心翻译 co_IntTranslateAccelerator |
win32ss/user/ntuser/hotkey.c |
604 | 全局热键:NtUserRegisterHotKey/NtUserUnregisterHotKey;内部 UserRegisterHotKey/UserUnregisterHotKey/co_UserProcessHotKeys/IsHotKey;WM_SETHOTKEY 支持(DefWndGetHotKey/DefWndSetHotKey) |
配套文件:
win32ss/user/ntuser/timer.h:TIMER结构、TMRF_*标志、ID_EVENT_SYSTIMER_*系统计时器 ID、MasterTimer声明。win32ss/user/ntuser/accelerator.h:ACCELERATOR_TABLE结构。win32ss/user/ntuser/hotkey.h:HOT_KEY结构、IDHK_*特殊热键 ID。win32ss/user/ntuser/input.c:原始输入线程RawInputThreadMain(驱动MasterTimer心跳)、InitInputImpl(分配MasterTimer)、系统热键注册。win32ss/user/ntuser/keyboard.c:ProcessKeyEvent在按键处理路径调用co_UserProcessHotKeys。win32ss/user/ntuser/message.c:WM_TIMER/WM_SYSTIMER的派发、PostTimerMessages的调用点。win32ss/user/ntuser/msgqueue.c:MsqWakeQueue/ClearMsgBitsMask(QS_TIMER位管理)、HungAppSysTimerProc。win32ss/user/user32/windows/prop.c:用户态SetPropA/W/GetPropA/W/RemovePropA/W。win32ss/user/user32/include/ntwrapper.h:用户态SetTimer/KillTimer/SetSystemTimer直通系统调用。
1.3 整体架构图
┌────────────────────────────────────────────────────────────────────────────┐
│ 应用程序 │
│ SetTimer / SetProp / TranslateAccelerator / RegisterHotKey │
└───────┬──────────────┬───────────────────┬───────────────────┬─────────────┘
│ user32.dll │ │ │
│ ntwrapper.h │ windows/prop.c │ 消息循环 │
│ SetTimer→ │ SetPropA/W │ GetMessage/ │
│ NtUserSetTimer│ GetPropA/W │ TranslateAccelerator
└──────┬───────┴───────┬───────────┴─────────┬─────────┴──────┬──────┘
│ │ │ │
▼ ▼ ▼ ▼
┌────────────────────────────────────────────────────────────────────────────┐
│ win32k.sys │
│ │
│ ┌──── timer.c ──────────────┐ ┌──── prop.c ──────────────┐ │
│ │ NtUserSetTimer/KillTimer │ │ NtUserSetProp/RemoveProp │ │
│ │ IntSetTimer/IntKillTimer │ │ IntGetProp/UserSetProp │ │
│ │ ProcessTimers (心跳) │ │ PROPERTY ←桌面堆(UserHeap)│ │
│ │ PostTimerMessages→WM_TIMER│ │ 挂窗口 PropListHead │ │
│ │ SystemTimerProc→WM_SYSTIMER│ └──────────────────────────┘ │
│ └──────────┬────────────────┘ │
│ │ KeSetTimer(MasterTimer, 10ms) │
│ ┌──────────▼────────────────────────────────────────────┐ │
│ │ input.c: RawInputThreadMain(原始输入线程/RIT) │ │
│ │ 等待 MasterTimer → ProcessTimers();读取鼠标/键盘设备 │ │
│ └──────────┬────────────────────────────────────────────┘ │
│ │ 键盘按键 │
│ ┌──────────▼────────────────────────────────────────────┐ │
│ │ keyboard.c: ProcessKeyEvent → co_UserProcessHotKeys │ │
│ │ → IsHotKey 匹配全局链表 → UserPostThreadMessage │ │
│ │ WM_HOTKEY / WM_SYSCOMMAND(SC_HOTKEY/SC_TASKLIST) │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ ┌──── accelerator.c ──────────┐ │
│ │ NtUserTranslateAccelerator │ 查窗口菜单(MENU_FindItem) │
│ │ co_IntTranslateAccelerator │ →WM_INITMENU/WM_INITMENUPOPUP │
│ │ 匹配后 co_IntSendMessage │ →WM_COMMAND(0x10000|cmd)/WM_SYSCOMMAND │
│ └─────────────────────────────┘ │
└────────────────────────────────────────────────────────────────────────────┘
1.4 数据流总览
【计时器】应用 SetTimer(hwnd, id, 100, NULL)
→ NtUserSetTimer(timer.c L633)→ IntSetTimer(L181)
→ FindTimer 查重 → CreateTimer 建 TIMER 对象(挂 TimersListHead)
→ 首个计时器出现 → KeSetTimer(MasterTimer, -97656×100ns≈10ms)
→ RIT(RawInputThreadMain)MasterTimer 到期 → ProcessTimers(L450)
→ 遍历链表倒计时;到期置 TMRF_READY + cTimersReady++ + MsqWakeQueue(QS_TIMER)
→ 重新 KeSetTimer
→ 应用 GetMessage → MsqGetMessage 处理 QS_TIMER → PostTimerMessages(L399)
→ 组装 MSG{WM_TIMER, nID, pfn} → MsqPostMessage 入队
→ 消息循环收到 WM_TIMER → 应用 WndProc
→ 应用 KillTimer → NtUserKillTimer → IntKillTimer → FindTimer+RemoveTimer
【属性】应用 SetProp(hwnd, "MyData", data)
→ GlobalAddAtomW("MyData") → NtUserSetProp(hwnd, atom, data)(prop.c L247)
→ UserSetProp → IntGetProp 查重 → UserHeapAlloc(PROPERTY) → 挂 PropListHead
→ GetProp 无系统调用:user32 直接 SharedPtrToUser 走桌面堆共享映射
→ RemoveProp → NtUserRemoveProp → UserHeapFree
【加速键】消息循环 TranslateAccelerator(hwnd, hAccel, &msg)
→ NtUserTranslateAccelerator(accelerator.c L365)
→ 遍历表项 → co_IntTranslateAccelerator(L42)
→ 按键/掩码匹配 → MENU_FindItem 查菜单 → WM_INITMENU 等
→ co_IntSendMessage(WM_COMMAND/WM_SYSCOMMAND)
【热键】应用 RegisterHotKey(hwnd, id, MOD_CONTROL, 'A')
→ NtUserRegisterHotKey(hotkey.c L534)→ UserRegisterHotKey(L446)
→ 冲突检测 IsHotKey → 新建 HOT_KEY 挂 gphkFirst
→ 用户按 Ctrl+A → ProcessKeyEvent(keyboard.c L1161)
→ co_UserProcessHotKeys(hotkey.c L190)→ IsHotKey 命中
→ UserPostThreadMessage/UserPostMessage(WM_HOTKEY)
2. 设计动机
2.1 计时器合并:一个内核定时器驱动全部窗口计时器
如果每个 SetTimer 都对应一个内核定时器对象,那么数百个窗口计时器就要挂数百个
KTIMER,每次到期都要经历内核定时器处理路径,开销大且难以与"消息队列唤醒"协同。
ReactOS 的选择是合并:
- win32k 只维护一个 内核定时器
MasterTimer(KTIMER,input.c L378 分配),
由原始输入线程(RIT)在KeWaitForMultipleObjects中等待(input.c L231/L302)。 - 所有窗口计时器(
TIMER对象)挂在同一个双向链表TimersListHead上,
MasterTimer每约 10ms 到期一次,ProcessTimers遍历链表统一倒计时,把
到期计时器标记为TMRF_READY并唤醒对应线程队列(MsqWakeQueue(pti, QS_TIMER))。 - 实际投递 不发生在 RIT 上:RIT 只负责"置就绪位";等到应用线程自己进入
GetMessage/PeekMessage处理QS_TIMER时,PostTimerMessages才真正把
WM_TIMER组装成 MSG 投进该线程的消息队列。这样到期与投递解耦,
单条线程的计时器消息在队列层面天然合并(cTimersReady计数)。
2.2 属性:窗口的键值附加存储
Win32 窗口本身只承载窗口过程、类、样式等固定字段,应用经常需要把"与窗口生命周期
一致"的私有数据挂到窗口上(例如控件缓存、文档指针、回调上下文)。设计选择:
- 以原子为键 :
SetProp(hwnd, "Name", data)先把字符串GlobalAddAtom成
ATOM,属性项存 ATOM 而非字符串,查找是整数比较,且共享字符串实例; - 不要求数据所有权 :
PROPERTY.Data只是 HANDLE(对应用而言是任意指针/句柄),
系统不解析、不释放,生命周期由应用管理;窗口销毁时系统只释放PROPERTY项本身; - 放桌面堆 :属性项(
PROPERTY)分配在桌面堆(UserHeapAlloc),桌面堆映射到
每个进程的用户态,因此GetProp不需要系统调用 ------user32 直接
SharedPtrToUser走共享映射遍历PropListHead,是窗口管理里"免系统调用"
的典型例子。
2.3 加速键:键盘输入到菜单命令的翻译层
菜单命令(如"复制")通常有键盘捷径(Ctrl+C)。设计动机:
- 把"按键→命令"映射从应用逻辑中抽出来 :映射表(
ACCEL数组)是资源
(.rc里的ACCELERATORS段)与运行时代码共享的声明式数据,不需要每个窗口
过程重复写 if/else; - 翻译必须过菜单验证 :加速键不是无条件触发,只有目标命令存在于窗口菜单
(IDMenu)或系统菜单(SystemMenu)中才发送WM_COMMAND,且菜单项被
MF_DISABLED/MF_GRAYED时不发------保证菜单状态是命令可用性的唯一权威; - 翻译发生在消息循环 :
TranslateAccelerator在GetMessage之后、
DispatchMessage之前调用,与IsDialogMessage同属"消息预处理器"层。
2.4 全局热键:跨窗口的输入窃听
普通键盘消息只发给焦点窗口;热键要求在任何应用处于前台 时都能响应特定组合键
(例如全局截图)。设计动机:
- 内核侧全局表 :热键注册表
gphkFirst是 win32k 内的全局链表,不挂在任何
窗口/线程上,因此与焦点无关; - 在输入管线上游拦截 :热键检测放在
ProcessKeyEvent(keyboard.c L1161)
的早期阶段------低层键盘钩子之后、键盘消息入队之前。命中后直接返回 TRUE 让按键
不再进入普通键盘消息流 (bPostMsg = FALSE),实现"抢占"; - 修饰键单独成键 :
MOD_WIN这类"只有修饰键"的热键(如 Win 键本身)在修饰键
抬起 时触发,用gfsModOnlyCandidate记录候选状态。
2.5 四类设施与消息队列的关系
| 设施 | 触发者 | 投递目标 | 消息 | 是否依赖焦点 |
|---|---|---|---|---|
| 计时器 | RIT 心跳 | 注册计时器的线程队列 | WM_TIMER / WM_SYSTIMER | 否 |
| 属性 | 应用显式 API | 无(纯存储) | 无 | 否 |
| 加速键 | 应用消息循环 | 目标窗口 | WM_COMMAND / WM_SYSCOMMAND | 是(键盘消息流向焦点) |
| 热键 | RIT 按键处理 | 注册线程/窗口 | WM_HOTKEY / WM_SYSCOMMAND | 否(全局) |
3. 核心数据结构
3.1 TIMER 结构与 TMRF_* 标志(timer.h L3-25)
c
typedef struct _TIMER
{
HEAD head; // USER 对象头(句柄表管理)
LIST_ENTRY ptmrList; // 挂到全局 TimersListHead
PTHREADINFO pti; // 所属线程(到期后唤醒/投递的目标)
PWND pWnd; // 关联窗口,NULL 表示"窗口无关计时器"
UINT_PTR nID; // 计时器 ID(窗口相关时由调用者给,无关时位图分配)
INT cmsCountdown; // 倒计时毫秒数(递减)
INT cmsRate; // 周期毫秒数(到期后重装)
FLONG flags; // TMRF_* 标志
TIMERPROC pfn; // 回调(系统计时器用;用户计时器作 lParam 传递)
} TIMER, *PTIMER;
标志位:
| 标志 | 值 | 含义 |
|---|---|---|
TMRF_READY |
0x0001 | 计时器已到期,等待 PostTimerMessages 投递 WM_TIMER |
TMRF_SYSTEM |
0x0002 | 系统计时器(WM_SYSTIMER,回调在内核/原始线程执行) |
TMRF_RIT |
0x0004 | 在原始输入线程(RIT)上下文中运行(如 HungAppSysTimerProc) |
TMRF_INIT |
0x0008 | 刚创建,第一次 ProcessTimers 跳过(避免注册即到期) |
TMRF_ONESHOT |
0x0010 | 单次计时器(到期后置 TMRF_WAITING 不再计数) |
TMRF_WAITING |
0x0020 | 已完成(一次性计时器用),跳过后续处理 |
TMRF_TIFROMWND |
0x0040 | pti 取自窗口线程(NtUserSetTimer 传入的类型) |
注意 TMRF_RIT 与 TMRF_SYSTEM 是正交 的:FindTimer 的匹配
(flags & (TMRF_SYSTEM|TMRF_RIT)) 同时比较两个位。StartTheTimers 注册的
HungAppSysTimerProc 是 TMRF_RIT 但不是 TMRF_SYSTEM------它在 RIT 内被
ProcessTimers 硬编码调用(pTmr->pfn(NULL, WM_SYSTIMER, ...)),不走消息队列。
3.2 系统计时器 ID 清单(timer.h L27-31)
| ID | 值 | 用途 |
|---|---|---|
ID_EVENT_SYSTIMER_MOUSEHOVER |
ID_TME_TIMER = 0xFFFA |
鼠标悬停检测(TrackMouseEvent,产生 WM_MOUSEHOVER/WM_NCMOUSEHOVER) |
ID_EVENT_SYSTIMER_FLASHWIN |
0xFFF8 | 窗口闪烁(FlashWindowEx,SystemTimerProc 回调 IntFlashWindowEx) |
ID_EVENT_SYSTIMER_TRACKWIN |
0xFFF7 | 窗口跟踪(定义但当前 SystemTimerProc 未处理) |
ID_EVENT_SYSTIMER_ANIMATEDFADE |
0xFFF6 | 动画淡入淡出(定义但当前未处理) |
ID_EVENT_SYSTIMER_INVALIDATEDCES |
0xFFF5 | DCE 失效(定义但当前未处理) |
另有插入符闪烁使用 IDCARETTIMER(caret.c),它不在 timer.h 的
ID_EVENT_SYSTIMER_* 系列里,但同样以 TMRF_SYSTEM + CaretSystemTimerProc
的方式工作。所有系统计时器 ID 都是负数区域的高位值(0xFFF5~0xFFFA),
与应用计时器 ID(通常为正的小整数)天然不冲突;窗口无关用户计时器 ID 由位图
分配在 [256, 32767],也避开了系统保留区。
3.3 PROPERTY 与 PROPLISTITEM(ntuser.h L1090-1104)
c
#define PROPERTY_FLAG_SYSTEM 1
typedef struct _PROPLISTITEM
{
ATOM Atom;
HANDLE Data;
} PROPLISTITEM, *PPROPLISTITEM; // 枚举快照项(NtUserBuildPropList 输出)
typedef struct _PROPERTY
{
LIST_ENTRY PropListEntry; // 挂到窗口的 PropListHead
HANDLE Data; // 应用数据(系统不解析)
ATOM Atom; // 属性名原子(键)
WORD fs; // PROPERTY_FLAG_SYSTEM 表示系统属性
} PROPERTY, *PPROPERTY;
窗口侧有两个字段(ntuser.h 的 WND 定义,prop.c 使用):PropListHead
(LIST_ENTRY 链表头,窗口创建时 InitializeListHead,window.c L2051)与
PropListItems(计数,供遍历与断言用)。系统属性 (PROPERTY_FLAG_SYSTEM)
与用户属性分开标记:IntGetProp 的 SystemProp 参数用来过滤,FlashWindowEx
的 AtomFlashWndState(painting.c L1384/L1462/L1500)就是系统属性的使用者------
它把闪烁状态机数据存成系统属性,避免与应用属性混在一起、也防止被
NtUserBuildPropList 枚举出去(该函数明确跳过系统属性,prop.c L176)。
3.4 ACCELERATOR_TABLE 与 ACCEL(accelerator.h L3-8)
c
typedef struct _ACCELERATOR_TABLE
{
PROCMARKHEAD head; // USER 对象头(进程标记)
ULONG Count; // 表项个数
LPACCEL Table; // ACCEL 数组(分页池,USERTAG_ACCEL)
} ACCELERATOR_TABLE, *PACCELERATOR_TABLE;
ACCEL 是 Win32 公共结构(psdk/winuser.h):
{ BYTE fVirt; WORD key; WORD cmd; }。fVirt 是标志字节:FVIRTKEY(0x01,
按虚拟键匹配)、FALT(0x10)、FCONTROL(0x08)、FSHIFT(0x04)。
accelerator.c 额外定义 FVIRT_TBL_END 0x80(未文档化的"表尾"标记,遇到即停)与
FVIRT_MASK 0x7F(写入时截断高位)。加速键表对象类型是 TYPE_ACCELTABLE(=8,
ntuser.h L48),由 UserCreateObject 创建,是标准 USER 对象。
3.5 HOT_KEY 与特殊 ID(hotkey.h L3-23)
c
typedef struct _HOT_KEY
{
PTHREADINFO pti; // 注册线程(pWnd 为 NULL 时是投递目标)
PWND pWnd; // 注册窗口;NULL=线程热键;PWND_BOTTOM=焦点窗口
UINT fsModifiers;// MOD_ALT/CONTROL/SHIFT/WIN 位
UINT vk; // 虚拟键(0 表示"仅修饰键",如 Win 键本身)
INT id; // 应用热键 ID 或 IDHK_* 系统热键
struct _HOT_KEY *pNext; // 全局链表 gphkFirst 的下一个
} HOT_KEY, *PHOT_KEY;
#define IDHK_F12 -5 // 调试热键(UserDebuggerHotKey)
#define IDHK_SHIFTF12 -6 // Shift+F12 调试热键
#define IDHK_WINKEY -7 // 单独的 Win 键(任务列表)
#define IDHK_WNDKEY -8 // WM_SETHOTKEY 注册的窗口热键
#define IDHK_SNAP_LEFT -10// Win+← 窗口吸附
#define IDHK_SNAP_RIGHT -11// Win+→
#define IDHK_SNAP_UP -12// Win+↑
#define IDHK_SNAP_DOWN -13// Win+↓
IsWindowHotKey(pHK)(hotkey.c L43)判定:pti == NULL && id == IDHK_WNDKEY,
即 WM_SETHOTKEY 路径注册的热键(pWnd 非空、pti 恒为 NULL)。应用 ID 若等于
IDHK_WINKEY/IDHK_SNAP_* 等负值会产生冲突,但应用很少使用负 ID,且这些
ID 由 win32k 在键盘设备初始化时注册(input.c L216-223)。
4. 计时器子系统:核心函数逐一展开
4.1 全局状态与锁(timer.c L14-49)
c
static LIST_ENTRY TimersListHead; // 全部计时器链表
static LONG TimeLast = 0; // 上次 ProcessTimers 的 tick
static PFAST_MUTEX Mutex; // 窗口无关 ID 位图锁(非分页池)
static RTL_BITMAP WindowLessTimersBitMap; // 窗口无关计时器 ID 位图
static PVOID WindowLessTimersBitMapBuffer;
static ULONG HintIndex = HINTINDEX_BEGIN_VALUE; // 位图分配游标
ERESOURCE TimerLock; // 计时器链表锁
锁协议两个宏:
TimerEnterExclusive():KeEnterCriticalRegion()+ExAcquireResourceExclusiveLite(&TimerLock, TRUE);TimerLeave()对称。所有链表遍历/修改都走它,TimerLock是 ERESOURCE,支持递归。IntLockWindowlessTimerBitmap()/IntUnlockWindowlessTimerBitmap():快速互斥,仅保护位图分配。
窗口无关计时器 ID 分配域:MAX_WINDOW_LESS_TIMER_ID = 32768-1 = 32767,
NUM_WINDOW_LESS_TIMERS = 32768-256 = 32512,即 ID 落在 [256, 32767],
与 Windows 2k 的 32768 个窗口无关计时器上限一致(注释 L19-22)。InitTimerImpl
(L597)从非分页池分配 FAST_MUTEX(TAG_INTERNAL_SYNC)与位图缓冲
(TAG_TIMERBMP),RtlInitializeBitMap + RtlClearAllBits 后初始化
TimerLock 与链表头。它在 win32k 初始化序列中位于
InitInputImpl()(分配 MasterTimer)之后(main.c L1081-1084),
因为 IntSetTimer 的路径上断言 MasterTimer != NULL。
4.2 CreateTimer / RemoveTimer / FindTimer
CreateTimer(L53-69):分配 TIMER 对象的唯一入口。
c
Ret = UserCreateObject(gHandleTable, NULL, NULL, &Handle, TYPE_TIMER, sizeof(TIMER));
if (Ret)
{
UserHMSetHandle(Ret, Handle);
InsertTailList(&TimersListHead, &Ret->ptmrList);
}
TYPE_TIMER = 16(ntuser.h L56)。对象一出生就挂入全局链表;注意此时
pWnd/pti/nID 等字段尚未填充(由 IntSetTimer 随后赋值)。
RemoveTimer(L71-97):
c
RemoveEntryList(&pTmr->ptmrList); // 先脱链
if ((pTmr->pWnd == NULL) && (!(pTmr->flags & TMRF_SYSTEM)))
{
// 窗口无关的用户计时器:归还位图位
ulBitmapIndex = (ULONG)(MAX_WINDOW_LESS_TIMER_ID - pTmr->nID);
IntLockWindowlessTimerBitmap();
RtlClearBit(&WindowLessTimersBitMap, ulBitmapIndex);
IntUnlockWindowlessTimerBitmap();
}
UserDereferenceObject(pTmr); // 释放创建时加的引用
Ret = UserDeleteObject(UserHMGetHandle(pTmr), TYPE_TIMER);
要点:系统计时器不归还位图 (注释"System timers are reusable",系统计时器
的 nID 是固定的 ID_EVENT_SYSTIMER_*,本就不在位图内);只有
pWnd == NULL 的窗口无关用户计时器 才需要清位图位------位图索引与 ID 反算
MAX_WINDOW_LESS_TIMER_ID - nID,与分配时的公式互逆。
FindTimer(L99-127) :加 TimerLock 遍历,匹配三元组
(nID, pWnd, (flags & (TMRF_SYSTEM|TMRF_RIT)))。把 TMRF_SYSTEM/TMRF_RIT
作为区分维度,保证系统计时器、RIT 计时器、用户计时器即使 ID/窗口相同也不互相
干扰。IntKillTimer 正是靠"系统计时器标志作为查询条件"来区分删除对象的。
FindSystemTimer(L129-151) :按 pMsg->lParam == pTmr->pfn && TMRF_SYSTEM
查找。它在 message.c L933 被用于 WM_SYSTIMER 派发:WM_SYSTIMER 消息的
lParam 携带回调地址,派发时反查 TIMER 对象拿到 pfn 直接以内核回调方式调用
(pTimer->pfn(pMsg->hwnd, WM_SYSTIMER, wParam, Time),message.c L937)。
4.3 IntSetTimer ------ 计时器注册核心(L181-279)
签名:UINT_PTR FASTCALL IntSetTimer(PWND Window, UINT_PTR IDEvent, UINT Elapse, TIMERPROC TimerFunc, INT Type)。
分五步:
(1) 间隔夹取(L194-215) 。两块 #if 0 对照说明行为演变:
c
#if 0 /* Windows NT/2k/XP 行为:超过最大值则重置为 1 */
if (Elapse > USER_TIMER_MAXIMUM) Elapse = 1;
#else /* XP SP2 / Server 2003 行为 */
if (Elapse > USER_TIMER_MAXIMUM) Elapse = USER_TIMER_MAXIMUM;
#endif
if (Elapse < USER_TIMER_MINIMUM) Elapse = USER_TIMER_MINIMUM; // 10ms
USER_TIMER_MAXIMUM = 2147483647、USER_TIMER_MINIMUM = 10(psdk/winuser.h
L916-917)。注意 USER_TIMER_MINIMUM 夹取与 sysparams.c L1513-1519 引用的
微软文档一致:"直到 XP SP2/2003 SP1 才强制 USER_TIMER_MAXIMUM/MINIMUM"。
(2) IDEvent 特例(L217-220) :(Window) && (IDEvent == 0) 时返回 1
(Ret = 1),但实际创建 的计时器 ID 仍是 0------这是 Win32 语义:
"SetTimer(hwnd, 0, ...) 返回 1,计时器 ID 为 0",编辑框控件(user32
controls/edit.c L3808)正依赖此语义创建无名计时器。
(3) 窗口无关计时器 ID 分配(L224-243) :仅当 !pTmr && Window == NULL && !(Type & TMRF_SYSTEM) 时:
c
ulBitmapIndex = RtlFindClearBitsAndSet(&WindowLessTimersBitMap, 1, HintIndex);
HintIndex = (ulBitmapIndex + 1) % NUM_WINDOW_LESS_TIMERS;
if (ulBitmapIndex == ULONG_MAX)
{
EngSetLastError(ERROR_NO_SYSTEM_RESOURCES);
return 0;
}
IDEvent = MAX_WINDOW_LESS_TIMER_ID - ulBitmapIndex;
RtlFindClearBitsAndSet 从 HintIndex 起查找空闲位,找到后顺手置位 ;
HintIndex 循环前移(取模 NUM_WINDOW_LESS_TIMERS)做简单的轮转分配;
位图耗尽(返回 ULONG_MAX)时报 ERROR_NO_SYSTEM_RESOURCES 返回 0。
ID 反算公式保证落在 [256, 32767]。
(4) 新建或更新 TIMER(L245-271)。新建时:
c
pTmr = CreateTimer();
if (!pTmr) return 0;
if (Window && (Type & TMRF_TIFROMWND))
pTmr->pti = Window->head.pti->pEThread->Tcb.Win32Thread; // 窗口线程
else
{
if (Type & TMRF_RIT)
pTmr->pti = ptiRawInput; // 原始输入线程
else
pTmr->pti = PsGetCurrentThreadWin32Thread(); // 当前线程
}
pTmr->pWnd = Window;
pTmr->cmsCountdown = Elapse;
pTmr->cmsRate = Elapse;
pTmr->pfn = TimerFunc;
pTmr->nID = IDEvent;
pTmr->flags = Type | TMRF_INIT;
pti 决定到期后唤醒/投递哪个线程;TMRF_INIT 让首次 ProcessTimers 跳过
(否则 Elapse 很小的时候"注册即到期")。已存在(FindTimer 命中)则只更新
cmsCountdown/cmsRate------同窗口同 ID 的 SetTimer 是重置计时器 ,即
"同窗口合并"(详见第 8 章)。
(5) 启动心跳(L273-276):
c
ASSERT(MasterTimer != NULL);
if (TimersListHead.Flink == TimersListHead.Blink) // 链表上只有这 1 个计时器
KeSetTimer(MasterTimer, DueTime, NULL);
DueTime.QuadPart = -97656(L192),以 100ns 为单位即约 9.7656ms (注释
称 1024Hz,约 10ms)。只有从"无计时器"变为"有第一个计时器"时才需要 KeSetTimer
启动 MasterTimer;链表非空时心跳本就在跑(ProcessTimers 每次末尾会重新
KeSetTimer,L519),无需重复设置。
4.4 NtUserSetTimer / NtUserSetSystemTimer(L633-703)
NtUserSetTimer(L633-656) :标准系统调用模式------UserEnterExclusive()、
UserGetWindowObject(hWnd) 校验句柄(NULL 句柄 → Window = NULL,即窗口无关
计时器)、调用 IntSetTimer(Window, nIDEvent, uElapse, lpTimerFunc, TMRF_TIFROMWND)、
UserLeave()。user32 的 SetTimer 在 ntwrapper.h L112-115 直接内联转发
NtUserSetTimer,无任何用户态预处理。
NtUserSetSystemTimer(L683-703) :IntSetTimer(UserGetWindowObject(hWnd), nIDEvent, uElapse, NULL, TMRF_SYSTEM)。注意两点:lpTimerFunc 传 NULL
(系统计时器的回调由 SystemTimerProc 统一接管,TimerFunc 参数被忽略);
标志是 TMRF_SYSTEM 而非 TMRF_TIFROMWND,所以 pti 走
PsGetCurrentThreadWin32Thread() 分支。它是 Windows 内部 API
(user32 的 SetSystemTimer 同样直通,ntwrapper.h L106-109),应用一般不直接调用。
4.5 ProcessTimers ------ 内核心跳(L450-525)
在 RIT 上以约 10ms 周期执行(input.c L302-305:MasterTimer 被 KeWaitForMultipleObjects
唤醒 → ProcessTimers())。加 TimerEnterExclusive() 后遍历:
c
while(pLE != &TimersListHead)
{
pTmr = CONTAINING_RECORD(pLE, TIMER, ptmrList);
TimerCount++;
if (pTmr->flags & TMRF_WAITING) { /* 跳过,直接下一项 */ continue; }
if (pTmr->flags & TMRF_INIT)
{
pTmr->flags &= ~TMRF_INIT; // 首次运行:清 INIT,跳过本次
}
else
{
if (pTmr->cmsCountdown < 0) // 到期(倒计时走成负数)
{
if ((!(pTmr->flags & TMRF_READY)) && (!(pTmr->pti->TIF_flags & TIF_INCLEANUP)))
{
if (pTmr->flags & TMRF_ONESHOT)
pTmr->flags |= TMRF_WAITING; // 单次:此后不再触发
if (pTmr->flags & TMRF_RIT)
{
// 硬编码调用:在原始输入线程上下文内直接执行
pTmr->pfn(NULL, WM_SYSTIMER, pTmr->nID, (LPARAM)pTmr);
}
else
{
pTmr->flags |= TMRF_READY; // 置就绪
pTmr->pti->cTimersReady++; // 队列级计数
ASSERT(pTmr->pti->pEventQueueServer != NULL);
MsqWakeQueue(pTmr->pti, QS_TIMER, TRUE); // 唤醒线程
}
}
pTmr->cmsCountdown = pTmr->cmsRate; // 重装周期
}
else
pTmr->cmsCountdown -= Time - TimeLast; // 正常倒计时
}
pLE = pLE->Flink;
}
// 末尾:重启心跳
KeSetTimer(MasterTimer, DueTime, NULL);
TimeLast = Time;
关键设计:
- 倒计时粒度 :每个计时器持有自己的
cmsCountdown,但ProcessTimers
以Time - TimeLast(两次心跳间的实际流逝)统一递减,因此所有计时器共享
同一个"时钟源"(EngGetTickCount32),互不漂移; - 到期≠投递 :非 RIT 计时器到期只置
TMRF_READY+cTimersReady+++
MsqWakeQueue(pti, QS_TIMER, TRUE)(msgqueue.c L413:置
fsWakeBits/fsChangeBits并KeSetEvent唤醒消息队列等待),WM_TIMER
的真正入队要等应用线程下次处理消息时由PostTimerMessages完成; - TMRF_READY 防重入 :到期且未投递期间即使心跳再走,也不重复置位------
计数只在PostTimerMessages投递后ClearMsgBitsMask递减
(timer.c L430 / msgqueue.c L469-475); - TMRF_RIT 直调 :RIT 计时器(当前只有
HungAppSysTimerProc)在 RIT
上下文同步执行 回调(pfn(NULL, WM_SYSTIMER, nID, pTmr)),
不走消息队列------它本来就是系统线程,无应用消息循环可唤醒; - TIF_INCLEANUP 抑制:线程清理期间不再置位,避免向正在销毁的线程投递;
- 单次计时器 (
TMRF_ONESHOT,当前源码无实际使用者,属预留)到期后置
TMRF_WAITING,此后每次心跳直接跳过,但对象仍在链表中直到被 KillTimer
删除------不是自动销毁。
4.6 PostTimerMessages ------ 投递 WM_TIMER(L399-448)
由 MsqGetMessage 在应用线程上下文 调用(message.c L1163-1167:
if ((ProcessMask & QS_TIMER) && PostTimerMessages(Window)) continue;)------
即应用自己 GetMessage 时,把该线程就绪的计时器真正变成消息。逻辑:
c
pti = PsGetCurrentThreadWin32Thread();
TimerEnterExclusive();
pLE = TimersListHead.Flink;
while (pLE != &TimersListHead)
{
pTmr = CONTAINING_RECORD(pLE, TIMER, ptmrList);
if ( (pTmr->flags & TMRF_READY) &&
(pTmr->pti == pti) &&
((pTmr->pWnd == Window) || (Window == NULL)) )
{
Msg.hwnd = (pTmr->pWnd ? UserHMGetHandle(pTmr->pWnd) : NULL);
Msg.message = (pTmr->flags & TMRF_SYSTEM) ? WM_SYSTIMER : WM_TIMER;
Msg.wParam = (WPARAM) pTmr->nID;
Msg.lParam = (LPARAM) pTmr->pfn; // 回调地址作为 lParam(校验用)
Msg.time = EngGetTickCount32();
Msg.pt = gpsi->ptCursor; // CORE-10867:WM_TIMER 也带鼠标位置
MsqPostMessage(pti, &Msg, FALSE, (QS_POSTMESSAGE|QS_ALLPOSTMESSAGE), 0, 0);
pTmr->flags &= ~TMRF_READY;
ClearMsgBitsMask(pti, QS_TIMER); // 递减 cTimersReady,归零则清 QS_TIMER 位
Hit = TRUE;
// 投递后移到链表尾:同一消息循环内不再重复处理
RemoveEntryList(&pTmr->ptmrList);
InsertTailList(&TimersListHead, &pTmr->ptmrList);
break; // 一次只投一个
}
pLE = pLE->Flink;
}
TimerLeave();
return Hit;
要点:
- 一次投一个 :
break保证每次调用只投递一个就绪计时器;调用点
PostTimerMessages(...) == TRUE时continue回到消息获取循环顶部,
继续处理下一个,直到没有就绪计时器为止------单次 GetMessage 调用可能取出
多个 WM_TIMER; - hwnd 可为 NULL :窗口无关计时器投递
hwnd = NULL的 WM_TIMER
(Msg.hwnd = NULL),Msg.message用WM_SYSTIMER还是WM_TIMER
取决于TMRF_SYSTEM------系统计时器的 WM_SYSTIMER 同样经由这里入队
(回调地址存 lParam,供 message.c L933FindSystemTimer反查); - 消息移尾 :投递后把计时器挪到链表尾部,配合"一次投一个 + continue"
实现公平轮转,防止某个高频计时器饿死其他计时器; - WM_TIMER 是低优先级消息 :投递标志是
QS_POSTMESSAGE|QS_ALLPOSTMESSAGE
(不是独立的 QS_TIMER 投递位),QS_TIMER仅作为"线程有就绪计时器"的唤醒位
存在,由cTimersReady计数维护(msgqueue.c L469-475)。
4.7 NtUserKillTimer / IntKillTimer(L578-680)
IntKillTimer(L578-595):
c
pTmr = FindTimer(Window, IDEvent, SystemTimer ? TMRF_SYSTEM : 0);
if (pTmr) RemoveTimer(pTmr);
return pTmr ? TRUE : FALSE;
SystemTimer 参数决定查询条件里的标志------NtUserKillTimer 传 FALSE(只杀用户
计时器),SystemTimerProc 内部杀自己传 TRUE(L369)。NtUserKillTimer
(L659-680) :UserEnterExclusive + 句柄校验后转发 IntKillTimer(Window, uIDEvent, FALSE)。Kill 不存在的计时器返回 FALSE 但不报错。
4.8 DestroyTimersForWindow / DestroyTimersForThread(L527-576)
c
DestroyTimersForWindow(PTHREADINFO pti, PWND Window) // L527
// 遍历,若 (pTmr->pti == pti) && (pTmr->pWnd == Window) → RemoveTimer
DestroyTimersForThread(PTHREADINFO pti) // L554
// 遍历,若 (pTmr->pti == pti) → RemoveTimer
两函数都先保存 pLE = pLE->Flink 再删除当前项(防悬垂)。区别在匹配条件:
窗口销毁只清"该线程在该窗口"上的计时器;线程退出清"该线程"的全部计时器。
它们由线程/进程清理路径调用(ExitThreadCallback/窗口销毁),保证计时器不会
在窗口或线程消失后继续触发(否则 PostTimerMessages 会向已死线程投递)。
4.9 SystemTimerSet / SystemTimerProc(L284-397)
SystemTimerSet(L383-397) :系统计时器注册的"受保护入口"------校验窗口属于
当前进程(Window->head.pti->pEThread->ThreadsProcess != PsGetCurrentProcess()
→ ERROR_ACCESS_DENIED),然后 IntSetTimer(..., TMRF_SYSTEM)。
当前树内没有调用者(内部各子系统直接调 IntSetTimer + TMRF_SYSTEM,
如 caret.c/mouse.c/painting.c)。
SystemTimerProc(L284-370) :TMRF_SYSTEM 计时器到期的统一回调。先校验
窗口句柄(无效 → ERR 返回;NULL → "Windowless Timer" TRACE 返回),再按
idEvent 分发:
c
case ID_EVENT_SYSTIMER_MOUSEHOVER: // 0xFFFA
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) // 客户区内 → WM_MOUSEHOVER
{
wParam = MsqGetDownKeyState(pWnd->head.pti->MessageQueue);
Msg = WM_MOUSEHOVER;
// 坐标换算到客户区(支持 WS_EX_LAYOUTRTL 镜像)
}
else // 非客户区 → WM_NCMOUSEHOVER
{
wParam = pDesk->htEx;
Msg = WM_NCMOUSEHOVER;
}
UserPostMessage(hwnd, Msg, wParam, MAKELPARAM(Point.x, Point.y));
pDesk->dwDTFlags &= ~DF_TME_HOVER;
break; // 跳出 switch,走末尾的 IntKillTimer 自杀
}
}
return; // 条件不满足(鼠标移出悬停矩形/换窗口)→ 不杀,继续等
case ID_EVENT_SYSTIMER_FLASHWIN: // 0xFFF8
FLASHWINFO fwi = {sizeof(FLASHWINFO), UserHMGetHandle(pWnd), FLASHW_SYSTIMER, 0, 0};
IntFlashWindowEx(pWnd, &fwi); // 驱动闪烁状态机
return;
default:
ERR("System Timer Proc invalid id %u!\n", idEvent);
break;
}
IntKillTimer(pWnd, idEvent, TRUE); // 末尾:一次性系统计时器自杀
两个 case 的"生命周期"策略不同:MOUSEHOVER 命中后自杀、未命中则保留继续等;
FLASHWIN 每次触发不自杀 ------闪烁循环由 IntFlashWindowEx 内部决定何时
IntKillTimer(pWnd, ID_EVENT_SYSTIMER_FLASHWIN, TRUE)(painting.c L1459,
闪烁完成 FLASHW_STOP 时)。MOUSEHOVER 计时器由 TrackMouseEvent 系列创建
(mouse.c L466、msgqueue.c L1473 的 IntSetTimer(pWnd, ID_EVENT_SYSTIMER_MOUSEHOVER, pDesk->dwMouseHoverTime, SystemTimerProc, TMRF_SYSTEM)),TME_CANCEL
时由 mouse.c L447 IntKillTimer 取消。
4.10 StartTheTimers / HungAppSysTimerProc(timer.c L372-381 + msgqueue.c L2204)
c
VOID FASTCALL StartTheTimers(VOID)
{
// 先启动 gdi 同步计时器,再启动带 Hung App 回调的计时器,
// 它调用 Idle 进程逻辑让屏幕保护程序知道该运行了......
IntSetTimer(NULL, 0, 1000, HungAppSysTimerProc, TMRF_RIT);
}
注册一个窗口无关 (Window = NULL)的 RIT 计时器:ID 由位图分配、间隔
1000ms、回调 HungAppSysTimerProc(msgqueue.c L2204):
c
VOID CALLBACK HungAppSysTimerProc(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime)
{
DoTheScreenSaver(); // input.c L63:检查空闲超时,启动屏幕保护
// Process list of windows that are hung and waiting.
}
DoTheScreenSaver(input.c L63-95):若 gspv.iScrSaverTimeout > 0 且
EngGetTickCount32() - LastInputTick > 1000*timeout,则给 SAS 窗口发
WM_LOGONNOTIFY(LN_START_SCREENSAVE),或直接设置 W32PF_IDLESCREENSAVER。
StartTheTimers 由 RIT 在线程启动早期调用(input.c L185,打开交互窗口站之后、
进入设备读取循环之前)------所以这个"每秒心跳"从系统启动起就常驻。
4.11 ValidateTimerCallback / NtUserValidateTimerCallback(L153-179, L705-718)
ValidateTimerCallback(pti, lParam) 遍历计时器链表,判断
lParam == (LPARAM)pTmr->pfn && !(flags & (TMRF_SYSTEM|TMRF_RIT)) && pTmr->pti->ppi == pti->ppi------即"lParam 是不是同进程 某个用户计时器的回调
地址"。它在 message.c L918 被用于 WM_TIMER 派发:若 WM_TIMER 的 lParam
(回调地址)通过校验,则把 WM_TIMER 作为回调调用 处理
(co_IntCallWindowProc((WNDPROC)pMsg->lParam, TRUE, hwnd, WM_TIMER, wParam, Time, -1),
message.c L921-927)------这就是 SetTimer(hwnd, id, elapse, TimerProc) 带回调
语义的内核侧实现。NtUserValidateTimerCallback(L705)是它的系统调用包装
(UserEnterShared),供 user32 侧校验使用。
4.12 初始化序列与 MasterTimer 心跳线程
InitInputImpl(input.c L376-388):MasterTimer = ExAllocatePoolWithTag(NonPagedPool, sizeof(KTIMER), USERTAG_SYSTEM)+KeInitializeTimer。- win32k 初始化(main.c L1078-1085):
InitUserImpl → InitWindowStationImpl → InitDesktopImpl → InitInputImpl → InitKeyboardImpl → MsqInitializeImpl → InitTimerImpl------MasterTimer 先于
TimerLock/位图存在。 - RIT(
RawInputThreadMain,input.c L140)把MasterTimer加入等待集
(input.c L231WaitObjects[cWaitObjects++] = MasterTimer),被
KeWaitForMultipleObjects唤醒后(L302-305)调用ProcessTimers()。
注意:任务描述中提到的 "TimerThreadProc" 在 ReactOS 中的实际对应物就是
RawInputThreadMain(input.c L140,注释明确 "Reads data from input devices
and supports win32 timers")------计时器与输入设备由同一个 系统线程驱动,
这也是TMRF_RIT(Raw Input Thread)标志名称的由来。
4.13 关于 NtUserGetTimerResolution 的说明
任务清单中列出的 NtUserGetTimerResolution 在当前源码树中不存在
(grep 全 win32ss 无任何 TimerResolution 符号)。ReactOS 的计时器精度由
USER_TIMER_MINIMUM(10ms)与 MasterTimer 心跳周期(约 9.7656ms)共同决定,
并没有实现 Windows 的 NtUserGetTimerResolution(Windows 上它返回系统计时器
分辨率供 timeBeginPeriod/timeGetDevCaps 使用)。本文按真实源码只记录
已实现部分,不虚构该函数。
5. 属性子系统:核心函数逐一展开
5.1 IntGetProp(prop.c L14-42)
c
PPROPERTY FASTCALL IntGetProp(PWND Window, ATOM Atom, BOOLEAN SystemProp)
{
WORD SystemFlag = SystemProp ? PROPERTY_FLAG_SYSTEM : 0;
NT_ASSERT(UserIsEntered());
ListEntry = Window->PropListHead.Flink;
for (i = 0; i < Window->PropListItems; i++)
{
Property = CONTAINING_RECORD(ListEntry, PROPERTY, PropListEntry);
ListEntry = ListEntry->Flink;
if (Property->Atom == Atom &&
(Property->fs & PROPERTY_FLAG_SYSTEM) == SystemFlag)
return Property;
}
NT_ASSERT(ListEntry == &Window->PropListHead);
return NULL;
}
线性遍历 PropListItems 项,按原子 + 系统标志 双重匹配。断言 UserIsEntered()
要求调用者已持有 win32k 锁(共享/独占均可,因为只读)。Atom 相等是整数比较
(属性名在 user32 侧已经 GlobalAddAtom/GlobalFindAtom 转成原子,见第 5.5 节)。
5.2 UserGetProp / UserSetProp / UserRemoveProp(内部例程)
UserGetProp(L44-56) :Prop = IntGetProp(...); return Prop ? Prop->Data : NULL;
纯读,无副作用。内核侧各子系统用它读系统属性(painting.c L1384 读
AtomFlashWndState)。
UserSetProp(L83-111):
c
NT_ASSERT(UserIsEnteredExclusive());
Prop = IntGetProp(Window, Atom, SystemProp);
if (Prop == NULL)
{
Prop = UserHeapAlloc(sizeof(PROPERTY)); // 桌面堆分配
if (Prop == NULL) return FALSE;
Prop->Atom = Atom;
Prop->fs = SystemProp ? PROPERTY_FLAG_SYSTEM : 0;
InsertTailList(&Window->PropListHead, &Prop->PropListEntry);
Window->PropListItems++;
}
Prop->Data = Data; // 已存在则只更新数据(覆盖语义)
return TRUE;
- 需要独占锁 (
UserEnterExclusive),因为可能改链表; - 属性项分配在桌面堆 (
UserHeapAlloc)------这正是"GetProp 免系统调用"的基础
(桌面堆映射到用户态,见第 5.5 节); - 已存在的同名属性是覆盖 :
Data直接替换,不重复分配------Windows 语义如此。
UserRemoveProp(L58-81):
c
Prop = IntGetProp(Window, Atom, SystemProp);
if (Prop == NULL) return NULL;
Data = Prop->Data;
RemoveEntryList(&Prop->PropListEntry);
UserHeapFree(Prop); // 只释放属性项,不释放 Data(数据归应用)
Window->PropListItems--;
return Data; // 返回旧数据,让应用自行善后
返回值是旧的 Data ------调用方(NtUserRemoveProp)把它原样返回给应用,
应用据此决定是否释放自己的资源。注意 UserHeapFree 释放的是 PROPERTY 项
本身,Data 的归属始终是应用。
5.3 NtUserSetProp / NtUserRemoveProp / NtUserBuildPropList
NtUserSetProp(L245-272):
c
Window = UserGetWindowObject(hWnd);
if (Window == NULL) { Ret = FALSE; goto Exit; }
Ret = UserSetProp(Window, Atom, Data, FALSE); // SystemProp = FALSE
独占锁,SystemProp 恒为 FALSE------应用永远只能操作用户属性,系统属性只由
win32k 内部例程(UserSetProp(..., TRUE))访问。句柄无效返回 FALSE 且
不设置上次错误(Win32 SetProp 失败一般也不设错误码)。
NtUserRemoveProp(L218-243) :同样独占锁,UserRemoveProp(Window, Atom, FALSE),
返回旧 Data(无属性返回 NULL)。
NtUserBuildPropList(L135-216):共享锁。两种模式:
Buffer != NULL:把窗口属性快照 成PROPLISTITEM{Atom, Data}数组拷给
调用者(MmCopyToCaller),跳过系统属性 (L176!(Property->fs & PROPERTY_FLAG_SYSTEM));校验BufferSize % sizeof(PROPLISTITEM) == 0;Buffer == NULL:返回PropListItems * sizeof(PROPLISTITEM)作为所需字节数
(注释 FIXME:这里把系统属性也算进去了,与拷贝路径不一致)。
它是 EnumProps 类 API 的内核支撑。
5.4 UserRemoveWindowProps 与窗口销毁(prop.c L113-131 + window.c L730)
c
VOID FASTCALL UserRemoveWindowProps(PWND Window)
{
while (!IsListEmpty(&Window->PropListHead))
{
ListEntry = Window->PropListHead.Flink;
Property = CONTAINING_RECORD(ListEntry, PROPERTY, PropListEntry);
RemoveEntryList(&Property->PropListEntry);
UserHeapFree(Property);
Window->PropListItems--;
}
}
窗口销毁路径 IntDestroyWindow(window.c L730-735)调用它:
c
if (Window->PropListItems)
{
UserRemoveWindowProps(Window);
ASSERT(Window->PropListItems==0);
}
窗口销毁时系统只负责清空属性项 (用户数据仍归应用,系统不触碰
Property->Data)------这是"属性不拥有数据"设计的最直接体现。
5.5 用户态 GetProp:免系统调用的桌面堆直读
user32 windows/prop.c 的 IntGetProp(L38-62)与内核 prop.c 的
IntGetProp 逻辑几乎相同,但直接走共享映射:
c
pWnd = ValidateHwnd(hWnd);
ListEntry = SharedPtrToUser(pWnd->PropListHead.Flink); // 内核指针→用户态地址
for (i = 0; i < pWnd->PropListItems; i++)
{
Property = CONTAINING_RECORD(ListEntry, PROPERTY, PropListEntry);
if (Property->Atom == Atom &&
(Property->fs & PROPERTY_FLAG_SYSTEM) == SystemFlag)
return Property;
ListEntry = SharedPtrToUser(ListEntry->Flink);
}
因为属性项在桌面堆 (映射到进程用户态),user32 无需系统调用即可遍历。
所以:
GetPropA/W(L347-391)= 原子解析(GlobalFindAtomW或直接取 LOWORD)+
IntGetProp直读------纯用户态;SetPropW(L476-488)=GlobalAddAtomW原子化 +NtUserSetProp系统调用
(写要进内核,因为改共享链表需加锁);RemovePropW(L427-440)=GlobalFindAtomW+NtUserRemoveProp。
"读免系统调用、写过系统调用"是桌面堆共享映射带来的典型折中------读路径的
热频率远高于写路径。
5.6 系统属性(SystemProp)与 AtomFlashWndState
系统属性(PROPERTY_FLAG_SYSTEM)是 win32k 内部给自己用的"窗口私有存储"。
当前最典型的使用者是 IntFlashWindowEx(painting.c L1376-1503),它把闪烁
状态机(FlashState 与剩余次数打包成 MAKELONG(LOWORD(FlashState), uCount))
存在系统属性 AtomFlashWndState 里:
- L1384:
FlashState = UserGetProp(pWnd, AtomFlashWndState, TRUE)恢复状态; - L1500:
UserSetProp(pWnd, AtomFlashWndState, (HANDLE)FlashState, TRUE)保存; - L1462:
FLASHW_STOP时UserRemoveProp(pWnd, AtomFlashWndState, TRUE)清理。
系统属性不受 NtUserBuildPropList 枚举影响、应用 API 无法触碰
(SystemProp 恒 FALSE),是隔离内部状态的好地方。
6. 加速键子系统:核心函数逐一展开
6.1 NtUserTranslateAccelerator(accelerator.c L365-446)
签名:int APIENTRY NtUserTranslateAccelerator(HWND hWnd, HACCEL hAccel, LPMSG pUnsafeMessage)。
流程:
UserEnterShared()(只读路径);hWnd == NULL→ 返回 0;- SEH 保护下
ProbeForRead(pUnsafeMessage, sizeof(MSG), 4)+
RtlCopyMemory把用户态 MSG 拷进内核栈(L388-398); - 消息类型过滤 (L400-406):只处理
WM_KEYDOWN / WM_SYSKEYDOWN / WM_SYSCHAR / WM_CHAR,其余直接返回 0------
TranslateAccelerator 对这四个消息之外的内容一律不碰; UserGetAccelObject(hAccel)(L21-39:UserGetObject(gHandleTable, hAccel, TYPE_ACCELTABLE),
无效句柄报ERROR_INVALID_ACCEL_HANDLE);UserRefObjectCo对 Accel 与 Window 各加一个"过程内引用"(防对象在翻译途中被
并发销毁),结束时UserDerefObjectCo对称释放;- 遍历表项(L426-437):
c
for (i = 0; i < Accel->Count; i++)
{
if (co_IntTranslateAccelerator(Window, &Message, &Accel->Table[i]))
{
Ret = 1;
break;
}
if (Accel->Table[i].fVirt & FVIRT_TBL_END) // 未文档化:表尾
break;
}
return Ret;
返回值语义:0 = 未翻译(消息照常派发),1 = 已翻译(消息已被消费,
DispatchMessage 不应再派发)。
6.2 co_IntTranslateAccelerator ------ 核心翻译逻辑(L42-163)
签名:BOOLEAN co_IntTranslateAccelerator(PWND Window, CONST MSG *pMsg, CONST ACCEL *pAccel)。
(1) 修饰键掩码计算(L62-65):
c
if (UserGetKeyState(VK_CONTROL) & 0x8000) Mask |= FCONTROL; // 0x08
if (UserGetKeyState(VK_MENU) & 0x8000) Mask |= FALT; // 0x10
if (UserGetKeyState(VK_SHIFT) & 0x8000) Mask |= FSHIFT; // 0x04
用当前按键状态 (而非消息 lParam 的位域)求掩码------因为 WM_KEYDOWN 的
lParam 不含可靠的修饰键信息,Ctrl/Alt/Shift 状态必须以实时按键状态为准。
(2) 按键匹配(L67-90):
c
if (pAccel->fVirt & FVIRTKEY) // 虚拟键加速键
{
if (pMsg->message == WM_KEYDOWN || pMsg->message == WM_SYSKEYDOWN)
{
if (pMsg->wParam == pAccel->key &&
Mask == (pAccel->fVirt & (FSHIFT | FCONTROL | FALT)))
bFound = TRUE;
}
}
else // 字符加速键
{
if (pMsg->message == WM_CHAR || pMsg->message == WM_SYSCHAR)
{
if (pMsg->wParam == pAccel->key &&
(Mask & FALT) == (pAccel->fVirt & FALT))
bFound = TRUE;
}
}
两套规则的差异:虚拟键加速键要求掩码完全相等 (Ctrl+C 的 FCONTROL 位必须
匹配且不得多按 Alt);字符加速键只比较 FALT 位 ------因为字符消息本身就是
键盘布局+Shift 作用的结果,Shift/Ctrl 状态已隐含在字符里,只有 Alt 需要显式
核对(Alt+字符会走 WM_SYSCHAR)。
(3) 菜单查找(L99-125):
c
hMenu = (Window->style & WS_CHILD) ? 0 : (HMENU)Window->IDMenu;
hSubMenu = NULL;
MenuObject = UserGetMenuObject(hMenu);
nPos = pAccel->cmd; // MF_BYPOSITION 语义:先按位置查
if (MenuObject)
{
if (MENU_FindItem(&MenuObject, &nPos, MF_BYPOSITION))
hSubMenu = UserHMGetHandle(MenuObject); // 命中 → 记录子菜单
else
hMenu = NULL;
}
if (!hMenu) // 窗口菜单没有该命令 → 查系统菜单
{
hMenu = Window->SystemMenu;
hSubMenu = hMenu; // 系统菜单本身是弹出菜单
MenuObject = UserGetMenuObject(hMenu);
nPos = pAccel->cmd;
if (MenuObject)
{
if (MENU_FindItem(&MenuObject, &nPos, MF_BYPOSITION))
hSubMenu = UserHMGetHandle(MenuObject);
else
hMenu = NULL;
}
}
关键点:
- 子窗口(
WS_CHILD)没有自己的菜单栏,直接跳过IDMenu查系统菜单; MENU_FindItem(menu.h L159:PITEM MENU_FindItem(PMENU *pmenu, UINT *nPos, UINT wFlags))是菜单子系统提供的"按命令 ID 反查位置"工具,同时把
nPos更新为项位置,供WM_INITMENUPOPUP的 lParam 使用;- 菜单存在性检查是加速键生效的硬前提 :命令不在窗口菜单也不在系统菜单里,
加速键静默不触发(hMenu == NULL时后续消息发送被跳过)。
(4) 菜单初始化消息(L127-138):若菜单命中、无鼠标捕获、窗口未禁用:
c
if (hMenu && !IntGetCaptureWindow())
{
co_IntSendMessage(hWnd, WM_INITMENU, (WPARAM)hMenu, 0L);
if (hSubMenu)
{
nPos = IntFindSubMenu(&hMenu, hSubMenu); // 找子菜单在主菜单中的位置
co_IntSendMessage(hWnd, WM_INITMENUPOPUP, (WPARAM)hSubMenu,
MAKELPARAM(nPos, TRUE));
}
}
IntFindSubMenu(menu.h L156)返回子菜单在主菜单中的索引位置,用于
WM_INITMENUPOPUP 的 lParam 低字(wParam 是弹出菜单句柄,高字 TRUE 表示系统菜单)。
(5) 发送命令(L140-159)。三个"禁止发送"条件 + 两条发送路径:
c
if (!(Window->style & WS_DISABLED) && // 窗口未禁用
!(hMenu && IntGetMenuState(hMenu, pAccel->cmd, MF_BYCOMMAND)
& (MF_DISABLED | MF_GRAYED)) && // 菜单项未禁用/置灰
!(hMenu && hMenu == (HMENU)Window->IDMenu &&
(Window->style & WS_MINIMIZED))) // 不是"最小化窗口的菜单"
{
if (hMenu && hMenu == Window->SystemMenu)
{
co_IntSendMessage(hWnd, WM_SYSCOMMAND, pAccel->cmd, 0x00010000L);
}
else
{
co_IntSendMessage(hWnd, WM_COMMAND, 0x10000 | pAccel->cmd, 0L);
}
}
- 系统菜单项 → WM_SYSCOMMAND :wParam 是命令 ID,lParam 固定
0x00010000L
(表示"由加速键/键盘触发"的魔数,与菜单鼠标点击的 0 区分); - 普通菜单项 → WM_COMMAND :wParam 是
0x10000 | cmd------高字位 16 置位表示
"来自加速键"(WIN32 约定:HIWORD(wParam) == 1表示加速键触发),低字是命令
ID;应用用LOWORD(wParam)取命令、用HIWORD(wParam)判断来源; MF_BYCOMMAND语义的菜单状态检查用IntGetMenuState(menu.c 的菜单状态
查询),保证与菜单栏显示状态一致。
6.3 NtUserCreateAcceleratorTable(L223-321)
HACCEL NtUserCreateAcceleratorTable(LPACCEL Entries, ULONG EntriesCount):
UserEnterExclusive(),参数校验:Entries == NULL || EntriesCount == 0 || EntriesCount > MAXULONG_PTR / sizeof(*Entries)→ 返回 NULL;UserCreateObject(gHandleTable, pti->rpdesk, pti, &hAccel, TYPE_ACCELTABLE, sizeof(ACCELERATOR_TABLE))------加速键表是标准 USER 对象(TYPE_ACCELTABLE=8),
归属当前线程及其桌面;Accel->Count = EntriesCount;Accel->Table = ExAllocatePoolWithTag(PagedPool, EntriesCount * sizeof(ACCEL), USERTAG_ACCEL)(分页池数组);- SEH 保护下
ProbeForRead+ 逐项拷贝(L275-292):
c
Accel->Table[Index].fVirt = Entries[Index].fVirt & FVIRT_MASK; // 截断到 0x7F
if (Accel->Table[Index].fVirt & FVIRTKEY)
Accel->Table[Index].key = Entries[Index].key; // 虚拟键:原样
else
RtlMultiByteToUnicodeN(&Accel->Table[Index].key, sizeof(WCHAR), NULL,
(PCSTR)&Entries[Index].key, sizeof(CHAR)); // 字符:ANSI→WCHAR
Accel->Table[Index].cmd = Entries[Index].cmd;
注意:非 FVIRTKEY 的表项,key 是单字节字符,内核对它做 RtlMultiByteToUnicodeN
转成 WCHAR------所以 key 字段在表内统一按 WCHAR 存储,与第 6.2 节
pMsg->wParam == pAccel->key 的比较一致(WM_CHAR 的 wParam 是 WCHAR);
- 失败路径:
ExFreePoolWithTag(Accel->Table)+UserDereferenceObject+
UserDeleteObject 完整回滚;成功路径 L312 释放 UserCreateObject 多加的
一个引用(注释"UserCreateObject added 2 references")。
6.4 Copy / Destroy / UserGetAccelObject(L169-221, L323-363)
NtUserCopyAcceleratorTable(L169-221) :共享锁。Entries == NULL 时返回
Accel->Count(查询表大小);否则 ProbeForWrite 后循环拷贝 fVirt/key/cmd
三项,EntriesCount 超出表长时截断为 Accel->Count。
UserDestroyAccelTable(L323-336) :ExFreePoolWithTag(Accel->Table) +
UserDeleteObject(UserHMGetHandle(Accel), TYPE_ACCELTABLE)。它作为
PROCMARKHEAD 的析构回调注册(对象删除时自动调用)。
NtUserDestroyAcceleratorTable(L338-363) :独占锁,UserGetAccelObject +
UserDestroyAccelTable。文件头注释(L346-349)留有 FIXME:LoadAcceleratorTable
加载的只读表应递减使用计数而非真销毁。
UserGetAccelObject(L21-39) :hAccel == NULL 或
UserGetObject(gHandleTable, hAccel, TYPE_ACCELTABLE) 失败都设
ERROR_INVALID_ACCEL_HANDLE 并返回 NULL------句柄校验的统一入口。
6.5 加速键翻译完整流程(文字图)
消息循环: while(GetMessage(&msg)) {
TranslateMessage(&msg);
if (TranslateAccelerator(hwnd, hAccel, &msg)) continue; // 已翻译→不派发
DispatchMessage(&msg);
}
│ msg.message ∈ {WM_KEYDOWN, WM_SYSKEYDOWN, WM_CHAR, WM_SYSCHAR}
▼
NtUserTranslateAccelerator (accelerator.c L365)
├─ 消息类型过滤(L400)+ 句柄/引用保护
├─ for 每个 ACCEL 表项:
│ └─ co_IntTranslateAccelerator (L42)
│ ├─ Mask = f(实时按键状态) [FCONTROL|FALT|FSHIFT]
│ ├─ 匹配: FVIRTKEY→(消息类型+虚拟键+掩码全等)
│ │ 非FVIRTKEY→(WM_(SYS)CHAR+FALT位)
│ ├─ 菜单查找: IDMenu → SystemMenu → MENU_FindItem(MF_BYPOSITION)
│ ├─ WM_INITMENU / WM_INITMENUPOPUP(无捕获时)
│ ├─ 禁用检查: WS_DISABLED / MF_DISABLED|MF_GRAYED / 最小化
│ └─ 命中系统菜单 → WM_SYSCOMMAND(cmd, 0x00010000)
│ 否则 → WM_COMMAND(0x10000|cmd, 0)
└─ 返回 1 → 消息循环 continue(消息被消费)
7. 热键子系统:核心函数逐一展开
7.1 NtUserRegisterHotKey / UserRegisterHotKey(hotkey.c L446-496, L534-581)
NtUserRegisterHotKey(L534-581)------系统调用入口:
c
if (fsModifiers & ~(MOD_ALT | MOD_CONTROL | MOD_SHIFT | MOD_WIN))
{
EngSetLastError(ERROR_INVALID_FLAGS); // 非法修饰键组合
return FALSE;
}
UserEnterExclusive();
if (hWnd == NULL)
pWnd = NULL;
else
{
pWnd = UserGetWindowObject(hWnd);
if (!pWnd) goto cleanup;
if (pWnd->head.pti != gptiCurrent) // FIXME?? wine test_hotkey
{
EngSetLastError(ERROR_WINDOW_OF_OTHER_THREAD);
goto cleanup;
}
}
bRet = UserRegisterHotKey(pWnd, id, fsModifiers, vk);
要点:
- 修饰键白名单校验在锁外 提前做(节省锁开销),
MOD_NOREPEAT(0x4000)
未被接受------注释 FIXME 问"Win2k3 是否支持 MOD_NOREPEAT?",当前按不支持处理; hWnd == NULL表示线程热键(WM_HOTKEY 投给注册线程本身);- 窗口必须属于当前线程 (
pWnd->head.pti != gptiCurrent→
ERROR_WINDOW_OF_OTHER_THREAD),否则拒绝------热键注册不允许跨线程用别人的
窗口,注释说这是"修复 wine 的 test_hotkey 窗口跨线程消息"。
UserRegisterHotKey(L446-496)------内部实现:
c
if (pWnd == NULL || pWnd == PWND_BOTTOM)
pHotKeyThread = PsGetCurrentThreadWin32Thread(); // 当前线程
else
pHotKeyThread = pWnd->head.pti; // 窗口线程
if (vk == VK_PACKET) return FALSE; // 非真实键盘输入
if (IsHotKey(fsModifiers, vk)) // 冲突检测
{
EngSetLastError(ERROR_HOTKEY_ALREADY_REGISTERED);
return FALSE;
}
pHotKey = ExAllocatePoolWithTag(PagedPool, sizeof(HOT_KEY), USERTAG_HOTKEY);
if (!pHotKey) { EngSetLastError(ERROR_NOT_ENOUGH_MEMORY); return FALSE; }
pHotKey->pti = pHotKeyThread;
pHotKey->pWnd = pWnd;
pHotKey->fsModifiers = fsModifiers;
pHotKey->vk = vk;
pHotKey->id = id;
pHotKey->pNext = gphkFirst; // 头插法进全局链表
gphkFirst = pHotKey;
return TRUE;
PWND_BOTTOM 是一个特殊伪窗口值("焦点窗口"占位符):pWnd == PWND_BOTTOM 的
热键在触发时把消息发给当前焦点窗口 (co_UserProcessHotKeys L291-297)。
热键对象分配在分页池 (不是 USER 对象!)------热键表是纯内核链表,没有句柄、
不经过 USER 对象系统,由 Unregister* 与进程/线程清理显式释放。
7.2 NtUserUnregisterHotKey / UserUnregisterHotKey(L498-529, L583-602)
NtUserUnregisterHotKey(L583-602) :独占锁;hWnd 无效(
hWnd && !(pWnd = UserGetWindowObject(hWnd)))直接失败;转发
UserUnregisterHotKey(pWnd, id)。注意:hWnd 可以为 NULL(注销线程热键)。
UserUnregisterHotKey(L498-529) :遍历 gphkFirst,删除
pWnd == pWnd && id == id 的节点:
c
while (pHotKey)
{
phkNext = pHotKey->pNext;
if (pHotKey->pWnd == pWnd && pHotKey->id == id)
{
*pLink = phkNext;
ExFreePoolWithTag(pHotKey, USERTAG_HOTKEY);
bRet = TRUE;
}
else
pLink = &pHotKey->pNext;
pHotKey = phkNext;
}
pLink 指针链技巧:删除时通过"上一个节点的 pNext 指针"重连,避免双指针遍历
开销;多个匹配项(理论上同窗口同 ID 只会有一个)会被全部删除。
7.3 IsHotKey / IntGetModifiers / IntSwapModHKF(L66-97, L164-183)
IsHotKey(L164-183) :线性遍历 gphkFirst,fsModifiers == fsModifiers && vk == wVk 即命中,返回第一个匹配项。全局热键冲突检测就靠它 (组合键在
全系统只允许一个注册者)。
IntGetModifiers(L66-85) :从 gafAsyncKeyState(全局异步键状态表,
2 位/键,input.h L97)读出当前按下的修饰键,映射成 MOD_* 位:
c
if (IS_KEY_DOWN(pKeyState, VK_SHIFT)) fModifiers |= MOD_SHIFT; // 0x0001
if (IS_KEY_DOWN(pKeyState, VK_CONTROL)) fModifiers |= MOD_CONTROL; // 0x0002
if (IS_KEY_DOWN(pKeyState, VK_MENU)) fModifiers |= MOD_ALT; // 0x0004
if (IS_KEY_DOWN(pKeyState, VK_LWIN) || IS_KEY_DOWN(pKeyState, VK_RWIN))
fModifiers |= MOD_WIN; // 0x0008
IntSwapModHKF(L92-97) :(Input & 2) | ((Input & 1) << 2) | ((Input >> 2) & 1)------
交换第 0/2 位。因为 MOD_SHIFT=1, MOD_CONTROL=2, MOD_ALT=4,而
HOTKEYF_SHIFT=1, HOTKEYF_CONTROL=2, HOTKEYF_ALT=4 的位序不同
(MOD_ 的 SHIFT/ALT 与 HOTKEYF_ 相反),转换用于 WM_SETHOTKEY 的
wParam 编解码(见 7.6)。
7.4 co_UserProcessHotKeys ------ 按键匹配与 WM_HOTKEY 投递(L190-329)
在键盘输入管线中被调用(keyboard.c L1161):ProcessKeyEvent 里
if (co_UserProcessHotKeys(wSimpleVk, bIsDown)) { bPostMsg = FALSE; }------
命中热键的按键不再进入普通键盘消息流 。wSimpleVk 是去掉 L/R 前缀的
简化虚拟键(keyboard.c L1120 IntSimplifyVk)。
(1)修饰键判定(L199-204) :VK_SHIFT/VK_CONTROL/VK_MENU/VK_LWIN/VK_RWIN
记为 IsModifier = TRUE。
(2)按下/抬起两条分支(L208-237):
c
fModifiers = IntGetModifiers(gafAsyncKeyState);
if (bIsDown)
{
if (IsModifier)
{
// 修饰键按下:不触发热键,但记住"仅修饰键候选"
gfsModOnlyCandidate = fModifiers;
return FALSE;
}
else
{
pHotKey = IsHotKey(fModifiers, wVk); // 常规按键:查表
gfsModOnlyCandidate = 0;
}
}
else // 键抬起
{
if (IsModifier)
{
// 修饰键抬起:此时才触发"仅修饰键"热键(如单独的 Win 键)
pHotKey = IsHotKey(gfsModOnlyCandidate, 0);
gfsModOnlyCandidate = 0;
}
else
{
gfsModOnlyCandidate = 0;
return FALSE; // 普通键抬起不查热键
}
}
这里实现了 Windows 的经典语义:"仅修饰键"热键(vk == 0)在修饰键抬起时
触发 (比如 Win 键单独按下:按下时只记 gfsModOnlyCandidate = MOD_WIN,
松开 Win 时 IsHotKey(MOD_WIN, 0) 命中)。
(3)特殊热键分支(L239-279):
c
if (pHotKey->id == IDHK_SHIFTF12 || pHotKey->id == IDHK_F12)
{
// 调试热键:当前仅打印 ERR(co_ActivateDebugger 被注释)
return DoNotPostMsg;
}
if (pHotKey->id == IDHK_WINKEY) // 单独 Win 键 → 任务列表
{
pWnd = ValidateHwndNoErr(InputWindowStation->ShellWindow);
if (pWnd)
{
UserPostMessage(UserHMGetHandle(pWnd), WM_SYSCOMMAND, SC_TASKLIST, 0);
co_IntShellHookNotify(HSHELL_TASKMAN, 0, 0);
return FALSE;
}
}
if (pHotKey->id == IDHK_SNAP_LEFT || ... || IDHK_SNAP_DOWN) // Win+方向键 → 窗口吸附
{
HWND topWnd = UserGetForegroundWindow();
if (topWnd) UserPostMessage(topWnd, WM_KEYDOWN, wVk, 0);
return TRUE; // 吞掉按键
}
- F12 调试热键:
co_ActivateDebugger()被注释(FIXME),只留 ERR 日志; IDHK_WINKEY由 input.c L216 在键盘设备初始化时注册(MOD_WIN, vk=0);- 窗口吸附热键(input.c L218-221 注册
MOD_WIN + VK_LEFT/RIGHT/UP/DOWN)把
按键转发给前台窗口(WM_KEYDOWN),由应用层实现吸附逻辑------win32k 只负责
拦截与转发。
(4)普通热键投递(L281-326):
c
if (!pHotKey->pWnd) // 线程热键
{
UserPostThreadMessage(pHotKey->pti, WM_HOTKEY,
pHotKey->id, MAKELONG(fModifiers, wVk));
return TRUE;
}
else
{
pWnd = pHotKey->pWnd;
if (pWnd == PWND_BOTTOM) // 焦点窗口热键
{
if (gpqForeground == NULL) return FALSE;
pWnd = gpqForeground->spwndFocus;
}
if (pWnd)
{
if (pWnd == ShellWindow && pHotKey->id == SC_TASKLIST)
{
UserPostMessage(UserHMGetHandle(pWnd), WM_SYSCOMMAND, SC_TASKLIST, 0);
co_IntShellHookNotify(HSHELL_TASKMAN, 0, 0);
}
else if (IsWindowHotKey(pHotKey)) // WM_SETHOTKEY 注册的热键
{
if (bIsDown)
{
if (gpqForeground && gpqForeground->spwndActive)
pWnd = gpqForeground->spwndActive;
UserPostMessage(UserHMGetHandle(pWnd), WM_SYSCOMMAND,
SC_HOTKEY, (LPARAM)UserHMGetHandle(pHotKey->pWnd));
}
}
else // 普通窗口热键
{
UserPostMessage(UserHMGetHandle(pWnd), WM_HOTKEY,
pHotKey->id, MAKELONG(fModifiers, wVk));
}
return TRUE;
}
}
return FALSE;
三种投递目标与消息:
| 注册方式 | pWnd | 投递 | 消息 |
|---|---|---|---|
RegisterHotKey(NULL, ...) |
NULL | 线程队列(UserPostThreadMessage) | WM_HOTKEY(id, MAKELONG(mods, vk)) |
RegisterHotKey(hwnd, ...) |
窗口 | 窗口(UserPostMessage) | WM_HOTKEY(id, MAKELONG(mods, vk)) |
RegisterHotKey(HWND_BOTTOM, ...) |
PWND_BOTTOM | 前台队列的焦点窗口 | WM_HOTKEY |
WM_SETHOTKEY(DefWndSetHotKey) |
窗口(IDHK_WNDKEY) | 前台活动窗口(按下时) | WM_SYSCOMMAND(SC_HOTKEY, 窗口句柄) |
WM_HOTKEY 的 wParam = 热键 ID,lParam = MAKELONG(fModifiers, wVk)(低字
修饰键、高字虚拟键)------与 Win32 文档一致。返回 TRUE 表示"按键已被消费",
调用方 ProcessKeyEvent 据此跳过普通键盘消息(bPostMsg = FALSE)。
7.5 特殊热键注册时机(input.c L185-224)
RIT 初始化流程(RawInputThreadMain)在键盘设备成功打开后注册系统热键:
c
UserInitKeyboard(ghKeyboardDevice);
UserEnterExclusive();
UserRegisterHotKey(PWND_BOTTOM, IDHK_WINKEY, MOD_WIN, 0); // 单独 Win 键
UserRegisterHotKey(PWND_BOTTOM, IDHK_SNAP_LEFT, MOD_WIN, VK_LEFT); // Win+←
UserRegisterHotKey(PWND_BOTTOM, IDHK_SNAP_RIGHT, MOD_WIN, VK_RIGHT); // Win+→
UserRegisterHotKey(PWND_BOTTOM, IDHK_SNAP_UP, MOD_WIN, VK_UP); // Win+↑
UserRegisterHotKey(PWND_BOTTOM, IDHK_SNAP_DOWN, MOD_WIN, VK_DOWN); // Win+↓
SetDebugHotKeys(); // F12 / Shift+F12(82 键键盘用 VK_SUBTRACT)
UserLeave();
SetDebugHotKeys(hotkey.c L45-58):ENHANCED_KEYBOARD(...) 决定用
VK_F12 还是 VK_SUBTRACT(82 键键盘),先注销旧的 F12/ShiftF12 再注册
新的(调试热键可被用户态 UserDebuggerHotKey 系统参数覆盖)。因为
PWND_BOTTOM + 负 ID,这些系统热键与应用热键理论上不会冲突(应用几乎不会
注册负 ID,且 NtUserRegisterHotKey 允许负 id 但 IsHotKey 冲突检查是
"修饰键+虚拟键"级,与应用同键会撞------所以 F12/Win 这类键被系统占用后应用
注册会得到 ERROR_HOTKEY_ALREADY_REGISTERED)。
7.6 WM_SETHOTKEY:DefWndGetHotKey / DefWndSetHotKey(L335-443)
WM_SETHOTKEY(0x32)是"窗口级热键"老机制:把一个 热键关联给窗口,
按下时系统给前台活动窗口 发 WM_SYSCOMMAND(SC_HOTKEY)。它由
DefWindowProc 处理(defwnd.c 转发到 hotkey.c 这两个函数)。
DefWndSetHotKey(L360-443):
c
if (pWnd->style & WS_CHILD) return 0; // 子窗口不允许
if (vk == VK_ESCAPE || vk == VK_SPACE || vk == VK_TAB || vk == VK_PACKET)
return -1; // 非法虚拟键
// wParam: HIBYTE=修饰键(HOTKEYF_ 编码), LOBYTE=虚拟键
const UINT fsModifiers = IntSwapModHKF(HIBYTE(wParam));
const UINT vk = LOBYTE(wParam);
冲突检查:若同键的窗口热键已属于别的窗口 ,返回 2("另一窗口已有该热键");
已存在则更新("一个窗口只能有一个热键");wParam == 0 表示清除。新节点
id = IDHK_WNDKEY、pti = NULL(满足 IsWindowHotKey 判定),同样头插
gphkFirst。
DefWndGetHotKey(L335-355) :遍历找 pWnd == pWnd && IsWindowHotKey,
返回 MAKEWORD(vk, IntSwapModHKF(fsModifiers))------把 MOD_ 位转回 HOTKEYF_
编码放进 HIBYTE,与 Set 方向互逆。
7.7 UnregisterWindowHotKeys / UnregisterThreadHotKeys(L104-157)
窗口销毁与线程退出时的清理钩子:
UnregisterWindowHotKeys(PWND)(L104):删除pHotKey->pWnd == pWnd的所有
热键(WM_SETHOTKEY 路径)------窗口没了,热键必须跟着消失;UnregisterThreadHotKeys(PTHREADINFO)(L134):删除pHotKey->pti == pti
的所有热键(线程热键 + 该线程注册的窗口热键)。
两者结构相同(pLink 重连 + ExFreePoolWithTag),由 IntDestroyWindow/
线程清理路径调用,防止热键悬挂引用已销毁的窗口/线程。
7.8 全局热键注册与冲突(汇总)
- 表结构 :单链表
gphkFirst,头插,无锁保护(所有读写都在
UserEnterExclusive/UserEnterShared或 RIT 独占上下文内); - 冲突检测 :
UserRegisterHotKey用IsHotKey(fsModifiers, vk)------
组合键全局唯一 ,重复注册返回ERROR_HOTKEY_ALREADY_REGISTERED; - 无句柄 :HOT_KEY 不是 USER 对象,注册/注销全凭指针匹配
(pWnd + id),线程/窗口清理时按pWnd/pti批量扫除; - 跨窗口站限制 :文件头注释(L9-14)指出------热键通知由键盘输入触发,
而只有 WinSta0 上的桌面能收到输入,因此允许非 WinSta0 桌面注册热键
"似乎很不对"(Gunnar 的 FIXME),当前代码未实施窗口站过滤。
8. 计时器合并机制
8.1 为什么需要合并
Windows 计时器模型允许任意数量窗口计时器共存,但硬件定时器资源是稀缺的。
合并策略(一个 MasterTimer 驱动全部)带来:
- O(1) 内核定时器:无论注册多少窗口计时器,win32k 只维护一个 KTIMER;
- 统一的 10ms 心跳 :所有计时器共享同一时钟源(
EngGetTickCount32),
心跳周期约 9.7656ms(-97656 × 100ns),与USER_TIMER_MINIMUM(10ms)
吻合; - 到期/投递分离 :RIT 上只做"计数到期 + 置位 + 唤醒",投递推迟到应用线程
自己的消息循环------避免 RIT 与多线程消息队列的同步开销,也让
WM_TIMER 天然遵循消息队列的优先级规则(QS_POSTMESSAGE 类)。
8.2 最小间隔 USER_TIMER_MINIMUM
IntSetTimer 把小于 10ms 的 Elapse 一律提升到 10ms(L211-215)------因为
心跳周期本身就是 10ms,更小的间隔没有意义(会导致每次心跳都"超期")。
这是"计时器合并"的精度代价:窗口计时器精度被钳制在心跳粒度。
8.3 同窗口合并
SetTimer(hwnd, id, ...) 二次调用同 id 时,FindTimer 命中已有 TIMER,
只更新 cmsCountdown/cmsRate(L267-271),不新建对象。效果:
- 重置倒计时(经典语义:"同 ID 的 SetTimer 重新计时");
- 不会产生重复 WM_TIMER(链表里始终只有一个该 (window,id) 计时器)。
8.4 窗口无关计时器 ID 分配
SetTimer(NULL, ...) 需要系统分配 ID:位图 WindowLessTimersBitMap(32512
位)管理 [256, 32767] 的 ID 空间,RtlFindClearBitsAndSet 从 HintIndex
轮转分配、RemoveTimer 归还。分配失败(位图耗尽)报
ERROR_NO_SYSTEM_RESOURCES------与 Windows 2k 的 32768 窗口无关计时器上限对齐
(timer.c L19-22 注释)。
8.5 心跳周期与误差
DueTime = -97656(100ns 单位)≈ 9.7656ms;注释"1024hz .9765625 ms"指
该值对应 1024Hz 的时钟粒度;ProcessTimers每次末尾KeSetTimer(MasterTimer, DueTime, NULL)重启心跳
(L519),是相对到期(负值)------从当前时刻再过 ~10ms;- 误差来源:RIT 的
KeWaitForMultipleObjects唤醒延迟、UserEnterExclusive
等待、链表遍历耗时;但每个计时器自己的倒计时 用
cmsCountdown -= Time - TimeLast(实际流逝量)校正,长期不会漂移累积。
9. 系统计时器用途清单
| 计时器 ID | 值 | 注册者(调用点) | 回调 | 触发消息/动作 |
|---|---|---|---|---|
ID_EVENT_SYSTIMER_MOUSEHOVER |
0xFFFA(=ID_TME_TIMER) | TrackMouseEvent(mouse.c L466;msgqueue.c L1473) | SystemTimerProc | WM_MOUSEHOVER / WM_NCMOUSEHOVER;命中后自毁(timer.c L346-348),TME_CANCEL 时 mouse.c L447 取消 |
ID_EVENT_SYSTIMER_FLASHWIN |
0xFFF8 | FlashWindowEx(painting.c L1481) | SystemTimerProc → IntFlashWindowEx | 每周期切换 WM_NCACTIVATE 激活态;FLASHW_STOP 时 painting.c L1459 自杀 |
ID_EVENT_SYSTIMER_TRACKWIN |
0xFFF7 | (未使用) | --- | 预留:窗口跟踪 |
ID_EVENT_SYSTIMER_ANIMATEDFADE |
0xFFF6 | (未使用) | --- | 预留:动画淡入淡出 |
ID_EVENT_SYSTIMER_INVALIDATEDCES |
0xFFF5 | (未使用) | --- | 预留:DCE 失效 |
IDCARETTIMER |
(caret.c 定义) | ShowCaret/HideCaret 路径(caret.c L215/L292/L364) | CaretSystemTimerProc | 翻转 CaretInfo.Showing + co_IntDrawCaret 重绘插入符;隐藏时 L249/L337 取消 |
| (位图分配) | 256, 32767 | StartTheTimers(timer.c L378):IntSetTimer(NULL, 0, 1000, HungAppSysTimerProc, TMRF_RIT) |
HungAppSysTimerProc | 每秒检查空闲超时 → DoTheScreenSaver(WM_LOGONNOTIFY 或置屏保标志) |
说明:
TMRF_SYSTEM计时器统一走SystemTimerProc或专用回调(Caret 用
CaretSystemTimerProc),消息类型为WM_SYSTIMER(0x0118,
undocuser.h L50);SystemTimerProc的 switch 目前只处理 MOUSEHOVER 与 FLASHWIN 两个 case,
TRACKWIN/ANIMATEDFADE/INVALIDATEDCES 仅定义了 ID,未实现(timer.h 中的
常量为未来预留);HungAppSysTimerProc是TMRF_RIT(非 TMRF_SYSTEM)计时器,在 RIT 内
同步执行(timer.c L494),不产生任何消息。
10. 加速键翻译流程(详细)
10.1 输入源与消息类型
TranslateAccelerator 只接受四类消息(accelerator.c L400-406):
WM_KEYDOWN(普通键)、WM_SYSKEYDOWN(Alt 组合的系统键)、WM_CHAR
(字符输入)、WM_SYSCHAR(Alt+字符)。键盘消息首先经过 ProcessKeyEvent
(keyboard.c)生成、TranslateMessage 再把 WM_KEYDOWN 翻译出 WM_CHAR,
最后消息循环调 TranslateAccelerator------所以虚拟键加速键查 KEYDOWN、
字符加速键查 CHAR 的输入都已就绪。
10.2 掩码计算与匹配
- 掩码 = 实时按键状态(
UserGetKeyState的 0x8000 高位),
FCONTROL=0x08 / FALT=0x10 / FSHIFT=0x04; - 虚拟键表项:
pMsg->wParam == pAccel->key且Mask与
fVirt & (FSHIFT|FCONTROL|FALT)完全相等; - 字符表项:
wParam == key且(Mask & FALT) == (fVirt & FALT)(只核对 Alt); - 匹配成功即"翻译命中",后续的菜单检查只决定发不发消息、发什么消息。
10.3 菜单查找
- 非 WS_CHILD 窗口:查
Window->IDMenu(窗口菜单栏);WS_CHILD 窗口跳过; MENU_FindItem(&MenuObject, &nPos, MF_BYPOSITION):菜单子系统按命令 ID
反查项位置,命中则hSubMenu指向该弹出菜单;- 窗口菜单未命中 → 查
Window->SystemMenu(系统菜单,本身是弹出菜单); - 两处都未命中 →
hMenu = NULL→ 不发任何消息(加速键静默无效)。
10.4 消息生成
| 条件 | 消息 | wParam | lParam |
|---|---|---|---|
| 菜单命中且无捕获 | WM_INITMENU(先发) | hMenu | 0 |
| 菜单命中且无捕获 | WM_INITMENUPOPUP | hSubMenu | MAKELPARAM(nPos, TRUE) |
| 系统菜单项 | WM_SYSCOMMAND | cmd | 0x00010000L |
| 普通菜单项 | WM_COMMAND | 0x10000 | cmd | 0 |
禁止条件:窗口 WS_DISABLED、菜单项 MF_DISABLED|MF_GRAYED(
IntGetMenuState 按 MF_BYCOMMAND 查询)、窗口菜单且窗口最小化。
10.5 与菜单子系统的接口
MENU_FindItem(menu.h L159):命令 ID → 项位置(MF_BYPOSITION 语义),
加速键的"菜单验证"全靠它;IntFindSubMenu(menu.h L156):子菜单在主菜单中的索引,供
WM_INITMENUPOPUP 定位;IntGetMenuState:菜单项启用状态查询;UserGetMenuObject:句柄 → PMENU 对象(USER 对象系统);co_IntSendMessage:内核侧直接发送消息(不经用户态),保证翻译消息
与窗口过程在同一上下文中同步完成。
11. 全局热键注册与冲突
11.1 MOD_* 修饰键
MOD_ALT=0x0001, MOD_CONTROL=0x0002, MOD_SHIFT=0x0004, MOD_WIN=0x0008
(winuser.h,MOD_WIN L2686)。NtUserRegisterHotKey 校验
fsModifiers & ~(MOD_ALT|MOD_CONTROL|MOD_SHIFT|MOD_WIN) 为 0,否则
ERROR_INVALID_FLAGS;MOD_NOREPEAT(0x4000,Vista 才支持)被拒绝。
IntGetModifiers 从 gafAsyncKeyState 实时生成 MOD_ 位;IntSwapModHKF
处理 MOD_ 与 HOTKEYF_ 的 SHIFT/ALT 位序差异。
11.2 冲突检测 ERROR_HOTKEY_ALREADY_REGISTERED
UserRegisterHotKey 先 IsHotKey(fsModifiers, vk) 全局查重------组合键
(修饰键+虚拟键)全局唯一 。命中即拒绝并设
ERROR_HOTKEY_ALREADY_REGISTERED。系统热键(Win 键、Win+方向键、F12)在
RIT 初始化时已占位,应用再注册同组合键会失败------这是 Windows 的预期行为。
11.3 修饰键单独热键(gfsModOnlyCandidate)
Windows 允许注册"仅修饰键"热键(RegisterHotKey(hwnd, id, MOD_WIN, 0))。
由于修饰键按下时无法区分"热键"与"普通修饰",实现用状态变量:
- 修饰键按下 →
gfsModOnlyCandidate = fModifiers(记住候选),不查表; - 修饰键抬起 →
IsHotKey(gfsModOnlyCandidate, 0)(用记住的修饰键状态查
vk==0 的热键),随后清空候选; - 普通键按下/抬起都会重置候选。
这样 IDHK_WINKEY(单独的 Win 键)在"按下 Win → 松开 Win"时触发任务列表
(WM_SYSCOMMAND SC_TASKLIST),而 Win+E 这类组合键(普通键按下时命中)在
Win 按下时就触发。
11.4 线程/窗口清理
UnregisterThreadHotKeys(pti):线程退出时清除其全部热键;UnregisterWindowHotKeys(pWnd):窗口销毁时清除挂该窗口的热键
(WM_SETHOTKEY 路径);- 普通
RegisterHotKey的窗口热键同时记录pti,两条清理路径都可覆盖。
11.5 与输入管线的集成
热键检测在 ProcessKeyEvent(keyboard.c L1106)中的位置:
ProcessKeyEvent(wVk, ...) // keyboard.c L1106
├─ IntSimplifyVk / IntHandleAltNumpad // L1120/L1135
├─ UpdateAsyncKeyState // L1143(更新 gafAsyncKeyState)
├─ co_CallLowLevelKeyboardHook // L1154(WH_KEYBOARD_LL)
├─ co_UserProcessHotKeys(wSimpleVk, bIsDown) // L1161 ← 热键拦截
│ └─ 命中 → return TRUE → bPostMsg = FALSE(按键不再入队)
├─ IntGetFocusMessageQueue → 键盘消息入队 // L1172+
└─ 键盘钩子/焦点队列处理
在低层键盘钩子之后、键盘消息入队之前 ,保证热键优先于普通输入、又不绕过
WH_KEYBOARD_LL 安全钩子。
12. 调用链与关系
12.1 mermaid 调用链
#mermaid-svg-V6aD7ZNDAwLKOBH7{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-V6aD7ZNDAwLKOBH7 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-V6aD7ZNDAwLKOBH7 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-V6aD7ZNDAwLKOBH7 .error-icon{fill:#552222;}#mermaid-svg-V6aD7ZNDAwLKOBH7 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-V6aD7ZNDAwLKOBH7 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-V6aD7ZNDAwLKOBH7 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-V6aD7ZNDAwLKOBH7 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-V6aD7ZNDAwLKOBH7 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-V6aD7ZNDAwLKOBH7 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-V6aD7ZNDAwLKOBH7 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-V6aD7ZNDAwLKOBH7 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-V6aD7ZNDAwLKOBH7 .marker.cross{stroke:#333333;}#mermaid-svg-V6aD7ZNDAwLKOBH7 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-V6aD7ZNDAwLKOBH7 p{margin:0;}#mermaid-svg-V6aD7ZNDAwLKOBH7 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-V6aD7ZNDAwLKOBH7 .cluster-label text{fill:#333;}#mermaid-svg-V6aD7ZNDAwLKOBH7 .cluster-label span{color:#333;}#mermaid-svg-V6aD7ZNDAwLKOBH7 .cluster-label span p{background-color:transparent;}#mermaid-svg-V6aD7ZNDAwLKOBH7 .label text,#mermaid-svg-V6aD7ZNDAwLKOBH7 span{fill:#333;color:#333;}#mermaid-svg-V6aD7ZNDAwLKOBH7 .node rect,#mermaid-svg-V6aD7ZNDAwLKOBH7 .node circle,#mermaid-svg-V6aD7ZNDAwLKOBH7 .node ellipse,#mermaid-svg-V6aD7ZNDAwLKOBH7 .node polygon,#mermaid-svg-V6aD7ZNDAwLKOBH7 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-V6aD7ZNDAwLKOBH7 .rough-node .label text,#mermaid-svg-V6aD7ZNDAwLKOBH7 .node .label text,#mermaid-svg-V6aD7ZNDAwLKOBH7 .image-shape .label,#mermaid-svg-V6aD7ZNDAwLKOBH7 .icon-shape .label{text-anchor:middle;}#mermaid-svg-V6aD7ZNDAwLKOBH7 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-V6aD7ZNDAwLKOBH7 .rough-node .label,#mermaid-svg-V6aD7ZNDAwLKOBH7 .node .label,#mermaid-svg-V6aD7ZNDAwLKOBH7 .image-shape .label,#mermaid-svg-V6aD7ZNDAwLKOBH7 .icon-shape .label{text-align:center;}#mermaid-svg-V6aD7ZNDAwLKOBH7 .node.clickable{cursor:pointer;}#mermaid-svg-V6aD7ZNDAwLKOBH7 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-V6aD7ZNDAwLKOBH7 .arrowheadPath{fill:#333333;}#mermaid-svg-V6aD7ZNDAwLKOBH7 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-V6aD7ZNDAwLKOBH7 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-V6aD7ZNDAwLKOBH7 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-V6aD7ZNDAwLKOBH7 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-V6aD7ZNDAwLKOBH7 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-V6aD7ZNDAwLKOBH7 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-V6aD7ZNDAwLKOBH7 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-V6aD7ZNDAwLKOBH7 .cluster text{fill:#333;}#mermaid-svg-V6aD7ZNDAwLKOBH7 .cluster span{color:#333;}#mermaid-svg-V6aD7ZNDAwLKOBH7 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-V6aD7ZNDAwLKOBH7 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-V6aD7ZNDAwLKOBH7 rect.text{fill:none;stroke-width:0;}#mermaid-svg-V6aD7ZNDAwLKOBH7 .icon-shape,#mermaid-svg-V6aD7ZNDAwLKOBH7 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-V6aD7ZNDAwLKOBH7 .icon-shape p,#mermaid-svg-V6aD7ZNDAwLKOBH7 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-V6aD7ZNDAwLKOBH7 .icon-shape .label rect,#mermaid-svg-V6aD7ZNDAwLKOBH7 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-V6aD7ZNDAwLKOBH7 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-V6aD7ZNDAwLKOBH7 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-V6aD7ZNDAwLKOBH7 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 内核 prop.c
内核 accelerator.c
内核 hotkey.c + keyboard.c
内核 timer.c
用户态
首个计时器
冲突
应用 SetTimer
user32 ntwrapper.h SetTimer
应用 GetProp
user32 windows/prop.c 桌面堆直读
消息循环 TranslateAccelerator
user32 TranslateAccelerator
应用 RegisterHotKey
user32 RegisterHotKey
NtUserSetTimer L633
IntSetTimer L181
FindTimer L99
CreateTimer L53 / 窗口无关ID位图
KeSetTimer MasterTimer
input.c RawInputThreadMain L140
ProcessTimers L450
置TMRF_READY + MsqWakeQueue QS_TIMER
应用 GetMessage → PostTimerMessages L399
WM_TIMER 入队
message.c 派发 L916 / L933
NtUserRegisterHotKey L534
UserRegisterHotKey L446 → gphkFirst
ERROR_HOTKEY_ALREADY_REGISTERED
keyboard.c ProcessKeyEvent L1161
co_UserProcessHotKeys L190
IsHotKey L164
UserPostThreadMessage / UserPostMessage WM_HOTKEY
NtUserTranslateAccelerator L365
co_IntTranslateAccelerator L42
MENU_FindItem 查菜单
WM_COMMAND / WM_SYSCOMMAND
NtUserSetProp L247 / NtUserRemoveProp L218
UserSetProp / IntGetProp
UserHeapAlloc PROPERTY → 桌面堆
窗口销毁 UserRemoveWindowProps window.c L730
12.2 与 msgqueue(消息队列)的关系
- QS_TIMER 位 (0x10)是"线程有就绪计时器"的唤醒位,由
cTimersReady
计数维护:ProcessTimers递增(timer.c L502)、PostTimerMessages投递后
ClearMsgBitsMask递减(timer.c L430 → msgqueue.c L469-475); MsqWakeQueue(pti, QS_TIMER, TRUE)(msgqueue.c L413)在 RIT 上唤醒
应用线程的消息队列等待(KeSetEvent(pEventQueueServer));MsqGetMessage(message.c L1163)处理QS_TIMER时回调
PostTimerMessages,把 WM_TIMER 组装成 MSG 经MsqPostMessage入队
(timer.c L428);WM_SYSTIMER的派发(message.c L931-940)依赖FindSystemTimer反查
回调------系统计时器消息同样走队列,但执行的是内核回调而非窗口过程。
12.3 与 menu(菜单)的关系
加速键翻译的合法性完全委托给菜单子系统:
MENU_FindItem验证命令存在并换算位置;IntFindSubMenu/IntGetMenuState支撑 WM_INITMENUPOPUP 与禁用检查;- 加速键触发的 WM_COMMAND 与菜单点击最终汇入同一条命令处理路径,保证
"菜单显示的状态 = 命令可用的状态"。
12.4 与 defwnd(默认窗口过程)的关系
WM_SETHOTKEY/WM_GETHOTKEY由 DefWindowProc 路由到
DefWndSetHotKey/DefWndGetHotKey(hotkey.c L335-443),把"窗口热键"
复用全局热键链表(IDHK_WNDKEY 节点);DefWindowProc对 WM_TIMER 默认无操作(丢弃),符合 Windows 语义;- 窗口闪烁(
IntFlashWindowEx)会向窗口发 WM_NCACTIVATE,与 defwnd 的
非客户区激活处理交互(painting.c L1445)。
12.5 与 winsta/desktop 及初始化顺序的关系
MasterTimer与ptiRawInput属于会话级 输入基础设施(input.c L22-23),
RIT 打开交互窗口站后才StartTheTimers()(input.c L185);- 热键表
gphkFirst是 win32k 全局(每会话),未按窗口站隔离(hotkey.c
L9-14 的 FIXME); - 初始化顺序(main.c L1078-1085):
InitUserImpl → InitWindowStationImpl → InitDesktopImpl → InitInputImpl (MasterTimer)→ InitKeyboardImpl → MsqInitializeImpl → InitTimerImpl (TimerLock+位图)。
12.6 源码索引
| 主题 | 位置 |
|---|---|
| 计时器注册/删除 | win32ss/user/ntuser/timer.c:IntSetTimer L181、IntKillTimer L578、NtUserSetTimer L633、NtUserKillTimer L659、NtUserSetSystemTimer L683 |
| 计时器心跳 | timer.c:ProcessTimers L450、PostTimerMessages L399、StartTheTimers L372;input.c RawInputThreadMain L140(MasterTimer 等待 L231/L302)、InitInputImpl L376 |
| 系统计时器 | timer.c:SystemTimerProc L284、SystemTimerSet L383;caret.c CaretSystemTimerProc L97;painting.c IntFlashWindowEx L1376;mouse.c TrackMouseEvent L466;msgqueue.c HungAppSysTimerProc L2204 |
| 计时器结构/标志/ID | win32ss/user/ntuser/timer.h L3-31;USER_TIMER_MIN/Max 见 psdk/winuser.h L916-917;WM_SYSTIMER 见 reactos/undocuser.h L50 |
| 计时器对象类型 | win32ss/include/ntuser.h L56(TYPE_TIMER=16) |
| 属性 | win32ss/user/ntuser/prop.c:IntGetProp L14、UserSetProp L83、NtUserSetProp L247、NtUserRemoveProp L218、NtUserBuildPropList L135、UserRemoveWindowProps L113;结构 ntuser.h L1090-1104;user32 直读 win32ss/user/user32/windows/prop.c L38-62/L347-488 |
| 加速键表 | win32ss/user/ntuser/accelerator.c:NtUserTranslateAccelerator L365、co_IntTranslateAccelerator L42、NtUserCreateAcceleratorTable L223、NtUserCopyAcceleratorTable L169、NtUserDestroyAcceleratorTable L338;结构 accelerator.h L3-8 |
| 热键 | win32ss/user/ntuser/hotkey.c:NtUserRegisterHotKey L534、UserRegisterHotKey L446、NtUserUnregisterHotKey L583、co_UserProcessHotKeys L190、IsHotKey L164、DefWndSetHotKey L360、DefWndGetHotKey L335;结构/ID hotkey.h;系统热键注册 input.c L216-223;调用点 keyboard.c L1161 |
| WM_TIMER/WM_SYSTIMER 派发 | win32ss/user/ntuser/message.c L916-940(回调/系统回调)、L1163(PostTimerMessages 调用点) |
| QS_TIMER 位管理 | win32ss/user/ntuser/msgqueue.c:MsqWakeQueue L413、ClearMsgBitsMask L469-475 |
| 窗口销毁清理 | window.c L730(UserRemoveWindowProps);thread/window 计时器清理 timer.c L527/L554 |
13. 总结
本册四类设施共同展示了 win32k 处理"附属服务"的四种典型姿势:
- 计时器 把稀缺的内核定时器资源抽象成"全局链表 + 单一心跳 + 队列位"
的合并模型,用TMRF_*标志区分用户/系统/RIT 三类执行语境,用
cTimersReady/QS_TIMER把"到期"与"投递"解耦; - 属性 用"原子键 + 桌面堆存储 + 系统标志隔离"实现了窗口的通用附加
存储,并且由于桌面堆共享映射,读路径完全免系统调用; - 加速键 把键盘翻译逻辑收敛到一张声明式表,翻译结果必须经过菜单
子系统验证才发 WM_COMMAND/WM_SYSCOMMAND,与菜单状态严格一致; - 热键 以全局链表 + 输入管线拦截实现跨窗口的输入抢占,用
gfsModOnlyCandidate解决"仅修饰键"热键的时序难题,并承担
Win 键任务列表、窗口吸附、调试热键等系统级功能。
四者都深度依赖消息队列(MsqPostMessage/MsqWakeQueue/QS_* 位)、USER 对象
系统(UserCreateObject/UserGetObject/TYPE_*)与 win32k 锁协议
(UserEnterExclusive/UserEnterShared),是窗口管理主线的有机组成部分。
系列下一册预告:第 15 册将分析钩子子系统 hook.c(SetWindowsHookEx、
WH_* 钩子类型、KeUserModeCallback 回调机制),对应总览文档第 5.7 节。