ReactOS 图形系统分析(50):画笔子系统 — pen.c

ReactOS 图形系统分析(50):画笔子系统 --- pen.c

1. 概述

1.1 本文范围

本文分析 ReactOS Win32 子系统(win32ss)中 ntgdi 的画笔(PEN)子系统 ,核心源码为

pen.c(file:///d:/reactos/win32ss/gdi/ntgdi/pen.c)(全文约 516 行)与

pen.h(file:///d:/reactos/win32ss/gdi/ntgdi/pen.h)。该子系统负责:

  1. 画笔对象创建NtGdiCreatePen(简单笔)与 NtGdiExtCreatePen(扩展笔)两个系统调用入口;
  2. 内部创建中枢IntGdiExtCreatePen------所有画笔最终都汇聚于此完成字段初始化与样式解析;
  3. 对象分配PEN_AllocPenWithHandle / PEN_AllocExtPenWithHandle(经 GDI 句柄表分配);
  4. 颜色修改IntGdiSetSolidPenColor(配合 SetDCPenColor 链路使用);
  5. 对象查询PEN_GetObject(填充 LOGPEN / EXTLOGPEN,供 GetObject 使用)。

画笔在 Windows GDI 中决定了线条的样式(实线/虚线/点线)、宽度与颜色 。ReactOS 的实现有一个

鲜明的特点:画笔(PEN)在内存布局上完全复用画刷(BRUSH) ------pen.h 中直接写

typedef BRUSH PEN, *PPEN;。这是因为线条本质上是"用笔的画刷沿路径扫掠出的区域",绘制引擎对

线条、填充、文字三者的画刷做了统一抽象(EBRUSHOBJeboLine / eboFill / eboText),

笔与刷共享 BRUSHBODY 内核布局可以极大复用代码(见第 2 章与第 7 章)。

1.2 设计动机

理解 pen.c 的设计,需要先回答几个"为什么":

  1. 为什么 PEN 复用 BRUSH 布局?

    在 GDI 内部,画直线 / 画边框 / 填区域最终都要生成一个"笔刷"交给显示驱动(Eng* 系列函数)。

    BRUSHBODY 的注释明确指出其布局取自冯远(Feng Yuan)《Windows Graphics Programming》一书,

    并要求"除非确有必要,否则不得修改该结构,修改前须先在邮件列表上讨论"。把 PEN 定义为 BRUSH,

    使 PEN_ShareLockPen 可以直接调用

    GDIOBJ_ReferenceObjectByHandle(hobj, GDIObjType_BRUSH_TYPE)画刷身份 引用句柄,

    使 dcobjs.cDC_vUpdateLineBrush 能用同一套画刷实现逻辑(EBRUSHOBJ_vUpdateFromDC

    来更新线条画刷。

  2. 为什么要有"简单笔"与"扩展笔"两种对象类型?

    老式 CreatePen 只支持宽度、样式、颜色三个要素;ExtCreatePen 额外支持几何笔

    PS_GEOMETRIC)、自定义样式数组(PS_USERSTYLE)、端点样式(PS_ENDCAP_*)与连接样式

    PS_JOIN_*)。ReactOS 为二者分配了不同的 GDI 低级对象类型

    GDILoObjType_LO_PEN_TYPEGDILoObjType_LO_EXTPEN_TYPE),并在 flAttrs 中打上

    BR_IS_OLDSTYLEPEN 标记。这一区分带来的直接收益是:

    • PEN_GetObject 可按对象类型返回 LOGPENEXTLOGPEN
    • PATH_WidenPathEx(宽化路径)可据此判断当前笔的类型,进而决定从 LOGPEN.lopnStyle
      还是 EXTLOGPEN.elpPenStyle 读取样式;
    • 宽笔判定宏 IntIsEffectiveWidePen 需要知道是否为几何笔 / 老式笔。
  3. 为什么宽笔要走"路径描边"而非直接画线?

    宽度 > 1 的线条不再是"数学上的线",而是一个有面积的区域。直接让引擎画一条粗线,

    不同驱动厂商的算法千差万别,且端点、连接的样式无法保证。ReactOS 的折中方案是:

    先把线条转成临时路径(PATH_MoveTo / PATH_LineTo),再调用 PATH_StrokePath

    对路径进行描边 ------若为宽笔则进一步调用 IntGdiWidenPath 把路径展开为笔宽的轮廓

    (一个闭合多边形),最后用 WINDING 填充模式填充该轮廓。这样粗细、端点、连接全部由

    path.c 的几何算法统一决定(详见第 7 章)。

  4. 为什么创建参数要在用户态与内核态各校验一遍?

    内核态是安全边界:NtGdiExtCreatePen 收到的 pUnsafeStyleulHatch 都是用户指针,

    必须用 ProbeForRead + SEH 保护复制,防止越权访问内核内存;而 gdi32ExtCreatePen

    先做一次"友好"校验(例如 PS_USERSTYLE 必须提供样式数组、PS_ALTERNATE 只允许美容笔),

    可以提前返回错误、避免不必要的系统调用开销。

1.3 整体架构图

复制代码
                应用层 (user32 / 应用程序)
   CreatePen          ExtCreatePen         SetDCPenColor      GetObject
      │                  │                     │                  │
      ▼                  ▼                     ▼                  ▼
┌─────────────────────────────────────────────────────────────────────────┐
│ gdi32.dll(用户态薄封装,还负责 EMF 记录)                                  │
│  objects/pen.c  CreatePen → NtGdiCreatePen                                │
│  objects/brush.c ExtCreatePen → NtGdiExtCreatePen(DIB 转换/预校验)        │
│  objects/dc.c    SetDCPenColor → 直接改 DC 属性 ulPenClr + DIRTY_LINE       │
└──────────────────────────────┬──────────────────────────────────────────┘
                               │ 系统调用(Nt*)
                               ▼
┌─────────────────────────────────────────────────────────────────────────┐
│ win32k.sys  ntgdi/pen.c(本文核心)                                        │
│  NtGdiCreatePen ──────────┐                                               │
│  NtGdiExtCreatePen ───────┼──▶ IntGdiExtCreatePen                        │
│                             │       ├─ PEN_AllocPenWithHandle(老式笔)   │
│                             │       │      └─ GDIOBJ_AllocObjWithHandle  │
│                             │       │      └─ PEN_vInit(pBrushAttr)    │
│                             │       ├─ PEN_AllocExtPenWithHandle(扩展笔)│
│                             │       ├─ 样式数组/端帽/连接/类型校验         │
│                             │       └─ flAttrs 标志与字段初始化            │
│  IntGdiSetSolidPenColor(改纯色,当前无调用方)                             │
│  PEN_GetObject(LOGPEN / EXTLOGPEN 查询)                                 │
│  PEN_ShareLockPen / PEN_UnlockPen / PEN_ShareUnlockPen(句柄引用)         │
└──────────────┬──────────────────────────────────────────────────────────┘
               │ 被选中/被查询
               ▼
┌─────────────────────────────────────────────────────────────────────────┐
│ 绘制链路(关联模块)                                                       │
│  dcobjs.c   DC_vUpdateLineBrush → eboLine(线条画刷)                     │
│  line.c     IntGdiLineTo / IntGdiPolyline → IntIsEffectiveWidePen 判定     │
│             ├─ 细线 → IntEngLineTo / IntEngPolyline(eboLine)             │
│             └─ 宽笔 → 临时路径 → PATH_StrokePath                          │
│  path.c     PATH_StrokePath → PATH_WidenPathEx → IntGdiWidenPath(轮廓)   │
│             → PATH_FillPathEx(WINDING 填充)                              │
│  fillshap.c 椭圆/矩形边框绘制同样按 IntIsEffectiveWidePen 分流               │
└─────────────────────────────────────────────────────────────────────────┘

1.4 文件组成

文件 角色
win32ss/gdi/ntgdi/pen.c 画笔子系统实现(2 个 NtGdi* 系统调用 + 内部函数)
win32ss/gdi/ntgdi/pen.h PEN 类型别名、分配/引用/查询接口、IntIsEffectiveWidePen
win32ss/gdi/ntgdi/brush.h BRUSH/BRUSHBODY 结构定义(PEN 复用的画刷布局)、BR_IS_* 标志
win32ss/gdi/ntgdi/line.c AddPenLinesBounds 实现、IntGdiLineTo/IntGdiPolyline 宽笔分流
win32ss/gdi/ntgdi/path.c PATH_StrokePathPATH_WidenPathExIntGdiWidenPath
win32ss/gdi/ntgdi/dcobjs.c DC_vUpdateLineBrush(画笔 → 线条画刷)
win32ss/gdi/ntgdi/brush.cpp BRUSH 析构(释放非默认样式数组)
win32ss/gdi/gdi32/objects/pen.c 用户态 CreatePen / CreatePenIndirect
win32ss/gdi/gdi32/objects/brush.c 用户态 ExtCreatePen(DIB 转换、预校验)
win32ss/gdi/gdi32/objects/dc.c 用户态 SetDCPenColor
win32ss/include/ntgdihdl.h BRUSH_ATTR 定义
sdk/include/psdk/wingdi.h PS_* 常量、LOGPENEXTLOGPEN 定义

2. 核心数据结构

2.1 PEN 与 BRUSH 的关系

pen.h(file:///d:/reactos/win32ss/gdi/ntgdi/pen.h) 全文如下(内部接口):

c 复制代码
#pragma once

/* Internal interface */

typedef BRUSH PEN, *PPEN;            // ← 关键:PEN 就是 BRUSH

PPEN  NTAPI PEN_AllocPenWithHandle(VOID);
PPEN  NTAPI PEN_AllocExtPenWithHandle(VOID);

#define PEN_UnlockPen(pPenObj)   GDIOBJ_vUnlockObject((POBJ)pPenObj)
#define PEN_ShareUnlockPen(ppen) GDIOBJ_vDereferenceObject((POBJ)ppen)

PPEN  FASTCALL PEN_ShareLockPen(HPEN hpen);

INT   NTAPI PEN_GetObject(
    _In_ PPEN pPen,
    _In_ INT Count,
    _Out_ PLOGPEN Buffer);

VOID FASTCALL AddPenLinesBounds(PDC, int, POINT *);

#define IntIsEffectiveWidePen(pbrLine) ( \
    (pbrLine)->lWidth > 1 && \
    ((pbrLine->flAttrs & BR_IS_OLDSTYLEPEN) || \
     ((pbrLine)->ulPenStyle & PS_TYPE_MASK) == PS_GEOMETRIC) \
)

要点:

  • PENBRUSH 的别名,PPENPBRUSH。因此 pen.c 中几乎所有函数都返回/操作 PBRUSH
    分配函数 PEN_AllocPenWithHandle 的返回值类型就是 PBRUSH(见 3.2 节源码)。
  • IntIsEffectiveWidePen("是否生效的宽笔")宏被 line.c、fillshap.c、path.c 三处使用,
    是笔对象与绘制引擎之间的关键纽带(详见 7.1 节)。

2.2 BRUSHBODY 结构(画笔/画刷共用布局)

