ReactOS 图形系统分析(24):指针(光标)管理 — mouse.c/h

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 作为 PPDEVOBJPointer 字段存在------每个物理设备(屏幕)一个光标状态。

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,-1GreMovePointer 内部还会处理**平移设备(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 failureSPS_ERROR);alpha 光标统一转 32bpp。掩码表面从单色源拷贝时经 EXLATEOBJgppalMono → 设备调色板,前白后黑)转换。

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 光标IntEngAlphaBlendAC_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_* 决定引擎是否回退软件
DrvMovePointerppdev->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 调用。先问驱动 DrvSetPointerShapeSPS_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 月)

相关推荐
上玄code1 小时前
【Agent精讲】调一个LLMAPI背后的工程问题
java·开发语言·python·chatgpt
welfare9621 小时前
新号别搞:c++类与对象
开发语言·c++
JavaPub-rodert1 小时前
我把 OpenAI 协议塞进了 Go 工具库:go-commons 开始支持 AI 了
开发语言·人工智能·golang
caimouse1 小时前
ReactOS 图形系统分析(23):引擎内存管理 — mem.c
c语言·开发语言
caimouse1 小时前
ReactOS 图形系统分析(15):窗口对象 — EWNDOBJ(engwindow.c)
c语言·开发语言
青 春 记 忆1 小时前
零基础入门python07:让程序记住数据——JSON文件和异常处理
开发语言·windows·python·json·python3.11
W_326002 小时前
Python 常用标准 / 第三方库:random、tqdm、turtle、jieba 用法
开发语言·python
caimouse2 小时前
ReactOS 图形系统分析(17):区域填充 — EngPaint / IntEngPaint(paint.c)
c语言·开发语言·算法
北风toto2 小时前
研发效能与后端核心技术全景指南:从CI/CD到共性组件实战
java·开发语言·ci/cd