ReactOS 窗口系统分析(20):光标与图标 — cursoricon.c

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.cdefwnd.cdesktop.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 在初始化时加载。

本册是对这段总览的逐函数展开。需要特别说明:当前源码树中部分传统函数名已经不存在 (如 NtUserCreateCursorNtUserCreateIconNtUserLoadCursorNtUserCopyImageIntAnimateCursorCursorServiceThreadProcCursorMotionThreadProcNtUserShowCursorIntShowCursor 等)。ReactOS 把这些能力重构进了别的函数与调用路径,文中对每个"历史函数"都会给出它在当前源码中的真实落点,绝不臆造。


2. 设计动机

为什么光标和图标要共用一个对象类型?为什么显隐要用"计数器"而不是布尔值?为什么系统光标要单独维护一张表?以下是设计层面的动机分析。

2.1 光标与图标的同构性

从数据角度看,光标和图标几乎没有区别:

  • 两者都是"一张 AND 掩码位图 + 一张 XOR/彩色位图"的组合(单色光标把彩色信息塞进掩码的下半部分);
  • 两者的句柄类型在 Win32 API 中互通(SetCursor 接受 HCURSORWM_SETCURSOR 传的也是 HCURSORGetIconInfo 同时服务两者);
  • 两者都可以从资源(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 把创建拆成两步:

  1. NtUserxCreateEmptyCurObject(bAnimated)(→ IntCreateCurIconHandle,simplecall.c L259)------只分配对象与句柄,不含任何位图
  2. 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;

各字段要点:

  • headPROCMARKHEAD(ntuser.h L223-228)= HEAD(句柄 + 类型 + 引用计数 cLockObj)+ hTaskWow + ppippi == 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)。
  • bppNtUserGetIconInfo 通过它回填调用者的 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 秒)。
  • NtUserGetCursorFrameInfoNtUserGetIconSize 遇到 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/iicurNtUserSetCursorIconData 会对三个指针数组做逐元素探测拷贝(见 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 是内核内部标志,IntSetCursorDataCURSORF_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 显示请求都会被 NtUserGetCursorInfoCURSOR_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_GLOBALppi = NULL、加引用,不链入任何表(L158-164)。
  • 其余 id 在 gasysico[6] 中查找:置 CURSORF_GLOBALhead.ppi = NULLIntInsertCursorIntoList 入全局链(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.cONEPARAM_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/hbmAlphaGreSetObjectOwner(..., 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 路径)时执行四道保护检查:
    1. head.ppi == NULL → 全局对象,拒绝("Trying to delete global cursor!",L815-820);
    2. ppi != 当前进程 → 不是你的光标,拒绝(L823-828);
    3. CURSORF_CURRENT → 正在使用,拒绝(L830-835);
    4. 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 各有一个 CursorObjectSetCursor 只改本线程队列的指针;屏幕上的实际形状由"鼠标命中的顶层窗口属于哪个队列"决定------这是多线程窗口体系下"谁的窗口在鼠标下,谁的光标生效"的关键。命中窗口属于别的队列时,本队列的修改只是"预存",等鼠标移回本窗口(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...
}
  • flagsCURSOR_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)的内核实现,也被 UserClipCursornonclient.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)

  • NtUserClipCursorProbeForRead 拷贝 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_READATTRIBUTESbClipped 时返回 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_GetCursorDataFromIconInfoNtUserxCreateEmptyCurObject + 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 > 1CreateBitmap(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:按 fIconrt = 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)