brush.h(file:///d:/reactos/win32ss/gdi/ntgdi/brush.h) 中定义:

c 复制代码
#define NB_HATCH_STYLES  6

/*
 * The layout of this structure is taken from "Windows Graphics Programming"
 * book written by Feng Yuan.
 *
 * DON'T MODIFY THIS STRUCTURE UNLESS REALLY NEEDED AND EVEN THEN ASK ON
 * A MAILING LIST FIRST.
 */
typedef struct _BRUSHBODY
{
    ULONG iHatch;           // This is not the brush style, but the hatch style!
    HBITMAP hbmPattern;
    HBITMAP hbmClient;
    ULONG flAttrs;

    ULONG ulBrushUnique;
    BRUSH_ATTR *pBrushAttr; // Pointer to the currently active brush attribute
    BRUSH_ATTR BrushAttr;   // Internal brush attribute for global brushes
    POINT ptOrigin;
    ULONG bCacheGrabbed;
    COLORREF crBack;
    COLORREF crFore;
    ULONG ulPalTime;
    ULONG ulSurfTime;
    PVOID pvRBrush;
    HDEV hdev;
    //DWORD unk054;

    /* The following members are for PENs only */
    LONG lWidth;
    FLOATOBJ eWidth;
    ULONG ulPenStyle;
    DWORD *pStyle;
    ULONG dwStyleCount;
    BYTE jJoin;             // 0x06c Join styles for geometric wide lines
    BYTE jEndCap;           //       end cap style for a geometric wide line
    //WORD unk06e;          // 0x06e
    INT iBrushStyle;        // 0x070
    //PREGION prgn;           // 0x074
    //DWORD unk078;         // 0x078
    //DWORD unk07c;           // 0x07c
    ULONG ulStyleSize;
    LIST_ENTRY ListHead;    // 0x080
} BRUSHBODY;

#ifndef __cplusplus
typedef struct _BRUSH
{
    /* Header for all gdi objects in the handle table.
       Do not (re)move this. */
    BASEOBJECT BaseObject;
    BRUSHBODY;
} BRUSH, *PBRUSH;
#else
class BRUSH;
typedef class BRUSH *PBRUSH;
#endif

BRUSH 由两部分组成:

  1. BASEOBJECT BaseObject :GDI 句柄表对象公共头(含 hHmgr 句柄、pEntry 句柄表项指针、
    对象类型、引用计数等)。IntGdiExtCreatePenhPen = pbrushPen->BaseObject.hHmgr;
    就是把句柄表里分配到的句柄直接作为画笔句柄返回。
  2. BRUSHBODY :画刷主体。前 17 个字段(iHatchhdev)是画刷与画笔共用 的;
    lWidth 开始是"仅画笔使用"的成员。

各字段对画笔的意义:

字段 画笔中的含义
iHatch 影线样式(对画笔一般不使用;注释强调"这不是画刷样式,而是影线样式")
hbmPattern 图案位图(BS_PATTERN 类画刷用)
hbmClient 客户端位图;在画笔中保存用户提供的 hatch 参数 ulClientHatchPEN_GetObject 将其作为 elpHatch 返回)
flAttrs 属性标志位BR_IS_* 集合),见 2.4 节
ulBrushUnique 画刷唯一序号(缓存/驱动同步用)
pBrushAttr 指向"当前生效的画刷属性";PEN_vInit 将其指向对象内嵌的 BrushAttr
BrushAttr 对象内嵌的画刷属性({AttrFlags, lbColor}),画笔的颜色 存放在 BrushAttr.lbColor
ptOrigin 画刷原点(图案对齐用)
bCacheGrabbed / crBack / crFore / ulPalTime / ulSurfTime / pvRBrush / hdev 画刷实现/调色板/设备句柄等(画笔基本不用)
lWidth 逻辑宽度 。老式笔与美容笔为设备单位;几何笔为世界单位。IntIsEffectiveWidePen 依据它判断宽笔
eWidth 宽度浮点表示(FLOATOBJ),IntGdiExtCreatePenFLOATOBJ_SetLonglWidth 生成
ulPenStyle 完整笔样式PS_STYLE_MASK 位(线型)+ PS_ENDCAP_MASK 位(端点)+ PS_JOIN_MASK 位(连接)+ PS_TYPE_MASK 位(类型)
pStyle 样式数组指针。默认样式指向 pen.c 的静态数组;PS_USERSTYLE 指向内核池拷贝
dwStyleCount 样式数组元素个数(PS_USERSTYLE 时为用户提供;默认样式为内建数组长度)
jJoin 几何宽线的连接样式(当前代码未单独写入,连接样式实际从 ulPenStyle 提取)
jEndCap 几何宽线的端点样式(同上)
iBrushStyle 画刷样式(BS_SOLID / BS_HATCHED / BS_PATTERN ...),即 ExtCreatePenlplb->lbStyle
ulStyleSize 样式数组元素之和(虚线/点线一个周期的总长度),IntGdiExtCreatePen 末尾累加
ListHead 链表节点(对象挂接用)

注意:结构注释中的 0x06c0x0700x080 等偏移标注来自冯远书中的

Windows 2000 布局,仅供参考,ReactOS 实际偏移以编译结果为准。

2.3 BRUSH_ATTR(画笔颜色存放处)

