ReactOS 窗口系统分析(20):光标与图标 --- cursoricon.c
系列:窗口系统三大主线之窗口管理(第 20 册)
主题:
win32ss/user/ntuser/cursoricon.c(2258 行)+cursoricon.h+ user32 层windows/cursoricon.c(约 3100 行)+ GDI 引擎gdi/eng/mouse.c关联:
msgqueue.c(UserSetCursor / UserShowCursor)、callback.c(co_IntLoadDefaultCursors / co_IntCopyImage)、simplecall.c、defwnd.c、desktop.c依据:ReactOS 源码(d:\reactos),所有行号以当前源码为准
1. 概述
光标(Cursor)与图标(Icon)是窗口管理中最"小"却也最特殊的一类 USER 对象:它们的本质都是位图对 (AND 掩码 + XOR 掩码 / 彩色位图 + 透明信息),再加上一个"热点"(hotspot,光标命中点)或"动画帧表"(.ani 光标)。ReactOS 在 win32k 中用同一个对象类型 CURICON_OBJECT(TYPE_CURSOR)统一表示光标与图标,把差异压进一个 rt(资源类型)字段与一组标志位里。
整个子系统的功能可以浓缩为一条主线:加载(Load)→ 设置(SetCursor)→ 显示与移动(ShowCursor / GDI 指针绘制)。
┌─────────────────────────── 用户态 user32.dll ───────────────────────────┐
│ LoadCursorW / LoadIconW / LoadImageW / CreateIconFromResourceEx │
│ └── CURSORICON_LoadImageW(资源/文件 → CURSORDATA 位图对) │
│ ├─ NtUserxCreateEmptyCurObject() ← 创建空对象(内核分配) │
│ └─ NtUserSetCursorIconData() ← 填充位图/热点/名称 │
│ SetCursor(hCursor) ── NtUserSetCursor ──► 内核:队列 CursorObject 更新 │
│ ShowCursor(TRUE/FALSE) ── NtUserxShowCursor ──► 内核:iCursorLevel 计数 │
│ DrawIconEx / CopyImage / GetIconInfo ── NtUser* ──► 内核 │
└──────────────────────────────────┬───────────────────────────────────────┘
│ 系统调用(win32u → w32ksvc)
┌──────────────────────────────────▼──────────── 内核 win32k.sys ─────────┐
│ ntuser/cursoricon.c │
│ CURICON_OBJECT(对象 + 位图 + 热点 + 动画帧) │
│ NtUserSetCursor ──► UserSetCursor(msgqueue.c) │
│ ├── 队列 CursorObject = 新光标 │
│ ├── 命中窗口属于本队列?→ GreSetPointerShape(换形状) │
│ └── gSysCursorInfo.CurrentCursorObject = 新光标 │
│ UserShowCursor(msgqueue.c)→ GreMovePointer(x,y) / (-1,-1) │
│ UserSetCursorPos(cursoricon.c)→ 裁剪 + 注入 WM_MOUSEMOVE │
└──────────────────────────────────┬───────────────────────────────────────┘
│ GreSetPointerShape / GreMovePointer
┌──────────────────────────────────▼──────────── GDI 引擎 gdi/eng/mouse.c ┐
│ IntEngSetPointerShape(保存新形状 → PDEV 指针状态) │
│ IntShowMousePointer / IntHideMousePointer(保存/恢复被遮像素,SRCAND+ │
│ SRCINVERT 或 AlphaBlend 画指针) │
│ 硬件指针(pfnMovePointer)/ 软件指针(EngMovePointer)双路径 │
└───────────────────────────────────────────────────────────────────────────┘
内核把"光标"抽象成每个线程消息队列 (USER_MESSAGE_QUEUE)里的一个 CursorObject 指针 + 一个显隐计数器 iCursorLevel,而"画在哪、画成什么样"完全委托给 GDI 引擎(GreSetPointerShape / GreMovePointer)。这与 Windows 的经典架构一致:USER 只管"哪个窗口/哪个队列控制光标",GDI/驱动管"像素"。
1.1 与总览第 5.12 节的定位
总览文档《ReactOS窗口系统架构分析.md》第 5.12 节给出结论性描述:
CURICON_OBJECT统一表示光标与图标:CreateCursor/CreateIcon/LoadCursor/SetCursor(NtUserSetCursor L1051)。光标对象包含 AND/XOR 掩码位图或彩色位图、热点(ptHotSpot)、帧动画(.ani)。SetCursor更新队列CursorObject与光标显示(UserSetCursor/IntShowCursor,msgqueue.c),系统光标(IDC_ARROW等)由co_IntLoadDefaultCursors在初始化时加载。
本册是对这段总览的逐函数展开。需要特别说明:当前源码树中部分传统函数名已经不存在 (如 NtUserCreateCursor、NtUserCreateIcon、NtUserLoadCursor、NtUserCopyImage、IntAnimateCursor、CursorServiceThreadProc、CursorMotionThreadProc、NtUserShowCursor、IntShowCursor 等)。ReactOS 把这些能力重构进了别的函数与调用路径,文中对每个"历史函数"都会给出它在当前源码中的真实落点,绝不臆造。
2. 设计动机
为什么光标和图标要共用一个对象类型?为什么显隐要用"计数器"而不是布尔值?为什么系统光标要单独维护一张表?以下是设计层面的动机分析。
2.1 光标与图标的同构性
从数据角度看,光标和图标几乎没有区别:
- 两者都是"一张 AND 掩码位图 + 一张 XOR/彩色位图"的组合(单色光标把彩色信息塞进掩码的下半部分);
- 两者的句柄类型在 Win32 API 中互通(
SetCursor接受HCURSOR,WM_SETCURSOR传的也是HCURSOR,GetIconInfo同时服务两者); - 两者都可以从资源(RT_ICON / RT_CURSOR)或文件(.ico / .cur / .ani)加载;
- 两者的句柄表条目都归属 TYPE_CURSOR(
NtUserDestroyCursor同时就是DestroyIcon)。
因此 ReactOS 用一个 CURICON_OBJECT + 一个 is_icon() 判断(rt == RT_ICON,cursoricon.c L192)就覆盖了两套 API,大幅减少重复代码。唯一的例外是动画光标 :它需要一整套"帧 + 帧序 + 每步 jiffies 延时"的额外状态,所以派生了一个 ACON 结构,与 CURICON_OBJECT 共用头部(cursoricon.h 用 C_ASSERT 保证 FIELD_OFFSET(ACON, cpcur) == FIELD_OFFSET(CURICON_OBJECT, xHotspot),即前半部分布局完全一致)。
2.2 私有的 vs 共享的(LR_SHARED)
文件头注释(cursoricon.c L8-20)明确区分了两类对象:
- 私有(Private) :未带
LR_SHARED加载,仅属于本进程,可被NtDestroyCursorIcon()删除;hModule/hRsrc/hGroupRsrc为 NULL。 - 共享(Shared) :带
LR_SHARED加载(LoadCursor/LoadIcon默认就是共享),可能被多个进程共享,免疫DestroyCursor;模块名与资源名有效(存为atomModName+strName)。
共享对象必须有一个"查重"机制:NtUserFindExistingCursorIcon 按(模块原子,资源名)在进程私有缓存 ppi->pCursorCache 和全局链表 gcurFirst 中查找,找到就直接返回已有句柄(引用计数 +1),避免同一资源被反复加载成多个对象。这也是 LoadCursor(NULL, IDC_ARROW) 每次调用都返回同一个句柄的原因。
2.3 系统光标表
Windows 规定 IDC_ARROW 等 15 个标准光标(OCR_*)由系统统一加载、全局共享,任何进程都能用 LoadCursor(NULL, IDC_*) 拿到同一句柄;SetSystemCursor 允许替换它们。ReactOS 用两个静态表实现:
gasyscur[16](OCR_NORMAL/IBEAM/WAIT/.../HELP,cursoricon.c L32-49)gasysico[6](OIC_SAMPLE/HAND/QUES/BANG/NOTE/WINLOGO,cursoricon.c L54-61)
每个表项是 SYSTEMCURICO {DWORD type; PCURICON_OBJECT handle;}(cursoricon.h L97-100),handle 在初始化时由 LoadSystemCursors()(user32 回调)填充。访问宏 SYSTEMCUR(func) / SYSTEMICO(func)(cursoricon.h L131-132)按 ROCR_*/ROIC_* 索引取对象。SYSTEMCUR(WAIT) 甚至在 MsqInitializeMessageQueue(msgqueue.c L2217)里被当作新线程队列的默认光标 (对齐 winetest test_initial_cursor)。
2.4 显隐计数而非布尔值
ShowCursor(FALSE) 与 ShowCursor(TRUE) 可以任意嵌套,Windows 用计数器保证对称性:只有计数从 0 变 -1 才真正隐藏光标,从 -1 变 0 才真正显示。ReactOS 在 USER_MESSAGE_QUEUE 里放了一个 INT iCursorLevel(win32.h L127),THREADINFO 也镜像了一份(pti->iCursorLevel)。为什么线程要一份?因为消息队列可以被多个线程共享 (AttachThreadInput),而 ShowCursor 是线程级的;两个计数同步增减(msgqueue.c L184-185),但真正决定显隐的只有队列那份。
2.5 一次重构的痕迹:两步式创建
本册源码没有 NtUserCreateCursor(hInst, xHot, yHot, w, h, pvAND, pvXOR) 这类"一把梭"的 syscall。ReactOS 把创建拆成两步:
NtUserxCreateEmptyCurObject(bAnimated)(→IntCreateCurIconHandle,simplecall.c L259)------只分配对象与句柄,不含任何位图;NtUserSetCursorIconData(hCur, module, rsrc, &CURSORDATA)------把掩码位图/彩色位图/热点/名称一次性灌入。
user32 的 CreateCursor/CreateIcon/CreateIconIndirect 内部都只是"把参数整理成 CURSORDATA,然后调这两步"(见第 7 节)。这个设计的优点:内核侧只需要维护"空对象 + 数据填充"两个原语,所有格式解析(.cur/.ico/.ani/DIB/PNG)都留在 user32 做,syscall 接口极简。
3. 核心数据结构
3.1 CURICON_OBJECT --- 光标/图标的统一对象
win32ss/user/ntuser/cursoricon.h L9-27:
c
typedef struct _CURICON_OBJECT
{
PROCMARKHEAD head; /* 对象头:HEAD + hTaskWow + ppi(所属进程)*/
struct _CURICON_OBJECT* pcurNext; /* 全局/进程光标链表的下一个 */
UNICODE_STRING strName; /* 资源名(可能是 INTRESOURCE==ATOM)*/
USHORT atomModName; /* 模块名的全局原子(gAtomTable)*/
USHORT rt; /* 资源类型:RT_ICON / RT_CURSOR */
ULONG CURSORF_flags; /* 见 3.6 标志位表 */
SHORT xHotspot; /* 热点 X(光标命中点;图标为 cx/2)*/
SHORT yHotspot; /* 热点 Y */
HBITMAP hbmMask; /* AND 掩码位图(1bpp,必要时含下半个=图像)*/
HBITMAP hbmColor; /* 彩色位图(XOR 面,可空)*/
HBITMAP hbmAlpha; /* 8bpp alpha 通道位图(半透明光标/图标)*/
RECT rcBounds; /* 包围盒(左 0 上 0 右 cx 下 cy)*/
HBITMAP hbmUserAlpha; /* 用户提供的 alpha(W7U 兼容字段)*/
ULONG bpp; /* 彩色位图位深(屏幕 bpp)*/
ULONG cx; /* 逻辑宽度 */
ULONG cy; /* 逻辑高度 */
} CURICON_OBJECT, *PCURICON_OBJECT;
各字段要点:
- head :
PROCMARKHEAD(ntuser.h L223-228)=HEAD(句柄 + 类型 + 引用计数cLockObj)+hTaskWow+ppi。ppi == NULL表示全局对象 (系统光标/图标),这是NtUserDestroyCursor拒绝删除的判据(cursoricon.c L815)。 - pcurNext :对象被链入
gcurFirst(全局)或ppi->pCursorCache(进程私有)单链表;CURSORF_LINKED标志标记是否在链上。注释// FIXME: should think about using a LIST_ENTRY!(L98)说明这是历史遗留的单链实现。 - strName / atomModName :仅共享对象(
CURSORF_LRSHARED)使用,用于NtUserFindExistingCursorIcon查重与GetIconInfoEx返回模块/资源名。strName.Buffer可能是一个MAKEINTRESOURCE值(==ATOM),此时不占用堆内存。 - hbmMask / hbmColor / hbmAlpha :三张位图。单色光标:
hbmMask高度为2*cy(上半 AND 掩码、下半 XOR 图像),hbmColor == NULL;彩色光标/图标:hbmMask为 1bpp AND 面,hbmColor为与屏幕兼容的彩色面;带 alpha 的半透明对象:hbmAlpha提供 8bpp alpha。所有权归 USER 句柄表(GDI_OBJ_HMGR_PUBLIC),释放时由FreeCurIconObject回收(cursoricon.c L335-392)。 - bpp :
NtUserGetIconInfo通过它回填调用者的pbpp参数(L467)。 - rcBounds :由
IntSetCursorData固定为{0,0,cx,cy}(L1222-1225)。
3.2 ACON --- 动画光标
cursoricon.h L29-43:
c
typedef struct tagACON
{
PROCMARKHEAD head; /* 与 CURICON_OBJECT 共用头部 */
struct _CURICON_OBJECT* pcurNext;
UNICODE_STRING strName;
USHORT atomModName;
USHORT rt;
ULONG CURSORF_flags; /* 含 CURSORF_ACON */
UINT cpcur; /* 帧数(独立位图帧的个数)*/
UINT cicur; /* 步数(序列下标个数,<= 或 > 帧数均可)*/
PCURICON_OBJECT * aspcur; /* 帧数组:每个元素是一个帧光标对象(CURSORF_ACONFRAME)*/
DWORD * aicur; /* 序列:第 i 步显示 aspcur[aicur[i]] */
INT * ajifRate; /* 每步停留的 jiffies 数 */
UINT iicur; /* 默认显示速率(display_rate)*/
} ACON, *PACON;
关键点:
C_ASSERT(FIELD_OFFSET(ACON, cpcur) == FIELD_OFFSET(CURICON_OBJECT, xHotspot))(L45)保证两结构前 6 个字段布局一致,因此IntCreateCurIconHandle(TRUE)可以安全地按sizeof(ACON)分配一个"大号 CURICON_OBJECT",再打上CURSORF_ACON标志(cursoricon.c L304-308)。aspcur[i]每个都是一份独立的CURICON_OBJECT(帧),带CURSORF_ACONFRAME标志;aicur/ajifRate定义播放序列 :第 i 步显示第aicur[i]帧、停留ajifRate[i]个 jiffies(约 1/1024 秒)。NtUserGetCursorFrameInfo与NtUserGetIconSize遇到 ACON 时都取aspcur[0]作为代表帧。
3.3 CURSORDATA --- 内核与 user32 之间的传输结构
ntuser.h L1173-1196,NtUserSetCursorIconData 的最后一个参数:
c
typedef struct tagCURSORDATA
{
LPWSTR lpName; /* 未使用(名称走独立参数)*/
LPWSTR lpModName;
USHORT rt; /* RT_ICON / RT_CURSOR */
USHORT dummy;
ULONG CURSORF_flags; /* 用户可设:CURSORF_USER_MASK 内 */
SHORT xHotspot;
SHORT yHotspot;
HBITMAP hbmMask;
HBITMAP hbmColor;
HBITMAP hbmAlpha;
RECT rcBounds;
HBITMAP hbmUserAlpha;
ULONG bpp;
ULONG cx;
ULONG cy;
UINT cpcur; /* ACON:帧数 */
UINT cicur; /* ACON:步数 */
struct tagCURSORDATA *aspcur; /* ACON:帧数据数组(用户态指针)*/
DWORD *aicur; /* ACON:序列数组(用户态指针)*/
INT *ajifRate; /* ACON:每步 jiffies 数组(用户态指针)*/
UINT iicur; /* ACON:默认速率 */
} CURSORDATA, *PCURSORDATA;
普通光标只用到 rt/flags/xHotspot/yHotspot/hbmMask/hbmColor/hbmAlpha/bpp/cx/cy;动画光标额外用 cpcur/cicur/aspcur/aicur/ajifRate/iicur。NtUserSetCursorIconData 会对三个指针数组做逐元素探测拷贝(见 7.3)。
3.4 SYSTEM_CURSORINFO --- 全局光标状态
cursoricon.h L65-95,全局实例 gSysCursorInfo(cursoricon.c L25):
c
typedef struct _SYSTEM_CURSORINFO
{
BOOL Enabled; /* 光标是否启用 */
BOOL ClickLockActive; /* 单击锁定(ClickLock)激活 */
DWORD ClickLockTime;
UINT ButtonsDown; /* 按下的鼠标键数 */
RECTL rcClip; /* 裁剪矩形(ClipCursor)*/
BOOL bClipped; /* 是否处于裁剪状态 */
PCURICON_OBJECT CurrentCursorObject; /* 当前正在屏幕显示的光标对象 */
INT ShowingCursor; /* 全局显隐计数(<0 隐藏,>=0 显示)*/
DWORD LastBtnDown; /* 上次按键时间(双击判定用)*/
LONG LastBtnDownX, LastBtnDownY;
HANDLE LastClkWnd;
BOOL ScreenSaverRunning; /* 屏保运行中 */
} SYSTEM_CURSORINFO, *PSYSTEM_CURSORINFO;
InitCursorImpl(cursoricon.c L63-76)在 win32k 初始化时清零,注意 ShowingCursor = -1(初始隐藏,因为初始化阶段指针尚未就绪)。IntGetSysCursorInfo()(L186-190)返回其地址;UserSetCursor/UserShowCursor/NtUserGetCursorInfo/UserClipCursor 都围绕它工作。
3.5 系统光标/图标 ID 表
gasyscur[](cursoricon.c L32-49)与 gasysico[](L54-61),类型 SYSTEMCURICO {DWORD type; PCURICON_OBJECT handle;}(cursoricon.h L97-100):
| 索引宏 | OCR_* | 说明 | 索引宏 | OIC_* | 说明 | |
|---|---|---|---|---|---|---|
| ROCR_ARROW | OCR_NORMAL | 正常箭头 | ROIC_SAMPLE | OIC_SAMPLE | 示例 | |
| ROCR_IBEAM | OCR_IBEAM | I 形文本 | ROIC_HAND | OIC_HAND | 手 | |
| ROCR_WAIT | OCR_WAIT | 沙漏/等待 | ROIC_QUES | OIC_QUES | 问号 | |
| ROCR_CROSS | OCR_CROSS | 十字 | ROIC_BANG | OIC_BANG | 感叹号 | |
| ROCR_UP | OCR_UP | 上箭头 | ROIC_NOTE | OIC_NOTE | 便签 | |
| ROCR_SIZE | OCR_SIZE | 尺寸(旧) | ROIC_WINLOGO | OIC_WINLOGO | Windows 徽标 | |
| ROCR_ICON | OCR_ICON | 图标(旧) | ||||
| ROCR_SIZENWSE | OCR_SIZENWSE | 左上-右下斜 | ||||
| ROCR_SIZENESW | OCR_SIZENESW | 右上-左下斜 | ||||
| ROCR_SIZEWE | OCR_SIZEWE | 水平 | ||||
| ROCR_SIZENS | OCR_SIZENS | 垂直 | ||||
| ROCR_SIZEALL | OCR_SIZEALL | 四向移动 | ||||
| ROCR_NO | OCR_NO | 禁止 | ||||
| ROCR_HAND | OCR_HAND | 手型(链接) | ||||
| ROCR_APPSTARTING | OCR_APPSTARTING | 后台运行 | ||||
| ROCR_HELP | OCR_HELP | 帮助问号 |
宏 SYSTEMCUR(ARROW) 即 gasyscur[ROCR_ARROW].handle(cursoricon.h L131)。注意 OIC_INTERNAL_WINSMALL(=6,cursoricon.h L112)是内部专用"小窗口图标",不能占用 gasysico 数组项(数组只有 6 项,ROIC_SHIELD 预留 6 号位),IntLoadSystenIcons 对它走特殊分支(见 4.3)。
3.6 标志位表(CURSORF_flags)
定义在 ntuser.h L1198-1207:
| 标志 | 值 | 含义 | 设置处 |
|---|---|---|---|
| CURSORF_FROMRESOURCE | 0x0001 | 来自资源(RT_GROUP_ICON 解析) | user32 CURSORICON_LoadImageW |
| CURSORF_GLOBAL | 0x0002 | 全局共享对象(ppi == NULL) |
IntLoadSystenIcons / NtUserSetSystemCursor |
| CURSORF_LRSHARED | 0x0004 | LR_SHARED 共享加载 | user32 在 CURSORDATA 中置位 |
| CURSORF_ACON | 0x0008 | 动画光标(对象是 ACON) | IntCreateCurIconHandle(TRUE) / ANI 解析 |
| CURSORF_WOWCLEANUP | 0x0010 | WOW 清理(未用) | --- |
| CURSORF_ACONFRAME | 0x0040 | 动画光标的一帧 | IntSetAconData / ANI 解析 |
| CURSORF_SECRET | 0x0080 | 秘密对象(未用) | --- |
| CURSORF_LINKED | 0x0100 | 已在全局/进程链表中 | IntInsertCursorIntoList |
| CURSORF_CURRENT | 0x0200 | 当前队列正在使用(禁止销毁) | NtUserSetCursor / desktop.c WM_SETCURSOR |
用户态通过 NtUserSetCursorIconData 只能设置 CURSORF_USER_MASK = FROMRESOURCE|LRSHARED|ACON(cursoricon.h L6-7);GLOBAL/LINKED/CURRENT 是内核内部标志,IntSetCursorData 用 CURSORF_USER_MASK 掩码截断用户输入(cursoricon.c L1216)。
3.7 相关外部结构
- USER_MESSAGE_QUEUE (msgqueue.h L44 起):
PCURICON_OBJECT CursorObject(L89)是本队列当前光标;iCursorLevel(win32.h L127,THREADINFO 内)是显隐计数;ptiMouse记录鼠标所属线程。 - gpqCursor (msgqueue.c L20):当前正控制屏幕光标形状 的队列。
UserSetCursor首次设置时若为空则指向本队列(L119-122);队列销毁时若等于它则清空(L2400-2403)。 - gDesktopCursor (desktop.c L55,callback.c L468 extern):桌面窗口(
gpqDesktop)默认光标,由co_IntLoadDefaultCursors回调结果填充(callback.c L500)。
4. 初始化与对象生命周期
4.1 InitCursorImpl --- 全局光标状态初始化
c
BOOL InitCursorImpl(VOID) // cursoricon.c L63-76
{
gSysCursorInfo.Enabled = FALSE;
gSysCursorInfo.ButtonsDown = 0;
gSysCursorInfo.bClipped = FALSE;
gSysCursorInfo.LastBtnDown = 0;
gSysCursorInfo.CurrentCursorObject = NULL;
gSysCursorInfo.ShowingCursor = -1; // 初始隐藏
gSysCursorInfo.ClickLockActive = FALSE;
gSysCursorInfo.ClickLockTime = 0;
return TRUE;
}
注意 ShowingCursor = -1:在系统光标(co_IntLoadDefaultCursors)加载完成前,任何 GreMovePointer 显示请求都会被 NtUserGetCursorInfo 的 CURSOR_SHOWING 判定挡住。
4.2 IntInsertCursorIntoList / IntRemoveCursorFromList --- 链表维护
c
static VOID IntInsertCursorIntoList(PCURICON_OBJECT pcur) // L78-96
{
ppcurHead = (pcur->CURSORF_flags & CURSORF_GLOBAL) ?
&gcurFirst : &ppi->pCursorCache; // 全局 or 进程私有
UserReferenceObject(pcur); // 链表持有引用
pcur->pcurNext = *ppcurHead;
*ppcurHead = pcur;
pcur->CURSORF_flags |= CURSORF_LINKED;
}
- 前置断言:对象必须带
CURSORF_GLOBAL|CURSORF_LRSHARED之一,且尚未 LINKED(L85-86)。只有全局或共享对象才进链表------私有对象由持有者显式持有,无需登记。 IntRemoveCursorFromList(L99-134)反向操作:遍历找到后摘链、UserDereferenceObject、清除 LINKED;找不到则NT_ASSERT(FALSE)。- 链表的用处有二:
NtUserFindExistingCursorIcon查重、IntCleanupCurIconCache进程退出回收。
4.3 IntLoadSystenIcons --- 登记系统图标
c
VOID IntLoadSystenIcons(HICON hcur, DWORD id) // L136-184
- 校验句柄得到
pcur;若调用进程还没创建窗口或 DC(W32PF_CREATEDWINORDC未置位)直接返回(L154-155)------系统图标只允许"就绪"的进程登记。 id == OIC_INTERNAL_WINSMALL:这是给窗口小图标用的内部项,只置CURSORF_GLOBAL、ppi = NULL、加引用,不链入任何表(L158-164)。- 其余 id 在
gasysico[6]中查找:置CURSORF_GLOBAL、head.ppi = NULL、IntInsertCursorIntoList入全局链(L166-182)。注释说明:这是把"LR shared"临时切换到"Global public"的 hack,用于系统启动早期。
4.4 IntCreateCurIconHandle --- 分配空对象
c
HANDLE IntCreateCurIconHandle(BOOLEAN Animated) // L284-314
{
CurIcon = UserCreateObject(gHandleTable, NULL, GetW32ThreadInfo(),
&hCurIcon, TYPE_CURSOR,
Animated ? sizeof(ACON) : sizeof(CURICON_OBJECT));
if (Animated) CurIcon->CURSORF_flags |= CURSORF_ACON;
UserDereferenceObject(CurIcon); // 交回创建引用,返回裸句柄
return hCurIcon;
}
- 通过
UserCreateObject(object.c)在gHandleTable中创建 TYPE_CURSOR 对象;动画光标按sizeof(ACON)分配。 - 创建时用户态没有持有引用,所以
UserCreateObject的初始引用被立即UserDereferenceObject掉;此后对象的引用完全靠后续操作(UserGetCurIconObject等)按需增加。这对应"空对象"语义:句柄有效,但数据为空,任何绘制/查询前必须先NtUserSetCursorIconData。 - 它由
simplecall.c的ONEPARAM_ROUTINE_CREATEEMPTYCUROBJECT(L259-267)暴露给 user32,即NtUserxCreateEmptyCurObject(ntwrapper.h L615-618)。
4.5 UserGetCurIconObject --- 句柄→对象(带引用)
c
PCURICON_OBJECT FASTCALL UserGetCurIconObject(HCURSOR hCurIcon) // L200-227
{
if (!hCurIcon) { EngSetLastError(ERROR_INVALID_CURSOR_HANDLE); return NULL; }
if (UserObjectInDestroy(hCurIcon)) { ... ERROR_INVALID_CURSOR_HANDLE ... }
CurIcon = UserReferenceObjectByHandle(hCurIcon, TYPE_CURSOR);
if (!CurIcon) { EngSetLastError(ERROR_INVALID_CURSOR_HANDLE); return NULL; }
ASSERT(CurIcon->head.cLockObj >= 1);
return CurIcon;
}
这是本文件被调用最多的"看门狗"函数:校验句柄非空、不在销毁中、类型正确(TYPE_CURSOR),并增加引用计数 。返回值必须成对地 UserDereferenceObject。注释(L220)点明:永远设 ERROR_INVALID_CURSOR_HANDLE 而非 ERROR_INVALID_ICON_HANDLE,"但愿没人检查它"。
4.6 IntDestroyCurIconObject / FreeCurIconObject
IntDestroyCurIconObject(L316-333):若对象在链表上先IntRemoveCursorFromList,然后UserDeleteObject(句柄, TYPE_CURSOR)。注释明确:这里只是标记句柄销毁,真正的资源回收推迟到结构体释放。FreeCurIconObject(L335-392,对象释放回调):- 普通对象:对
hbmMask/hbmColor/hbmAlpha先GreSetObjectOwner(..., GDI_OBJ_HMGR_POWNED)再GreDeleteObject(把 GDI 句柄从全局句柄表移交私有后再删,避免与 USER 所有权冲突); - ACON:遍历
aspcur[0..cpcur),每个帧UserDereferenceObject+IntDestroyCurIconObject,最后ExFreePoolWithTag(aspcur, USERTAG_CURSOR); CURSORF_LRSHARED对象:释放strName.Buffer(非 INTRESOURCE 时)、删除atomModName原子;- 最后
FreeProcMarkObject释放 PROCMARKHEAD 残余。
- 普通对象:对
4.7 IntCleanupCurIconCache --- 进程退出回收
c
VOID FASTCALL IntCleanupCurIconCache(PPROCESSINFO Win32Process) // L394-406
{
while (Win32Process->pCursorCache)
{
CurIcon = Win32Process->pCursorCache;
Win32Process->pCursorCache = CurIcon->pcurNext;
UserDereferenceObject(CurIcon);
}
}
进程终结时(IntCleanupProcessInfo 路径)把 pCursorCache 里所有共享/全局对象摘链并解除链表引用,让引用计数最终归零触发销毁。
4.8 NtUserDestroyCursor --- DestroyCursor / DestroyIcon
c
BOOL APIENTRY NtUserDestroyCursor(HANDLE hCurIcon, BOOL bForce) // L793-855
bForce == FALSE(API 路径)时执行四道保护检查:head.ppi == NULL→ 全局对象,拒绝("Trying to delete global cursor!",L815-820);ppi != 当前进程→ 不是你的光标,拒绝(L823-828);CURSORF_CURRENT→ 正在使用,拒绝(L830-835);CURSORF_LRSHARED→ 共享对象,返回 TRUE 但不动(L837-843,"This one is not an error (LoadImage shared icons)")。
bForce == TRUE(内部路径,如CURSORICON_LoadImageW失败回滚时用NtUserDestroyCursor(hCurIcon, TRUE))跳过所有检查直接销毁。- 真正销毁调用
IntDestroyCurIconObject。user32 的DestroyCursor/DestroyIcon都映射到它(user32 cursoricon.c L3087-3092、L2426-2431)。
4.9 NtUserFindExistingCursorIcon --- LR_SHARED 查重
c
HICON NTAPI NtUserFindExistingCursorIcon(
PUNICODE_STRING pustrModule, PUNICODE_STRING pustrRsrc,
FINDEXISTINGCURICONPARAM* param) // L861-990
- 先把参数探测拷贝进内核(
ProbeAndCaptureUnicodeStringOrAtom处理"资源名可能是 INTRESOURCE==ATOM"的情形,L890),再把模块名RtlLookupAtomInAtomTable转成原子atomModName(L896);模块不在原子表里则直接失败(没有登记过任何图标)。 - 匹配规则(两轮,先进程缓存
pProcInfo->pCursorCache后全局gcurFirst):param->bIcon != is_icon(CurIcon)→ 类型不匹配,跳过;atomModName相同;- 资源名同"种类"(都是 INTRESOURCE 或都是字符串),且 INTRESOURCE 时指针值相等、字符串时
RtlCompareUnicodeString相等(L918-937 / L958-977)。
- 命中返回
UserHMGetHandle(CurIcon)(不加引用,调用方得到的是共享句柄------共享对象本来就由链表持有引用,不会中途消失)。 - 这就是
LoadCursor第二次调用立刻命中缓存、返回同一句柄的机制;失败路径释放捕获的字符串。
5. 设置光标:NtUserSetCursor / UserSetCursor
5.1 NtUserSetCursor(L1049-1113)
user32.spec:@ stdcall SetCursor(long) NtUserSetCursor ------ 用户态 SetCursor 直接 syscall。
c
HCURSOR APIENTRY NtUserSetCursor(HCURSOR hCursor)
{
UserEnterExclusive();
if (hCursor)
{
pcurNew = UserGetCurIconObject(hCursor); // 校验 + 引用
if (!pcurNew) { EngSetLastError(ERROR_INVALID_CURSOR_HANDLE); goto leave; }
pcurNew->CURSORF_flags |= CURSORF_CURRENT; // 标记为当前使用
}
else pcurNew = NULL;
pcurOld = UserSetCursor(pcurNew, FALSE); // msgqueue.c,核心切换
if (pcurOld && (pcurOld = UserGetObjectNoErr(gHandleTable,
UserHMGetHandle(pcurOld), TYPE_CURSOR)))
{
hOldCursor = UserHMGetHandle(pcurOld);
/* 系统全局光标可能被返回两次,注释:修 SeaMonkey 跨边界崩溃 */
if (pcurOld->CURSORF_flags & CURSORF_GLOBAL) { /* 不递减引用 */ }
if (UserObjectInDestroy(hOldCursor)) hOldCursor = NULL;
pcurOld->CURSORF_flags &= ~CURSORF_CURRENT;
UserDereferenceObject(pcurOld);
}
leave:
UserLeave();
return hOldCursor; // 返回旧光标句柄(可能 NULL)
}
行为要点:
- 传入 NULL 表示"清除光标"(对应
SetCursor(NULL)),此时只做切换不设 CURRENT 标志。 - 返回旧光标句柄 ,语义与 Win32
SetCursor一致。注释解释了CURSORF_GLOBAL分支(L1085-1101):系统全局光标初始至少有 2 个引用,若默认光标在生命周期内被返回给调用者两次,引用会降到 0 触发对象断言------这正是 SeaMonkey 鼠标跨边界时崩溃的根因,因此对全局光标不再递减引用。 CURSORF_CURRENT的用途:NtUserDestroyCursor第 3 道检查会拒绝销毁"当前光标";desktop.c的 WM_SETCURSOR 处理器也按同样模式设置/清除该标志(见第 12 节)。
5.2 UserSetCursor(msgqueue.c L90-164)--- 真正干活的人
c
PCURICON_OBJECT FASTCALL UserSetCursor(PCURICON_OBJECT NewCursor, BOOL ForceChange)
{
pti = PsGetCurrentThreadWin32Thread();
MessageQueue = pti->MessageQueue;
OldCursor = MessageQueue->CursorObject;
if (OldCursor == NewCursor) return OldCursor; // 相同则无操作
MessageQueue->CursorObject = NewCursor; // 1. 更新队列光标
if (MessageQueue->iCursorLevel < 0) return OldCursor; // 隐藏中,无需换形状
if (gpqCursor == NULL) gpqCursor = MessageQueue; // 2. 首次:登记为光标控制队列
/* 3. 只有当光标命中的顶层窗口属于本队列时才真正改屏幕 */
pWnd = IntTopLevelWindowFromPoint(gpsi->ptCursor.x, gpsi->ptCursor.y);
if (pWnd && pWnd->head.pti->MessageQueue == MessageQueue)
{
hdcScreen = IntGetScreenDC();
if (NewCursor)
{
CursorFrame = NewCursor;
if (NewCursor->CURSORF_flags & CURSORF_ACON)
{
FIXME("Should animate the cursor, using only the first frame now.\n");
CursorFrame = ((PACON)NewCursor)->aspcur[0]; // 动画退化为首帧
}
GreSetPointerShape(hdcScreen,
CursorFrame->hbmAlpha ? NULL : NewCursor->hbmMask,
CursorFrame->hbmAlpha ? NewCursor->hbmAlpha : NewCursor->hbmColor,
CursorFrame->xHotspot, CursorFrame->yHotspot,
gpsi->ptCursor.x, gpsi->ptCursor.y,
CursorFrame->hbmAlpha ? SPS_ALPHA : 0);
}
else
GreMovePointer(hdcScreen, -1, -1); // 隐藏
IntGetSysCursorInfo()->CurrentCursorObject = NewCursor;
}
return OldCursor;
}
解读:
- 队列是光标的归属单位 :每个
USER_MESSAGE_QUEUE各有一个CursorObject。SetCursor只改本线程队列的指针;屏幕上的实际形状由"鼠标命中的顶层窗口属于哪个队列"决定------这是多线程窗口体系下"谁的窗口在鼠标下,谁的光标生效"的关键。命中窗口属于别的队列时,本队列的修改只是"预存",等鼠标移回本窗口(co_IntProcessMouseMessage处理 WM_MOUSEMOVE 时,msgqueue.c L664-702)才真正GreSetPointerShape。 - 隐藏中不换形状 :
iCursorLevel < 0(队列被 ShowCursor(FALSE) 隐藏)时只更新指针,不动屏幕------下次 ShowCursor(TRUE) 时会按新形状显示。 - 动画退化为首帧 :L140 的
FIXME说明当前 win32k 尚未实现光标动画播放 (对应经典 Windows 的CursorServiceThreadProc/IntAnimateCursor线程,见第 10 节),只画第一帧。 ForceChange参数在本文件内都是 FALSE;display.c(模式切换)用UserSetCursor(NULL, TRUE)强制隐藏再恢复(display.c L837-843)。
5.3 IntSystemSetCursor(cursoricon.c L229-236)
c
PCURICON_OBJECT IntSystemSetCursor(PCURICON_OBJECT pcurNew)
{
PCURICON_OBJECT pcurOld = UserSetCursor(pcurNew, FALSE);
if (pcurNew) UserReferenceObject(pcurNew);
if (pcurOld) UserDereferenceObject(pcurOld);
return pcurOld;
}
UserSetCursor 本身不加引用 (队列 CursorObject 是裸指针,生命周期由队列持有/释放管理)。IntSystemSetCursor 是为 DefWndHandleSetCursor 这类"把系统光标设到当前队列"的调用准备的:新光标补一次引用(防御队列外释放),旧光标解一次引用,然后返回旧值以便调用方继续处理。defwnd.c 的 HTCLIENT/HTLEFT/... 分支都通过它切换(见第 12 节)。
5.4 GetCursor / NtUserGetThreadState(THREADSTATE_GETCURSOR)
user32 GetCursor(cursoricon.c L3082-3085):
c
HCURSOR WINAPI GetCursor(void)
{
return (HCURSOR)NtUserGetThreadState(THREADSTATE_GETCURSOR);
}
内核侧(misc.c L316-319):
c
case THREADSTATE_GETCURSOR:
ret = (DWORD_PTR)(GetW32ThreadInfo()->MessageQueue->CursorObject ?
UserHMGetHandle(GetW32ThreadInfo()->MessageQueue->CursorObject) : 0);
即返回本线程队列 的 CursorObject 句柄------即使该队列并未实际控制屏幕指针。NtUserGetCursorInfo(见 5.5)返回的 hCursor 则来自全局 gSysCursorInfo.CurrentCursorObject,两者语义不同:前者"本线程设过什么",后者"屏幕上正在显示什么"。
5.5 NtUserGetCursorInfo(cursoricon.c L647-698)
c
BOOL APIENTRY NtUserGetCursorInfo(PCURSORINFO pci)
{
CurInfo = IntGetSysCursorInfo();
CurIcon = CurInfo->CurrentCursorObject;
SafeCi.cbSize = sizeof(CURSORINFO);
SafeCi.flags = ((CurIcon && CurInfo->ShowingCursor >= 0) ? CURSOR_SHOWING : 0);
SafeCi.hCursor = (CurIcon ? UserHMGetHandle(CurIcon) : NULL);
SafeCi.ptScreenPos = gpsi->ptCursor;
...ProbeForWrite + cbSize 校验,错误置 ERROR_INVALID_PARAMETER...
}
flags的CURSOR_SHOWING需要同时满足 :有当前光标对象 且 全局显隐计数ShowingCursor >= 0(没被隐藏)。ptScreenPos直接取全局gpsi->ptCursor(这是鼠标输入管线的"光标位置"权威值,见第 6 节)。- 调用方需先置
pci->cbSize = sizeof(CURSORINFO),否则返回ERROR_INVALID_PARAMETER(L674-682),与 Win32 行为一致。
6. 光标位置与裁剪
6.1 UserSetCursorPos(cursoricon.c L238-282)
c
BOOL UserSetCursorPos(INT x, INT y, DWORD flags, ULONG_PTR dwExtraInfo, BOOL Hook)
{
DesktopWindow = UserGetDesktopWindow();
CurInfo = IntGetSysCursorInfo();
/* 裁剪:bClipped 时用 rcClip,否则用桌面客户区 */
if (!CurInfo->bClipped) rcClip = DesktopWindow->rcClient;
else rcClip = CurInfo->rcClip;
/* 把坐标夹到裁剪矩形内 */
if (x >= rcClip.right) x = rcClip.right - 1;
...
if (x == gpsi->ptCursor.x && y == gpsi->ptCursor.y) return TRUE; // 无变化
/* 1. 生成 WM_MOUSEMOVE(顺带更新 htEx 与 TrackWindow)*/
Msg.message = WM_MOUSEMOVE;
Msg.wParam = UserGetMouseButtonsState();
Msg.lParam = MAKELPARAM(x, y);
Msg.pt = pt;
co_MsqInsertMouseMessage(&Msg, flags, dwExtraInfo, Hook);
/* 2. 存储新位置 */
gpsi->ptCursor = pt;
return TRUE;
}
- 这是
SetCursorPos/NtUserxSetCursorPos(→TWOPARAM_ROUTINE_SETCURSORPOS,ntwrapper.h L675-678)的内核实现,也被UserClipCursor、nonclient.c拖拽循环内部调用(如 nonclient.c L484、L524)。 - 精髓:移动光标 = 注入一条 WM_MOUSEMOVE 消息 。所有命中测试(
htEx)、WM_SETCURSOR 重新评估、WM_MOUSELEAVE/HOVER 跟踪都顺带完成,光标位置gpsi->ptCursor与消息流严格一致。 flags/dwExtraInfo/Hook透传给co_MsqInsertMouseMessage(mouse 消息注入管道)。
6.2 UserClipCursor / NtUserClipCursor(L700-787)
NtUserClipCursor先ProbeForRead拷贝RECTL(L761-777),然后UserEnterExclusive调内部UserClipCursor(L782)。UserClipCursor校验调用者拥有WINSTA_WRITEATTRIBUTES权限(L708),把请求矩形与桌面窗口矩形相交得到有效裁剪区(注释特别说明不用RECTL_bIntersectRect,因为空矩形会被置成 0,0,0,0,导致 monitor winetest 失败,L727-737),置bClipped = TRUE后立即UserSetCursorPos(当前坐标...)把光标拉回裁剪区(L740)。prcl == NULL表示解除裁剪(bClipped = FALSE,L742-745)。
6.3 NtUserGetClipCursor(L996-1043)
需要 WINSTA_READATTRIBUTES;bClipped 时返回 rcClip,否则返回整个屏幕 {0,0,SM_CXSCREEN,SM_CYSCREEN}(L1018-1028);用 MmCopyToCaller 安全写回。
6.4 ONEPARAM_ROUTINE_GETCURSORPOS(simplecall.c L326-348)
c
case ONEPARAM_ROUTINE_GETCURSORPOS:
pti = PsGetCurrentThreadWin32Thread();
if (pti->rpdesk != IntGetActiveDesktop()) { Result = FALSE; break; } // 桌面不符
ProbeForWrite(...); *pptl = gpsi->ptCursor;
即 user32 GetCursorPos(cursoricon.c L3068-3073)的落点。注意桌面校验:只有活动桌面的线程能读全局光标位置。
7. 创建光标/图标:两步式(IntCreateCurIconHandle + SetCursorIconData)
7.1 传统 API 在当前源码中的落点
任务清单中的 NtUserCreateCursor / NtUserCreateIcon 在当前源码树中不存在(win32u.spec 无对应条目)。它们的职责由下面两个原语组合完成:
| 传统 API | 当前实现 |
|---|---|
| CreateCursor(hInst, xHot, yHot, w, h, pvAND, pvXOR) | user32 CreateCursor(L3017-3042):用 CreateBitmap 造 AND/XOR 位图 → CreateIconIndirect |
| CreateIcon(hInst, w, h, planes, bitsPixel, lpbAND, lpbXOR) | user32 CreateIcon(L2768-2804):同上,fIcon=TRUE |
| CreateIconIndirect(&ICONINFO) | user32 CreateIconIndirect(L2979-3015):CURSORICON_GetCursorDataFromIconInfo → NtUserxCreateEmptyCurObject + NtUserSetCursorIconData |
| NtUserCreateIconFromResourceEx | 不存在;CreateIconFromResourceEx(user32 L2816)纯用户态实现后走同一两步式 |
user32.spec 中只有 CreateCursor/CreateIcon 两个导出,它们内部调 CreateIconIndirect。
7.2 CreateCursor / CreateIcon / CreateIconIndirect(user32)
c
HCURSOR WINAPI CreateCursor(HINSTANCE hInst, int xHotSpot, int yHotSpot,
int nWidth, int nHeight,
const VOID *pvANDPlane, const VOID *pvXORPlane) // L3017
{
info.fIcon = FALSE;
info.xHotspot = xHotSpot; info.yHotspot = yHotSpot;
info.hbmMask = CreateBitmap(nWidth, nHeight, 1, 1, pvANDPlane);
info.hbmColor = CreateBitmap(nWidth, nHeight, 1, 1, pvXORPlane);
hCursor = CreateIconIndirect(&info);
DeleteObject(info.hbmMask); DeleteObject(info.hbmColor); // 位图所有权已转移
return hCursor;
}
CreateIcon(L2768)几乎相同,区别是:fIcon = TRUE、热点取 nWidth/2, nHeight/2;若 cPlanes*cBitsPixel > 1 用 CreateBitmap(nWidth, nHeight, cPlanes, cBitsPixel, lpbXORbits) 造彩色位图 + 单独 1bpp AND 掩码,否则单色方案 hbmMask = CreateBitmap(nWidth, nHeight*2, 1, 1, lpbANDbits) 且 hbmColor = NULL(L2787-2796)。
CreateIconIndirect(L2979-3015)是共同核心:
c
ZeroMemory(&cursorData, sizeof(cursorData));
if(!CURSORICON_GetCursorDataFromIconInfo(&cursorData, piconinfo)) return NULL;
hiconRet = NtUserxCreateEmptyCurObject(FALSE); // 1. 空对象
if(!NtUserSetCursorIconData(hiconRet, NULL, NULL, &cursorData)) // 2. 填数据
{ NtUserDestroyCursor(hiconRet, FALSE); goto end_error; }
CURSORICON_GetCursorDataFromIconInfo(L1005 起)负责把 ICONINFO(fIcon/xHotspot/yHotspot/hbmMask/hbmColor)翻译成 CURSORDATA:按 fIcon 填 rt = RT_ICON/RT_CURSOR,用 GetObject 取位图尺寸填 cx/cy,位图直接放进 hbmMask/hbmColor(所有权随对象转移,所以 CreateCursor 之后能安全 DeleteObject 原位图)。失败时 end_error 清理三张位图。
7.3 NtUserSetCursorIconData(cursoricon.c L1509-1680)
c
__kernel_entry BOOL APIENTRY NtUserSetCursorIconData(
HCURSOR hcursor, PUNICODE_STRING pustrModule,
PUNICODE_STRING pustrRsrc, const CURSORDATA* pCursorData)
流程:
- SEH 探测 :
ProbeForRead拷贝CURSORDATA本体(L1538-1539)。 - ACON 分支 (
CURSORF_ACON置位):校验0 < cpcur/cicur <= 1000(L1545-1551);在内核池PagedPool一次性分配cpcur*sizeof(CURSORDATA) + cicur*(sizeof(DWORD)+sizeof(INT)),然后对aspcur/aicur/ajifRate三个用户数组逐一ProbeForRead+RtlCopyMemory(L1553-1594)------彻底消除 TOCTOU 竞态。 - 非 ACON:三个指针置 NULL(L1596-1602)。
- 字符串捕获 :模块名
ProbeAndCaptureUnicodeString(L1615);资源名ProbeAndCaptureUnicodeStringOrAtom(L1627,兼容 INTRESOURCE)。 - 标志校验 :
CURSORF_flags & ~CURSORF_USER_MASK非零 → 拒绝(L1636-1640)。 UserEnterExclusive→ 内部UserSetCursorIconData→UserLeave。- 清理:释放捕获的模块名、
pvBuffer;失败时补释放ustrRsrc.Buffer(L1654-1675)。
7.4 UserSetCursorIconData(内部,L1419-1503)
c
BOOL APIENTRY UserSetCursorIconData(HCURSOR hcursor, PUNICODE_STRING pustrModule,
PUNICODE_STRING pustrRsrc, PCURSORDATA pcursordata)
- 有模块名时
RtlAddAtomToAtomTable生成atomModName(L1433-1450)。 UserGetCurIconObject(hcursor)引用对象(L1453)。- 按
CURSORF_ACON分流:IntSetAconData或IntSetCursorData(L1461-1475)。 - 成功后若对象带
CURSORF_LRSHARED则IntInsertCursorIntoList入链表(L1480-1488)------共享标志在数据填充时才生效,空对象阶段不进链表。 - 失败时回滚原子(L1491-1496);最后解除对象引用。
7.5 IntSetCursorData(L1130-1236)--- 填充普通对象
c
if (pcursordata->CURSORF_flags & CURSORF_ACON) → 不匹配,拒绝(对象不是 ACON)
if (pcur->hbmMask != NULL) → "Cursor data already set!",拒绝二次填充
if (pcursordata->hbmMask == NULL) → ERROR_INVALID_PARAMETER
/* 三张位图逐一转移所有权到 GDI_OBJ_HMGR_PUBLIC(失败回滚前面已转移的)*/
GreSetBitmapOwner(hbmMask, GDI_OBJ_HMGR_PUBLIC);
GreSetBitmapOwner(hbmColor, ...); GreSetBitmapOwner(hbmAlpha, ...);
/* 释放旧的 strName / atomModName(当前必须为 NULL,NT_ASSERT)*/
pcur->atomModName = atomModName;
pcur->rt = pcursordata->rt;
pcur->CURSORF_flags = pcursordata->CURSORF_flags & CURSORF_USER_MASK;
pcur->xHotspot/yHotspot = ...;
pcur->hbmMask/hbmColor/hbmAlpha = ...;
pcur->rcBounds = {0,0,cx,cy};
pcur->hbmUserAlpha = ...; pcur->bpp = ...; pcur->cx = ...; pcur->cy = ...;
if (pustrName) pcur->strName = *pustrName;
注意错误处理顺序:掩码必须存在;彩色/alpha 位图转移所有权失败时回滚已转移的位图(L1169-1194),保证不泄漏所有权。
7.6 IntSetAconData(L1238-1417)--- 填充动画对象
c
NT_ASSERT((pacon->CURSORF_flags & CURSORF_ACON) != 0);
for (i = 0; i < pcursordata->cicur; i++)
if (pcursordata->aicur[i] >= pcursordata->cpcur) → 越界,拒绝
/* 内核池分配:cpcur 个对象指针 + cicur 个 DWORD 序列 + cicur 个 INT 速率 */
cjSize = cpcur*sizeof(PCURICON_OBJECT) + cicur*(sizeof(DWORD)+sizeof(INT));
aspcur = ExAllocatePoolWithTag(PagedPool, cjSize, USERTAG_CURSOR);
for (i = 0; i < cpcur; i++)
{
hcurFrame = IntCreateCurIconHandle(FALSE); // 每个帧一个普通对象
aspcur[i] = UserGetCurIconObject(hcurFrame);
if (pcdFrame->CURSORF_flags & ~(CURSORF_USER_MASK|CURSORF_ACONFRAME)) → 拒绝
IntSetCursorData(aspcur[i], NULL, 0, &pcdFrame[i]); // 帧数据(无名称/模块)
aspcur[i]->CURSORF_flags |= CURSORF_ACONFRAME;
}
pacon->cpcur/cicur/aspcur/aicur/ajifRate/iicur = ...;
- 帧校验:
aicur[i]必须小于cpcur(L1278-1286);帧标志只允许CURSORF_USER_MASK|CURSORF_ACONFRAME(L1337)。 Cleanup标签处理部分创建失败的清理:逐个销毁已建帧 + 释放池(L1400-1416)。- 之后
NtUserSetCursorIconData在退出时释放内核池中的aspcur拷贝(L1662-1665),真正的帧数组常驻对象。
8. 加载流程:LoadCursor / LoadIcon / CreateIconFromResourceEx / CopyImage
8.1 API 到 LoadImage 的统一路由
user32/windows/cursoricon.c:
| API | 实现 |
|---|---|
| LoadCursorA/W(L2463-2491) | `LoadImageA/W(hinst, name, IMAGE_CURSOR, 0, 0, LR_SHARED |
| LoadIconA/W(L2433-2461) | `LoadImageA/W(..., IMAGE_ICON, ..., LR_SHARED |
| LoadCursorFromFileA/W(L2493-2519) | `LoadImageW(NULL, file, IMAGE_CURSOR, 0, 0, LR_LOADFROMFILE |
| LoadBitmapA/W(L2521-2549) | LoadImageW(..., IMAGE_BITMAP, ..., 0) |
| LoadImageA(L2551-2574) | ANSI→Wide 转换后调 LoadImageW(INTRESOURCE 直接透传) |
| LoadImageW(L2576-2600) | 按 uType 分发:IMAGE_BITMAP→BITMAP_LoadImageW;IMAGE_CURSOR/ICON→CURSORICON_LoadImageW(hinst, lpszName, cx, cy, fuLoad, bIcon) |
CURSORICON_LoadImageW(L1724-1957)是光标/图标的统一装载器:
LR_DEFAULTSIZE:cx/cy 为 0 时取GetSystemMetrics(SM_CXICON/CXCURSOR)(L1745-1749)。LR_LOADFROMFILE→CURSORICON_LoadFromFileW(L1751-1754,见 8.3)。hinst == NULL(OEM):改指User32Instance,并把IDI_APPLICATION..IDI_SHIELD映射为 100... 段资源号(IDI_ERROR与IDI_WARNING互换,L1756-1773)------这是 ReactOS 对标准图标的 OEM 资源排布。- 构造
ustrModule(LDR_IS_RESOURCE时伪造\x01%08IX名字,否则GetModuleFileNameW,L1788-1835)与ustrRsrc(INTRESOURCE 或字符串,L1775-1786)。 - LR_SHARED 查重 :
NtUserFindExistingCursorIcon(&ustrModule, &ustrRsrc, ¶m),命中直接返回(L1837-1854)。 - 资源解析:
FindResourceW(RT_GROUP_ICON/RT_GROUP_CURSOR)→LockResource→LookupIconIdFromDirectoryEx选最合适的条目 id → 再FindResourceW(RT_ICON/RT_CURSOR)拿具体位图(L1856-1894)。 - 光标资源(
dir->idType == 2)前 4 字节是两个 SHORT 热点,解析后bits += 4(L1901-1908)。 CURSORICON_GetCursorDataFromBMI把 DIB 转成位图对;CURSORF_flags = CURSORF_FROMRESOURCE;LR_SHARED时附加CURSORF_LRSHARED(L1896-1933)。- 两步式创建:
NtUserxCreateEmptyCurObject(FALSE)+NtUserSetCursorIconData;失败NtUserDestroyCursor(hCurIcon, TRUE)强制回滚(L1924-1942)。
8.2 CURSORICON_GetCursorDataFromBMI(L855 起)--- DIB → 位图对
图标/光标资源是"一张 BITMAPINFO,高度 = 实际高度的 2 倍(上半图像、下半 AND 掩码)"。该函数:
bitmap_info_size+is_dib_monochrome判断单色(L860-861);DIB_GetBitmapInfo解出 width/height/bpp/compr(L872),BI_RGB之外拒绝(L878)。- 尺寸缺省时
pdata->cy = height/2(L882-883);热点换算:光标按比例缩放热点(L886-892),图标热点=中心(L893-897)。 - 建屏幕 DC 与兼容 DC(L899-907);把
biHeight/2的副本 DIB 用StretchDIBits画进位图(L909-959):- 单色:
hbmMask = CreateBitmap(cx, cy*2, 1,1)上半存图像(L926-942); - 彩色:
hbmColor = CreateCompatibleBitmap+ 1bpphbmMask+create_alpha_bitmap生成 alpha 面(L943-961);
- 单色:
- 掩码面从彩色图转换:把调色板压成黑/白 1bpp 再
StretchDIBits画进掩码(L963 起)。
bpp 取 GetDeviceCaps(hdcScreen, BITSPIXEL)(L960),与 UserDrawIconEx 的 EXLATEOBJ 色彩转换配合。
8.3 CURSORICON_LoadFromFileW(L1628-1721)--- .cur/.ico 文件
map_fileW(L338-358)用CreateFileMapping + MapViewOfFile只读映射文件。RIFF头 → 是 .ani,当前UNIMPLEMENTED(L1651-1655)------动画光标的文件加载在 user32 层尚未实现 ,但CreateIconFromResourceEx内存路径支持 ANI(见 8.5)。- 否则解析
CURSORICONFILEDIR(.ico/.cur 目录),get_best_icon_file_entry按目标尺寸挑选条目(L1657-1658);光标取entry->xHotspot/yHotspot(L1666-1670)。 - DIB 直接失败则尝试 PNG:
CURSORICON_ConvertPngToBmpIcon(L1676-1698,Vista 起图标可用 PNG 压缩)。 - 同样两步式创建。
8.4 系统光标加载链:LoadSystemCursors → co_IntLoadDefaultCursors → User32SetupDefaultCursors
初始化时序(winsta.c L335-339):
c
/* Setup the cursor */
co_IntLoadDefaultCursors();
/* Setup the icons */
co_IntSetWndIcons();
co_IntLoadDefaultCursors(callback.c L470-503):KeUserModeCallback(USER32_CALLBACK_LOADDEFAULTCURSORS, &DefaultCursor, sizeof(BOOL), ...),把结果写入全局gDesktopCursor(L500)。断言当前线程不是桌面线程(桌面线程不能做用户态回调,L479)。- user32 回调
User32SetupDefaultCursors(L295-321):先LoadSystemCursors()(L304),然后LoadCursorW(0, IDC_ARROW)+SetCursor设为默认(L306-318),ZwCallbackReturn返回句柄。 LoadSystemCursors(L270-292)为全部 16 个 OCR_* 调LoadImageW(0, IDC_*, IMAGE_CURSOR, 0, 0, LR_DEFAULTSIZE)后NtUserSetSystemCursor(hcur, OCR_*)。注释说明:曾用gpsi->hIconSmWindows做过载保护,但那是小 WINLOGO 图标与光标加载状态无关,故去掉守卫、无条件加载------因为NtUserSetSystemCursor内部已安全跳过已存在的项(见第 11 节)。
8.5 CreateIconFromResourceEx(user32 L2816-2977)
c
HICON WINAPI CreateIconFromResourceEx(PBYTE pbIconBits, DWORD cbIconBits, BOOL fIcon,
DWORD dwVersion, int cxDesired, int cyDesired, UINT uFlags)
- 空指针或
cbIconBits < 2*sizeof(DWORD)拒绝(L2834-2838)。 LR_DEFAULTSIZE补全尺寸(L2840-2844);cursorData.rt = LOWORD(fIcon ? RT_ICON : RT_CURSOR)(L2846-2849)。- 数据
memcmp(pbIconBits, "RIFF", 4) == 0→ .ani 内存格式 :CURSORICON_GetCursorDataFromANI(L2852-2860)。 - 否则可能是指向图标目录 的指针(
LookupIconIdFromDirectoryEx返回值非 0 且第 5 字节不是 BITMAPINFOHEADER 大小,L2864-2870):用GetModuleHandleEx(FROM_ADDRESS)反查所属模块 → 校验idType与fIcon匹配 →FindResourceW+LoadResource取出真实位图(L2878-2911)。 - 光标数据前 4 字节是热点(L2912-2918)。
CURSORICON_GetCursorDataFromBMI失败则 PNG 转换兜底(L2923-2941)。- 单帧/动画分支:
isAnimated = !!(cursorData.CURSORF_flags & CURSORF_ACON);uFlags & LR_SHARED时附加CURSORF_LRSHARED(L2947-2948)。 - 收尾:
isAnimated为真时NtUserxCreateEmptyCurObject(TRUE)(分配 ACON),否则FALSE(L2950);随后NtUserSetCursorIconData(hIcon, NULL, NULL, &cursorData)(L2954-2959);失败路径end_error清理pbBmpIcon、cursorData.aspcur与三张位图(L2968-2976)。 - 由此,
CreateIconFromResourceEx成为 .ani 动画光标唯一可用的加载入口 (文件路径CURSORICON_LoadFromFileW对 RIFF 返回UNIMPLEMENTED)。
8.6 CURSORICON_GetCursorDataFromANI(user32 L1162-1334)--- .ani 解析
用 RIFF 分块遍历解析 .ani 内存数据:
riff_find_chunk(ANI_ACON_ID, ANI_RIFF_ID, ...)定位根块,再取anih(ANI 头)块(L1181-1196)。- 头字段:
cpcur = num_frames、cicur = num_steps、iicur = display_rate,置CURSORF_ACON(L1198-1202)。 - 可选块:
seq(序列,ANI_FLAG_SEQUENCE时必读)、rate(每步速率,L1204-1218)。 LIST fram内逐帧:帧数据是"图标文件"(ANI_FLAG_ICON,用get_best_icon_file_entry挑条目)或"裸位图"(L1255-1291);每帧用CURSORICON_GetCursorDataFromBMI转位图对,多帧时置CURSORF_ACONFRAME(L1293-1299)。- 单帧(
num_frames <= 1)直接返回------此时CURSORF_ACON已在 L1199 置位,但 L1299 会清除单帧的 ACONFRAME 且对象仍按普通光标创建(调用方isAnimated按 flags 判断为真则建 ACON)。 - 多帧:填
ajifRate(无 rate 块用 display_rate 填充)与aicur(无 seq 块用0,1,2,...顺序)(L1307-1328);失败error释放aspcur并清零CURSORDATA(L1330-1333)。
8.7 CopyImage:user32 主实现 + 内核回调
任务清单中的 NtUserCopyImage / IntCopyImage 在当前源码中不存在。ReactOS 的实现是"user32 主 + 内核回调"的镜像结构:
- user32
CopyImage(L2311-2382):先校验fuFlags只含COPYIMAGE_VALID_FLAGS(L2305-2309);按uType分发------IMAGE_BITMAP→BITMAP_CopyImage;IMAGE_CURSOR/ICON→CURSORICON_CopyImage(hImage, uType==IMAGE_ICON, cx, cy, fuFlags)(L2328-2376)。LR_COPYFROMRESOURCE且失败时,用GetIconInfo检查尺寸一致则去掉该标志重试(L2347-2374)。 - CURSORICON_CopyImage (L2153-2284):
LR_COPYFROMRESOURCE:NtUserGetIconInfo取模块/资源名 →LoadLibraryExW(..., LOAD_LIBRARY_AS_DATAFILE)→CURSORICON_LoadImageW重载(L2167-2247);- 普通复制:
GetIconInfo取位图 →CURSORICON_GetCursorDataFromIconInfo→NtUserxCreateEmptyCurObject+NtUserSetCursorIconData;LR_SHARED附加CURSORF_LRSHARED,LR_COPYDELETEORG复制后销毁原图(L2249-2283)。
- User32CallCopyImageFromKernel (L2286-2300):
USER32_CALLBACK_COPYIMAGE回调处理例程,转发给CopyImage。 - co_IntCopyImage (callback.c L984-1036):内核侧入口,打包
COPYIMAGE_CALLBACK_ARGUMENTS后KeUserModeCallback(USER32_CALLBACK_COPYIMAGE, ...),把结果句柄带回内核。class.c注册窗口类时用它把大图标缩小成小图标(class.c L2065/2075/2135/2145),window.c处理 WM_GETICON 时也用(window.c L1934/1944)。
9. 绘制:DrawIconEx / UserDrawIconEx
9.1 NtUserDrawIconEx(cursoricon.c L2084-2126)
c
BOOL APIENTRY NtUserDrawIconEx(HDC hdc, int xLeft, int yTop, HICON hIcon,
int cxWidth, int cyHeight, UINT istepIfAniCur,
HBRUSH hbrFlickerFreeDraw, UINT diFlags,
BOOL bMetaHDC, PVOID pDIXData) // bMetaHDC:元文件 DC 时需在 User32 处理 GDI
UserGetCurIconObject(hIcon)引用对象后调内部UserDrawIconEx,退出时解引用(L2105-2122)。bMetaHDC/pDIXData是为元文件(metafile)DC 预留的("When TRUE, GDI functions need to be handled in User32!"),本实现未使用。user32DrawIconEx(L2401-2416)把这两个参数传 0;DrawIcon(L2391-2399)则用DI_NORMAL | DI_COMPAT | DI_DEFAULTSIZE调用 DrawIconEx。
9.2 UserDrawIconEx(L1690-2079)--- 低层位图合成
文件头注释(L1682-1689)说明了为什么不用高层 GDI:
- 此时图标位图位深可能不同于 DC(模式切换后会这样),无法用 CreateCompatibleDC + SelectObject;
- 避免大规模 GDI 对象加锁(只需锁目标表面);
- 有(少量)性能收益。
流程:
- 参数检查:
diFlags & DI_NORMAL为 0 → 拒绝(L1712-1716)。 - 动画帧选择 :ACON 时校验
istepIfAniCur < cicur,取pIcon = aspcur[aicur[istepIfAniCur]](L1718-1727)------istepIfAniCur是"播放第几步",用于DrawIconEx手动逐帧绘制动画图标。 SURFACE_ShareLockSurface共享锁住掩码/彩色位图(只读,L1737-1756);DC_LockDc锁目标 DC(L1758-1765)。- 尺寸缺省:
DI_DEFAULTSIZE→ 图标取SM_CXICON/SM_CYICON、光标取SM_CXCURSOR/SM_CYCURSOR;否则用pIcon->cx/cy(L1768-1785)。 - 目标矩形:
RECTL_vSetRect(rcDest, xLeft, yTop, xLeft+cxWidth, yTop+cyHeight)→IntLPtoDP(逻辑转设备)+ 加ptlDCOrig偏移(L1787-1790);DC_vPrepareDCsForBlit准备合成(L1793)。 - 防闪烁背景 :
hbrFlickerFreeDraw是画刷句柄时,IntEngBitBlt(..., ROP4_PATCOPY)用画刷预涂目标区(L1812-1895)。注意#if 0块(L1829-1894)里"离屏表面再回拷"的旧方案被禁用------注释说全程已锁目标表面,离屏无意义,改为直接PATCOPY到目标。 - alpha 路径 :
hbmAlpha && (diFlags & DI_NORMAL) == DI_NORMAL→IntEngAlphaBlend(AC_SRC_OVER + AC_SRC_ALPHA的BLENDOBJ,L1905-1933)。 - 掩码步 :
diFlags & DI_MASK→IntEngStretchBlt,ROP4 为DI_IMAGE ? ROP4_SRCAND : ROP4_SRCCOPY(L1935-1961)------AND 掩码先"镂空"目标像素。 - 图像步 :
diFlags & DI_IMAGE:- 有彩色位图:ROP4 =
DI_MASK ? ROP4_SRCINVERT : ROP4_SRCCOPY(L1965-1991); - 无彩色位图(单色):源矩形下移
pIcon->cy用掩码位图下半部SRCINVERT(L1992-2020)。
- 有彩色位图:ROP4 =
- 清理:
DC_vFinishBlit+DC_UnlockDc+ 解锁各表面(L2061-2078)。
掩码/图像两步合成的 ROP4 数学:
DI_MASK|DI_IMAGE时先SRCAND(目标 &= 掩码)再SRCINVERT(目标 ^= 彩色),等价于"掩码 1 处显示彩色、掩码 0 处保留底色",实现透明。
10. 光标掩码机制(AND/XOR 与单色/彩色/alpha)
10.1 经典单色光标
- 数据模型:一张
nWidth × 2*nHeight的 1bpp 位图。上半 = AND 掩码,下半 = XOR 图像;hbmColor == NULL。 - 绘制算法(GDI 引擎
IntShowMousePointer,mouse.c L249-322):SRCAND画上半(把掩码 0 的像素清 0)→ 源矩形下移Size.cy后SRCINVERT画下半(XOR 异或出形状)。这样得到三种效果:掩码 1/图像 0 = 黑、掩码 1/图像 1 = 白、掩码 0 = 透过(原像素保留)。 - 单色光标与"热点":
GreSetPointerShape收到的xHot/yHot决定指针尖位置;引擎保存HotSpot供显示/恢复换算。
10.2 彩色光标/图标
hbmColor(屏幕兼容位深)+ 独立 1bpphbmMask。绘制仍是"先 AND 镂空、后 XOR 上色",EXLATEOBJ负责调色板/位深转换(GreSetPointerShapeL756-757;UserDrawIconExL1969)。- 掩码的"1"意味着"该像素显示图像","0"意味着"该像素让底色透过"------因此彩色光标的透明形状靠掩码勾勒。
10.3 alpha 半透明
hbmAlpha(8bpp)+ 彩色面,SPS_ALPHA标志;UserSetCursor/co_IntProcessMouseMessage传SPS_ALPHA时hbmMask传 NULL、把hbmAlpha当彩色面传给GreSetPointerShape(msgqueue.c L143-150、L675-684)。- 引擎
IntShowMousePointer的SPS_ALPHA分支用IntEngAlphaBlend按 alpha 合成(mouse.c L277-293);UserDrawIconEx同样走IntEngAlphaBlend(L1905-1933)。 - 选择逻辑统一为:
hbmAlpha ? (mask=NULL, color=alpha) : (mask=hbmMask, color=hbmColor)。
10.4 热点
- 光标:加载/创建时给出(.cur 头两个 SHORT、
CreateCursor参数、CURSORICON_GetCursorDataFromBMI按缩放比例换算 L886-892);图标:固定取cx/2, cy/2(L893-897)。 - 作用点:
GreSetPointerShape的xHot/yHot→ 引擎ptlPointer = 屏幕坐标 - HotSpot计算指针绘制矩形(mouse.c L170-171、L220-221)。 NtUserGetIconInfo原样回填xHotspot/yHotspot(cursoricon.c L459-460)。
11. 动画光标:现状与缺口
任务清单中的 CursorServiceThreadProc / CursorMotionThreadProc / IntAnimateCursor 在当前源码树中不存在。ReactOS 对动画光标的支持目前停留在"数据结构 + 加载 + 查询"层面:
| 环节 | 状态 | 证据 |
|---|---|---|
| 数据结构(ACON) | 完整 | cursoricon.h L29-43 |
| 内存加载(.ani → ACON) | 完整(user32) | CURSORICON_GetCursorDataFromANI L1162 |
| 传输(CURSORDATA → ACON) | 完整 | IntSetAconData L1238 |
| 查询(帧/速率/步数) | 完整 | NtUserGetCursorFrameInfo L2131 |
| 绘制指定帧(DrawIconEx) | 完整 | UserDrawIconEx L1718-1727 |
| 播放(按时序自动换帧) | 未实现 | UserSetCursor 中 FIXME("Should animate the cursor, using only the first frame now."),msgqueue.c L140-141 |
- 经典 Windows 的
CursorServiceThreadProc(系统定时器驱动、约 1/1024s jiffies 基准)与CursorMotionThreadProc(按ajifRate推进iicur步进、换帧后GreSetPointerShape)在 ReactOS 中没有对应线程;ACON.iicur字段(当前步进)因此只在IntSetAconData初始化时被写 0,再无消费者。 - 后果:
SetCursor一个.ani光标时屏幕上只显示第一帧(aspcur[0]);动画只在DrawIconEx(istepIfAniCur=...)手动逐帧绘制时可见。 NtUserGetCursorFrameInfo(cursoricon.c L2131-2196):ACON 时校验istep < cicur,回填ajifRate[istep](jiffies)与cicur(总步数),返回当前步对应帧的句柄;非 ACON 时返回对象自身句柄、steps=1。user32GetCursorFrameInfo(L3094-3099)直接转发。NtUserGetIconSize(L588-641):ACON 时取aspcur[0]的 cx/cy 返回。
12. 系统光标设置:SetSystemCursor / NtUserSetSystemCursor
user32 SetSystemCursor(L3044-3058):
c
BOOL WINAPI SetSystemCursor(HCURSOR hcur, DWORD id)
{
if (hcur == NULL)
{
hcur = LoadImageW(NULL, MAKEINTRESOURCEW(id), IMAGE_CURSOR, 0, 0, LR_DEFAULTSIZE);
if (hcur == NULL) return FALSE;
}
return NtUserSetSystemCursor(hcur, id);
}
内核 NtUserSetSystemCursor(cursoricon.c L2201-2256):
c
BOOL APIENTRY NtUserSetSystemCursor(HCURSOR hcur, DWORD id)
{
UserEnterExclusive();
if (!CheckWinstaAttributeAccess(WINSTA_WRITEATTRIBUTES)) goto Exit; // 权限
if (hcur)
{
pcur = UserGetCurIconObject(hcur);
...
for (i = 0; i < 16; i++)
{
if (gasyscur[i].type == id)
{
pcurOrig = gasyscur[i].handle;
if (pcurOrig) break; // 已存在 → 走 FIXME 分支
if (ppi->W32PF_flags & W32PF_CREATEDWINORDC)
{
gasyscur[i].handle = pcur; // 登记
pcur->CURSORF_flags |= CURSORF_GLOBAL;
pcur->head.ppi = NULL; // 转全局
IntInsertCursorIntoList(pcur);
Ret = TRUE;
}
break;
}
}
if (pcurOrig)
FIXME("Need to copy cursor data or do something! pcurOrig %p new pcur %p\n", ...);
}
Exit:
UserLeave();
return Ret;
}
解读:
- 首次加载 (
LoadSystemCursors初始化路径):gasyscur[i].handle == NULL→ 直接登记当前对象为全局(CURSORF_GLOBAL+ppi = NULL+ 入全局链)。 - 重复加载 (用户
SetSystemCursor或初始化时对已加载项再调):pcurOrig非空 → 命中FIXME:当前实现不会真正替换位图 ,仅打日志。这是已知限制:Windows 语义要求替换后所有后续LoadCursor(NULL, IDC_*)返回新形状,ReactOS 暂时只支持"首次填充"。 - 权限检查
WINSTA_WRITEATTRIBUTES保证只有能写窗口站属性的进程能改系统光标。
13. 与消息/窗口系统的关系(WM_SETCURSOR 链路)
光标形状的真正"决策权"在 WM_SETCURSOR------鼠标每次移动/按键时,命中窗口被问"你要什么光标"。链路如下:
13.1 触发点
- 鼠标消息入队 :
co_IntProcessMouseMessage(msgqueue.c)在鼠标消息被应用取走/丢弃前,向命中窗口发送WM_SETCURSOR:hittest == HTERROR/HTNOWHERE时直接发送并吞掉消息(L1709-1716);- 正常路径:按钮按下时先处理
WM_MOUSEACTIVATE/WM_PARENTNOTIFY,最后co_IntSendMessage(msg->hwnd, WM_SETCURSOR, (WPARAM)msg->hwnd, MAKELONG(hittest, msg->message))(L1773-1777)。
- WM_MOUSEMOVE 处理 (msgqueue.c L664-702):若队列
CursorObject与全局CurrentCursorObject不同,立即GreSetPointerShape换形状;并同步ShowingCursor/CurrentCursorObject/gpqCursor。 - 非客户区命中 (nonclient.c L244):
co_IntSendMessage(..., WM_SETCURSOR, ..., MAKELONG(hittest, WM_MOUSEMOVE))。
13.2 默认处理:DefWndHandleSetCursor(defwnd.c L241-360)
按命中码决定系统光标(全部经 IntSystemSetCursor → UserSetCursor):
| 命中码 | 光标 |
|---|---|
| HTCLIENT | 类光标 pWnd->pcls->spcur(未设则返回 FALSE 让系统用箭头) |
| HTLEFT/HTRIGHT | SYSTEMCUR(SIZEWE)(最大化的窗口 break 走箭头) |
| HTTOP/HTBOTTOM | SYSTEMCUR(SIZENS) |
| HTTOPLEFT/HTBOTTOMRIGHT | SYSTEMCUR(SIZENWSE) |
| HTBOTTOMLEFT/HTTOPRIGHT | SYSTEMCUR(SIZENESW) |
| 其他 | SYSTEMCUR(ARROW),返回 FALSE |
WM_SETCURSOR 的 DefWindowProc(defwnd.c L1094-1109):子窗口 且命中码不是尺寸边框(HTLEFT...HTBOTTOMRIGHT)时,先给父窗口机会(父返回 TRUE 则用父的),否则走 DefWndHandleSetCursor。
13.3 桌面窗口(desktop.c L1500-1517)
c
case WM_SETCURSOR:
pcurNew = UserGetCurIconObject(gDesktopCursor);
if (!pcurNew) return TRUE;
pcurNew->CURSORF_flags |= CURSORF_CURRENT;
pcurOld = UserSetCursor(pcurNew, FALSE);
if (pcurOld) { pcurOld->CURSORF_flags &= ~CURSORF_CURRENT; UserDereferenceObject(pcurOld); }
return TRUE;
即鼠标在桌面空白处时使用 gDesktopCursor(由 co_IntLoadDefaultCursors 回调设置为 IDC_ARROW 句柄)。
13.4 类光标注册(class.c)
- 注册窗口类:
Class->spcur = lpwcx->hCursor ? UserGetCurIconObject(lpwcx->hCursor) : NULL(class.c L1136)------类光标以对象指针形式常驻类结构。 - 服务端内置类:
wc.hCursor为OCR_NORMAL的内置类在创建时被替换为UserHMGetHandle(SYSTEMCUR(ARROW))(class.c L2391-2400)。 - 类结构查询:
lpwcx->hCursor = Class->spcur ? UserHMGetHandle(Class->spcur) : NULL(class.c L2298,GetClassInfo路径)。
13.5 线程/队列切换时的光标流转
- AttachThreadInput (input.c L554-577):附着时若目标队列无光标,把源队列
CursorObject传过去(带引用),保证附着线程共享同一光标语义。 - 队列创建 (msgqueue.c L2217):
CursorObject = SYSTEMCUR(WAIT)(对齐 winetesttest_initial_cursor:新线程默认等待光标)。 - 队列销毁 (msgqueue.c L2370-2403):若当前对象正在全局显示(
CurrentCursorObject == pCursor)先GreMovePointer(-1,-1)隐藏并清空全局状态,再解引用;gpqCursor == MessageQueue时清空。 - 窗口拖动 (nonclient.c L475-476/L643-644):
UserSetCursor(DragCursor, FALSE)+UserShowCursor(TRUE)拖动中换光标,结束后UserShowCursor(FALSE)+UserSetCursor(OldCursor, FALSE)恢复。
14. 调用链(mermaid)
14.1 加载链
#mermaid-svg-RRuTcSQshVv8OBtt{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-RRuTcSQshVv8OBtt .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-RRuTcSQshVv8OBtt .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-RRuTcSQshVv8OBtt .error-icon{fill:#552222;}#mermaid-svg-RRuTcSQshVv8OBtt .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-RRuTcSQshVv8OBtt .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-RRuTcSQshVv8OBtt .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-RRuTcSQshVv8OBtt .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-RRuTcSQshVv8OBtt .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-RRuTcSQshVv8OBtt .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-RRuTcSQshVv8OBtt .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-RRuTcSQshVv8OBtt .marker{fill:#333333;stroke:#333333;}#mermaid-svg-RRuTcSQshVv8OBtt .marker.cross{stroke:#333333;}#mermaid-svg-RRuTcSQshVv8OBtt svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-RRuTcSQshVv8OBtt p{margin:0;}#mermaid-svg-RRuTcSQshVv8OBtt .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-RRuTcSQshVv8OBtt .cluster-label text{fill:#333;}#mermaid-svg-RRuTcSQshVv8OBtt .cluster-label span{color:#333;}#mermaid-svg-RRuTcSQshVv8OBtt .cluster-label span p{background-color:transparent;}#mermaid-svg-RRuTcSQshVv8OBtt .label text,#mermaid-svg-RRuTcSQshVv8OBtt span{fill:#333;color:#333;}#mermaid-svg-RRuTcSQshVv8OBtt .node rect,#mermaid-svg-RRuTcSQshVv8OBtt .node circle,#mermaid-svg-RRuTcSQshVv8OBtt .node ellipse,#mermaid-svg-RRuTcSQshVv8OBtt .node polygon,#mermaid-svg-RRuTcSQshVv8OBtt .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-RRuTcSQshVv8OBtt .rough-node .label text,#mermaid-svg-RRuTcSQshVv8OBtt .node .label text,#mermaid-svg-RRuTcSQshVv8OBtt .image-shape .label,#mermaid-svg-RRuTcSQshVv8OBtt .icon-shape .label{text-anchor:middle;}#mermaid-svg-RRuTcSQshVv8OBtt .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-RRuTcSQshVv8OBtt .rough-node .label,#mermaid-svg-RRuTcSQshVv8OBtt .node .label,#mermaid-svg-RRuTcSQshVv8OBtt .image-shape .label,#mermaid-svg-RRuTcSQshVv8OBtt .icon-shape .label{text-align:center;}#mermaid-svg-RRuTcSQshVv8OBtt .node.clickable{cursor:pointer;}#mermaid-svg-RRuTcSQshVv8OBtt .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-RRuTcSQshVv8OBtt .arrowheadPath{fill:#333333;}#mermaid-svg-RRuTcSQshVv8OBtt .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-RRuTcSQshVv8OBtt .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-RRuTcSQshVv8OBtt .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-RRuTcSQshVv8OBtt .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-RRuTcSQshVv8OBtt .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-RRuTcSQshVv8OBtt .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-RRuTcSQshVv8OBtt .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-RRuTcSQshVv8OBtt .cluster text{fill:#333;}#mermaid-svg-RRuTcSQshVv8OBtt .cluster span{color:#333;}#mermaid-svg-RRuTcSQshVv8OBtt 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-RRuTcSQshVv8OBtt .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-RRuTcSQshVv8OBtt rect.text{fill:none;stroke-width:0;}#mermaid-svg-RRuTcSQshVv8OBtt .icon-shape,#mermaid-svg-RRuTcSQshVv8OBtt .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-RRuTcSQshVv8OBtt .icon-shape p,#mermaid-svg-RRuTcSQshVv8OBtt .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-RRuTcSQshVv8OBtt .icon-shape .label rect,#mermaid-svg-RRuTcSQshVv8OBtt .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-RRuTcSQshVv8OBtt .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-RRuTcSQshVv8OBtt .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-RRuTcSQshVv8OBtt :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
命中
未命中
否
是
是
LoadCursorW/LoadIconW/LoadImageW
CURSORICON_LoadImageW user32
LR_LOADFROMFILE?
CURSORICON_LoadFromFileW
LR_SHARED 查重
NtUserFindExistingCursorIcon
FindResource RT_GROUP_ICON/CURSOR
LookupIconIdFromDirectoryEx 选条目
FindResource RT_ICON/RT_CURSOR
CURSORICON_GetCursorDataFromBMI
CURSORICON_GetCursorDataFromBMI
NtUserxCreateEmptyCurObject
NtUserSetCursorIconData
UserSetCursorIconData 内核
ACON?
IntSetCursorData
IntSetAconData
CURSORF_LRSHARED?
IntInsertCursorIntoList
14.2 设置与显示链
#mermaid-svg-CmYUib2NTciRYd8t{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-CmYUib2NTciRYd8t .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-CmYUib2NTciRYd8t .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-CmYUib2NTciRYd8t .error-icon{fill:#552222;}#mermaid-svg-CmYUib2NTciRYd8t .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-CmYUib2NTciRYd8t .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-CmYUib2NTciRYd8t .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-CmYUib2NTciRYd8t .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-CmYUib2NTciRYd8t .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-CmYUib2NTciRYd8t .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-CmYUib2NTciRYd8t .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-CmYUib2NTciRYd8t .marker{fill:#333333;stroke:#333333;}#mermaid-svg-CmYUib2NTciRYd8t .marker.cross{stroke:#333333;}#mermaid-svg-CmYUib2NTciRYd8t svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-CmYUib2NTciRYd8t p{margin:0;}#mermaid-svg-CmYUib2NTciRYd8t .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-CmYUib2NTciRYd8t .cluster-label text{fill:#333;}#mermaid-svg-CmYUib2NTciRYd8t .cluster-label span{color:#333;}#mermaid-svg-CmYUib2NTciRYd8t .cluster-label span p{background-color:transparent;}#mermaid-svg-CmYUib2NTciRYd8t .label text,#mermaid-svg-CmYUib2NTciRYd8t span{fill:#333;color:#333;}#mermaid-svg-CmYUib2NTciRYd8t .node rect,#mermaid-svg-CmYUib2NTciRYd8t .node circle,#mermaid-svg-CmYUib2NTciRYd8t .node ellipse,#mermaid-svg-CmYUib2NTciRYd8t .node polygon,#mermaid-svg-CmYUib2NTciRYd8t .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-CmYUib2NTciRYd8t .rough-node .label text,#mermaid-svg-CmYUib2NTciRYd8t .node .label text,#mermaid-svg-CmYUib2NTciRYd8t .image-shape .label,#mermaid-svg-CmYUib2NTciRYd8t .icon-shape .label{text-anchor:middle;}#mermaid-svg-CmYUib2NTciRYd8t .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-CmYUib2NTciRYd8t .rough-node .label,#mermaid-svg-CmYUib2NTciRYd8t .node .label,#mermaid-svg-CmYUib2NTciRYd8t .image-shape .label,#mermaid-svg-CmYUib2NTciRYd8t .icon-shape .label{text-align:center;}#mermaid-svg-CmYUib2NTciRYd8t .node.clickable{cursor:pointer;}#mermaid-svg-CmYUib2NTciRYd8t .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-CmYUib2NTciRYd8t .arrowheadPath{fill:#333333;}#mermaid-svg-CmYUib2NTciRYd8t .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-CmYUib2NTciRYd8t .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-CmYUib2NTciRYd8t .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-CmYUib2NTciRYd8t .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-CmYUib2NTciRYd8t .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-CmYUib2NTciRYd8t .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-CmYUib2NTciRYd8t .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-CmYUib2NTciRYd8t .cluster text{fill:#333;}#mermaid-svg-CmYUib2NTciRYd8t .cluster span{color:#333;}#mermaid-svg-CmYUib2NTciRYd8t 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-CmYUib2NTciRYd8t .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-CmYUib2NTciRYd8t rect.text{fill:none;stroke-width:0;}#mermaid-svg-CmYUib2NTciRYd8t .icon-shape,#mermaid-svg-CmYUib2NTciRYd8t .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-CmYUib2NTciRYd8t .icon-shape p,#mermaid-svg-CmYUib2NTciRYd8t .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-CmYUib2NTciRYd8t .icon-shape .label rect,#mermaid-svg-CmYUib2NTciRYd8t .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-CmYUib2NTciRYd8t .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-CmYUib2NTciRYd8t .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-CmYUib2NTciRYd8t :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
NULL
有
是
否
是
SetCursor hCursor
NtUserSetCursor
UserSetCursor msgqueue.c
命中顶层窗口属于本队列?
仅更新队列 CursorObject
NewCursor?
GreMovePointer -1,-1 隐藏
GreSetPointerShape 换形状
IntEngSetPointerShape gdi/eng/mouse.c
硬件指针?
pfnMovePointer 驱动
软件指针 EngMovePointer
IntShowMousePointer SRCAND+SRCINVERT/AlphaBlend
更新 gSysCursorInfo.CurrentCursorObject
ShowCursor bShow
NtUserxShowCursor
simplecall ONEPARAM_ROUTINE_SHOWCURSOR
UserShowCursor msgqueue.c
iCursorLevel += bShow?1:-1
命中窗口属于本队列 且 计数穿越 0/-1?
GreMovePointer 显示/隐藏 + 更新 ShowingCursor
14.3 WM_SETCURSOR 链
#mermaid-svg-3pUm9A7rzWRZ1YO0{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-3pUm9A7rzWRZ1YO0 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-3pUm9A7rzWRZ1YO0 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-3pUm9A7rzWRZ1YO0 .error-icon{fill:#552222;}#mermaid-svg-3pUm9A7rzWRZ1YO0 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-3pUm9A7rzWRZ1YO0 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-3pUm9A7rzWRZ1YO0 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-3pUm9A7rzWRZ1YO0 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-3pUm9A7rzWRZ1YO0 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-3pUm9A7rzWRZ1YO0 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-3pUm9A7rzWRZ1YO0 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-3pUm9A7rzWRZ1YO0 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-3pUm9A7rzWRZ1YO0 .marker.cross{stroke:#333333;}#mermaid-svg-3pUm9A7rzWRZ1YO0 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-3pUm9A7rzWRZ1YO0 p{margin:0;}#mermaid-svg-3pUm9A7rzWRZ1YO0 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-3pUm9A7rzWRZ1YO0 .cluster-label text{fill:#333;}#mermaid-svg-3pUm9A7rzWRZ1YO0 .cluster-label span{color:#333;}#mermaid-svg-3pUm9A7rzWRZ1YO0 .cluster-label span p{background-color:transparent;}#mermaid-svg-3pUm9A7rzWRZ1YO0 .label text,#mermaid-svg-3pUm9A7rzWRZ1YO0 span{fill:#333;color:#333;}#mermaid-svg-3pUm9A7rzWRZ1YO0 .node rect,#mermaid-svg-3pUm9A7rzWRZ1YO0 .node circle,#mermaid-svg-3pUm9A7rzWRZ1YO0 .node ellipse,#mermaid-svg-3pUm9A7rzWRZ1YO0 .node polygon,#mermaid-svg-3pUm9A7rzWRZ1YO0 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-3pUm9A7rzWRZ1YO0 .rough-node .label text,#mermaid-svg-3pUm9A7rzWRZ1YO0 .node .label text,#mermaid-svg-3pUm9A7rzWRZ1YO0 .image-shape .label,#mermaid-svg-3pUm9A7rzWRZ1YO0 .icon-shape .label{text-anchor:middle;}#mermaid-svg-3pUm9A7rzWRZ1YO0 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-3pUm9A7rzWRZ1YO0 .rough-node .label,#mermaid-svg-3pUm9A7rzWRZ1YO0 .node .label,#mermaid-svg-3pUm9A7rzWRZ1YO0 .image-shape .label,#mermaid-svg-3pUm9A7rzWRZ1YO0 .icon-shape .label{text-align:center;}#mermaid-svg-3pUm9A7rzWRZ1YO0 .node.clickable{cursor:pointer;}#mermaid-svg-3pUm9A7rzWRZ1YO0 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-3pUm9A7rzWRZ1YO0 .arrowheadPath{fill:#333333;}#mermaid-svg-3pUm9A7rzWRZ1YO0 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-3pUm9A7rzWRZ1YO0 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-3pUm9A7rzWRZ1YO0 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3pUm9A7rzWRZ1YO0 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-3pUm9A7rzWRZ1YO0 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3pUm9A7rzWRZ1YO0 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-3pUm9A7rzWRZ1YO0 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-3pUm9A7rzWRZ1YO0 .cluster text{fill:#333;}#mermaid-svg-3pUm9A7rzWRZ1YO0 .cluster span{color:#333;}#mermaid-svg-3pUm9A7rzWRZ1YO0 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-3pUm9A7rzWRZ1YO0 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-3pUm9A7rzWRZ1YO0 rect.text{fill:none;stroke-width:0;}#mermaid-svg-3pUm9A7rzWRZ1YO0 .icon-shape,#mermaid-svg-3pUm9A7rzWRZ1YO0 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3pUm9A7rzWRZ1YO0 .icon-shape p,#mermaid-svg-3pUm9A7rzWRZ1YO0 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-3pUm9A7rzWRZ1YO0 .icon-shape .label rect,#mermaid-svg-3pUm9A7rzWRZ1YO0 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3pUm9A7rzWRZ1YO0 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-3pUm9A7rzWRZ1YO0 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-3pUm9A7rzWRZ1YO0 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} DefWindowProc
是
否
HTCLIENT
HTLEFT/HTRIGHT
HTTOP/HTBOTTOM
其他
桌面窗口
鼠标输入 UserProcessMouseInput
co_MsqInsertMouseMessage
co_IntProcessMouseMessage msgqueue.c
发送 WM_SETCURSOR 给命中窗口
窗口处理
defwnd.c WM_SETCURSOR
WS_CHILD?
先问父窗口
DefWndHandleSetCursor
命中码
pcls->spcur 类光标
SYSTEMCUR SIZEWE
SYSTEMCUR SIZENS
SYSTEMCUR ARROW
IntSystemSetCursor
UserSetCursor
desktop.c WM_SETCURSOR → gDesktopCursor
15. 源码索引
15.1 本文覆盖的 ntuser/cursoricon.c 函数(共 32 个)
| 函数 | 行号 | 说明 |
|---|---|---|
| InitCursorImpl | L63-76 | 全局光标状态初始化(ShowingCursor=-1) |
| IntInsertCursorIntoList | L78-96 | 入全局/进程光标链 |
| IntRemoveCursorFromList | L99-134 | 出链 + 解引用 |
| IntLoadSystenIcons | L136-184 | 登记系统图标(含 OIC_INTERNAL_WINSMALL 特例) |
| IntGetSysCursorInfo | L186-190 | 取 &gSysCursorInfo |
| is_icon | L192-197 | rt == RT_ICON 判定 |
| UserGetCurIconObject | L200-227 | 句柄→对象(校验+引用) |
| IntSystemSetCursor | L229-236 | UserSetCursor + 引用平衡包装 |
| UserSetCursorPos | L238-282 | 裁剪 + 注入 WM_MOUSEMOVE + 更新 gpsi->ptCursor |
| IntCreateCurIconHandle | L284-314 | 分配空对象(ACON 大块) |
| IntDestroyCurIconObject | L316-333 | 摘链 + UserDeleteObject |
| FreeCurIconObject | L335-392 | 释放位图/帧/名称/原子 |
| IntCleanupCurIconCache | L394-406 | 进程退出回收 pCursorCache |
| NtUserGetIconInfo | L411-582 | ICONINFO + 模块/资源名 + bpp |
| NtUserGetIconSize | L588-641 | cx/cy(ACON 取首帧) |
| NtUserGetCursorInfo | L647-698 | CURSORINFO(CURSOR_SHOWING) |
| UserClipCursor | L700-748 | 裁剪实现(权限/相交/拉回) |
| NtUserClipCursor | L753-787 | 探测 + 调 UserClipCursor |
| NtUserDestroyCursor | L793-855 | DestroyCursor/DestroyIcon 四道检查 |
| NtUserFindExistingCursorIcon | L861-990 | LR_SHARED 查重 |
| NtUserGetClipCursor | L996-1043 | 读裁剪区 |
| NtUserSetCursor | L1049-1113 | SetCursor(返回旧光标) |
| NtUserSetCursorContents | L1119-1127 | 未实现 stub |
| IntSetCursorData | L1130-1236 | CURSORDATA → 普通对象 |
| IntSetAconData | L1238-1417 | CURSORDATA → ACON 帧表 |
| UserSetCursorIconData | L1419-1503 | 内部入口(原子/分流/入链) |
| NtUserSetCursorIconData | L1509-1680 | 用户态入口(SEH 探测) |
| UserDrawIconEx | L1690-2079 | 低层位图合成绘制 |
| NtUserDrawIconEx | L2084-2126 | DrawIconEx 内核入口 |
| NtUserGetCursorFrameInfo | L2131-2196 | 动画帧/速率查询 |
| NtUserSetSystemCursor | L2201-2256 | 系统光标登记/替换 |
15.2 其他文件关键点
| 文件 | 内容 |
|---|---|
| win32ss/user/ntuser/cursoricon.h | CURICON_OBJECT、ACON、SYSTEM_CURSORINFO、SYSTEMCURICO、ROIC_/ROCR_、宏 |
| win32ss/user/ntuser/msgqueue.c | UserSetCursor L90-164、UserShowCursor L168-218、gpqCursor L20、WM_SETCURSOR L1709-1777、WM_MOUSEMOVE 换形 L664-702、初始 WAIT L2217、销毁 L2370-2403 |
| win32ss/user/ntuser/msgqueue.h | CursorObject L89、声明 L263-267 |
| win32ss/user/ntuser/callback.c | co_IntLoadDefaultCursors L470-503、co_IntCopyImage L984-1036 |
| win32ss/user/ntuser/simplecall.c | SHOWCURSOR L218-220、CREATEEMPTYCUROBJECT L259-267、GETCURSORPOS L326-348 |
| win32ss/user/ntuser/misc.c | THREADSTATE_GETCURSOR L316-319 |
| win32ss/user/ntuser/defwnd.c | DefWndHandleSetCursor L241-360、WM_SETCURSOR L1094-1109 |
| win32ss/user/ntuser/desktop.c | gDesktopCursor L55、WM_SETCURSOR L1500-1517 |
| win32ss/user/ntuser/input.c | AttachThreadInput 光标传递 L554-577 |
| win32ss/user/ntuser/class.c | 类光标 spcur L1136/2298/2391-2400 |
| win32ss/user/ntuser/nonclient.c | 拖拽换光标 L475-476/L643-644 |
| win32ss/user/user32/windows/cursoricon.c | 全部 user32 层:LoadSystemCursors L270、User32SetupDefaultCursors L295、BMI/ANI/IconInfo 解析 L855-1334、LoadFromFile L1628、LoadImageW L1724、CopyImage L2153-2382、Load*/Draw*/Create* L2391-3099 |
| win32ss/user/user32/include/ntwrapper.h | NtUserxShowCursor L605、NtUserxCreateEmptyCurObject L615、NtUserxSetCursorPos L675 |
| win32ss/gdi/eng/mouse.c | IntHideMousePointer L142、IntShowMousePointer L196、EngSetPointerShape L330、EngMovePointer L575、IntEngSetPointerShape L615、GreSetPointerShape L703、GreMovePointer L799 |
| win32ss/include/ntuser.h | CURSORDATA L1173-1196、CURSORF_* L1198-1207、PROCMARKHEAD L223-228 |
16. 小结
本册以 ntuser/cursoricon.c 为中心,串联起 ReactOS 光标/图标子系统的完整图景:
- 统一对象 :
CURICON_OBJECT用一个对象类型同时服务光标与图标(is_icon判定),动画光标用同头部的ACON扩展;对象在全局gcurFirst与进程私有pCursorCache两条链上共享/登记。 - 两步式创建 :
IntCreateCurIconHandle(空对象)→NtUserSetCursorIconData(数据填充)取代了传统的一次性 syscall;所有格式解析下沉到 user32,内核只维护两个原语。 - 队列归属 :光标是消息队列(
CursorObject+iCursorLevel)的属性,屏幕形状由"鼠标下窗口所属队列"决定;UserSetCursor/UserShowCursor与全局gSysCursorInfo协同维护"显示什么、是否可见"。 - 委托绘制 :USER 层只做决策,像素工作全部交给 GDI 引擎(
GreSetPointerShape/GreMovePointer→IntShowMousePointer的 SRCAND+SRCINVERT/AlphaBlend 合成,硬件指针走驱动pfnMovePointer)。 - 明确缺口 :系统光标替换(
NtUserSetSystemCursor的 FIXME 分支)、动画光标播放(UserSetCursor的首帧退化)、.ani 文件加载(CURSORICON_LoadFromFileW的 UNIMPLEMENTED)三处仍是已知限制,是后续实现的重点。
本册完。下一册预告:窗口管理主线收尾------非客户区绘制与标题栏交互(nonclient.c)。