流程:

  1. SEH 探测ProbeForRead 拷贝 CURSORDATA 本体(L1538-1539)。
  2. 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 竞态。
  3. 非 ACON:三个指针置 NULL(L1596-1602)。
  4. 字符串捕获 :模块名 ProbeAndCaptureUnicodeString(L1615);资源名 ProbeAndCaptureUnicodeStringOrAtom(L1627,兼容 INTRESOURCE)。
  5. 标志校验CURSORF_flags & ~CURSORF_USER_MASK 非零 → 拒绝(L1636-1640)。
  6. UserEnterExclusive → 内部 UserSetCursorIconDataUserLeave
  7. 清理:释放捕获的模块名、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 分流:IntSetAconDataIntSetCursorData(L1461-1475)。
  • 成功后若对象带 CURSORF_LRSHAREDIntInsertCursorIntoList 入链表(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)是光标/图标的统一装载器

  1. LR_DEFAULTSIZE:cx/cy 为 0 时取 GetSystemMetrics(SM_CXICON/CXCURSOR)(L1745-1749)。
  2. LR_LOADFROMFILECURSORICON_LoadFromFileW(L1751-1754,见 8.3)。
  3. hinst == NULL(OEM):改指 User32Instance,并把 IDI_APPLICATION..IDI_SHIELD 映射为 100... 段资源号(IDI_ERRORIDI_WARNING 互换,L1756-1773)------这是 ReactOS 对标准图标的 OEM 资源排布。
  4. 构造 ustrModuleLDR_IS_RESOURCE 时伪造 \x01%08IX 名字,否则 GetModuleFileNameW,L1788-1835)与 ustrRsrc(INTRESOURCE 或字符串,L1775-1786)。
  5. LR_SHARED 查重NtUserFindExistingCursorIcon(&ustrModule, &ustrRsrc, &param),命中直接返回(L1837-1854)。
  6. 资源解析:FindResourceW(RT_GROUP_ICON/RT_GROUP_CURSOR)LockResourceLookupIconIdFromDirectoryEx 选最合适的条目 id → 再 FindResourceW(RT_ICON/RT_CURSOR) 拿具体位图(L1856-1894)。
  7. 光标资源(dir->idType == 2)前 4 字节是两个 SHORT 热点,解析后 bits += 4(L1901-1908)。
  8. CURSORICON_GetCursorDataFromBMI 把 DIB 转成位图对;CURSORF_flags = CURSORF_FROMRESOURCELR_SHARED 时附加 CURSORF_LRSHARED(L1896-1933)。
  9. 两步式创建:NtUserxCreateEmptyCurObject(FALSE) + NtUserSetCursorIconData;失败 NtUserDestroyCursor(hCurIcon, TRUE) 强制回滚(L1924-1942)。

8.2 CURSORICON_GetCursorDataFromBMI(L855 起)--- DIB → 位图对

图标/光标资源是"一张 BITMAPINFO,高度 = 实际高度的 2 倍(上半图像、下半 AND 掩码)"。该函数:

  1. bitmap_info_size + is_dib_monochrome 判断单色(L860-861);DIB_GetBitmapInfo 解出 width/height/bpp/compr(L872),BI_RGB 之外拒绝(L878)。
  2. 尺寸缺省时 pdata->cy = height/2(L882-883);热点换算:光标按比例缩放热点(L886-892),图标热点=中心(L893-897)。
  3. 建屏幕 DC 与兼容 DC(L899-907);把 biHeight/2 的副本 DIB 用 StretchDIBits 画进位图(L909-959):
    • 单色:hbmMask = CreateBitmap(cx, cy*2, 1,1) 上半存图像(L926-942);
    • 彩色:hbmColor = CreateCompatibleBitmap + 1bpp hbmMask + create_alpha_bitmap 生成 alpha 面(L943-961);
  4. 掩码面从彩色图转换:把调色板压成黑/白 1bpp 再 StretchDIBits 画进掩码(L963 起)。

bppGetDeviceCaps(hdcScreen, BITSPIXEL)(L960),与 UserDrawIconExEXLATEOBJ 色彩转换配合。

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) 反查所属模块 → 校验 idTypefIcon 匹配 → 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 清理 pbBmpIconcursorData.aspcur 与三张位图(L2968-2976)。
  • 由此,CreateIconFromResourceEx 成为 .ani 动画光标唯一可用的加载入口 (文件路径 CURSORICON_LoadFromFileW 对 RIFF 返回 UNIMPLEMENTED)。

8.6 CURSORICON_GetCursorDataFromANI(user32 L1162-1334)--- .ani 解析