ntgdihdl.h(file:///d:/reactos/win32ss/include/ntgdihdl.h) 第 353 行:

c 复制代码
typedef struct _BRUSH_ATTR /* Used with pen too. */
{
    FLONG    AttrFlags;
    COLORREF lbColor;
} BRUSH_ATTR, *PBRUSH_ATTR;

注释明确写着"画笔也使用它 "。IntGdiExtCreatePen 设置 pbrushPen->BrushAttr.lbColor = ulColor;

即画笔颜色。PEN_vInitpBrushAttr 指向内嵌 BrushAttr,构成

"pBrushAttr → 当前生效属性"的双层结构:若画笔被跨进程共享并需要不同的颜色/属性时,

可把 pBrushAttr 换到进程池(GdiPoolFree(GetBrushAttrPool(), ...),见 brush.cpp 的析构与

vReleaseAttribute)。

2.4 flAttrs 标志位(BR_IS_*)

brush.h 中与画笔相关的标志位:

c 复制代码
#define BR_IS_SOLID         0x00000010 /* Solid brush */
#define BR_IS_NULL          0x00000100 /* Null/hollow brush */
#define BR_IS_PEN           0x00000400 /* Pen */
#define BR_IS_OLDSTYLEPEN   0x00000800 /* Geometric pen */
#define BR_IS_DEFAULTSTYLE  0x00004000
#define BR_IS_INSIDEFRAME   0x00010000

IntGdiExtCreatePenflAttrs 的赋值逻辑:

  • 老式笔:flAttrs = BR_IS_OLDSTYLEPEN;扩展笔:flAttrs = BR_IS_PEN
  • PS_NULL → 追加 BR_IS_NULL
  • PS_SOLID → 追加 BR_IS_SOLID
  • PS_ALTERNATE / PS_DOT / PS_DASH / PS_DASHDOT / PS_DASHDOTDOT
    追加 BR_IS_SOLID | BR_IS_DEFAULTSTYLE(样式由内建静态数组提供,故标记"默认样式");
  • PS_INSIDEFRAME → 追加 BR_IS_SOLID | BR_IS_INSIDEFRAME
  • PS_USERSTYLE → 追加 BR_IS_SOLID(不做 DEFAULTSTYLE 标记,因为样式数组来自用户)。

这些标志在绘制链路中起决定作用:

  • BR_IS_NULL:line.c 中 if (!(pbrLine->flAttrs & BR_IS_NULL)) 决定是否真的画线;
  • BR_IS_OLDSTYLEPENIntIsEffectiveWidePenPEN_GetObject 判断老式/扩展笔;
  • BR_IS_DEFAULTSTYLEPEN_GetObject 计算 elpNumEntries 时把默认样式记作 0;
    brush.cpp 析构时不释放默认样式(静态数组)而只释放用户样式。

2.5 PS_* 样式 / 端点 / 连接常量(wingdi.h)

wingdi.h(file:///d:/reactos/sdk/include/psdk/wingdi.h) 中的定义:

c 复制代码
#define PS_GEOMETRIC   65536        /* 0x00010000 */
#define PS_COSMETIC    0
#define PS_ALTERNATE   8
#define PS_SOLID       0
#define PS_DASH        1
#define PS_DOT         2
#define PS_DASHDOT     3
#define PS_DASHDOTDOT  4
#define PS_NULL        5
#define PS_USERSTYLE   7
#define PS_INSIDEFRAME 6

#define PS_ENDCAP_ROUND   0
#define PS_ENDCAP_SQUARE  256        /* 0x00000100 */
#define PS_ENDCAP_FLAT    512        /* 0x00000200 */

#define PS_JOIN_BEVEL     4096       /* 0x00001000 */
#define PS_JOIN_MITER     8192       /* 0x00002000 */
#define PS_JOIN_ROUND     0

#define PS_JOIN_MASK    0x0000F000
#define PS_STYLE_MASK   15           /* 0x0000000F */
#define PS_ENDCAP_MASK  3840         /* 0x00000F00 */
#define PS_TYPE_MASK    983040       /* 0x000F0000 */

注意几个"0 即默认值"的陷阱:

  • PS_SOLID == 0PS_COSMETIC == 0PS_ENDCAP_ROUND == 0PS_JOIN_ROUND == 0------
    与"无标志"同值,因此校验时必须用掩码(& PS_STYLE_MASK 等)而不能直接比较;
  • PS_ALTERNATE (8) 数值上大于 PS_INSIDEFRAME (6),所以 gdi32 的 CreatePen
    在范围钳制时会把 ALTERNATE 归为非法并降级为 PS_SOLID
  • PS_GEOMETRIC (0x10000)PS_TYPE_MASK (0xF0000) 位于高位,与低 4 位线型、
    8-11 位端点、12-15 位连接互不冲突,可以 | 组合成一个 32 位 ulPenStyle

掩码布局(32 位 dwPenStyle):

复制代码
bit 0-3    线型  PS_SOLID/DASH/DOT/DASHDOT/DASHDOTDOT/NULL/INSIDEFRAME/USERSTYLE/ALTERNATE
bit 8-11   端点  PS_ENDCAP_ROUND/SQUARE/FLAT
bit 12-15  连接  PS_JOIN_ROUND/BEVEL/MITER
bit 16-19  类型  PS_COSMETIC/PS_GEOMETRIC

2.6 LOGPEN 与 EXTLOGPEN(用户可见结构)

wingdi.h 第 2290、2387 行:

c 复制代码
typedef struct tagLOGPEN {
    UINT lopnStyle;
    POINT lopnWidth;
    COLORREF lopnColor;
} LOGPEN,*PLOGPEN,*LPLOGPEN;

typedef struct tagEXTLOGPEN {
    DWORD elpPenStyle;
    DWORD elpWidth;
    UINT elpBrushStyle;
    COLORREF elpColor;
    ULONG_PTR elpHatch;
    DWORD elpNumEntries;
    DWORD elpStyleEntry[1];
} EXTLOGPEN,*PEXTLOGPEN,*LPEXTLOGPEN,*NPEXTLOGPEN;
  • LOGPENCreatePen/CreatePenIndirect 的落点:lopnWidthPOINT(只用 .x 表示宽度),
    lopnColor 即颜色;
  • EXTLOGPENExtCreatePen 的落点:elpWidthDWORD(几何笔世界单位),
    elpBrushStyle 对应 LOGBRUSH.lbStyleelpHatch 对应 LOGBRUSH.lbHatch
    elpStyleEntry[] 为柔性数组存放用户样式。
  • PEN_GetObject 正是按老式/扩展笔分别填充这两个结构(见第 6 章)。

3. 内部辅助函数

3.1 PEN_vInit(pen.c:16-23)

c 复制代码
static
VOID
PEN_vInit(
    PPEN ppen)
{
    /* Start with kmode brush attribute */
    ppen->pBrushAttr = &ppen->BrushAttr;
}
  • 签名static VOID PEN_vInit(PPEN ppen)------静态函数,仅本文件内调用。
  • 作用 :把画笔对象的 pBrushAttr 指针指向对象内嵌BrushAttr 字段。
    注释"Start with kmode brush attribute"说明初始状态是"内核模式画刷属性",
    即属性直接内嵌在对象里,无需从进程 GDI 池(GetBrushAttrPool)单独分配。
  • 使用方式 :被 PEN_AllocPenWithHandlePEN_AllocExtPenWithHandle 在对象分配成功后调用。
  • 注意事项 :若后续画笔被跨进程共享(share),可能把 pBrushAttr 指向进程池中的独立属性块,
    此时析构函数 BRUSH::~BRUSH 会先 GdiPoolFree 释放它(见 brush.cpp:59-63)。

3.2 PEN_AllocPenWithHandle(pen.c:25-40)

c 复制代码
PBRUSH
NTAPI
PEN_AllocPenWithHandle(
    VOID)
{
    PPEN ppen;

    ppen = (PBRUSH)GDIOBJ_AllocObjWithHandle(GDILoObjType_LO_PEN_TYPE, sizeof(PEN));
    if (ppen == NULL)
    {
        return NULL;
    }

    PEN_vInit(ppen);
    return ppen;
}
  • 签名PBRUSH NTAPI PEN_AllocPenWithHandle(VOID)------注意返回类型是 PBRUSH
    再次印证 PEN 与 BRUSH 同构。
  • 作用 :从 GDI 句柄表分配一个老式(简单)笔 对象:
    1. 调用 GDIOBJ_AllocObjWithHandle(GDILoObjType_LO_PEN_TYPE, sizeof(PEN))------
      LO_PEN_TYPE(低级对象类型)登记并分配 sizeof(PEN)(即 BRUSH)大小的对象,
      同时从句柄表取得句柄存入 BaseObject.hHmgr
    2. 失败返回 NULL(调用方 IntGdiExtCreatePen 随即设置 ERROR_NOT_ENOUGH_MEMORY);
    3. 成功则 PEN_vInit 初始化属性指针。
  • 使用方式 :仅在 IntGdiExtCreatePenbOldStylePen == TRUE 分支调用
    (即来自 NtGdiCreatePen 的路径)。

3.3 PEN_AllocExtPenWithHandle(pen.c:42-57)

c 复制代码
PBRUSH
NTAPI
PEN_AllocExtPenWithHandle(
    VOID)
{
    PPEN ppen;

    ppen = (PBRUSH)GDIOBJ_AllocObjWithHandle(GDILoObjType_LO_EXTPEN_TYPE, sizeof(PEN));
    if (ppen == NULL)
    {
        return NULL;
    }

    PEN_vInit(ppen);
    return ppen;
}

与 3.2 完全对称,唯一区别是对象类型为 GDILoObjType_LO_EXTPEN_TYPE

这一个类型位的差别决定了后续:

  • PEN_ShareLockPen 的类型校验分支(两种类型都接受);
  • PEN_GetObject 返回 LOGPEN 还是 EXTLOGPEN(按 flAttrs & BR_IS_OLDSTYLEPEN 判断,见第 6 章);
  • PATH_WidenPathExGDI_HANDLE_GET_TYPE 判断对象类型(GDI_OBJECT_TYPE_PEN /
    GDI_OBJECT_TYPE_EXTPEN)以决定读取 lopnStyle 还是 elpPenStyle(见 7.5 节)。

3.4 PEN_ShareLockPen(pen.c:59-70)

c 复制代码
PBRUSH
FASTCALL
PEN_ShareLockPen(HPEN hobj)
{
    if ((GDI_HANDLE_GET_TYPE(hobj) != GDILoObjType_LO_PEN_TYPE) &&
        (GDI_HANDLE_GET_TYPE(hobj) != GDILoObjType_LO_EXTPEN_TYPE))
    {
        return NULL;
    }

    return (PBRUSH)GDIOBJ_ReferenceObjectByHandle(hobj, GDIObjType_BRUSH_TYPE);
}
  • 签名PBRUSH FASTCALL PEN_ShareLockPen(HPEN hobj)------FASTCALL 约定,按句柄取对象。
  • 作用 :把画笔句柄"共享锁定"为对象指针:
    1. 先用 GDI_HANDLE_GET_TYPE(hobj) 校验句柄确为 LO_PEN_TYPELO_EXTPEN_TYPE
      否则返回 NULL
    2. 调用 GDIOBJ_ReferenceObjectByHandle(hobj, GDIObjType_BRUSH_TYPE)------
      以 BRUSH 对象类型 增加引用计数并返回对象指针。这里显式传 BRUSH_TYPE
      正是 PEN 复用 BRUSH 布局的直接体现:画笔句柄可以放心地按画刷对象来引用。
  • 使用方式
    • IntGdiSetSolidPenColor(改颜色前取对象);
    • dcobjs.cDC_vUpdateLineBrush(选中新笔时换 pbrLine)。
  • 注意事项 :返回的指针必须配对 PEN_ShareUnlockPen(即 GDIOBJ_vDereferenceObject)释放引用。

3.5 PEN_UnlockPen / PEN_ShareUnlockPen 宏(pen.h:17-18)

c 复制代码
#define PEN_UnlockPen(pPenObj)     GDIOBJ_vUnlockObject((POBJ)pPenObj)
#define PEN_ShareUnlockPen(ppen)   GDIOBJ_vDereferenceObject((POBJ)ppen)
  • PEN_UnlockPen:撤销"独占锁"(对象创建后持有的锁)。IntGdiExtCreatePen 分配对象后
    GDIOBJ_AllocObjWithHandle 持有独占锁,字段初始化完成后调用 PEN_UnlockPen 解锁,
    使句柄可被其他进程/线程引用;
  • PEN_ShareUnlockPen:撤销 PEN_ShareLockPen 增加的共享引用。
    二者语义不同:前者是"解锁对象(降级为可共享)",后者是"减少一次共享引用"。

3.6 AddPenLinesBounds(声明于 pen.h:31,实现于 line.c:19)

c 复制代码
VOID FASTCALL AddPenLinesBounds(PDC, int, POINT *);
  • 虽声明在 pen.h,但实现在 line.c(file:///d:/reactos/win32ss/gdi/ntgdi/line.c) 第 19 行。
  • 作用:当 DC 处于累积模式(DC_ACCUM_APP / DC_ACCUM_WMGR)时,把折线各点加入
    "笔线边界"(用于窗口管理器累积绘制时的边界合并)。IntGdiLineToIntGdiPolyline
    在正式绘制前调用它。

4. 创建流程

4.1 用户态入口 CreatePen(gdi32/objects/pen.c)

c 复制代码
HPEN WINAPI
CreatePen(
    int nPenStyle,
    int nWidth,
    COLORREF crColor)
{
    if (nPenStyle < PS_SOLID) nPenStyle = PS_SOLID;
    if (nPenStyle > PS_DASHDOTDOT)
    {
        if (nPenStyle == PS_NULL) return GetStockObject(NULL_PEN);
        if (nPenStyle != PS_INSIDEFRAME) nPenStyle = PS_SOLID;
    }
    return NtGdiCreatePen(nPenStyle, nWidth, crColor, NULL);
}

行为要点:

  • 样式钳制:小于 PS_SOLID 一律按实线;大于 PS_DASHDOTDOT 时,
    PS_NULL 直接返回库存空笔(不再进内核)、PS_INSIDEFRAME 放行、其余(含
    PS_USERSTYLEPS_ALTERNATE)降级为 PS_SOLID------因为简单笔不支持自定义样式;
  • CreatePenIndirect 只是解包 LOGPEN 后调用 CreatePen(注意:Windows 系实现
    不检查 lplgpn 是否为 NULL,直接解引用,可能崩溃,与 2K/XP/Vista 行为一致);
  • 最终调用系统调用 NtGdiCreatePen(nPenStyle, nWidth, crColor, NULL)
    第 4 个参数 hbr 恒为 NULL。

4.2 用户态入口 ExtCreatePen(gdi32/objects/brush.c)

c 复制代码
HPEN APIENTRY
ExtCreatePen(DWORD dwPenStyle,
             DWORD dwWidth,
             CONST LOGBRUSH *lplb,
             DWORD dwStyleCount,
             CONST DWORD *lpStyle)
{
    ...
    if ((dwPenStyle & PS_STYLE_MASK) == PS_USERSTYLE)
    {
        if(!lpStyle) { SetLastError(ERROR_INVALID_PARAMETER); return 0; }
    }
    else if ((dwPenStyle & PS_STYLE_MASK) == PS_INSIDEFRAME &&
             (dwPenStyle & PS_TYPE_MASK) != PS_GEOMETRIC)
    {
        SetLastError(ERROR_INVALID_PARAMETER); return 0;
    }
    else if ((dwPenStyle & PS_STYLE_MASK) == PS_ALTERNATE &&
             (dwPenStyle & PS_TYPE_MASK) != PS_COSMETIC)
    {
        SetLastError(ERROR_INVALID_PARAMETER); return 0;
    }
    else
    {
        if (dwStyleCount || lpStyle)
        {
            SetLastError(ERROR_INVALID_PARAMETER); return 0;
        }
    }
    ...
    hPen = NtGdiExtCreatePen(dwPenStyle, dwWidth, lbStyle, lplb->lbColor,
                             lplb->lbHatch, (ULONG_PTR)pConvertedInfo,
                             dwStyleCount, (PULONG)lpStyle,
                             ConvertedInfoSize, FALSE, NULL);
    ...
}

行为要点(预校验,避免无谓的系统调用):

  • PS_USERSTYLE 必须携带样式数组指针;
  • PS_INSIDEFRAME 只允许几何笔(Windows 规则:框架内样式仅几何笔合法);
  • PS_ALTERNATE 只允许美容笔;
  • 其余样式禁止携带 dwStyleCount/lpStyle
  • BS_DIBPATTERN 类会先把用户 DIB 句柄 GlobalLock 并做 ConvertBitmapInfo 转换
    ConvertedInfoSize 作为 cjDIB 传入内核,内核据此 ProbeForRead);
  • 调用内核时 bOldStylePen = FALSE(扩展笔)、hBrush = NULL

4.3 NtGdiCreatePen(pen.c:355-380)

c 复制代码
HPEN
APIENTRY
NtGdiCreatePen(
    INT PenStyle,
    INT Width,
    COLORREF Color,
    IN HBRUSH hbr)
{
    if ((PenStyle < PS_SOLID) ||( PenStyle > PS_INSIDEFRAME))
    {
        EngSetLastError(ERROR_INVALID_PARAMETER);
        return NULL;
    }

    return IntGdiExtCreatePen(PenStyle,
                              Width,
                              BS_SOLID,
                              Color,
                              0,
                              0,
                              0,
                              NULL,
                              0,
                              TRUE,
                              hbr);
}
  • 签名HPEN APIENTRY NtGdiCreatePen(INT PenStyle, INT Width, COLORREF Color, IN HBRUSH hbr)
  • 参数PenStyle 仅允许 [PS_SOLID(0) .. PS_INSIDEFRAME(6)] 闭区间
    (排除了 PS_USERSTYLE=7PS_ALTERNATE=8);hbr 保留参数恒为 NULL。
  • 作用 :简单笔的极薄封装------校验后原样转发给 IntGdiExtCreatePen
    • ulBrushStyle = BS_SOLID(简单笔永远是纯色);
    • 样式数组为 0, NULL
    • bOldStylePen = TRUE → 分配 LO_PEN_TYPE 对象并标记 BR_IS_OLDSTYLEPEN
  • 注意事项WidthINT,可为负。IntGdiExtCreatePen 开头的
    dwWidth = abs(dwWidth) 会把负数取绝对值(Windows 文档:宽度取绝对值)。
    宽度 0 的老式笔=1 像素笔(除非样式为非实线时强制 dwWidth = 1,见 4.5.2)。

4.4 NtGdiExtCreatePen(pen.c:382-514)

c 复制代码
HPEN
APIENTRY
NtGdiExtCreatePen(
    DWORD dwPenStyle,
    DWORD ulWidth,
    IN ULONG ulBrushStyle,
    IN ULONG ulColor,
    IN ULONG_PTR ulClientHatch,
    IN ULONG_PTR ulHatch,
    DWORD dwStyleCount,
    PULONG pUnsafeStyle,
    IN ULONG cjDIB,
    IN BOOL bOldStylePen,
    IN OPTIONAL HBRUSH hBrush)

参数说明

参数 含义
dwPenStyle 完整笔样式(线型
ulWidth 宽度(几何笔为世界单位)
ulBrushStyle 画刷样式 BS_*BS_SOLID/BS_HATCHED/BS_PATTERN/BS_DIBPATTERN/BS_DIBPATTERNPT/BS_NULL
ulColor 颜色(纯色笔)
ulClientHatch 客户端 hatch(LOGBRUSH.lbHatch
ulHatch 图案 DIB 指针(BS_PATTERN 类时为用户指针,需探测)
dwStyleCount 用户样式元素个数
pUnsafeStyle 用户态指针的样式数组(非安全指针,必须探测复制)
cjDIB DIB 字节数(供 ProbeForRead
bOldStylePen 是否老式笔(gdi32 的 CreatePen→TRUE,ExtCreatePen→FALSE)
hBrush 保留参数(OPTIONAL,当前恒为 NULL)

处理流程(按源码顺序):

  1. 样式个数边界

    c 复制代码
    if ((int)dwStyleCount < 0) return 0;
    if (dwStyleCount > 16) { EngSetLastError(ERROR_INVALID_PARAMETER); return 0; }

    把 DWORD 按有符号判负(堵住超大值),上限 16 个元素。

  2. 美容笔强制纯色

    c 复制代码
    if (((dwPenStyle & PS_TYPE_MASK) == PS_COSMETIC) && (ulBrushStyle != BS_SOLID))
    { EngSetLastError(ERROR_INVALID_PARAMETER); return 0; }

    美容笔只能是实心纯色(宽度恒 1 像素)。

  3. 空笔捷径

    c 复制代码
    if (((dwPenStyle & PS_STYLE_MASK) == PS_NULL) || (ulBrushStyle == BS_NULL))
        return StockObjects[NULL_PEN];

    返回库存 NULL_PEN(全局共享对象,无需分配)。

  4. 画刷样式归类

    c 复制代码
    if ((ulBrushStyle == BS_PATTERN) || (ulBrushStyle == BS_DIBPATTERN) ||
        (ulBrushStyle == BS_DIBPATTERNPT))
        ulColor = 0;                      // 图案笔:颜色无意义,清零
    else if ((ulBrushStyle != BS_SOLID) && (ulBrushStyle != BS_HATCHED))
    { EngSetLastError(ERROR_INVALID_PARAMETER); return 0; }

    图案类笔不携带颜色;其余只接受 BS_SOLID / BS_HATCHED

  5. 非 USERSTYLE 清空样式参数

    c 复制代码
    if ((dwPenStyle & PS_STYLE_MASK) != PS_USERSTYLE)
    {
        dwStyleCount = 0;
        pUnsafeStyle = NULL;
    }
  6. 样式数组安全复制(核心安全逻辑)

    c 复制代码
    if (dwStyleCount > 0)
    {
        if (pUnsafeStyle == NULL) { EngSetLastError(ERROR_INVALID_PARAMETER); return 0; }
        pSafeStyle = ExAllocatePoolWithTag(NonPagedPool,
                                           dwStyleCount * sizeof(DWORD),
                                           GDITAG_PENSTYLE);
        if (!pSafeStyle) { SetLastNtError(ERROR_NOT_ENOUGH_MEMORY); return 0; }
        _SEH2_TRY
        {
            ProbeForRead(pUnsafeStyle, dwStyleCount * sizeof(DWORD), 1);
            RtlCopyMemory(pSafeStyle, pUnsafeStyle, dwStyleCount * sizeof(DWORD));
        }
        _SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
        {
            Status = _SEH2_GetExceptionCode();
        }
        _SEH2_END
        if(!NT_SUCCESS(Status)) { SetLastNtError(Status);
            ExFreePoolWithTag(pSafeStyle, GDITAG_PENSTYLE); return 0; }
    }

    要点:

    • 分配非分页池NonPagedPool)并以 GDITAG_PENSTYLE 标记------样式数组可能被
      DPC/中断级路径读取,不能分页;
    • ProbeForRead + RtlCopyMemory 包在 SEH 里,用户指针非法(坏地址/越界)时
      转为异常码返回,绝不 crash 内核;
    • 所有权转移 :成功后 pSafeStyle 不再释放------它成为画笔对象的一部分
      IntGdiExtCreatePen 存入 pStyle),由 BRUSH::~BRUSH 在对象销毁时释放
      (brush.cpp:73-76,且仅在非 BR_IS_DEFAULTSTYLE 时释放)。
  7. BS_PATTERN 的 DIB 探测

    c 复制代码
    if (ulBrushStyle == BS_PATTERN)
    {
        _SEH2_TRY { ProbeForRead((PVOID)ulHatch, cjDIB, 1); }
        _SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER) { Status = _SEH2_GetExceptionCode(); }
        _SEH2_END
        if(!NT_SUCCESS(Status)) { SetLastNtError(Status);
            if (pSafeStyle) ExFreePoolWithTag(pSafeStyle, GDITAG_PENSTYLE); return 0; }
    }

    只做探测 (验证可读),不复制------IntGdiExtCreatePenulHatch 原样存入

    hbmClient(注释 // FIXME: Copy the bitmap first ? 表明当前尚未复制,是已知待办)。

  8. 调用内部中枢并清理

    c 复制代码
    hPen = IntGdiExtCreatePen(dwPenStyle, ulWidth, ulBrushStyle, ulColor,
                              ulClientHatch, ulHatch, dwStyleCount, pSafeStyle,
                              cjDIB, bOldStylePen, hBrush);
    if (!hPen && pSafeStyle) ExFreePoolWithTag(pSafeStyle, GDITAG_PENSTYLE);
    return hPen;

    成功时 pSafeStyle 留在笔对象里;失败(IntGdiExtCreatePen 返回 NULL 且已自清理)

    时在此回收样式池。

4.5 IntGdiExtCreatePen 详解(pen.c:72-269,创建中枢)

c 复制代码
HPEN APIENTRY
IntGdiExtCreatePen(
    DWORD dwPenStyle,
    DWORD dwWidth,
    IN ULONG ulBrushStyle,
    IN ULONG ulColor,
    IN ULONG_PTR ulClientHatch,
    IN ULONG_PTR ulHatch,
    DWORD dwStyleCount,
    PULONG pStyle,
    IN ULONG cjDIB,
    IN BOOL bOldStylePen,
    IN OPTIONAL HBRUSH hbrush)
4.5.1 默认样式静态数组
c 复制代码
static ULONG aulStyleAlternate[]  = { 1, 1 };
static ULONG aulStyleDash[]       = { 6, 2 };
static ULONG aulStyleDot[]        = { 1, 1 };
static ULONG aulStyleDashDot[]    = { 3, 2, 1, 2 };
static ULONG aulStyleDashDotDot[] = { 3, 1, 1, 1, 1, 1 };
  • 这些数组是静态共享 的,绝不修改(只读),因此多个笔可以安全地让 pStyle 指向同一数组;
  • 数值单位是"逻辑单位":PS_DASH = 画 6 停 2;PS_DOT = 画 1 停 1;
    PS_DASHDOT = 画 3 停 2 画 1 停 2;PS_DASHDOTDOT = 画 3 停 1 画 1 停 1 画 1 停 1;
    PS_ALTERNATE = 每像素交替(画 1 停 1)。
4.5.2 预处理与参数校验
c 复制代码
dwWidth = abs(dwWidth);

if ( (dwPenStyle & PS_STYLE_MASK) == PS_NULL)
{
    return StockObjects[NULL_PEN];
}

if (bOldStylePen)
    pbrushPen = PEN_AllocPenWithHandle();
else
    pbrushPen = PEN_AllocExtPenWithHandle();

if (!pbrushPen)
{
    EngSetLastError(ERROR_NOT_ENOUGH_MEMORY);
    DPRINT("Can't allocate pen\n");
    return 0;
}

hPen = pbrushPen->BaseObject.hHmgr;

if (bOldStylePen)
{
    // If nWidth is zero, the pen is a single pixel wide, regardless of the current transformation.
    if (!dwWidth && (dwPenStyle & PS_STYLE_MASK) != PS_SOLID)
        dwWidth = 1;
}
else
{
    switch (dwPenStyle & PS_ENDCAP_MASK)
    {
        case PS_ENDCAP_ROUND: case PS_ENDCAP_SQUARE: case PS_ENDCAP_FLAT:
            break;
        default:
            goto ExitCleanup;
    }
    switch (dwPenStyle & PS_JOIN_MASK)
    {
        case PS_JOIN_ROUND: case PS_JOIN_BEVEL: case PS_JOIN_MITER:
            break;
        default:
            goto ExitCleanup;
    }
    switch (dwPenStyle & PS_TYPE_MASK)
    {
        case PS_COSMETIC:
            if (dwWidth != 1 || ulBrushStyle != BS_SOLID)
                goto ExitCleanup;
            break;
        case PS_GEOMETRIC:
            break;
        default:
            goto ExitCleanup;
    }
}

校验要点:

  • 宽度取绝对值(abs);
  • PS_NULL 直接返回库存空笔;
  • 老式笔:宽度 0 且非实线时强制 1 (注释强调"宽度为 0 时笔恒为单像素,与当前变换无关");
    实线 + 宽度 0 保持 0(等价 1 像素细线,IntIsEffectiveWidePen 不成立);
  • 扩展笔三层校验:
    • 端点必须是 ROUND/SQUARE/FLAT 之一;
    • 连接必须是 ROUND/BEVEL/MITER 之一;
    • 类型:PS_COSMETIC 要求 dwWidth == 1 && ulBrushStyle == BS_SOLID(美容笔必须 1 像素纯色),
      PS_GEOMETRIC 任意;
    • 任一不合法 → goto ExitCleanup(设 ERROR_INVALID_PARAMETER 并删除对象)。
4.5.3 字段初始化
c 复制代码
pbrushPen->lWidth = dwWidth;
FLOATOBJ_SetLong(&pbrushPen->eWidth, pbrushPen->lWidth);
pbrushPen->ulPenStyle = dwPenStyle;
pbrushPen->BrushAttr.lbColor = ulColor;
pbrushPen->iBrushStyle = ulBrushStyle;
// FIXME: Copy the bitmap first ?
pbrushPen->hbmClient = (HANDLE)ulClientHatch;
pbrushPen->dwStyleCount = 0;
pbrushPen->pStyle = NULL;
pbrushPen->ulStyleSize = 0;
pbrushPen->flAttrs = bOldStylePen ? BR_IS_OLDSTYLEPEN : BR_IS_PEN;
  • lWidtheWidth 双份宽度:整型(逻辑/设备)与浮点(世界单位);
  • BrushAttr.lbColor 存颜色(这是 PEN_GetObjectIntGdiSetSolidPenColor 读写的字段);
  • hbmClient 存用户 hatch;
  • flAttrs 先打类型标记,随后按线型追加。
4.5.4 样式分发(switch PS_STYLE_MASK)
线型 flAttrs 追加 pStyle / dwStyleCount
PS_NULL BR_IS_NULL 不设样式(保持 NULL/0)
PS_SOLID BR_IS_SOLID 不设样式
PS_ALTERNATE `BR_IS_SOLID BR_IS_DEFAULTSTYLE`
PS_DOT `BR_IS_SOLID BR_IS_DEFAULTSTYLE`
PS_DASH `BR_IS_SOLID BR_IS_DEFAULTSTYLE`
PS_DASHDOT `BR_IS_SOLID BR_IS_DEFAULTSTYLE`
PS_DASHDOTDOT `BR_IS_SOLID BR_IS_DEFAULTSTYLE`
PS_INSIDEFRAME `BR_IS_SOLID BR_IS_INSIDEFRAME`
PS_USERSTYLE BR_IS_SOLID 用户数组(见 4.5.5)
其他 --- DPRINT1 警告后 goto ExitCleanup
4.5.5 PS_USERSTYLE 的合法性检查
c 复制代码
case PS_USERSTYLE:
    {
        UINT i;
        BOOL has_neg = FALSE, all_zero = TRUE;

        for(i = 0; (i < dwStyleCount) && !has_neg; i++)
        {
            has_neg = has_neg || (((INT)(pStyle[i])) < 0);
            all_zero = all_zero && (pStyle[i] == 0);
        }

        if(all_zero || has_neg)
        {
            goto ExitCleanup;
        }
    }
    /* FIXME: What style here? */
    pbrushPen->flAttrs |= BR_IS_SOLID;
    pbrushPen->dwStyleCount = dwStyleCount;
    pbrushPen->pStyle = pStyle;
    break;
  • 规则:样式数组不允许全零 (否则永无落笔,无意义),也不允许负数 (长度必须非负);
    任一违规 → ERROR_INVALID_PARAMETER
  • 注意 pStyle 此时已是内核安全拷贝(pSafeStyle)或默认静态数组;
  • /* FIXME: What style here? */ 是源码遗留注释:USERSTYLE 笔仍被标记为 BR_IS_SOLID
    (因为绘制时样式由数组驱动,底色实心)。
4.5.6 ulStyleSize 累加与断言
c 复制代码
if (pbrushPen->pStyle != NULL)
{
    for (i = 0; i < pbrushPen->dwStyleCount; i++)
    {
        pbrushPen->ulStyleSize += pbrushPen->pStyle[i];
    }
}

NT_ASSERT((pbrushPen->dwStyleCount == 0) || (pbrushPen->pStyle != NULL));

PEN_UnlockPen(pbrushPen);
return hPen;
  • ulStyleSize = 样式数组元素之和,即一个"画-停"周期总长度(供描边时推进样式状态机);
  • 断言保证"有样式个数必有样式指针";
  • 成功路径最后 PEN_UnlockPen 解锁对象并返回 hPen
4.5.7 失败路径 ExitCleanup
c 复制代码
ExitCleanup:
    EngSetLastError(ERROR_INVALID_PARAMETER);
    pbrushPen->pStyle = NULL;
    GDIOBJ_vDeleteObject(&pbrushPen->BaseObject);

    return NULL;
  • 所有参数错误统一落到这里:设 ERROR_INVALID_PARAMETER、清空 pStyle
    (避免把 pSafeStyle 交给析构重复释放------NtGdiExtCreatePen 会在外部回收它)、
    直接删除 GDI 对象、返回 NULL。
  • 内存分配失败则走另一条路:ERROR_NOT_ENOUGH_MEMORY + 返回 0(在 4.5.2 中)。

4.6 简单笔 vs 几何笔 vs 样式笔对比

维度 简单笔(CreatePen / 老式) 几何笔(PS_GEOMETRIC) 美容笔(PS_COSMETIC)
对象类型 LO_PEN_TYPE LO_EXTPEN_TYPE LO_EXTPEN_TYPE
宽度语义 设备单位,0 或 1 = 单像素 世界单位(受世界变换影响) 恒 1 像素
颜色 BrushAttr.lbColor 可搭配图案/影线画刷 必须 BS_SOLID 纯色
端点/连接 不适用(简单笔无此概念) PS_ENDCAP_*/PS_JOIN_* 生效 仅有样式
宽笔判定 宽度 > 1 即宽笔 宽度 > 1 即宽笔 永远窄笔
样式 仅内建 7 种 内建或 PS_USERSTYLE 用户数组 内建或用户数组
宽化路径 支持 支持 不支持(PATH_WidenPathEx 拒绝)

4.7 创建调用链(mermaid)

#mermaid-svg-0KmtWqNF52Hzdyna{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-0KmtWqNF52Hzdyna .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-0KmtWqNF52Hzdyna .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-0KmtWqNF52Hzdyna .error-icon{fill:#552222;}#mermaid-svg-0KmtWqNF52Hzdyna .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-0KmtWqNF52Hzdyna .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-0KmtWqNF52Hzdyna .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-0KmtWqNF52Hzdyna .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-0KmtWqNF52Hzdyna .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-0KmtWqNF52Hzdyna .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-0KmtWqNF52Hzdyna .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-0KmtWqNF52Hzdyna .marker{fill:#333333;stroke:#333333;}#mermaid-svg-0KmtWqNF52Hzdyna .marker.cross{stroke:#333333;}#mermaid-svg-0KmtWqNF52Hzdyna svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-0KmtWqNF52Hzdyna p{margin:0;}#mermaid-svg-0KmtWqNF52Hzdyna .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-0KmtWqNF52Hzdyna .cluster-label text{fill:#333;}#mermaid-svg-0KmtWqNF52Hzdyna .cluster-label span{color:#333;}#mermaid-svg-0KmtWqNF52Hzdyna .cluster-label span p{background-color:transparent;}#mermaid-svg-0KmtWqNF52Hzdyna .label text,#mermaid-svg-0KmtWqNF52Hzdyna span{fill:#333;color:#333;}#mermaid-svg-0KmtWqNF52Hzdyna .node rect,#mermaid-svg-0KmtWqNF52Hzdyna .node circle,#mermaid-svg-0KmtWqNF52Hzdyna .node ellipse,#mermaid-svg-0KmtWqNF52Hzdyna .node polygon,#mermaid-svg-0KmtWqNF52Hzdyna .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-0KmtWqNF52Hzdyna .rough-node .label text,#mermaid-svg-0KmtWqNF52Hzdyna .node .label text,#mermaid-svg-0KmtWqNF52Hzdyna .image-shape .label,#mermaid-svg-0KmtWqNF52Hzdyna .icon-shape .label{text-anchor:middle;}#mermaid-svg-0KmtWqNF52Hzdyna .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-0KmtWqNF52Hzdyna .rough-node .label,#mermaid-svg-0KmtWqNF52Hzdyna .node .label,#mermaid-svg-0KmtWqNF52Hzdyna .image-shape .label,#mermaid-svg-0KmtWqNF52Hzdyna .icon-shape .label{text-align:center;}#mermaid-svg-0KmtWqNF52Hzdyna .node.clickable{cursor:pointer;}#mermaid-svg-0KmtWqNF52Hzdyna .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-0KmtWqNF52Hzdyna .arrowheadPath{fill:#333333;}#mermaid-svg-0KmtWqNF52Hzdyna .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-0KmtWqNF52Hzdyna .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-0KmtWqNF52Hzdyna .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0KmtWqNF52Hzdyna .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-0KmtWqNF52Hzdyna .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0KmtWqNF52Hzdyna .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-0KmtWqNF52Hzdyna .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-0KmtWqNF52Hzdyna .cluster text{fill:#333;}#mermaid-svg-0KmtWqNF52Hzdyna .cluster span{color:#333;}#mermaid-svg-0KmtWqNF52Hzdyna 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-0KmtWqNF52Hzdyna .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-0KmtWqNF52Hzdyna rect.text{fill:none;stroke-width:0;}#mermaid-svg-0KmtWqNF52Hzdyna .icon-shape,#mermaid-svg-0KmtWqNF52Hzdyna .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0KmtWqNF52Hzdyna .icon-shape p,#mermaid-svg-0KmtWqNF52Hzdyna .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-0KmtWqNF52Hzdyna .icon-shape .label rect,#mermaid-svg-0KmtWqNF52Hzdyna .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0KmtWqNF52Hzdyna .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-0KmtWqNF52Hzdyna .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-0KmtWqNF52Hzdyna :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是

失败
通过
应用: CreatePen
gdi32 objects/pen.c CreatePen

样式钳制/PS_NULL 走库存
应用: ExtCreatePen
gdi32 objects/brush.c ExtCreatePen

预校验 + DIB 转换
NtGdiCreatePen

范围校验 PS_SOLID..PS_INSIDEFRAME
NtGdiExtCreatePen

样式个数<=16 / 美容笔强制纯色

SEH 复制样式数组到 NonPagedPool

BS_PATTERN 探测 DIB
IntGdiExtCreatePen

bOldStylePen=TRUE
bOldStylePen?
PEN_AllocPenWithHandle

LO_PEN_TYPE
PEN_AllocExtPenWithHandle

LO_EXTPEN_TYPE
PEN_vInit: pBrushAttr=&BrushAttr
三层校验

ENDCAP / JOIN / TYPE
ExitCleanup:

ERROR_INVALID_PARAMETER

GDIOBJ_vDeleteObject
字段初始化 lWidth/eWidth/ulPenStyle

BrushAttr.lbColor/flAttrs
线型分发 switch PS_STYLE_MASK
内建样式数组

  • BR_IS_DEFAULTSTYLE
    USERSTYLE

检查全零/负数
SOLID/NULL/INSIDEFRAME
累加 ulStyleSize
NT_ASSERT + PEN_UnlockPen
返回 HPEN


5. 颜色修改

5.1 IntGdiSetSolidPenColor(pen.c:271-286)

c 复制代码
VOID
FASTCALL
IntGdiSetSolidPenColor(HPEN hPen, COLORREF Color)
{
    PBRUSH pbrPen;

    pbrPen = PEN_ShareLockPen(hPen);
    if (pbrPen)
    {
        if (pbrPen->flAttrs & BR_IS_SOLID)
        {
            pbrPen->BrushAttr.lbColor = Color & 0xFFFFFF;
        }
        PEN_ShareUnlockPen(pbrPen);
    }
}
  • 签名VOID FASTCALL IntGdiSetSolidPenColor(HPEN hPen, COLORREF Color)
  • 作用 :按句柄把纯色笔的颜色改为 Color
    1. PEN_ShareLockPen 共享锁定(非法句柄返回 NULL,静默忽略);
    2. 仅当 flAttrs & BR_IS_SOLID 才修改------非实心笔(图案/影线/空笔)不改颜色
    3. Color & 0xFFFFFF 屏蔽高位(COLORREF 只取低 24 位 RGB,忽略高位标志位);
    4. PEN_ShareUnlockPen 释放引用。
  • 使用方式 :这是内核内部工具,理论上配合 SetDCPenColor 使用
    SetDCPenColor 改 DC 属性后,若 DC 的画笔恰好是实心笔,可同步改笔对象颜色)。
    经全仓库检索,当前树内没有任何调用方 (仅 pen.c 定义 + pen.h 无声明),属于"留作内部 API"
    的未使用函数------这是源码事实,阅读时不要误以为它是 SetDCPenColor 的直接实现。

5.2 SetDCPenColor 用户态链路(gdi32/objects/dc.c:939-970)

c 复制代码
COLORREF WINAPI
SetDCPenColor(_In_ HDC hdc, _In_ COLORREF crColor)
{
    PDC_ATTR pdcattr;

    pdcattr = GdiGetDcAttr(hdc);
    if (pdcattr == NULL) { SetLastError(ERROR_INVALID_PARAMETER); return CLR_INVALID; }

    HANDLE_EMETAFDC(COLORREF, SetDCPenColor, CLR_INVALID, hdc, crColor);

    crOldColor = pdcattr->ulPenClr;
    pdcattr->ulPenClr = (ULONG)crColor;

    if (pdcattr->crPenClr != crColor)
    {
        pdcattr->ulDirty_ |= DIRTY_LINE;
        pdcattr->crPenClr = crColor;
    }

    return crOldColor;
}

要点:

  • ReactOS 的 DC 属性(PDC_ATTR)在用户态可读GdiGetDcAttr),
    所以 SetDCPenColor 不走系统调用,直接写 ulPenClr 并置 DIRTY_LINE 脏标记;
  • HANDLE_EMETAFDC 宏负责在增强图元文件 DC 上同时记录 EMR;
  • 真正的"落笔"发生在内核绘制时:DIRTY_LINE 会随下一次绘制被
    DC_vUpdateLineBrush 消费(见 5.4)。

5.3 内核态 IntSetDCPenColor(dcutil.c:258-279)

c 复制代码
COLORREF FASTCALL
IntSetDCPenColor(HDC hdc, COLORREF crColor)
{
   COLORREF OldColor;
   PDC dc;
   if (!(dc = DC_LockDc(hdc)))
   {
      EngSetLastError(ERROR_INVALID_PARAMETER);
      return CLR_INVALID;
   }

   OldColor = (COLORREF)dc->pdcattr->ulPenClr;
   dc->pdcattr->ulPenClr = (ULONG)crColor;

   if (dc->pdcattr->crPenClr != crColor)
   {
      dc->pdcattr->ulDirty_ |= DIRTY_LINE;
      dc->pdcattr->crPenClr = crColor;
   }
   DC_UnlockDc(dc);
   return OldColor;
}
  • 内核态用户模块 (ntuser 的 draw.cnonclient.cmenu.c 等绘制 3D 边框时)使用:
    IntSetDCPenColor(hdc, IntGetSysColor(...))
  • 与 gdi32 版本逻辑完全一致(锁 DC → 改 ulPenClr → 置 DIRTY_LINE),
    只是发生在内核侧。

5.4 DC_vUpdateLineBrush(dcobjs.c:60-104)------"笔 → 线条画刷"的兑现点

c 复制代码
VOID FASTCALL
DC_vUpdateLineBrush(PDC pdc)
{
    PDC_ATTR pdcattr = pdc->pdcattr;
    PBRUSH pbrLine;

    /* 1. 画笔句柄是否更换 */
    if (pdcattr->hpen != pdc->dclevel.pbrLine->BaseObject.hHmgr)
    {
        pbrLine = PEN_ShareLockPen(pdcattr->hpen);
        if (pbrLine)
        {
            BRUSH_ShareUnlockBrush(pdc->dclevel.pbrLine);  // 释放旧笔
            pdc->dclevel.pbrLine = pbrLine;                 // 换新笔
            pdcattr->ulDirty_ |= DIRTY_LINE;
        }
        else
        {
            pdcattr->hpen = pdc->dclevel.pbrLine->BaseObject.hHmgr; // 非法句柄还原
        }
    }

    /* 2. EBRUSHOBJ 重实现 */
    if (pdcattr->ulDirty_ & DIRTY_LINE)
    {
        EBRUSHOBJ_vUpdateFromDC(&pdc->eboLine, pdc->dclevel.pbrLine, pdc);
    }

    /* 3. DC_PEN 特例:用 SetDCPenColor 的颜色覆盖线条画刷 */
    if (pdcattr->hpen == StockObjects[DC_PEN])
    {
        EBRUSHOBJ_vSetSolidRGBColor(&pdc->eboLine, pdcattr->crPenClr);
    }

    /* 4. 清脏 */
    pdcattr->ulDirty_ &= ~(DIRTY_LINE | DC_PEN_DIRTY);
}

这就是 SetDCPenColor完整生效链路

复制代码
SetDCPenColor(crColor) → pdcattr->ulPenClr = crColor; DIRTY_LINE |= ...
        ↓(下一次 SelectObject/绘制前)
DC_vUpdateLineBrush → EBRUSHOBJ_vUpdateFromDC(&eboLine, pbrLine, pdc)
        ↓(若 hpen == DC_PEN 库存笔)
EBRUSHOBJ_vSetSolidRGBColor(&eboLine, crPenClr)
        ↓
IntEngLineTo / PATH_StrokePath ... 使用 eboLine.BrushObject 绘制

调用 DC_vUpdateLineBrush 的地方包括:IntGdiSelectBrush(dcobjs.c:305,换笔)、

IntGdiLineTo(line.c:560)、IntGdiEllipse/IntGdiRoundRect/IntGdiRectangle

(fillshap.c)、圆弧(arc.c)、PATH_StrokePath(path.c:2713/3060/3117)等。

另外 SetDCPenColor 之后若未发生绘制,DC_PEN_DIRTY 也会在下次 DC_vUpdateLineBrush

调用时被处理(dcobjs.c:647-648 的 if (pdcattr->ulDirty_ & (DIRTY_LINE | DC_PEN_DIRTY)))。

SetDCPenColor 链路(mermaid):
#mermaid-svg-AzBoOuRSDHu4zLq8{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-AzBoOuRSDHu4zLq8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-AzBoOuRSDHu4zLq8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-AzBoOuRSDHu4zLq8 .error-icon{fill:#552222;}#mermaid-svg-AzBoOuRSDHu4zLq8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-AzBoOuRSDHu4zLq8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-AzBoOuRSDHu4zLq8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-AzBoOuRSDHu4zLq8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-AzBoOuRSDHu4zLq8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-AzBoOuRSDHu4zLq8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-AzBoOuRSDHu4zLq8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-AzBoOuRSDHu4zLq8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-AzBoOuRSDHu4zLq8 .marker.cross{stroke:#333333;}#mermaid-svg-AzBoOuRSDHu4zLq8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-AzBoOuRSDHu4zLq8 p{margin:0;}#mermaid-svg-AzBoOuRSDHu4zLq8 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-AzBoOuRSDHu4zLq8 .cluster-label text{fill:#333;}#mermaid-svg-AzBoOuRSDHu4zLq8 .cluster-label span{color:#333;}#mermaid-svg-AzBoOuRSDHu4zLq8 .cluster-label span p{background-color:transparent;}#mermaid-svg-AzBoOuRSDHu4zLq8 .label text,#mermaid-svg-AzBoOuRSDHu4zLq8 span{fill:#333;color:#333;}#mermaid-svg-AzBoOuRSDHu4zLq8 .node rect,#mermaid-svg-AzBoOuRSDHu4zLq8 .node circle,#mermaid-svg-AzBoOuRSDHu4zLq8 .node ellipse,#mermaid-svg-AzBoOuRSDHu4zLq8 .node polygon,#mermaid-svg-AzBoOuRSDHu4zLq8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-AzBoOuRSDHu4zLq8 .rough-node .label text,#mermaid-svg-AzBoOuRSDHu4zLq8 .node .label text,#mermaid-svg-AzBoOuRSDHu4zLq8 .image-shape .label,#mermaid-svg-AzBoOuRSDHu4zLq8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-AzBoOuRSDHu4zLq8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-AzBoOuRSDHu4zLq8 .rough-node .label,#mermaid-svg-AzBoOuRSDHu4zLq8 .node .label,#mermaid-svg-AzBoOuRSDHu4zLq8 .image-shape .label,#mermaid-svg-AzBoOuRSDHu4zLq8 .icon-shape .label{text-align:center;}#mermaid-svg-AzBoOuRSDHu4zLq8 .node.clickable{cursor:pointer;}#mermaid-svg-AzBoOuRSDHu4zLq8 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-AzBoOuRSDHu4zLq8 .arrowheadPath{fill:#333333;}#mermaid-svg-AzBoOuRSDHu4zLq8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-AzBoOuRSDHu4zLq8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-AzBoOuRSDHu4zLq8 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AzBoOuRSDHu4zLq8 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-AzBoOuRSDHu4zLq8 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AzBoOuRSDHu4zLq8 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-AzBoOuRSDHu4zLq8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-AzBoOuRSDHu4zLq8 .cluster text{fill:#333;}#mermaid-svg-AzBoOuRSDHu4zLq8 .cluster span{color:#333;}#mermaid-svg-AzBoOuRSDHu4zLq8 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-AzBoOuRSDHu4zLq8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-AzBoOuRSDHu4zLq8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-AzBoOuRSDHu4zLq8 .icon-shape,#mermaid-svg-AzBoOuRSDHu4zLq8 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AzBoOuRSDHu4zLq8 .icon-shape p,#mermaid-svg-AzBoOuRSDHu4zLq8 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-AzBoOuRSDHu4zLq8 .icon-shape .label rect,#mermaid-svg-AzBoOuRSDHu4zLq8 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AzBoOuRSDHu4zLq8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-AzBoOuRSDHu4zLq8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-AzBoOuRSDHu4zLq8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是

应用 SetDCPenColor
gdi32 objects/dc.c

写 pdcattr->ulPenClr + DIRTY_LINE
ntuser 内核绘制

IntSetDCPenColor
dcutil.c

锁 DC 写 ulPenClr + DIRTY_LINE
DC_vUpdateLineBrush

dcobjs.c
EBRUSHOBJ_vUpdateFromDC

重实现 eboLine
hpen == DC_PEN?
EBRUSHOBJ_vSetSolidRGBColor

eboLine 颜色 = crPenClr
使用画笔自身 BrushAttr.lbColor
IntEngLineTo / PATH_StrokePath ...


6. 对象查询:PEN_GetObject(pen.c:288-350)

c 复制代码
INT
APIENTRY
PEN_GetObject(PBRUSH pbrushPen, INT cbCount, PLOGPEN pBuffer)
{
    PLOGPEN pLogPen;
    PEXTLOGPEN pExtLogPen;
    INT cbRetCount;

    if (pbrushPen->flAttrs & BR_IS_OLDSTYLEPEN)
    {
        /* ---------- 老式(简单)笔 ---------- */
        cbRetCount = sizeof(LOGPEN);
        if (pBuffer)
        {
            if (cbCount < cbRetCount) return 0;

            if (((pbrushPen->ulPenStyle & PS_STYLE_MASK) == PS_NULL) &&
                (cbCount == sizeof(EXTLOGPEN)))
            {
                /* 特例:空笔 + 缓冲区恰好是 EXTLOGPEN 大小 → 填 EXTLOGPEN */
                pExtLogPen = (PEXTLOGPEN)pBuffer;
                pExtLogPen->elpPenStyle = pbrushPen->ulPenStyle;
                pExtLogPen->elpWidth = 0;
                pExtLogPen->elpBrushStyle = pbrushPen->iBrushStyle;
                pExtLogPen->elpColor = pbrushPen->BrushAttr.lbColor;
                pExtLogPen->elpHatch = 0;
                pExtLogPen->elpNumEntries = 0;
                cbRetCount = sizeof(EXTLOGPEN);
            }
            else
            {
                pLogPen = (PLOGPEN)pBuffer;
                pLogPen->lopnWidth.x = pbrushPen->lWidth;
                pLogPen->lopnWidth.y = 0;
                pLogPen->lopnStyle = pbrushPen->ulPenStyle;
                pLogPen->lopnColor = pbrushPen->BrushAttr.lbColor;
            }
        }
    }
    else
    {
        DWORD dwStyleCount = (pbrushPen->flAttrs & BR_IS_DEFAULTSTYLE) ?
            0 : pbrushPen->dwStyleCount;
        cbRetCount = sizeof(EXTLOGPEN) - sizeof(DWORD) + dwStyleCount * sizeof(DWORD);
        if (pBuffer)
        {
            ULONG i;

            if (cbCount < cbRetCount) return 0;
            pExtLogPen = (PEXTLOGPEN)pBuffer;
            pExtLogPen->elpPenStyle = pbrushPen->ulPenStyle;
            pExtLogPen->elpWidth = pbrushPen->lWidth;
            pExtLogPen->elpBrushStyle = pbrushPen->iBrushStyle;
            pExtLogPen->elpColor = pbrushPen->BrushAttr.lbColor;
            pExtLogPen->elpHatch = (ULONG_PTR)pbrushPen->hbmClient;
            pExtLogPen->elpNumEntries = dwStyleCount;
            for (i = 0; i < dwStyleCount; i++)
            {
                pExtLogPen->elpStyleEntry[i] = pbrushPen->pStyle[i];
            }
        }
    }

    return cbRetCount;
}

6.1 函数分析

  • 签名INT APIENTRY PEN_GetObject(PBRUSH pbrushPen, INT cbCount, PLOGPEN pBuffer)
    注意入参是已锁定的对象指针 而非句柄------由上层 GreGetObject 按对象类型分发后调用
    (画笔对象 → 本函数;画刷对象 → BRUSH_GetObject)。
  • 返回语义 :返回需要/写入的字节数;pBuffer == NULL 时只返回字节数(这是
    GetObject(hPen, 0, NULL) 的两步查询惯例,PATH_WidenPathEx 正是先这样取大小再分配缓冲);
    缓冲区过小(cbCount < cbRetCount)返回 0。

分支一:老式笔(flAttrs & BR_IS_OLDSTYLEPEN

  • 默认返回 sizeof(LOGPEN),填充 LOGPEN
    • lopnWidth.x = lWidth(宽度),lopnWidth.y = 0
    • lopnStyle = ulPenStyle(完整样式,含高位的类型位);
    • lopnColor = BrushAttr.lbColor
  • 特例 :当笔为 PS_NULL 且调用方给的缓冲区恰好是 sizeof(EXTLOGPEN) 时,
    填充 EXTLOGPEN 并返回 sizeof(EXTLOGPEN)
    elpPenStyle = ulPenStyleelpWidth = 0elpBrushStyle = iBrushStyle
    elpColor = lbColorelpHatch = 0elpNumEntries = 0
    这是为了兼容"用 ExtCreatePen 风格代码查询库存空笔"的场景
    GetStockObject(NULL_PEN) 是老式库存笔,但调用方可能按扩展笔查询)。

分支二:扩展笔(else)

  • dwStyleCount 的实际输出值:BR_IS_DEFAULTSTYLE记作 0 (内建样式不导出
    elpStyleEntry),否则取 pbrushPen->dwStyleCount(用户样式);
  • cbRetCount = sizeof(EXTLOGPEN) - sizeof(DWORD) + dwStyleCount * sizeof(DWORD)
    减去 elpStyleEntry[1] 内置的一个 DWORD,再加上实际样式个数对应的空间;
  • 填充 EXTLOGPEN
    • elpPenStyle = ulPenStyleelpWidth = lWidthelpBrushStyle = iBrushStyle
      elpColor = lbColor
    • elpHatch = (ULONG_PTR)hbmClient------把创建时暂存的用户 hatch 原样返回;
    • elpNumEntries = dwStyleCount,随后逐项拷贝 pStyle[i]elpStyleEntry[i]

6.2 使用场景

场景 调用链
应用 GetObject(hPen, sizeof(LOGPEN), &lp) gdi32 GetObjectNtGdiGetObject → 按句柄类型分发到 PEN_GetObject
宽化路径取笔参数 PATH_WidenPathExGreGetObject(pdcattr->hpen, 0, NULL) 取大小 → 分配 → 再次调用取数据 → 按 GDI_OBJECT_TYPE_PEN/EXTPEN 读样式与宽度(见 7.5)
查询库存空笔 命中老式分支的 EXTLOGPEN 特例

7. 绘制链路与宽笔判定

7.1 宽笔判定宏 IntIsEffectiveWidePen(pen.h:33-37)

c 复制代码
#define IntIsEffectiveWidePen(pbrLine) ( \
    (pbrLine)->lWidth > 1 && \
    ((pbrLine->flAttrs & BR_IS_OLDSTYLEPEN) || \
     ((pbrLine)->ulPenStyle & PS_TYPE_MASK) == PS_GEOMETRIC) \
)

