ReactOS 图形系统分析(24):指针(光标)管理 --- mouse.c/h
1. 概述
mouse.c / mouse.h 位于 win32ss/gdi/eng/,实现 GDI 引擎的鼠标指针(光标)管理 。它解决的核心问题:光标必须始终绘制在所有窗口内容之上,且不干扰(不被破坏/不破坏)正在进行的绘图。
1.1 光标如何做到"不影响窗口绘图"
关键在于四层机制,从"空间隔离"到"时序隔离"逐级兜底:
| 机制 | 所在层 | 原理 | 解决的问题 |
|---|---|---|---|
| ① 光标不属于窗口内容 | 架构层 | 光标绘制在 PDEV 屏幕表面(ppdev->pSurface) 之上,独立于 DC/窗口表面;窗口剪裁区域(prgnVis/prgnRao)根本不含光标,窗口 BitBlt/画刷永远"看不见"光标 | 窗口重绘不会把光标抹掉,光标也不参与任何窗口剪裁计算 |
| ② 硬件光标隔离 | 驱动层 | 硬件指针由显卡**合成层(overlay)**绘制,不进帧缓冲 ;绘图时引擎只需 pfnMovePointer 通知驱动"移开/隐藏",帧缓冲里根本没有光标像素 |
硬件路径下绘图与光标天然零交互 |
| ③ 保存-擦除-重画循环 | 软件指针层 | 显示光标前先把其下方屏幕像素存入 psurfSave ;移动/改形状时 IntHideMousePointer 先把保存像素拷回屏幕(SRCCOPY 擦除) ,再在新位置 IntShowMousePointer 重新"保存+合成" |
软件路径下光标不留残影,且擦除不依赖窗口重绘 |
| ④ 绘图安全区(MouseSafety) | 时序层 | 任何窗口/DC 绘图前调用 MouseSafetyOnDrawStart:若绘图矩形与光标 Exclude 矩形相交 ,先临时隐藏光标;绘图结束 MouseSafetyOnDrawEnd 再恢复 |
防止画刷/位块把光标"画花",也防止光标盖住新内容后被错误恢复 |
④ 安全区的嵌套细节(代码实证):
c
MouseSafetyOnDrawStart(ppdev, Hazard...) // 绘图前(持 hsemDevLock)
{
if (pgp->Exclude.right == -1) return FALSE; // 光标本就隐藏,无需处理
ppdev->SafetyRemoveCount++; // 每层绘图计数 +1
if (ppdev->SafetyRemoveLevel != 0) return FALSE; // 已在隐藏状态(嵌套绘图),不再重复隐藏
if (矩形相交)
{
SafetyRemoveLevel = SafetyRemoveCount; // 记录本次移除深度
隐藏光标(硬件: pfnMovePointer(-1,-1) / 软件: EngMovePointer(-1,-1))
}
}
MouseSafetyOnDrawEnd(ppdev) // 绘图后
{
if (--SafetyRemoveCount >= SafetyRemoveLevel) return FALSE; // 外层绘图尚未结束
恢复光标到 gpsi->ptCursor 位置;
SafetyRemoveLevel = 0;
}
SafetyRemoveCount(总嵌套层数)与 SafetyRemoveLevel(隐藏时的深度)配合,保证多个 DC 并发/嵌套绘图时光标只隐藏一次、全部完成后才恢复------这就是"光标不影响窗口绘图"的时序保证。
总结一句话: 光标靠"不属于窗口(空间隔离)→ 硬件合成(硬件隔离)→ 保存/擦除(软件自恢复)→ 相交即隐藏(时序安全区)"四层机制,做到既永远浮在窗口之上、又不干扰任何窗口绘图。
1.2 光标变化(形状/位置)是怎么实现的
光标的变化分两种:形状变化 (SetCursor 换光标)与位置变化 (鼠标移动)。二者本质都遵循同一个模式:"先隐藏旧的 → 更新状态 → 再显示新的"------隐藏/显示就是 1.1 的"保存-擦除-合成"循环,因此光标无论如何变化都不会在屏幕上留下残影或破坏窗口内容。
① 形状变化(GreSetPointerShape → IntEngSetPointerShape):
#mermaid-svg-SpTqtvqdkBYedBA8{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-SpTqtvqdkBYedBA8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-SpTqtvqdkBYedBA8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-SpTqtvqdkBYedBA8 .error-icon{fill:#552222;}#mermaid-svg-SpTqtvqdkBYedBA8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-SpTqtvqdkBYedBA8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-SpTqtvqdkBYedBA8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-SpTqtvqdkBYedBA8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-SpTqtvqdkBYedBA8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-SpTqtvqdkBYedBA8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-SpTqtvqdkBYedBA8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-SpTqtvqdkBYedBA8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-SpTqtvqdkBYedBA8 .marker.cross{stroke:#333333;}#mermaid-svg-SpTqtvqdkBYedBA8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-SpTqtvqdkBYedBA8 p{margin:0;}#mermaid-svg-SpTqtvqdkBYedBA8 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-SpTqtvqdkBYedBA8 .cluster-label text{fill:#333;}#mermaid-svg-SpTqtvqdkBYedBA8 .cluster-label span{color:#333;}#mermaid-svg-SpTqtvqdkBYedBA8 .cluster-label span p{background-color:transparent;}#mermaid-svg-SpTqtvqdkBYedBA8 .label text,#mermaid-svg-SpTqtvqdkBYedBA8 span{fill:#333;color:#333;}#mermaid-svg-SpTqtvqdkBYedBA8 .node rect,#mermaid-svg-SpTqtvqdkBYedBA8 .node circle,#mermaid-svg-SpTqtvqdkBYedBA8 .node ellipse,#mermaid-svg-SpTqtvqdkBYedBA8 .node polygon,#mermaid-svg-SpTqtvqdkBYedBA8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-SpTqtvqdkBYedBA8 .rough-node .label text,#mermaid-svg-SpTqtvqdkBYedBA8 .node .label text,#mermaid-svg-SpTqtvqdkBYedBA8 .image-shape .label,#mermaid-svg-SpTqtvqdkBYedBA8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-SpTqtvqdkBYedBA8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-SpTqtvqdkBYedBA8 .rough-node .label,#mermaid-svg-SpTqtvqdkBYedBA8 .node .label,#mermaid-svg-SpTqtvqdkBYedBA8 .image-shape .label,#mermaid-svg-SpTqtvqdkBYedBA8 .icon-shape .label{text-align:center;}#mermaid-svg-SpTqtvqdkBYedBA8 .node.clickable{cursor:pointer;}#mermaid-svg-SpTqtvqdkBYedBA8 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-SpTqtvqdkBYedBA8 .arrowheadPath{fill:#333333;}#mermaid-svg-SpTqtvqdkBYedBA8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-SpTqtvqdkBYedBA8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-SpTqtvqdkBYedBA8 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SpTqtvqdkBYedBA8 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-SpTqtvqdkBYedBA8 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SpTqtvqdkBYedBA8 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-SpTqtvqdkBYedBA8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-SpTqtvqdkBYedBA8 .cluster text{fill:#333;}#mermaid-svg-SpTqtvqdkBYedBA8 .cluster span{color:#333;}#mermaid-svg-SpTqtvqdkBYedBA8 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-SpTqtvqdkBYedBA8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-SpTqtvqdkBYedBA8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-SpTqtvqdkBYedBA8 .icon-shape,#mermaid-svg-SpTqtvqdkBYedBA8 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SpTqtvqdkBYedBA8 .icon-shape p,#mermaid-svg-SpTqtvqdkBYedBA8 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-SpTqtvqdkBYedBA8 .icon-shape .label rect,#mermaid-svg-SpTqtvqdkBYedBA8 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SpTqtvqdkBYedBA8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-SpTqtvqdkBYedBA8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-SpTqtvqdkBYedBA8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
SetCursor 换光标
ntuser → GreSetPointerShape
IntEngSetPointerShape 分发
DrvSetPointerShape 接受?
硬件路径:驱动换形状
(显卡 overlay 更新,帧缓冲无感)
软件路径:EngSetPointerShape
① 分配新三表面
Save(保存下像素)/Color(彩色)/Mask(掩码)
alpha 光标强制转 32bpp
② IntHideMousePointer
把旧光标下像素拷回屏幕(擦除)
③ 释放旧三表面
(psurfColor/Mask/Save)
④ 安装新表面/热点/尺寸
pgp->psurfXxx + HotSpot + Size + flags
⑤ 更新 Pointer.Exclude 包围矩形
(供绘图安全区判断)
⑥ IntShowMousePointer
保存新位置像素 + 合成新光标
② 位置变化(GreMovePointer → EngMovePointer):
c
IntHideMousePointer(ppdev, pso); // ① 擦除:保存像素拷回屏幕
ppdev->ptlPointer.x = x; .y = y; // ② 更新 PDEV 中的光标位置
if (x != -1)
IntShowMousePointer(ppdev, pso); // ③ 新位置:保存像素 + 合成光标
else
/* -1,-1 表示只隐藏(如光标移出屏幕/被隐藏) */
关键点:
- 软件路径的变化代价 :每次形状/位置变化都伴随"保存光标下像素 + 擦除 + 重新合成"多个 BitBlt------所以鼠标频繁移动时引擎会反复执行
IntShow/HideMousePointer(这是软件光标比硬件光标慢的根本原因)。 - 硬件路径的变化代价 :仅为驱动调用
pfnMovePointer/DrvSetPointerShape,光标不在帧缓冲里,变化几乎零成本。 - Exclude 的联动 :每次变化后都会刷新
Pointer.Exclude包围矩形,使 1.1 的绘图安全区(MouseSafety)始终基于最新光标位置做相交判断。
引擎同时支持两种指针实现,并自动分发:
-
硬件指针 :显示驱动实现
DrvSetPointerShape/DrvMovePointer,光标由显卡层合成,绘图时引擎只需通知驱动"隐藏/移动"; -
软件指针:引擎自己用位块操作(BitBlt/AlphaBlend)把光标画到屏幕表面------需要"保存光标下像素"以便擦除恢复。
ntuser/msgqueue.c(光标对象变更/鼠标移动,调用引擎入口)
↓ GreSetPointerShape / GreMovePointer(锁 DC + hsemDevLock)
IntEngSetPointerShape(硬件/软件分发决策)
├─ 硬件:DrvSetPointerShape(SPS_ACCEPT_NOEXCLUDE → 硬件接管)
└─ 软件:EngSetPointerShape(构造 3 个表面并保存)
↓ IntShowMousePointer / IntHideMousePointer
IntEngBitBlt / IntEngAlphaBlend(光标合成到屏幕)
同时:ntgdi/dclife.c 绘制前/后调用 MouseSafetyOnDrawStart/End
(光标与绘图矩形相交时临时隐藏,避免光标被绘"花")
2. 核心数据结构
2.1 GDIPOINTER --- PDEV 内嵌的指针状态(pdevobj.h(file:///d:/reactos/win32ss/gdi/eng/pdevobj.h#L27-L40))
c
typedef struct _GDIPOINTER
{
/* 软件指针模拟所需的私有状态 */
BOOL Enabled; // 当前是否已显示(隐藏/显示嵌套)
SIZEL Size; // 光标尺寸(宽×高)
POINTL HotSpot; // 热点(锚点,相对左上角)
SURFACE *psurfColor; // 彩色光标位图(可空)
SURFACE *psurfMask; // 掩码光标位图(单色 2 倍高或彩色掩码)
SURFACE *psurfSave; // "光标下像素"保存位图(擦除用)
FLONG flags; // SPS_* 标志(如 SPS_ALPHA)
/* 公开部分 */
RECTL Exclude; // 光标当前包围矩形(供 SPS_ACCEPT_EXCLUDE / 安全区判断)
} GDIPOINTER, *PGDIPOINTER;
GDIPOINTER 作为 PPDEVOBJ 的 Pointer 字段存在------每个物理设备(屏幕)一个光标状态。
2.2 PDEV 指针标志(pdevobj.h(file:///d:/reactos/win32ss/gdi/eng/pdevobj.h#L8-L9))
| 标志 | 含义 |
|---|---|
PDEV_HARDWARE_POINTER (0x2) |
当前使用硬件光标(pfnMovePointer 生效) |
PDEV_SOFTWARE_POINTER (0x4) |
当前使用软件光标(EngMovePointer 生效) |
2.3 SPS_* 标志(winddi.h(file:///d:/reactos/sdk/include/psdk/winddi.h#L4029-L4039))
| 常量 | 含义 |
|---|---|
SPS_DECLINE (0x1) |
驱动拒绝该光标形状 |
SPS_ACCEPT_NOEXCLUDE (0x2) |
驱动接受,且不需要 Exclude 矩形 |
SPS_ACCEPT_EXCLUDE (0x3) |
驱动接受,需要 Exclude 矩形(绘图安全区) |
SPS_CHANGE (0x1,fl 参数) |
光标形状改变请求 |
SPS_ALPHA (0x10,fl 参数) |
光标含 alpha 通道(32bpp 彩色光标) |
3. 函数族详解
3.1 入口层 --- GreSetPointerShape / GreMovePointer(被 ntuser 调用)
c
ULONG GreSetPointerShape(HDC hdc, HBITMAP hbmMask, HBITMAP hbmColor,
LONG xHot, LONG yHot, LONG x, LONG y, FLONG fl)
{
pdc = DC_LockDc(hdc);
EngAcquireSemaphore(pdc->ppdev->hsemDevLock); // 设备锁
psurf = pdc->ppdev->pSurface; // 用 PDEV 表面(非 DC 表面)
psurfMask = SURFACE_ShareLockSurface(hbmMask); // 锁定位图
EXLATEOBJ_vInitialize(&exlo, psurfColor->ppal, psurf->ppal, ...);
ulResult = IntEngSetPointerShape(&psurf->SurfObj, ..., &ppdev->Pointer.Exclude,
fl | SPS_CHANGE);
... /* 清理锁 */
}
调用方(ntuser/msgqueue.c(file:///d:/reactos/win32ss/user/ntuser/msgqueue.c#L144-L210)): 光标对象切换时 GreSetPointerShape(带 SPS_ALPHA 若有 alpha 光标);鼠标移动消息处理时 GreMovePointer(hdcScreen, x, y),隐藏时传 -1,-1。GreMovePointer 内部还会处理**平移设备(GCAPS_PANNING)**通知。
3.2 分发层 --- IntEngSetPointerShape(硬件/软件决策)
c
ULONG IntEngSetPointerShape(...)
{
pfnSetPointerShape = GDIDEVFUNCS(pso).SetPointerShape; // 驱动 DrvSetPointerShape
if (pfnSetPointerShape)
{
ulResult = pfnSetPointerShape(...); // 先问硬件
if (ulResult == SPS_ACCEPT_NOEXCLUDE) bHardwarePointer = TRUE;
bSoftwarePointer = !bHardwarePointer;
}
if (bSoftwarePointer)
ulResult = EngSetPointerShape(...); // 软件兜底
/* 按结果切换 PDEV_HARDWARE_POINTER / PDEV_SOFTWARE_POINTER */
}
核心:硬件优先,软件兜底。 驱动 DrvSetPointerShape 返回 SPS_DECLINE 时(如 vga 只接受特定尺寸),自动回退软件实现;切换时确保另一路被隐藏(EngMovePointer(-1,-1) 或 pfnMovePointer(-1,-1))。
3.3 软件指针实现 --- EngSetPointerShape
c
/* 1. 分配三个表面(格式与屏幕一致) */
hbmSave = EngCreateBitmap(sizel, lDelta, pso->iBitmapFormat, BMF_TOPDOWN|BMF_NOZEROINIT, NULL);
hbmColor = EngCreateBitmap(...); // 彩色光标(非 alpha 要求与屏幕同格式,否则失败 SPS_ERROR)
hbmMask = EngCreateBitmap(...); // 掩码(单色源经 gppalMono→调色板 EXLATEOBJ 转换)
/* 2. SPS_ALPHA:强制转存为 32bpp RGB 表面(IntEngCopyBits + exloSrcRGB) */
/* 3. 隐藏旧光标 → 删除旧三表面 → 挂入新表面/热点/尺寸 */
/* 4. 若给定了坐标 x!=-1 → IntShowMousePointer 立即显示,并回填 prcl 包围矩形 */
格式约束: 非 alpha 彩色光标必须与屏幕位图格式相同 (否则 goto failure → SPS_ERROR);alpha 光标统一转 32bpp。掩码表面从单色源拷贝时经 EXLATEOBJ(gppalMono → 设备调色板,前白后黑)转换。
3.4 显示/隐藏 --- IntShowMousePointer / IntHideMousePointer
隐藏(IntHideMousePointer)------三步擦除:
pt = ptlPointer - HotSpot(光标左上角,裁剪到屏幕)
IntEngBitBlt(屏幕, psurfSave, ..., rclDest, ptlSave, SRCCOPY)
把之前保存的"光标下像素"拷回屏幕 → 光标被擦除
显示(IntShowMousePointer)------先保存后合成:
#mermaid-svg-BBWstrQ3vO4sFvEP{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-BBWstrQ3vO4sFvEP .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-BBWstrQ3vO4sFvEP .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-BBWstrQ3vO4sFvEP .error-icon{fill:#552222;}#mermaid-svg-BBWstrQ3vO4sFvEP .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-BBWstrQ3vO4sFvEP .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-BBWstrQ3vO4sFvEP .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-BBWstrQ3vO4sFvEP .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-BBWstrQ3vO4sFvEP .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-BBWstrQ3vO4sFvEP .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-BBWstrQ3vO4sFvEP .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-BBWstrQ3vO4sFvEP .marker{fill:#333333;stroke:#333333;}#mermaid-svg-BBWstrQ3vO4sFvEP .marker.cross{stroke:#333333;}#mermaid-svg-BBWstrQ3vO4sFvEP svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-BBWstrQ3vO4sFvEP p{margin:0;}#mermaid-svg-BBWstrQ3vO4sFvEP .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-BBWstrQ3vO4sFvEP .cluster-label text{fill:#333;}#mermaid-svg-BBWstrQ3vO4sFvEP .cluster-label span{color:#333;}#mermaid-svg-BBWstrQ3vO4sFvEP .cluster-label span p{background-color:transparent;}#mermaid-svg-BBWstrQ3vO4sFvEP .label text,#mermaid-svg-BBWstrQ3vO4sFvEP span{fill:#333;color:#333;}#mermaid-svg-BBWstrQ3vO4sFvEP .node rect,#mermaid-svg-BBWstrQ3vO4sFvEP .node circle,#mermaid-svg-BBWstrQ3vO4sFvEP .node ellipse,#mermaid-svg-BBWstrQ3vO4sFvEP .node polygon,#mermaid-svg-BBWstrQ3vO4sFvEP .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-BBWstrQ3vO4sFvEP .rough-node .label text,#mermaid-svg-BBWstrQ3vO4sFvEP .node .label text,#mermaid-svg-BBWstrQ3vO4sFvEP .image-shape .label,#mermaid-svg-BBWstrQ3vO4sFvEP .icon-shape .label{text-anchor:middle;}#mermaid-svg-BBWstrQ3vO4sFvEP .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-BBWstrQ3vO4sFvEP .rough-node .label,#mermaid-svg-BBWstrQ3vO4sFvEP .node .label,#mermaid-svg-BBWstrQ3vO4sFvEP .image-shape .label,#mermaid-svg-BBWstrQ3vO4sFvEP .icon-shape .label{text-align:center;}#mermaid-svg-BBWstrQ3vO4sFvEP .node.clickable{cursor:pointer;}#mermaid-svg-BBWstrQ3vO4sFvEP .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-BBWstrQ3vO4sFvEP .arrowheadPath{fill:#333333;}#mermaid-svg-BBWstrQ3vO4sFvEP .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-BBWstrQ3vO4sFvEP .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-BBWstrQ3vO4sFvEP .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BBWstrQ3vO4sFvEP .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-BBWstrQ3vO4sFvEP .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BBWstrQ3vO4sFvEP .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-BBWstrQ3vO4sFvEP .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-BBWstrQ3vO4sFvEP .cluster text{fill:#333;}#mermaid-svg-BBWstrQ3vO4sFvEP .cluster span{color:#333;}#mermaid-svg-BBWstrQ3vO4sFvEP 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-BBWstrQ3vO4sFvEP .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-BBWstrQ3vO4sFvEP rect.text{fill:none;stroke-width:0;}#mermaid-svg-BBWstrQ3vO4sFvEP .icon-shape,#mermaid-svg-BBWstrQ3vO4sFvEP .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BBWstrQ3vO4sFvEP .icon-shape p,#mermaid-svg-BBWstrQ3vO4sFvEP .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-BBWstrQ3vO4sFvEP .icon-shape .label rect,#mermaid-svg-BBWstrQ3vO4sFvEP .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BBWstrQ3vO4sFvEP .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-BBWstrQ3vO4sFvEP .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-BBWstrQ3vO4sFvEP :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
是
否
计算光标/屏幕矩形(含边界裁剪)
① 保存:IntEngBitBlt(psurfSave, 屏幕, SRCCOPY)
把光标下的屏幕像素暂存
彩色光标?
SPS_ALPHA?
② 掩码: IntEngBitBlt(屏幕, mask, SRCAND)
③ 彩色: IntEngBitBlt(屏幕, color, SRCINVERT)
(先清除再异或合成)
② IntEngAlphaBlend(屏幕, color32, AC_SRC_OVER)
EXLATEOBJ RGB→设备色
② 掩码上半: SRCAND
③ 掩码下半(Size.cy 偏移): SRCINVERT
(单色光标=AND+INVERT 两步)
- 彩色(非 alpha)三步合成 :
SRCAND(用掩码清出洞)→SRCINVERT(彩色异或进去)------经典 Windows 光标合成; - alpha 光标 :
IntEngAlphaBlend(AC_SRC_OVER, 255, AC_SRC_ALPHA),预先把颜色转成设备格式的 32bpp 表面; - 单色光标 :掩码表面高度加倍 (上半 AND、下半 INVERT,见 3.5 尺寸计算
cy/2)。
3.5 移动 --- EngMovePointer
c
IntHideMousePointer(ppdev, pso); // 擦除旧位置
ppdev->ptlPointer.x = x; .y = y; // 更新 PDEV 光标位置
if (x != -1) IntShowMousePointer(ppdev, pso); // 新位置重画
3.6 绘制安全区 --- MouseSafetyOnDrawStart / MouseSafetyOnDrawEnd
目的: 当绘图矩形与光标矩形(pgp->Exclude)相交时,先把光标隐藏,等绘图完成再恢复------防止画刷/位块把光标画"花",也防止光标挡住新内容。
c
MouseSafetyOnDrawStart(ppdev, HazardX1..Y2) // 绘图前,需持 hsemDevLock
{
if (pgp->Exclude.right == -1) return FALSE; // 光标已隐藏(Exclude 无效)
ppdev->SafetyRemoveCount++; // 嵌套计数++
if (SafetyRemoveLevel != 0) return FALSE; // 已在移除状态(嵌套,不再动)
if (绘图矩形与 Exclude 相交)
{
SafetyRemoveLevel = SafetyRemoveCount; // 记录本次移除深度
if (PDEV_HARDWARE_POINTER) pfnMovePointer(-1,-1,NULL); // 硬件隐藏
else if (PDEV_SOFTWARE_POINTER) EngMovePointer(-1,-1,NULL); // 软件隐藏
}
}
MouseSafetyOnDrawEnd(ppdev) // 绘图后
{
if (--SafetyRemoveCount >= SafetyRemoveLevel) return FALSE; // 嵌套未结束
/* 恢复光标到 gpsi->ptCursor 位置 */
if (HARDWARE) pfnMovePointer(ptCursor.x, ptCursor.y, &Exclude);
else EngMovePointer(ptCursor.x, ptCursor.y, &Exclude);
SafetyRemoveLevel = 0;
}
调用方: ntgdi/dclife.c(file:///d:/reactos/win32ss/gdi/ntgdi/dclife.c#L569-L629)------DC 表面绑定/绘图前后成对调用,用 SafetyRemoveCount/SafetyRemoveLevel 支持嵌套安全区(多 DC 同时绘图)。
3.7 EngSetPointerTag(已废弃)
Win2K 起恒返回 FALSE(驱动不应再使用;保留 ABI 兼容)。
4. 与驱动 DDI 交互
| DDI | 方向 | 说明 |
|---|---|---|
DrvSetPointerShape |
驱动实现 | 硬件光标形状;返回 SPS_* 决定引擎是否回退软件 |
DrvMovePointer(ppdev->pfnMovePointer) |
驱动实现 | 硬件光标移动/隐藏(-1,-1 隐藏) |
EngSetPointerShape / EngMovePointer |
win32k 导出(win32k.spec(file:///d:/reactos/win32ss/win32k.spec#L98-L122)) | 软件指针服务,驱动可调用 |
树内驱动示例: framebuf/pointer.c(file:///d:/reactos/win32ss/drivers/displays/framebuf/pointer.c)(软件指针:EngMovePointer 驱动,DrvSetPointerShape 拒绝对接引擎软件实现)、vga/objects/pointer.c(file:///d:/reactos/win32ss/drivers/displays/vga/objects/pointer.c#L214-L275)(硬件指针:接受特定尺寸返回 SPS_ACCEPT_NOEXCLUDE,用 VGA 显存字库画光标)。
5. 导出与调用链
5.1 win32k.spec 导出
@ stdcall EngMovePointer(ptr long long ptr) # L98
@ stdcall EngSetPointerShape(ptr ptr ptr ptr long long long long ptr long) # L121
@ stdcall EngSetPointerTag(ptr ptr ptr ptr long) # L122
(GreSetPointerShape/GreMovePointer/IntEngSetPointerShape/IntShow/HideMousePointer/MouseSafety* 为引擎内部接口,声明于 mouse.h(file:///d:/reactos/win32ss/gdi/eng/mouse.h)。)
5.2 完整调用链
#mermaid-svg-Snqcqvz9akb3LyXr{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-Snqcqvz9akb3LyXr .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Snqcqvz9akb3LyXr .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Snqcqvz9akb3LyXr .error-icon{fill:#552222;}#mermaid-svg-Snqcqvz9akb3LyXr .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Snqcqvz9akb3LyXr .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Snqcqvz9akb3LyXr .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Snqcqvz9akb3LyXr .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Snqcqvz9akb3LyXr .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Snqcqvz9akb3LyXr .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Snqcqvz9akb3LyXr .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Snqcqvz9akb3LyXr .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Snqcqvz9akb3LyXr .marker.cross{stroke:#333333;}#mermaid-svg-Snqcqvz9akb3LyXr svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Snqcqvz9akb3LyXr p{margin:0;}#mermaid-svg-Snqcqvz9akb3LyXr .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Snqcqvz9akb3LyXr .cluster-label text{fill:#333;}#mermaid-svg-Snqcqvz9akb3LyXr .cluster-label span{color:#333;}#mermaid-svg-Snqcqvz9akb3LyXr .cluster-label span p{background-color:transparent;}#mermaid-svg-Snqcqvz9akb3LyXr .label text,#mermaid-svg-Snqcqvz9akb3LyXr span{fill:#333;color:#333;}#mermaid-svg-Snqcqvz9akb3LyXr .node rect,#mermaid-svg-Snqcqvz9akb3LyXr .node circle,#mermaid-svg-Snqcqvz9akb3LyXr .node ellipse,#mermaid-svg-Snqcqvz9akb3LyXr .node polygon,#mermaid-svg-Snqcqvz9akb3LyXr .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Snqcqvz9akb3LyXr .rough-node .label text,#mermaid-svg-Snqcqvz9akb3LyXr .node .label text,#mermaid-svg-Snqcqvz9akb3LyXr .image-shape .label,#mermaid-svg-Snqcqvz9akb3LyXr .icon-shape .label{text-anchor:middle;}#mermaid-svg-Snqcqvz9akb3LyXr .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Snqcqvz9akb3LyXr .rough-node .label,#mermaid-svg-Snqcqvz9akb3LyXr .node .label,#mermaid-svg-Snqcqvz9akb3LyXr .image-shape .label,#mermaid-svg-Snqcqvz9akb3LyXr .icon-shape .label{text-align:center;}#mermaid-svg-Snqcqvz9akb3LyXr .node.clickable{cursor:pointer;}#mermaid-svg-Snqcqvz9akb3LyXr .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Snqcqvz9akb3LyXr .arrowheadPath{fill:#333333;}#mermaid-svg-Snqcqvz9akb3LyXr .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Snqcqvz9akb3LyXr .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Snqcqvz9akb3LyXr .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Snqcqvz9akb3LyXr .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Snqcqvz9akb3LyXr .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Snqcqvz9akb3LyXr .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Snqcqvz9akb3LyXr .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Snqcqvz9akb3LyXr .cluster text{fill:#333;}#mermaid-svg-Snqcqvz9akb3LyXr .cluster span{color:#333;}#mermaid-svg-Snqcqvz9akb3LyXr 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-Snqcqvz9akb3LyXr .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Snqcqvz9akb3LyXr rect.text{fill:none;stroke-width:0;}#mermaid-svg-Snqcqvz9akb3LyXr .icon-shape,#mermaid-svg-Snqcqvz9akb3LyXr .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Snqcqvz9akb3LyXr .icon-shape p,#mermaid-svg-Snqcqvz9akb3LyXr .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Snqcqvz9akb3LyXr .icon-shape .label rect,#mermaid-svg-Snqcqvz9akb3LyXr .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Snqcqvz9akb3LyXr .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Snqcqvz9akb3LyXr .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Snqcqvz9akb3LyXr :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
ntuser/msgqueue.c
光标切换 / 鼠标移动
GreSetPointerShape / GreMovePointer
IntEngSetPointerShape
DrvSetPointerShape 接受?
硬件指针 pfnMovePointer
(vga 等)
EngSetPointerShape(软件)
IntHideMousePointer / IntShowMousePointer
IntEngBitBlt / IntEngAlphaBlend
(保存+合成光标)
ntgdi/dclife.c 绘图
MouseSafetyOnDrawStart/End
(与光标相交则临时隐藏)
6. 与《分析_4》标注对照
| 标注 | 核对 |
|---|---|
| 《_4》第 44 行:mouse.c/h =「指针(光标)管理」 | 准确。补充:软件/硬件双实现 + 自动分发;核心是"保存-擦除-重画"循环(psurfSave)与绘图安全区机制 |
7. 源码索引
| 文件 | 关键内容 |
|---|---|
| mouse.c(file:///d:/reactos/win32ss/gdi/eng/mouse.c) | GreSetPointerShape/GreMovePointer、IntEngSetPointerShape、EngSetPointerShape、IntShow/HideMousePointer、EngMovePointer、MouseSafetyOnDrawStart/End、EngSetPointerTag |
| mouse.h(file:///d:/reactos/win32ss/gdi/eng/mouse.h) | 引擎内部指针接口声明 |
| pdevobj.h(file:///d:/reactos/win32ss/gdi/eng/pdevobj.h) | GDIPOINTER 结构、PDEV_HARDWARE/SOFTWARE_POINTER、pfnMovePointer |
| ntuser/msgqueue.c(file:///d:/reactos/win32ss/user/ntuser/msgqueue.c#L144-L210) | 光标对象/鼠标消息 → Gre* 入口 |
| ntgdi/dclife.c(file:///d:/reactos/win32ss/gdi/ntgdi/dclife.c#L569-L629) | 绘图安全区调用方 |
| framebuf/pointer.c(file:///d:/reactos/win32ss/drivers/displays/framebuf/pointer.c) | 软件指针驱动示例 |
| vga/objects/pointer.c(file:///d:/reactos/win32ss/drivers/displays/vga/objects/pointer.c) | 硬件指针驱动示例 |
关联模块:
- 合成底层:
IntEngBitBlt(《分析_4》3.1)、IntEngAlphaBlend(alphablend.c)、IntEngCopyBits(copybits.c) - 颜色转换:
EXLATEOBJ_vInitialize(《分析_19》) - 设备锁:
hsemDevLock(《分析_20》) - 光标位置全局:
gpsi->ptCursor(win32k 全局会话信息)
8. 各函数使用说明
8.1 GreSetPointerShape --- 设置光标形状(引擎入口,ntuser 调用)
c
ULONG GreSetPointerShape(HDC hdc, HBITMAP hbmMask, HBITMAP hbmColor,
LONG xHot, LONG yHot, LONG x, LONG y, FLONG fl);
| 参数 | 说明 |
|---|---|
hdc |
屏幕 DC(DCTYPE_DIRECT) |
hbmMask |
掩码位图(单色光标:高度为 2×光标高;彩色光标可为掩码) |
hbmColor |
彩色光标位图(可 NULL → 单色光标) |
xHot/yHot |
热点(锚点)坐标 |
x/y |
新光标位置(-1,-1 表示只设形状不显示) |
fl |
SPS_ALPHA(alpha 光标)等 |
使用方式: 由 ntuser/msgqueue.c(file:///d:/reactos/win32ss/user/ntuser/msgqueue.c#L144-L151) 在光标对象切换时调用;内部锁 DC + hsemDevLock,自动取 PDEV 表面并 IntEngSetPointerShape 分发。返回值:SPS_DECLINE/SPS_ACCEPT_NOEXCLUDE 等。调用方需保证:持有有效 DC、光标位图已创建。
8.2 GreMovePointer --- 移动光标位置(引擎入口,ntuser 调用)
c
VOID GreMovePointer(HDC hdc, LONG x, LONG y);
使用方式: 鼠标移动消息处理时调用(x,y 为热点新位置);隐藏光标传 -1,-1。内部锁 DC + hsemDevLock,按当前 PDEV_HARDWARE/SOFTWARE_POINTER 选路:硬件→pfnMovePointer,软件→EngMovePointer;平移设备(GCAPS_PANNING)额外通知驱动。SafetyRemoveLevel != 0(绘图安全区进行中)时跳过本次移动,避免与安全区冲突。
8.3 IntEngSetPointerShape --- 硬件/软件分发(内部)
c
ULONG IntEngSetPointerShape(SURFOBJ *pso, SURFOBJ *psoMask, SURFOBJ *psoColor,
XLATEOBJ *pxlo, LONG xHot, LONG yHot,
LONG x, LONG y, RECTL *prcl, FLONG fl);
使用方式: 仅被 GreSetPointerShape 调用。先问驱动 DrvSetPointerShape,SPS_ACCEPT_NOEXCLUDE 则硬件接管;否则 EngSetPointerShape 软件兜底,并按结果更新 PDEV_HARDWARE/SOFTWARE_POINTER 标志、隐藏另一路。
8.4 EngSetPointerShape --- 软件指针形状设置(win32k 导出,驱动可调用)
c
ULONG EngSetPointerShape(SURFOBJ *pso, SURFOBJ *psoMask, SURFOBJ *psoColor,
XLATEOBJ *pxlo, LONG xHot, LONG yHot,
LONG x, LONG y, RECTL *prcl, FLONG fl);
使用方式: 软件光标的核心实现(见 3.3)。注意约束: 非 alpha 彩色光标必须与屏幕位图格式一致(否则 SPS_ERROR);alpha 光标内部转 32bpp。prcl 输出光标包围矩形(供 Exclude)。成功返回 SPS_ACCEPT_NOEXCLUDE,失败 SPS_ERROR(需清理已建表面------函数内部 failure 已处理)。
8.5 EngMovePointer --- 软件指针移动(win32k 导出,驱动可调用)
c
VOID EngMovePointer(SURFOBJ *pso, LONG x, LONG y, RECTL *prcl);
使用方式: 软件光标移动 = "擦除旧位置 → 更新 ptlPointer → 新位置合成"(见 3.5)。-1,-1 只隐藏;prcl 输出新包围矩形。必须与 EngSetPointerShape 成对使用(先设形状后移动);驱动实现软件指针(如 framebuf)用它渲染光标。
8.6 IntShowMousePointer --- 显示光标(内部)
c
VOID IntShowMousePointer(PDEVOBJ *ppdev, SURFOBJ *psoDest);
使用方式: 仅引擎内部调用(EngSetPointerShape/EngMovePointer)。流程:计算光标/屏幕矩形 → IntEngBitBlt 保存光标下像素到 psurfSave → 按彩色/alpha/单色路径合成(见 3.4)。前置条件: pgp->psurfSave 必须有效,否则直接返回(不显示)。
8.7 IntHideMousePointer --- 隐藏光标(内部)
c
VOID IntHideMousePointer(PDEVOBJ *ppdev, SURFOBJ *psoDest);
使用方式: 与 IntShowMousePointer 成对。把 psurfSave 中保存的像素 SRCCOPY 拷回屏幕完成擦除。注意: 多次隐藏(未显示就隐藏)时 Enabled 标志防重入,不重复擦除。
8.8 MouseSafetyOnDrawStart --- 绘图前安全区(内部,ntgdi 调用)
c
BOOL MouseSafetyOnDrawStart(PPDEVOBJ ppdev, LONG HazardX1, LONG HazardY1,
LONG HazardX2, LONG HazardY2);
使用方式: 由 ntgdi/dclife.c(file:///d:/reactos/win32ss/gdi/ntgdi/dclife.c#L569-L629) 在任何 DC 绘图开始前调用;传入绘图边界矩形 (顺序无关,内部会规范化)。前置条件:必须已持有 hsemDevLock (函数标注 _Requires_lock_held_)。返回 TRUE 表示进入安全区。若绘图矩形与光标 Exclude 相交,光标被临时隐藏(嵌套时只隐藏一次)。
8.9 MouseSafetyOnDrawEnd --- 绘图后安全区(内部,ntgdi 调用)
c
BOOL MouseSafetyOnDrawEnd(PPDEVOBJ ppdev);
使用方式: 与 MouseSafetyOnDrawStart 严格成对 (同一 hsemDevLock 下)。绘图结束后恢复光标到 gpsi->ptCursor。嵌套规则: 仅当最外层绘图结束时才真正恢复光标(SafetyRemoveCount < SafetyRemoveLevel 时)。务必保证成对调用,漏调会导致光标永久隐藏或提前恢复。
8.10 EngSetPointerTag --- 已废弃(win32k 导出)
c
BOOL EngSetPointerTag(HDEV hdev, SURFOBJ *psoMask, SURFOBJ *psoColor,
XLATEOBJ *pxlo, FLONG fl);
使用方式: 恒返回 FALSE,不要调用(Windows 2000 起废弃,仅保留 ABI 兼容)。
本文档基于 ReactOS 源代码
win32ss/gdi/eng/mouse.c/h及关联模块分析(2026 年 8 月)