用 RIFF 分块遍历解析 .ani 内存数据:

  1. riff_find_chunk(ANI_ACON_ID, ANI_RIFF_ID, ...) 定位根块,再取 anih(ANI 头)块(L1181-1196)。
  2. 头字段:cpcur = num_framescicur = num_stepsiicur = display_rate,置 CURSORF_ACON(L1198-1202)。
  3. 可选块:seq (序列,ANI_FLAG_SEQUENCE 时必读)、rate(每步速率,L1204-1218)。
  4. LIST fram 内逐帧:帧数据是"图标文件"(ANI_FLAG_ICON,用 get_best_icon_file_entry 挑条目)或"裸位图"(L1255-1291);每帧用 CURSORICON_GetCursorDataFromBMI 转位图对,多帧时置 CURSORF_ACONFRAME(L1293-1299)。
  5. 单帧(num_frames <= 1)直接返回------此时 CURSORF_ACON 已在 L1199 置位,但 L1299 会清除单帧的 ACONFRAME 且对象仍按普通光标创建(调用方 isAnimated 按 flags 判断为真则建 ACON)。
  6. 多帧:填 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_BITMAPBITMAP_CopyImageIMAGE_CURSOR/ICONCURSORICON_CopyImage(hImage, uType==IMAGE_ICON, cx, cy, fuFlags)(L2328-2376)。LR_COPYFROMRESOURCE 且失败时,用 GetIconInfo 检查尺寸一致则去掉该标志重试(L2347-2374)。
  • CURSORICON_CopyImage (L2153-2284):
    • LR_COPYFROMRESOURCENtUserGetIconInfo 取模块/资源名 → LoadLibraryExW(..., LOAD_LIBRARY_AS_DATAFILE)CURSORICON_LoadImageW 重载(L2167-2247);
    • 普通复制:GetIconInfo 取位图 → CURSORICON_GetCursorDataFromIconInfoNtUserxCreateEmptyCurObject + NtUserSetCursorIconDataLR_SHARED 附加 CURSORF_LRSHAREDLR_COPYDELETEORG 复制后销毁原图(L2249-2283)。
  • User32CallCopyImageFromKernel (L2286-2300):USER32_CALLBACK_COPYIMAGE 回调处理例程,转发给 CopyImage
  • co_IntCopyImage (callback.c L984-1036):内核侧入口,打包 COPYIMAGE_CALLBACK_ARGUMENTSKeUserModeCallback(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!"),本实现未使用。user32 DrawIconEx(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 对象加锁(只需锁目标表面);
  • 有(少量)性能收益。

流程:

  1. 参数检查:diFlags & DI_NORMAL 为 0 → 拒绝(L1712-1716)。
  2. 动画帧选择 :ACON 时校验 istepIfAniCur < cicur,取 pIcon = aspcur[aicur[istepIfAniCur]](L1718-1727)------istepIfAniCur 是"播放第几步",用于 DrawIconEx 手动逐帧绘制动画图标。
  3. SURFACE_ShareLockSurface 共享锁住掩码/彩色位图(只读,L1737-1756);DC_LockDc 锁目标 DC(L1758-1765)。
  4. 尺寸缺省:DI_DEFAULTSIZE → 图标取 SM_CXICON/SM_CYICON、光标取 SM_CXCURSOR/SM_CYCURSOR;否则用 pIcon->cx/cy(L1768-1785)。
  5. 目标矩形:RECTL_vSetRect(rcDest, xLeft, yTop, xLeft+cxWidth, yTop+cyHeight)IntLPtoDP(逻辑转设备)+ 加 ptlDCOrig 偏移(L1787-1790);DC_vPrepareDCsForBlit 准备合成(L1793)。
  6. 防闪烁背景hbrFlickerFreeDraw 是画刷句柄时,IntEngBitBlt(..., ROP4_PATCOPY) 用画刷预涂目标区(L1812-1895)。注意 #if 0 块(L1829-1894)里"离屏表面再回拷"的旧方案被禁用------注释说全程已锁目标表面,离屏无意义,改为直接 PATCOPY 到目标。
  7. alpha 路径hbmAlpha && (diFlags & DI_NORMAL) == DI_NORMALIntEngAlphaBlendAC_SRC_OVER + AC_SRC_ALPHABLENDOBJ,L1905-1933)。
  8. 掩码步diFlags & DI_MASKIntEngStretchBlt,ROP4 为 DI_IMAGE ? ROP4_SRCAND : ROP4_SRCCOPY(L1935-1961)------AND 掩码先"镂空"目标像素。
  9. 图像步diFlags & DI_IMAGE
    • 有彩色位图:ROP4 = DI_MASK ? ROP4_SRCINVERT : ROP4_SRCCOPY(L1965-1991);
    • 无彩色位图(单色):源矩形下移 pIcon->cy 用掩码位图下半部 SRCINVERT(L1992-2020)。
  10. 清理: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.cySRCINVERT 画下半(XOR 异或出形状)。这样得到三种效果:掩码 1/图像 0 = 黑、掩码 1/图像 1 = 白、掩码 0 = 透过(原像素保留)。
  • 单色光标与"热点":GreSetPointerShape 收到的 xHot/yHot 决定指针尖位置;引擎保存 HotSpot 供显示/恢复换算。