语义逐项拆解:

  1. lWidth > 1 :宽度必须大于 1。宽度 0/1 一律按细线处理
    (即使几何笔,若宽度为 1 也走细线快路径);
  2. 老式笔(BR_IS_OLDSTYLEPENCreatePen 创建的笔宽度 > 1 即视为宽笔;
  3. 几何笔(PS_TYPE_MASK == PS_GEOMETRIC:扩展几何笔宽度 > 1 同样视为宽笔;
  4. 美容笔 :恒为细线(PS_COSMETIC 要求宽度恒 1,本就无法满足 lWidth > 1)。

也就是说:"宽笔" = 宽度 > 1 且(老式笔或几何笔)。这个判定结果直接决定绘制路径。

使用位置(全树):

位置 作用
line.c:208IntGdiLineTo 宽笔 → 建临时路径描边;否则 IntEngLineTo
line.c:371IntGdiPolyline 宽笔 → 建临时路径描边;否则 IntEngPolyline
fillshap.c:109IntGdiEllipse 等) 边框绘制分流
fillshap.c:680(矩形/圆角矩形边框) 同上
path.c:1628PATH_StrokePath 宽笔 → PATH_WidenPathEx 展开轮廓填充;否则折线化

7.2 line.c 中的分流实现

IntGdiLineTo(line.c:206-246,节选关键逻辑):

c 复制代码
if (!(pbrLine->flAttrs & BR_IS_NULL))
{
    if (IntIsEffectiveWidePen(pbrLine))
    {
        /* Clear the path */
        PATH_Delete(dc->dclevel.hPath);
        dc->dclevel.hPath = NULL;

        /* Begin a path */
        pPath = PATH_CreatePath(2);
        dc->dclevel.flPath |= DCPATH_ACTIVE;
        dc->dclevel.hPath = pPath->BaseObject.hHmgr;
        IntGetCurrentPositionEx(dc, &pPath->pos);
        IntLPtoDP(dc, &pPath->pos, 1);

        PATH_MoveTo(dc, pPath);
        PATH_LineTo(dc, XEnd, YEnd);

        /* Close the path */
        pPath->state = PATH_Closed;
        dc->dclevel.flPath &= ~DCPATH_ACTIVE;

        /* Actually stroke a path */
        Ret = PATH_StrokePath(dc, pPath);
        ...
    }
    else
    {
        Ret = IntEngLineTo(&psurf->SurfObj, (CLIPOBJ *)&dc->co,
                           &dc->eboLine.BrushObject,
                           Points[0].x, Points[0].y,
                           Points[1].x, Points[1].y,
                           &Bounds, ROP2_TO_MIX(pdcattr->jROP2));
    }
}

要点:

  • 细线路径 :直接调用 IntEngLineTo / IntEngPolyline,画刷用 &dc->eboLine.BrushObject
    (即 DC_vUpdateLineBrush 维护的线条画刷)------单像素,样式数组由引擎推进;
  • 宽笔路径 :临时创建一个 2 点路径(PATH_CreatePath(2)),MoveTo 起点 +
    LineTo 终点,置 PATH_Closed 后交给 PATH_StrokePath。注意这里手动置 Closed
    因为 PATH_StrokePath 只接受闭合路径(PATH_WidenPathEx 开头有
    if (pPath->state != PATH_Closed) 检查);
  • 每次宽笔绘制都要新建、删除临时路径,性能上不如细线快路径------这是宽笔的固有代价;
  • 绘制前还有 AddPenLinesBounds(累积模式边界合并)与 DC_vPrepareDCsForBlit

IntGdiPolyline(line.c:371-401)完全相同的模式,只是把 N 个点逐段

PATH_LineTo 进路径后再 PATH_StrokePathIntGdiPolyBezier 则先把贝塞尔

GDI_Bezier 展平为折线再走 IntGdiPolyline

7.3 fillshap.c 中的使用

IntGdiEllipseIntGdiRectangleIntGdiRoundRect 等在画边框时同样调用

DC_vUpdateLineBrush 后用 IntIsEffectiveWidePen 分流(fillshap.c:109、680):

宽笔 → PATH_StrokePath;细线 → IntEngEllipse/IntEngRectangle 等引擎函数。

所以"椭圆/矩形边框 + 宽笔"同样会走宽化轮廓。

7.4 PATH_StrokePath(path.c:1608-1642)

c 复制代码
BOOL FASTCALL
PATH_StrokePath(DC *dc, PPATH pPath)
{
    ...
    pbrLine = dc->dclevel.pbrLine;
    if (IntIsEffectiveWidePen(pbrLine))
    {
        pNewPath = PATH_WidenPathEx(dc, pPath);
        if (pNewPath)
        {
            /* Fill the path with the WINDING fill mode */
            jOldFillMode = pdcattr->jFillMode;
            pdcattr->jFillMode = WINDING;
            PATH_FillPathEx(dc, pNewPath, pbrLine);
            pdcattr->jFillMode = jOldFillMode;

            PATH_Delete(pNewPath->BaseObject.hHmgr);
            return TRUE;
        }
    }
    /* ... 否则:把路径展平为折线,逐段 IntGdiPolyline ... */
}
  • 宽笔PATH_WidenPathEx 用当前笔参数把路径展开为闭合轮廓路径
    (新路径),然后临时切到 WINDING 填充模式PATH_FillPathEx 填充
    (画笔此时作为填充画刷 pbrLine 使用)------这正印证了 1.2 节"线条=笔刷扫掠区域"的论断;
  • 细笔 :把路径按 PT_MOVETO 分段展平成折线,逐段 IntGdiPolyline
    (此时不再宽笔,走 IntEngPolyline 细线快路径)。

7.5 PATH_WidenPathEx(path.c:2195-2260)------宽化前的笔参数提取

c 复制代码
PPATH FASTCALL
PATH_WidenPathEx(DC *dc, PPATH pPath)
{
    INT size;
    UINT penWidth, penStyle;
    DWORD obj_type;
    LPEXTLOGPEN elp;

    if (pPath->state != PATH_Closed)      // 只接受闭合路径
    { EngSetLastError(ERROR_CAN_NOT_COMPLETE); return NULL; }

    size = GreGetObject(pdcattr->hpen, 0, NULL);   // 两步查询:先取大小
    if (!size) { ...; return NULL; }

    elp = ExAllocatePoolWithTag(PagedPool, size, TAG_PATH);
    GreGetObject(pdcattr->hpen, size, elp);        // 再取内容

    obj_type = GDI_HANDLE_GET_TYPE(pdcattr->hpen);
    if (obj_type == GDI_OBJECT_TYPE_PEN)
        penStyle = ((LOGPEN*)elp)->lopnStyle;      // 老式笔
    else if (obj_type == GDI_OBJECT_TYPE_EXTPEN)
        penStyle = elp->elpPenStyle;               // 扩展笔
    else { ...; return NULL; }

    penWidth = elp->elpWidth;
    ExFreePoolWithTag(elp, TAG_PATH);

    /* The function cannot apply to cosmetic pens */
    if (obj_type == GDI_OBJECT_TYPE_EXTPEN &&
        (PS_TYPE_MASK & penStyle) == PS_COSMETIC)
    { EngSetLastError(ERROR_CAN_NOT_COMPLETE); return FALSE; }

    return IntGdiWidenPath(pPath, penWidth, penStyle,
                           dc->dclevel.laPath.eMiterLimit);
}
  • GreGetObject 两次查询 (先 0 大小取字节数,再分配缓冲取内容),
    与 6.2 节呼应;
  • 按句柄类型决定从 LOGPEN.lopnStyle 还是 EXTLOGPEN.elpPenStyle 读样式,
    宽度统一取 elp->elpWidth(注意:LOGPENEXTLOGPEN 中宽度字段偏移一致
    ------LOGPEN.lopnWidth.x 前有 lopnStyle(4B),EXTLOGPEN.elpWidth 前也有
    elpPenStyle(4B),两者低 32 位宽度正好同偏移,代码直接复用);
  • 美容扩展笔被拒绝ERROR_CAN_NOT_COMPLETE)------美容笔不该宽化;
  • eMiterLimit 来自 DC 路径属性的 laPath.eMiterLimit,用于斜接限制。

