ReactOS 窗口系统分析(14):计时器/属性/加速键/热键 — timer.c + prop.c + accelerator.c + hotkey.c

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 册)。本册补全四条相对独立的支线:

  1. 窗口计时器(timer.c,720 行)SetTimer/KillTimer 的内核实现。它以
    一个内核 KTIMER(MasterTimer)驱动所有 窗口计时器,到期后把 WM_TIMER
    消息投递到线程消息队列;同时提供 WM_SYSTIMER 内部系统计时器机制(光标闪烁、
    窗口闪烁、鼠标悬停等)。
  2. 窗口属性(prop.c,274 行)SetProp/GetProp/RemoveProp 的内核实现。
    原子 (ATOM)为键,把应用自定义数据附加到任意窗口上,属性项分配在
    桌面堆中。
  3. 加速键表(accelerator.c,446 行)CreateAcceleratorTable/
    TranslateAccelerator 的内核实现。把 WM_KEYDOWN/WM_CHAR 翻译成
    WM_COMMAND/WM_SYSCOMMAND,并负责查窗口菜单确认命令合法。
  4. 全局热键(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/IsHotKeyWM_SETHOTKEY 支持(DefWndGetHotKey/DefWndSetHotKey

配套文件:

  • win32ss/user/ntuser/timer.hTIMER 结构、TMRF_* 标志、ID_EVENT_SYSTIMER_* 系统计时器 ID、MasterTimer 声明。
  • win32ss/user/ntuser/accelerator.hACCELERATOR_TABLE 结构。
  • win32ss/user/ntuser/hotkey.hHOT_KEY 结构、IDHK_* 特殊热键 ID。
  • win32ss/user/ntuser/input.c:原始输入线程 RawInputThreadMain(驱动 MasterTimer 心跳)、InitInputImpl(分配 MasterTimer)、系统热键注册。
  • win32ss/user/ntuser/keyboard.cProcessKeyEvent 在按键处理路径调用 co_UserProcessHotKeys
  • win32ss/user/ntuser/message.cWM_TIMER/WM_SYSTIMER 的派发、PostTimerMessages 的调用点。
  • win32ss/user/ntuser/msgqueue.cMsqWakeQueue/ClearMsgBitsMaskQS_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 时不发------保证菜单状态是命令可用性的唯一权威;
  • 翻译发生在消息循环TranslateAcceleratorGetMessage 之后、
    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_RITTMRF_SYSTEM正交 的:FindTimer 的匹配

(flags & (TMRF_SYSTEM|TMRF_RIT)) 同时比较两个位。StartTheTimers 注册的

HungAppSysTimerProcTMRF_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

与用户属性分开标记:IntGetPropSystemProp 参数用来过滤,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 = 2147483647USER_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;

RtlFindClearBitsAndSetHintIndex 起查找空闲位,找到后顺手置位

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/fsChangeBitsKeSetEvent 唤醒消息队列等待),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(...) == TRUEcontinue 回到消息获取循环顶部,
    继续处理下一个,直到没有就绪计时器为止------单次 GetMessage 调用可能取出
    多个 WM_TIMER;
  • hwnd 可为 NULL :窗口无关计时器投递 hwnd = NULL 的 WM_TIMER
    Msg.hwnd = NULL),Msg.messageWM_SYSTIMER 还是 WM_TIMER
    取决于 TMRF_SYSTEM------系统计时器的 WM_SYSTIMER 同样经由这里入队
    (回调地址存 lParam,供 message.c L933 FindSystemTimer 反查);
  • 消息移尾 :投递后把计时器挪到链表尾部,配合"一次投一个 + 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_LOGONNOTIFYLN_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 L231 WaitObjects[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.cIntGetProp(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)= GlobalFindAtomWNtUserRemoveProp

"读免系统调用、写过系统调用"是桌面堆共享映射带来的典型折中------读路径的

热频率远高于写路径。

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_STOPUserRemoveProp(pWnd, AtomFlashWndState, TRUE) 清理。

系统属性不受 NtUserBuildPropList 枚举影响、应用 API 无法触碰

SystemProp 恒 FALSE),是隔离内部状态的好地方。


6. 加速键子系统:核心函数逐一展开

6.1 NtUserTranslateAccelerator(accelerator.c L365-446)

签名:int APIENTRY NtUserTranslateAccelerator(HWND hWnd, HACCEL hAccel, LPMSG pUnsafeMessage)