10.2 彩色光标/图标

  • hbmColor(屏幕兼容位深)+ 独立 1bpp hbmMask。绘制仍是"先 AND 镂空、后 XOR 上色",EXLATEOBJ 负责调色板/位深转换(GreSetPointerShape L756-757;UserDrawIconEx L1969)。
  • 掩码的"1"意味着"该像素显示图像","0"意味着"该像素让底色透过"------因此彩色光标的透明形状靠掩码勾勒。

10.3 alpha 半透明

  • hbmAlpha(8bpp)+ 彩色面,SPS_ALPHA 标志;UserSetCursor/co_IntProcessMouseMessageSPS_ALPHAhbmMask 传 NULL、把 hbmAlpha 当彩色面传给 GreSetPointerShape(msgqueue.c L143-150、L675-684)。
  • 引擎 IntShowMousePointerSPS_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)。
  • 作用点:GreSetPointerShapexHot/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
播放(按时序自动换帧) 未实现 UserSetCursorFIXME("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。user32 GetCursorFrameInfo(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)

按命中码决定系统光标(全部经 IntSystemSetCursorUserSetCursor):

命中码 光标
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.hCursorOCR_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)(对齐 winetest test_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 光标/图标子系统的完整图景:

  1. 统一对象CURICON_OBJECT 用一个对象类型同时服务光标与图标(is_icon 判定),动画光标用同头部的 ACON 扩展;对象在全局 gcurFirst 与进程私有 pCursorCache 两条链上共享/登记。
  2. 两步式创建IntCreateCurIconHandle(空对象)→ NtUserSetCursorIconData(数据填充)取代了传统的一次性 syscall;所有格式解析下沉到 user32,内核只维护两个原语。
  3. 队列归属 :光标是消息队列(CursorObject + iCursorLevel)的属性,屏幕形状由"鼠标下窗口所属队列"决定;UserSetCursor/UserShowCursor 与全局 gSysCursorInfo 协同维护"显示什么、是否可见"。
  4. 委托绘制 :USER 层只做决策,像素工作全部交给 GDI 引擎(GreSetPointerShape/GreMovePointerIntShowMousePointer 的 SRCAND+SRCINVERT/AlphaBlend 合成,硬件指针走驱动 pfnMovePointer)。
  5. 明确缺口 :系统光标替换(NtUserSetSystemCursor 的 FIXME 分支)、动画光标播放(UserSetCursor 的首帧退化)、.ani 文件加载(CURSORICON_LoadFromFileW 的 UNIMPLEMENTED)三处仍是已知限制,是后续实现的重点。

本册完。下一册预告:窗口管理主线收尾------非客户区绘制与标题栏交互(nonclient.c)。

相关推荐
H_oRIZoN_1 小时前
Linux入门DAY23(C语言树形结构)
linux·c语言·数据结构
longxiaozhang62 小时前
C#运算符重载:让对象也能使用“加减乘除”
开发语言·c#
mounter6253 小时前
GCCRS 突破与 Rust 入核:Linux 工具链的重大变革
linux·开发语言·rust·linux kernel·kernel
Buke..3 小时前
【APP 逆向】哔哩哔哩 sign 参数逆向(上):Frida 反调试绕过与 unidbg 调用
java·开发语言·爬虫·python·安卓
abcefg_h3 小时前
MCP 实战指南:如何在项目中使用 Model Context Protocol 及其通信原理
开发语言·后端·golang·mcp
梦痕长情3 小时前
记一次关于AI在数仓中的应用
开发语言·人工智能·python·ai
何以解忧,唯有..3 小时前
Python 线程编程:从入门到实战
开发语言·python
SomeB1oody4 小时前
【RustyML入门】7.2. 深入模型持久化
开发语言·后端·机器学习·rust·教程