7.6 IntGdiWidenPath(path.c:1801-2174)------宽化核心算法

c 复制代码
#define round(x) ((int)((x)>0?(x)+0.5:(x)-0.5))

PPATH FASTCALL
IntGdiWidenPath(PPATH pPath, UINT penWidth, UINT penStyle, FLOAT eMiterLimit)
{
    INT i, j, numStrokes, numOldStrokes, penWidthIn, penWidthOut;
    PPATH flat_path, pNewPath = NULL, *pStrokes = NULL, ...;
    DWORD joint, endcap;
    KFLOATING_SAVE fpsave;

    endcap = (PS_ENDCAP_MASK & penStyle);
    joint  = (PS_JOIN_MASK & penStyle);

    if (!(flat_path = PATH_FlattenPath(pPath))) return NULL;

    penWidthIn  = penWidth / 2;      // 内半宽(奇数宽度时右侧/内侧少 1)
    penWidthOut = penWidth / 2;
    if (penWidthIn + penWidthOut < penWidth) penWidthOut++;  // 奇数宽度外扩

    numStrokes = 0;
    ...(按 PT_MOVETO 把扁平路径拆成若干独立子路径 pStrokes[])...

    pNewPath = PATH_CreatePath(flat_path->numEntriesUsed);

    KeSaveFloatingPointState(&fpsave);   // 全程使用浮点三角函数

    for (i = 0; i < numStrokes; i++)
    {
        ...(对每条子路径生成 pUpPath(外侧/左侧边)与 pDownPath(内侧/右侧边))...
    }
    ...
    KeRestoreFloatingPointState(&fpsave);
    ...
    return pNewPath;
}