流程:

  1. UserEnterShared()(只读路径);
  2. hWnd == NULL → 返回 0;
  3. SEH 保护下 ProbeForRead(pUnsafeMessage, sizeof(MSG), 4) +
    RtlCopyMemory 把用户态 MSG 拷进内核栈(L388-398);
  4. 消息类型过滤 (L400-406):只处理
    WM_KEYDOWN / WM_SYSKEYDOWN / WM_SYSCHAR / WM_CHAR,其余直接返回 0------
    TranslateAccelerator 对这四个消息之外的内容一律不碰;
  5. UserGetAccelObject(hAccel)(L21-39:UserGetObject(gHandleTable, hAccel, TYPE_ACCELTABLE)
    无效句柄报 ERROR_INVALID_ACCEL_HANDLE);
  6. UserRefObjectCo 对 Accel 与 Window 各加一个"过程内引用"(防对象在翻译途中被
    并发销毁),结束时 UserDerefObjectCo 对称释放;
  7. 遍历表项(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)

  1. UserEnterExclusive(),参数校验:Entries == NULL || EntriesCount == 0 || EntriesCount > MAXULONG_PTR / sizeof(*Entries) → 返回 NULL;
  2. UserCreateObject(gHandleTable, pti->rpdesk, pti, &hAccel, TYPE_ACCELTABLE, sizeof(ACCELERATOR_TABLE))------加速键表是标准 USER 对象(TYPE_ACCELTABLE=8),
    归属当前线程及其桌面;
  3. Accel->Count = EntriesCountAccel->Table = ExAllocatePoolWithTag(PagedPool, EntriesCount * sizeof(ACCEL), USERTAG_ACCEL)(分页池数组);
  4. 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);

  1. 失败路径: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) :线性遍历 gphkFirstfsModifiers == 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_WNDKEYpti = 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 独占上下文内);
  • 冲突检测UserRegisterHotKeyIsHotKey(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 空间,RtlFindClearBitsAndSetHintIndex

轮转分配、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 中的
    常量为未来预留);
  • HungAppSysTimerProcTMRF_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->keyMask
    fVirt & (FSHIFT|FCONTROL|FALT) 完全相等
  • 字符表项:wParam == key(Mask & FALT) == (fVirt & FALT)(只核对 Alt);
  • 匹配成功即"翻译命中",后续的菜单检查只决定发不发消息、发什么消息。

10.3 菜单查找

  1. 非 WS_CHILD 窗口:查 Window->IDMenu(窗口菜单栏);WS_CHILD 窗口跳过;
  2. MENU_FindItem(&MenuObject, &nPos, MF_BYPOSITION):菜单子系统按命令 ID
    反查项位置,命中则 hSubMenu 指向该弹出菜单;
  3. 窗口菜单未命中 → 查 Window->SystemMenu(系统菜单,本身是弹出菜单);
  4. 两处都未命中 → 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

IntGetMenuStateMF_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_FLAGSMOD_NOREPEAT(0x4000,Vista 才支持)被拒绝。

IntGetModifiersgafAsyncKeyState 实时生成 MOD_ 位;IntSwapModHKF

处理 MOD_ 与 HOTKEYF_ 的 SHIFT/ALT 位序差异。

11.2 冲突检测 ERROR_HOTKEY_ALREADY_REGISTERED

UserRegisterHotKeyIsHotKey(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 反查
    回调------系统计时器消息同样走队列,但执行的是内核回调而非窗口过程。

加速键翻译的合法性完全委托给菜单子系统:

  • 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 及初始化顺序的关系

  • MasterTimerptiRawInput 属于会话级 输入基础设施(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 处理"附属服务"的四种典型姿势:

  1. 计时器 把稀缺的内核定时器资源抽象成"全局链表 + 单一心跳 + 队列位"
    的合并模型,用 TMRF_* 标志区分用户/系统/RIT 三类执行语境,用
    cTimersReady/QS_TIMER 把"到期"与"投递"解耦;
  2. 属性 用"原子键 + 桌面堆存储 + 系统标志隔离"实现了窗口的通用附加
    存储,并且由于桌面堆共享映射,读路径完全免系统调用;
  3. 加速键 把键盘翻译逻辑收敛到一张声明式表,翻译结果必须经过菜单
    子系统验证才发 WM_COMMAND/WM_SYSCOMMAND,与菜单状态严格一致;
  4. 热键 以全局链表 + 输入管线拦截实现跨窗口的输入抢占,用
    gfsModOnlyCandidate 解决"仅修饰键"热键的时序难题,并承担
    Win 键任务列表、窗口吸附、调试热键等系统级功能。

四者都深度依赖消息队列(MsqPostMessage/MsqWakeQueue/QS_* 位)、USER 对象

系统(UserCreateObject/UserGetObject/TYPE_*)与 win32k 锁协议

(UserEnterExclusive/UserEnterShared),是窗口管理主线的有机组成部分。


系列下一册预告:第 15 册将分析钩子子系统 hook.c(SetWindowsHookEx、

WH_* 钩子类型、KeUserModeCallback 回调机制),对应总览文档第 5.7 节。

相关推荐
circuitsosk1 小时前
Python 模块与包管理:import 机制、虚拟环境与 pip 完全指南
开发语言·python·pip·依赖管理·模块与包
就叫飞六吧1 小时前
两道门:X-Frame-Options 和 SameSite 到底谁管什么
开发语言·chrome·ai编程
水饺编程1 小时前
第5章,[Win32 章节] :贝塞尔样条曲线(一)
c语言·c++·windows·visual studio
fl1768312 小时前
基于C#WPF实现的内存加速球清理类似360加速球内存清理
开发语言·c#·wpf
weixin_440730502 小时前
python+request实现接口-小结
开发语言·python
ZJU_统一阿萨姆2 小时前
【算子开发】扫描(Scan)与前缀和
开发语言·arm开发·架构·系统架构·硬件架构
-凌凌漆-2 小时前
【C语言】结构体typedef struct与struct的区别
c语言·开发语言
caimouse2 小时前
ReactOS 窗口系统分析(24):输入法 — ime.c
c语言·reactos
for_ever_love__2 小时前
PySpark学习: 数据输出
开发语言·python·学习·spark