算法分步

  1. 展平PATH_FlattenPath 把贝塞尔曲线离散成折线段(后续只出现
    PT_MOVETO/PT_LINETOPT_BEZIERTO 分支打了 "Should never happen" 的错误日志);
  2. 半宽分配penWidthIn = penWidthOut = penWidth / 2;若二者之和小于总宽
    (即奇数宽度),把多出的 1 像素给外侧 penWidthOut。外缘总是 ≥ 内缘;
  3. 拆分子路径 :遍历扁平路径,遇到 PT_MOVETO 开启新子路径(pStrokes[] 动态扩容),
    并把前一条置 PATH_ClosedPT_LINETO / PT_LINETO|PT_CLOSEFIGURE 追加到当前子路径;
  4. 逐子路径生成上下两条边 (核心几何,见 7.7):
    • 子路径的首尾端点endcap 处理:
      • PS_ENDCAP_SQUARE:沿线段方向 -halfWidth 延伸出方形帽(两个角点);
      • PS_ENDCAP_FLAT:不延伸,仅垂直方向的两个边点;
      • PS_ENDCAP_ROUND(默认):用 PATH_DoArcPart 画四段 90° 圆弧拼成半圆帽;
    • 子路径的内部顶点joint 处理(见 7.7);
    • 对"单线段开放路径"(恰好 2 个点、未闭合)特殊补充上下边体端点,保证
      帽与边体正确衔接(path.c:2107-2132 的注释与代码);
  5. 组装闭合轮廓pUpPath 点序正序拷入 pNewPathpDownPath
    reverse_points 反序后拷入------这样两条边首尾相接形成逆/顺时针闭合轮廓
    内部自交由 WINDING 填充模式处理;最后 pNewPath->state = PATH_Closed

7.7 连接样式(join)的几何处理

对每个内部顶点,先计算前后两段的夹角 alphaatan2 差值归一化到 [-π, π]),

再按 joint 生成外侧点:

c 复制代码
theta = atan2(yo - ya, xo - xa);
alpha = atan2(yb - yo, xb - xo) - theta;
if (alpha > 0) alpha -= M_PI;
else           alpha += M_PI;

if (_joint == PS_JOIN_MITER && eMiterLimit < fabs(1 / sin(alpha / 2)))
{
    _joint = PS_JOIN_BEVEL;      // 超出斜接极限 → 降级为斜切
}
  • PS_JOIN_MITER(斜接) :外侧点沿角平分线延伸到
    miterWidth = |penWidthOut / cos(π/2 - |α|/2)|。若
    eMiterLimit < |1/sin(α/2)|(角度太尖、尖角太长)则自动降级为 BEVEL
    (这正是 eMiterLimit 的用途);
  • PS_JOIN_BEVEL(斜切):外侧直接连两个边端点(切掉尖角);
  • PS_JOIN_ROUND(圆角,默认) :外侧用三段 PT_BEZIERTO 模拟圆弧。
  • 内侧(pInsidePath)始终是简单折线:alpha > 0 时顶点在
    +penWidthIn 一侧,alpha < 0 时在 -penWidthIn 一侧(两侧分别收集到
    pUpPath/pDownPath,随 α 符号交换"内/外"归属)。

7.8 宽笔/描边完整调用链(mermaid)

渲染错误: Mermaid 渲染失败: Parse error on line 3: ...en?
lWidth>1 && (老式笔 || PS_GEOMETRIC -----------------------^ Expecting 'SQE', 'DOUBLECIRCLEEND', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'PS'


8. 源码索引

8.1 函数索引(pen.c 全部函数)

函数 位置 类型 一句话作用
PEN_vInit pen.c:16-23 static pBrushAttr 指向对象内嵌 BrushAttr
PEN_AllocPenWithHandle pen.c:25-40 内部 API 分配老式笔对象(LO_PEN_TYPE
PEN_AllocExtPenWithHandle pen.c:42-57 内部 API 分配扩展笔对象(LO_EXTPEN_TYPE
PEN_ShareLockPen pen.c:59-70 内部 API 校验句柄类型并按 BRUSH 类型共享引用
IntGdiExtCreatePen pen.c:72-269 内部 API 创建中枢:校验/初始化/样式分发
IntGdiSetSolidPenColor pen.c:271-286 内部 API 改实心笔颜色(当前无调用方)
PEN_GetObject pen.c:288-350 内部 API LOGPEN/EXTLOGPEN
NtGdiCreatePen pen.c:355-380 系统调用 简单笔入口
NtGdiExtCreatePen pen.c:382-514 系统调用 扩展笔入口(SEH 安全复制)

8.2 pen.h 宏与接口

名称 位置 说明
typedef BRUSH PEN, *PPEN pen.h:5 PEN 复用 BRUSH 布局
PEN_UnlockPen pen.h:17 解锁对象(GDIOBJ_vUnlockObject
PEN_ShareUnlockPen pen.h:18 释放共享引用(GDIOBJ_vDereferenceObject
AddPenLinesBounds pen.h:31 累积模式笔线边界(实现在 line.c:19)
IntIsEffectiveWidePen pen.h:33-37 宽笔判定宏

8.3 关联源码位置速查

主题 文件:行
宽笔分流(LineTo) line.c:208
宽笔分流(Polyline) line.c:371
宽笔分流(椭圆/矩形边框) fillshap.c:109, 680
描边入口 path.c:1610PATH_StrokePath
宽化准备 path.c:2195PATH_WidenPathEx
宽化算法 path.c:1801IntGdiWidenPath
线条画刷更新 dcobjs.c:60DC_vUpdateLineBrush
换笔 dcobjs.c:305IntGdiSelectBrush
样式数组释放 brush.cpp:73-76BRUSH::~BRUSH
用户态 CreatePen gdi32/objects/pen.c:21
用户态 ExtCreatePen gdi32/objects/brush.c:13
用户态 SetDCPenColor gdi32/objects/dc.c:941
内核态 IntSetDCPenColor dcutil.c:259
BRUSH_ATTR ntgdihdl.h:353

9. 总结

pen.c 虽然只有约 516 行,却是整个 GDI 线条绘制的起点

  1. 统一模型PEN == BRUSH,画笔就是"用于画线的画刷"。颜色放在 BrushAttr.lbColor
    样式放在 pStyle/dwStyleCount/ulPenStyle,宽窄由 lWidth 决定;
  2. 双入口NtGdiCreatePen(简单笔)与 NtGdiExtCreatePen(扩展笔)最终都汇聚到
    IntGdiExtCreatePen,后者完成对象分配(老式/扩展两种 GDI 对象类型)、三层参数校验
    (端帽/连接/类型)、线型样式分发(内建静态数组或用户数组)与 flAttrs 标记;
  3. 安全边界 :用户样式数组在内核用 ProbeForRead + SEH 复制到非分页池
    GDITAG_PENSTYLE),所有权随对象移交、析构时按 BR_IS_DEFAULTSTYLE 区分释放;
  4. 查询对称PEN_GetObjectBR_IS_OLDSTYLEPEN 返回 LOGPEN/EXTLOGPEN
    并被 PATH_WidenPathEx 的"两步查询"复用;
  5. 绘制挂钩IntIsEffectiveWidePen 把"宽笔"精确限定为
    "宽度>1 且(老式笔或几何笔)"。细线走 IntEngLineTo/IntEngPolyline 快路径;
    宽笔转临时路径 → PATH_StrokePathPATH_WidenPathExIntGdiWidenPath
    展开为轮廓后按 WINDING 填充------端帽(SQUARE/FLAT/ROUND)与连接
    (MITER/BEVEL/ROUND,含斜接极限降级)全部在这一步由几何算法统一实现;
  6. 颜色链路SetDCPenColor(gdi32/ntuser 两条路径)只改 DC 属性并置 DIRTY_LINE
    真正的落笔在 DC_vUpdateLineBrushDC_PEN 特例用 crPenClr 覆盖 eboLine)。

遗留问题(源码注释)IntGdiExtCreatePen// FIXME: Copy the bitmap first ?

说明 BS_PATTERN 类画笔的 DIB 目前只探测未复制;/* FIXME: What style here? */

说明 USERSTYLE 笔被统一标记 BR_IS_SOLIDIntGdiSetSolidPenColor 当前无调用方。


本文档基于 ReactOS 源代码 win32ss/gdi/ntgdi/pen.c 及关联模块

(pen.h / brush.h / line.c / path.c / dcobjs.c / dcutil.c / gdi32 objects)

分析(2026 年 8 月)。所有行号均对应分析时的源码版本。

相关推荐
lingzhilab1 小时前
零知派——STM32F103驱动ST7789与INA219太阳能充电功率实时监测系统
stm32·单片机·嵌入式硬件
caimouse2 小时前
ReactOS 图形系统分析(48):弧线绘制 — arc.c
c语言·开发语言
cypking3 小时前
Objective-C 语法完整学习手册(小白自学 + 开发备查)
c语言·开发语言·学习·objective-c
ambition202424 小时前
操作系统同步:读者-写者问题与读写公平法详解(附每个 PV 操作含义)
linux·开发语言·数据结构·unix
chuan.bai7 小时前
Java RAG 实战(第 11 篇):RAG 知识工作台网页
java·开发语言·人工智能
离陌在学C#9 小时前
C# 事件(Event)详解:从概念到实战
开发语言·c#
Terra.K10 小时前
Java异常学习[特殊字符]
java·开发语言·学习
葡萄城技术团队11 小时前
InfluxDB 2\.x 深度解析:核心架构、Flux 函数与制造业落地指南(三)
java·开发语言·架构
counting money11 小时前
Java IO流详解:从InputStream到文件操作实战
java·开发语言·python