ReactOS 图形系统分析(37):路径子系统 --- path.c
本文基于 ReactOS 源码
win32ss/gdi/ntgdi/path.c(2026 年 8 月,全文 3168 行)与
win32ss/gdi/ntgdi/path.h逐函数分析。路径子系统是 GDI 中"先记录、后执行"思想的典型实现:
BeginPath/EndPath之间的一切绘图调用不再直接画到表面,而是被折算成"点 + 标志"数组累积到 PATH 对象中,
EndPath之后再统一交给填充/描边/宽化/区域化等输出管线消费。
1. 概述
1.1 什么是"路径"(Path)
路径是 GDI 提供的一种**延迟渲染(deferred rendering)**机制。应用可以:
c
BeginPath(hdc); // 进入路径记录模式
MoveToEx(hdc, 10, 10, NULL);
LineTo(hdc, 100, 50); // 直线
PolyBezier(hdc, pts, 9); // 三次贝塞尔曲线
RoundRect(hdc, 0, 0, 80, 80, 10, 10); // 圆角矩形(贝塞尔近似)
EndPath(hdc); // 结束记录,路径进入 PATH_Closed 状态
StrokePath(hdc); // 用当前画笔描边
FillPath(hdc); // 用当前画刷填充
StrokeAndFillPath(hdc);
WidenPath(hdc); // 把路径按笔宽"轮廓化"成新路径
FlattenPath(hdc); // 把贝塞尔全部转成折线
HRGN r = PathToRegion(hdc); // 路径转区域
GetPath(hdc, pts, types, size); // 取出路径的点与类型
在记录期间,LineTo/PolylineTo/PolyBezierTo/Arc/Ellipse/RoundRect/Rectangle/
PolyDraw/PolyPolygon/PolyPolyline/TextOut(ExtTextOut)等都会把图元折算成
POINT + BYTE 标志对写入 PATH 的数组。当前画笔、画刷、变换等一律不参与记录 ,
直到输出阶段才结合 DC 属性真正光栅化。
1.2 分层架构
┌─────────────────────────────────────────────────────────────────────┐
│ 应用层(GDI API) │
│ BeginPath / LineTo / PolyBezier / ExtTextOut / EndPath │
│ StrokePath / FillPath / StrokeAndFillPath / WidenPath / │
│ FlattenPath / PathToRegion / GetPath / CloseFigure / AbortPath │
└───────────────────────────────┬─────────────────────────────────────┘
↓ 用户态 gdi32.dll → win32u.dll
┌───────────────────────────────┴─────────────────────────────────────┐
│ win32ss/gdi/ntgdi/path.c(本分析对象) │
│ 13 个 NtGdi* 系统调用入口(第 6 节逐一展开) │
│ ├─ 生命周期/管理 PATH_CreatePath / PATH_Delete / PATH_InitGdiPath │
│ │ PATH_AssignGdiPath / PATH_SavePath / │
│ │ PATH_RestorePath / PATH_EmptyPath / │
│ │ PATH_DestroyGdiPath / IntGdiCloseFigure │
│ ├─ 记录层 PATH_AddEntry / PATH_ReserveEntries(数组扩容) │
│ ├─ 图元层 PATH_MoveTo / LineTo / Rectangle / RoundRect / │
│ │ Ellipse / Arc(+DoArcPart) / PolyBezier(To) / │
│ │ PolyDraw / PolylineTo / PolyPolygon / │
│ │ PolyPolyline / ExtTextOut(+add_outline) │
│ ├─ 变换层 PATH_FlattenPath(+AddFlatBezier) 贝塞尔→折线 │
│ │ IntGdiWidenPath / PATH_WidenPath(Ex) 宽化轮廓 │
│ └─ 输出层 PATH_PathToRegion / PATH_FillPath(Ex) / │
│ PATH_StrokePath │
└───────────────┬───────────────────────────────┬──────────────────────┘
↓ ↓
细笔描边: IntGdiPolyline → IntEngPolyline/ 宽笔/填充: PATH_WidenPathEx →
IntEngLineTo(line.c,《分析_5》) PATH_FillPathEx → IntGdiFillRgn
(bitblt.c,《分析_17》)
1.3 双表示关系(与《分析_18》衔接)
PATH 结构里同时存在两套坐标表示:
- Wine/ReactOS 风格 :
pPoints(POINT数组,设备坐标)+pFlags(BYTE
标志数组,PT_MOVETO/PT_LINETO/PT_BEZIERTO/PT_CLOSEFIGURE)。path.c 的全部
记录、图元、输出逻辑都建立在这套数组之上。 - DDI 风格 :
ppdFirst/ppdLast/ppdCurrent串起的EXTPATHDATA链表(内含
PATHDATA,定点数POINTFIX坐标,PD_*标志),由eng/pathobj.c的
PATHOBJ_*服务维护。正如《分析_18》所指出,这套链表只是为 DDI 契约预留 ,
全树没有任何驱动实际调用,路径的真正消费完全走第一套表示。
1.4 设计动机
- 解耦"几何构造"与"光栅化" :贝塞尔曲线的折线化、宽笔的轮廓化(endcap/join)
等昂贵计算只在输出阶段执行一次;FillPath与StrokePath可复用同一份几何。 - 字体轮廓复用 :
BeginPath下ExtTextOut通过 FreeType 的GGO_NATIVE
直接取字形原生轮廓折成路径(PATH_add_outline),使文字可作为几何参与
Stroke/Fill/区域裁剪------这是"把文字变成路径"的唯一内核途径。 - 区域化 :路径可以折成多边形区域(
PATH_PathToRegion→REGION_SetPolyPolygonRgn),
从而获得区域的所有能力(命中测试、裁剪、CombineRgn 等)。 - 统一坐标基准 :记录阶段就把逻辑坐标转成设备坐标 存入路径(
IntLPtoDP),
输出阶段再临时把 DC 置为MM_TEXT+ 单位世界变换,避免二次变换失真。
2. 核心数据结构(path.h(file:///d:/reactos/win32ss/gdi/ntgdi/path.h))
2.1 DCPATH_* 标志(enum _DCPATHFLAGS)
定义在 path.h 头部,存放在 DC.dclevel.flPath 中,描述路径与 DC 的关系:
c
enum _DCPATHFLAGS
{
DCPATH_ACTIVE = 0x0001, /* DC 正处于 BeginPath..EndPath 记录区间 */
DCPATH_SAVE = 0x0002, /* 当前路径是被 SaveDC 保存的副本 */
DCPATH_CLOCKWISE = 0x0004, /* 多边形类图元按顺时针生成(Arc 缺省方向)*/
/* ReactOS only */
DCPATH_SAVESTATE = 0x80000000 /* SaveDC 保存的状态标志(ReactOS 私有) */
};
| 标志 | 作用点 | 说明 |
|---|---|---|
DCPATH_ACTIVE |
NtGdiBeginPath 置位、NtGdiEndPath/AbortPath 清位 |
配合 PATH_IsPathOpen 判定"DC 正在记录路径";图元 API(line.c/arc.c/fillshap.c/freetype.c)据此转向 PATH_* 记录函数 |
DCPATH_SAVE |
SaveDC 期间 |
BeginPath 若遇 SAVE 状态不清除旧路径而新建副本;RestorePath 用它决定重建还是覆盖 |
DCPATH_CLOCKWISE |
NtGdiSetArcDirection 等 |
PATH_Rectangle/RoundRect/Ellipse/Arc 的缺省绕行方向(点序反向用 reverse_points) |
配套宏:
c
#define PATH_IsPathOpen(dclevel) ( ((dclevel).hPath) && ((dclevel).flPath & DCPATH_ACTIVE) )
2.2 路径状态机 GdiPathState
c
typedef enum tagGdiPathState
{
PATH_Null, /* 空路径(无内容或刚初始化) */
PATH_Open, /* BeginPath 之后、EndPath 之前:可继续累积图元 */
PATH_Closed /* EndPath 之后:只读,可被 Stroke/Fill/Widen/GetPath 消费 */
} GdiPathState;
状态流转(第 3、6 节有详述):
PATH_Null ──PATH_CreatePath──▶ PATH_Open ──NtGdiEndPath──▶ PATH_Closed
▲ │ │
└──────────PATH_EmptyPath────┘ ├─StrokePath/FillPath/...
(NtGdiBeginPath 复用/重建) └─消费后 PATH_Delete/重置
要点:PATH_Closed 后所有 PATH_* 记录函数(如 PATH_ExtTextOut 的
state != PATH_Open 检查)会拒绝写入;WidenPathEx/GetPath/PathToRegion 则
要求必须是 PATH_Closed ,否则返回 ERROR_CAN_NOT_COMPLETE。
2.3 PATHTYPE_*(历史遗留标志)
c
// Path type flags
#define PATHTYPE_KEEPME 1
#define PATHTYPE_STACK 2
这两个宏在 path.c/path.h 中仅定义、从未被使用 ,属于从 Wine 早期版本继承的
"路径类型"占位标志,保留以兼容外部引用。
2.4 PATH 结构全字段(path.h L34-L59)
c
typedef struct _PATH
{
BASEOBJECT BaseObject; /* GDI 对象头(含句柄 hHmgr、类型 GDI_OBJECT_TYPE_PATH)*/
/* ---- DDI/PATHOBJ 表示(《分析_18》:eng/pathobj.c 维护,预留未用) ---- */
//PVOID ppachain; /* 已注释:旧版子路径链 */
RECTFX rcfxBoundBox; /* 路径包围盒(28.4 定点数)*/
POINTFX ptfxSubPathStart; /* 当前子路径起点(定点数)*/
FLONG flType; /* 路径类型标志 */
PEXTPATHDATA ppdFirst; /* EXTPATHDATA 链表头 */
PEXTPATHDATA ppdLast; /* EXTPATHDATA 链表尾 */
FLONG flags; /* 当前 PATHDATA 段标志 */
PEXTPATHDATA ppdCurrent; /* 枚举游标(PATHOBJ_bEnum 用)*/
FLONG fl; /* 保存的 PATHOBJ 标志(PO_*)*/
ULONG cCurves; /* 保存的子路径/曲线计数 */
struct _EPATHOBJ *epo; /* 反向指针:EPATHOBJ(含 PATHOBJ 头 + CLIPOBJ)*/
/* ---- Wine/ReactOS 表示:path.c 真正使用的记录数组 ---- */
FLONG state; /* GdiPathState:PATH_Null/Open/Closed */
POINT *pPoints; /* 点数组(设备坐标),TAG_PATH 分页池 */
BYTE *pFlags; /* 标志数组:PT_MOVETO/PT_LINETO/PT_BEZIERTO(|PT_CLOSEFIGURE) */
int numEntriesUsed; /* 已用条目数 */
int numEntriesAllocated;/* 已分配容量(条目数)*/
BOOL newStroke; /* 下一个图元是否应开启新子路径(true 时先写 PT_MOVETO)*/
POINT pos; /* 路径内"当前点"(设备坐标,stroke 起点/续接依据)*/
} PATH, *PPATH;
配套句柄宏(path.h L68-L72):
c
#define PATH_AllocPath() ((PPATH) GDIOBJ_AllocObj(GDIObjType_PATH_TYPE))
#define PATH_AllocPathWithHandle()((PPATH) GDIOBJ_AllocObjWithHandle(GDI_OBJECT_TYPE_PATH, sizeof(PATH)))
#define PATH_LockPath(hPath) ((PPATH)GDIOBJ_ShareLockObj((HGDIOBJ)hPath, GDI_OBJECT_TYPE_PATH))
#define PATH_UnlockPath(pPath) (GDIOBJ_vDereferenceObject((POBJ)pPath))
- 路径句柄是共享句柄 :同一线程内递归调用(如
IntGdiPolyline宽笔分支内部
再建路径)也允许重复锁定,故PATH_CreatePath使用 ShareLock。 - 加锁/解锁必须成对:几乎所有
PATH_*函数都以PATH_LockPath(dc->dclevel.hPath)
开始、PATH_UnlockPath结束;PATH_LockPath失败(句柄非法)返回 NULL 并导致
函数直接返回 FALSE。
2.5 EXTPATHDATA 与 eng/pathobj.c 的双表示关系
c
/* extended PATHDATA */
typedef struct _EXTPATHDATA
{
PATHDATA pd; /* 公开 DDI 单元:flags + count + pptfx(POINTFIX*) */
struct _EXTPATHDATA *ppdNext; /* 链表后继 */
} EXTPATHDATA, *PEXTPATHDATA;
其中 PATHDATA(winddi.h)为:
c
typedef struct _PATHDATA {
FLONG flags; /* PD_BEGINSUBPATH/PD_ENDSUBPATH/PD_CLOSEFIGURE/PD_BEZIERS */
ULONG count; /* 本段点数 */
POINTFIX *pptfx; /* 定点数坐标(1/64 像素)*/
} PATHDATA, *PPATHDATA;
eng/pathobj.c 的 PATHOBJ_bMoveTo/bPolyLineTo/bPolyBezierTo/bCloseFigure/ vEnumStart/bEnum/vGetBounds 全部透过 EPATHOBJ.pPath 操作这套链表:追加
EXTPATHDATA 节点、合并同类型连续段、用 ppdCurrent 做枚举游标、把边界盒写入
rcfxBoundBox。但正如《分析_18》的结论:没有任何驱动调用这些 DDI 服务 ,
EngStrokePath/EngFillPath 在 eng/stubs.c 中仍是桩;path.c 的双表示只是
"结构与 DDI 兼容",实际数据流完全走 pPoints/pFlags 数组。
2.6 常量与调试设施
c
#define NUM_ENTRIES_INITIAL 16 /* pPoints/pFlags 初始容量(PATH_CreatePath 缺省)*/
#define GROW_FACTOR_NUMER 2 /* 扩容因子分子 */
#define GROW_FACTOR_DENOM 1 /* 扩容因子分母(即 ×2 指数增长,保证摊还 O(1))*/
DBG_DEFAULT_CHANNEL(GdiPath); /* 调试通道:GdiPath(TRACE/ERR 前缀 trace:/err:)*/
#ifdef DBG
static int PathCount = 0; /* 调试用:现存路径计数(Create++/Delete--)*/
#endif
内存标签一律 TAG_PATH(ExAllocatePoolWithTag(PagedPool, ...)),贝塞尔折线化
临时点用 TAG_BEZIER(bezier.c 分配)。
PT_ 记录标志 *(wingdi.h):pFlags 数组每个字节取值如下,其中
PT_CLOSEFIGURE 是可与前三种 OR 的组合位:
| 标志 | 值 | 含义 |
|---|---|---|
PT_CLOSEFIGURE |
0x01 | 本记录(通常为最后一点)闭合当前子路径 |
PT_LINETO |
0x02 | 直线段端点(起点为上一记录) |
PT_BEZIERTO |
0x04 | 三次贝塞尔控制点(每 3 条一组,曲线从上一记录到第 3 条) |
PT_MOVETO |
0x06 | 子路径起点(新笔画) |
典型组合:PT_LINETO | PT_CLOSEFIGURE(0x03,多边形的末点)、
PT_BEZIERTO | PT_CLOSEFIGURE(0x05,闭合贝塞尔段末点,如椭圆)。
路径合法性的不变量(被消费方 ASSERT/校验):
- 首条记录必须是
PT_MOVETO(PATH_PathToRegion的ASSERT(pFlags[0]==PT_MOVETO)、
PATH_StrokePath对 i==0 的检查); - 每条子路径以 PT_MOVETO 开头,其后紧跟 LINETO 或 3 条一组的 BEZIERTO;
PT_CLOSEFIGURE之后必须紧跟 PT_MOVETO(PATH_StrokePath/IntGdiWidenPath
都会校验"上一记录 CLOSEFIGURE 后本记录必须 MOVETO");- 贝塞尔段必须连续 3 条 PT_BEZIERTO(
PATH_StrokePath校验
pFlags[i+1]==PT_BEZIERTO && (pFlags[i+2]&~PT_CLOSEFIGURE)==PT_BEZIERTO)。
3. 生命周期与管理函数
3.1 PATH_CreatePath(int count) --- 创建并初始化路径
c
PPATH FASTCALL PATH_CreatePath(int count);
作用 :分配一个带句柄的 PATH 对象,初始容量为 max(16, count),并预分配
pPoints(count*sizeof(POINT))与 pFlags(count*sizeof(BYTE))两块分页池。
实现流程:
PATH_AllocPathWithHandle()(GDIOBJ_AllocObjWithHandle(GDI_OBJECT_TYPE_PATH, sizeof(PATH)))分配对象;失败则EngSetLastError(ERROR_NOT_ENOUGH_MEMORY)返回 NULL。GDIOBJ_vUnlockObject解锁后改用PATH_LockPath(pPath->BaseObject.hHmgr)做
共享锁 ------注释说明"Path handles are shared. Also due to recursion with in
the same thread"(句柄共享、允许同线程递归加锁)。PATH_EmptyPath确保对象初始为空。count = max(NUM_ENTRIES_INITIAL, count);numEntriesAllocated = count;
两块数组RtlZeroMemory清零。- 初始化
numEntriesUsed = 0、newStroke = TRUE(第一条图元需 MOVETO)、
state = PATH_Open、pos = (0,0)。 - 调试构建下
PathCount++。
使用方式 :NtGdiBeginPath、PATH_SavePath、PATH_FlattenPath、
IntGdiWidenPath、IntGdiPolyline(宽笔临时路径)等所有需要新路径的地方。
注意事项 :返回的路径处于 PATH_Open 且已加共享锁 ,调用方用完必须
PATH_UnlockPath;若不再需要则用 PATH_Delete(hPath)。
3.2 PATH_DestroyGdiPath(PPATH pPath) --- 释放记录数组
c
VOID FASTCALL PATH_DestroyGdiPath(PPATH pPath);
作用 :ExFreePoolWithTag 释放 pPoints 与 pFlags 两块数组(各判 NULL)。
不释放 PATH 结构本身、不删除 GDI 对象、不改状态------它只是"清内存"。
使用方式 :PATH_Delete 删除对象前调用;IntGdiWidenPath 在丢弃临时 stroke/
上下路径时逐个调用。
3.3 PATH_Delete(HPATH hPath) --- 按句柄删除路径
c
BOOL FASTCALL PATH_Delete(HPATH hPath);
流程 :hPath 为 NULL 返回 FALSE → PATH_LockPath 加锁(失败返回 FALSE)→
PATH_DestroyGdiPath 释放数组 → GDIOBJ_vDeleteObject(&pPath->BaseObject) 删除
GDI 对象 → 调试 PathCount-- → 返回 TRUE。
使用方式 :NtGdiAbortPath、NtGdiEndPath 系列输出后清理、NtGdiWidenPath
换路径、PATH_FlattenPath 失败清理等。注意调用前通常需要先 PATH_UnlockPath
或者依赖 GDIOBJ_vDeleteObject 内部对引用计数的处理(源码中两种用法都有:
如 PATH_FlattenPath 失败分支先 UnlockPath 再 Delete;NtGdiFillPath 则先
UnlockPath 后 Delete)。
3.4 PATH_InitGdiPath(PPATH pPath) --- 初始化(置空,不分配)
c
VOID FASTCALL PATH_InitGdiPath(PPATH pPath);
把 state = PATH_Null、pPoints = NULL、pFlags = NULL、
numEntriesUsed = numEntriesAllocated = 0。用于把一块裸内存变成合法的空 PATH
(IntGdiWidenPath 中手工 ExAllocatePool(sizeof(PATH)) 的临时 stroke 路径就是
先 Init 再填充)。
与 PATH_EmptyPath 的区别 :Init 用于"从未使用过的对象"(指针置 NULL);
EmptyPath 用于"已分配过数组的对象"(保留内存、仅清逻辑内容)。
3.5 PATH_AssignGdiPath(PPATH pPathDest, const PPATH pPathSrc) --- 深拷贝
c
BOOL FASTCALL PATH_AssignGdiPath(PPATH pPathDest, const PPATH pPathSrc);
作用 :把 src 的记录数组逐字节拷贝 到 dest(深拷贝,不是指针共享),并复制
pos/state/numEntriesUsed/newStroke。注释强调:dest 应先用 PATH_InitGdiPath
初始化过(语义上相当于 C++ 的"赋值运算符"而非"拷贝构造")。
流程 :PATH_ReserveEntries(dest, src->numEntriesUsed) 确保容量(失败返回
FALSE)→ memcpy 两块数组 → 复制四个标量字段 → TRUE。
使用方式 :PATH_SavePath/PATH_RestorePath 的核心;SaveDC 对路径做快照。
3.6 PATH_SavePath(DC *dst, DC *src) --- SaveDC 时保存路径
c
BOOL PATH_SavePath(DC *, DC *);
流程 :锁 src 的路径 → PATH_CreatePath(src->numEntriesAllocated) 按原容量新建
→ dst->dclevel.flPath = src->dclevel.flPath(连同 DCPATH_ACTIVE/SAVE/CLOCKWISE
一起复制)→ dst->dclevel.hPath = 新路径句柄 → PATH_AssignGdiPath 深拷贝 →
解锁两者。
说明 :SaveDC 路径快照是整对象复制 (含记录数组内容),这使
BeginPath...EndPath 区间内 SaveDC/RestoreDC 也能正确还原路径状态。调用方
(dcobjs.c 的 NtGdiSaveDC)在保存前会给 flPath 置 DCPATH_SAVE 标志。
3.7 PATH_RestorePath(DC *dst, DC *src) --- RestoreDC 时恢复路径
c
BOOL PATH_RestorePath(DC *, DC *);
流程(按 dst 当前是否有路径分两支):
dst->dclevel.hPath == NULL:与 SavePath 相同------新建并深拷贝 src 路径,
flPath整体复制。- 已有路径:锁两边的路径,
dst->dclevel.flPath = src->dclevel.flPath & (DCPATH_CLOCKWISE|DCPATH_ACTIVE)(只回拷方向与激活位 ,不清 SAVE 位),
再PATH_AssignGdiPath覆盖内容。
注意事项:恢复后旧路径对象不复存在(被覆盖),RestoreDC 层负责最终释放。
3.8 PATH_EmptyPath(PPATH pPath) --- 清空(保留容量)
c
VOID FASTCALL PATH_EmptyPath(PPATH pPath);
state = PATH_Null; numEntriesUsed = 0;。不释放、不重分配数组 ,下次写入复用
现有容量。PATH_CreatePath 用它确保新对象为空。
3.9 PATH_AddEntry(PPATH pPath, const POINT *pPoint, BYTE flags) --- 追加一条记录
c
BOOL FASTCALL PATH_AddEntry (PPATH pPath, const POINT *pPoint, BYTE flags);
作用 :路径记录层的唯一写入原语:追加一个 (点, 标志) 对。
流程 :PATH_ReserveEntries(pPath, numEntriesUsed + 1) 保证容量(失败 FALSE)→
pPoints[numEntriesUsed] = *pPoint; pFlags[numEntriesUsed] = flags; →
numEntriesUsed++ → TRUE。
flags 合法值 :PT_MOVETO(6)/PT_LINETO(2)/PT_BEZIERTO(4),可 OR
PT_CLOSEFIGURE(1)。源码中带 FIXME:newStroke 为 true 时应当校验传入的是
PT_MOVETO(当前未做校验)。
使用方式 :所有图元函数(PATH_LineTo、PATH_Rectangle、PATH_Arc、
PATH_PolyPolyline、PATH_AddFlatBezier、PATH_add_outline 等)以及
IntGdiWidenPath 的轮廓生成都调用它。
3.10 PATH_ReserveEntries(PPATH pPath, INT numEntries) --- 数组扩容
c
BOOL FASTCALL PATH_ReserveEntries (PPATH pPath, INT numEntries);
作用 :确保 pPoints/pFlags 至少可容纳 numEntries 条记录,必要时扩容并迁移。
流程:
numEntries > numEntriesAllocated才动作。- 已有容量则按
×2/1指数增长 直到不小于 numEntries(注释:指数增长保证
总复杂度线性,摊还 O(1)/次);首分配(容量 0)则直接分配恰好 numEntries。 - 先分配新
pPoints(失败返回 FALSE),再分配新pFlags(失败则回滚释放
pPoints 并返回 FALSE)------两段分配防止半途泄漏。 - 若有旧数组:
memcpy已用部分到新数组,ExFreePoolWithTag旧数组。 - 更新指针与
numEntriesAllocated。
注意 :调用者(如 add_log_points)在 Reserve 之后直接向
pFlags[numEntriesUsed] 批量写标志 并一次性推进 numEntriesUsed,因此本函数
返回后数组内容未初始化区域必须被立即填充。
3.11 PATH_ScaleNormalizedPoint(POINT corners[], FLOATL x, FLOATL y, POINT *pPoint)(static)
把归一化坐标 (x,y) ∈ [-1,1]×[-1,1] 映射到以 corners[0](左上)、corners[1]
(右下)为对角顶点的框内:
pPoint->x = corners[0].x + (corners[1].x - corners[0].x) * 0.5 * (x + 1.0);
pPoint->y = corners[0].y + (corners[1].y - corners[0].y) * 0.5 * (y + 1.0);
全程用 FLOATOBJ 做定点/浮点混合运算(gef1/gef2 为预置常量 1.0/2.0),最后
FLOATOBJ_bConvertToLong 取整;转换溢出返回 FALSE (调用方 PATH_DoArcPart
据此失败退出)。
用途:圆弧/端帽的贝塞尔控制点(单位圆上的三角函数值)投影到椭圆框内。
3.12 PATH_NormalizePoint(POINTL corners[], const POINTL *pPoint, FLOATL *pX, FLOATL *pY)(static)
ScaleNormalizedPoint 的逆映射(框内点 → 归一化 -1,1):
*pX = (pPoint->x - corners[0].x) * 2.0 / (corners[1].x - corners[0].x) - 1.0;
*pY = (pPoint->y - corners[0].y) * 2.0 / (corners[1].y - corners[0].y) - 1.0;
用途 :PATH_Arc 计算起点/终点相对于包围框的极角(atan2(y,x))。
3.13 PATH_CheckRect(DC *dc, RECTL* rect, INT x1, INT y1, INT x2, INT y2)(static)
PATH_Rectangle/PATH_RoundRect/PATH_Ellipse 的公共前置处理:
RECTL_vSetRect(rect, x1, y1, x2, y2);IntLPtoDP(dc, (PPOINT)rect, 2)把两对角点转设备坐标;RECTL_vMakeWellOrdered(rect)保证 left≤right、top≤bottom;- GM_COMPATIBLE 语义 :若
left == right || top == bottom(退化矩形)返回
FALSE;否则rect->right--; rect->bottom--(兼容模式下矩形不包含右/下边缘,
与 GDI 的 Rectangle 光栅化规则一致)。
返回值约定 :FALSE 表示"无需记录"------注意调用方对 FALSE 的处理不一 :
PATH_Rectangle/RoundRect 返回 TRUE(静默成功),PATH_Ellipse 也返回 TRUE,
只有路径锁失败才返回 FALSE。
3.14 IntGdiCloseFigure(PPATH pPath) --- 标记图元闭合
c
VOID FASTCALL IntGdiCloseFigure(PPATH pPath);
作用 :把路径当前子路径 标记为闭合------给末条记录的标志补上
PT_CLOSEFIGURE 位,并把 newStroke 置 TRUE(下一条图元将开启新子路径)。
流程(源码 L106-L119):
c
ASSERT(pPath->state == PATH_Open); // 仅记录期有效
// FIXME: Shouldn't we draw a line to the beginning of the figure?
if (pPath->numEntriesUsed)
{
pPath->pFlags[pPath->numEntriesUsed - 1] |= PT_CLOSEFIGURE;
pPath->newStroke = TRUE;
}
关键语义:
- 不真正补线 :源码自带 FIXME("Shouldn't we draw a line to the beginning of
the figure?")------闭合线段并不写入路径,而是由消费方隐式处理:
PATH_StrokePath遇 CLOSEFIGURE 回填pLinePts[0](起点);FillPath的区域
构造与IntGdiWidenPath的闭合 stroke 判定也都依赖该标志而非实际线段。 newStroke = TRUE使CloseFigure之后的LineTo/PolyBezierTo自动以
start_new_stroke补写 PT_MOVETO 开启新子路径。numEntriesUsed == 0(空路径)时静默跳过。
使用方式 :PATH_Rectangle/RoundRect/Ellipse 构造完闭合图形后调用;
PATH_Arc 的 chord 分支调用;PATH_PolyPolygon 用
PT_LINETO | PT_CLOSEFIGURE 直接写标志等价实现;PATH_add_outline 每个字形
轮廓结束后调用;NtGdiCloseFigure 是对它的唯一系统调用包装。
3.15 GdiPathDPtoLP(PDC pdc, PPOINT ppt, INT count) --- 路径点回逻辑坐标
c
BOOL FASTCALL GdiPathDPtoLP(PDC pdc, PPOINT ppt, INT count);
用 XFORMOBJ(mxDeviceToWorld 矩阵)做 XF_LTOL(设备→逻辑)批量变换,
供 NtGdiGetPath 把设备坐标点转回逻辑坐标返回给应用。注释引用 MSDN:
设备坐标超过 27 位、或转换后逻辑坐标超过 32 位时调用失败 (对应
NtGdiGetPath 的 ERROR_ARITHMETIC_OVERFLOW)。
4. 图元累积层(BeginPath 期间)
4.0 内部辅助函数(batch 写入与笔画管理)
这些 static 函数是图元层的"积木",全部围绕"点+标志"数组操作:
c
/* 追加 count 个逻辑点(已转设备坐标后写入),返回首个标志字节指针以便改写 */
static BYTE *add_log_points(DC *dc, PPATH path, const POINT *points,
DWORD count, BYTE type, BOOL bExtraPt);
static BYTE *add_points(PPATH path, const POINT *points, DWORD count, BYTE type);
static void reverse_points(POINT *points, UINT count); /* 反序(顺时针) */
static BOOL start_new_stroke(PPATH path); /* 需要时写 PT_MOVETO */
static void update_current_pos(PPATH path); /* pos = 最后一点 */
static void close_figure(PPATH path); /* 末点 |= PT_CLOSEFIGURE */
static BOOL add_log_points_new_stroke(DC *dc, PPATH path, const POINT *points,
DWORD count, BYTE type, BOOL bExtraPt);
add_log_points:PATH_ReserveEntries(numEntriesUsed+count)后:bExtraPt == TRUE && numEntriesUsed == 1(刚由 start_new_stroke 写入唯一的
PT_MOVETO)时,把输入缓冲的 count+1 个点 整体拷入pPoints[0..count]并
一次性IntLPtoDP------首点直接充当新笔画的 MOVETO 位置(合并/覆盖 MoveTo
点,供PolyBezierTo/PolylineTo复用输入缓冲)。- 否则在
pPoints[numEntriesUsed]处拷入 count 个点并转换。 memset(ret, type, count)批量写标志(ret 指向首个新标志),推进
numEntriesUsed += count,返回 ret(调用方据此把 type0 改成 PT_MOVETO 等)。
add_points:同前者但不做坐标变换(点已是设备坐标)。start_new_stroke:若!newStroke && 已有记录 && 末条非 CLOSEFIGURE && 末点 == path->pos则直接返回 TRUE(延续当前笔画);否则置newStroke=FALSE并
add_points(&path->pos, 1, PT_MOVETO)------每条子路径都以 PT_MOVETO 开头。update_current_pos:path->pos = pPoints[numEntriesUsed-1]。close_figure:末条标志|= PT_CLOSEFIGURE。add_log_points_new_stroke:串联start_new_stroke→add_log_points→
update_current_pos→IntGdiMoveToEx(dc, pos.x, pos.y, NULL)(把 DC 的当前点
同步到路径终点,保证后续LineTo语义连续)。几乎所有"To"系列图元(LineTo、
PolylineTo、PolyBezierTo)都走它。
4.1 PATH_MoveTo(PDC dc, PPATH pPath) --- 登记新笔画起点
c
BOOL FASTCALL PATH_MoveTo(PDC dc, PPATH pPath);
语义 :MoveToEx 作用在开路径 DC 上时,DC 层的当前位置已被移动(DIRTY_ STYLESTATE 脏位置位);本函数只是把新的当前位置登记为下一条 stroke 的起点。
流程 :若 dc->pdcattr->ulDirty_ & DIRTY_STYLESTATE:newStroke = TRUE;
IntGetCurrentPositionEx(dc, &pPath->pos) 取 DC 当前点并 IntLPtoDP 转设备坐标;
返回 TRUE。否则返回 FALSE(无需处理)。
使用方式 :PATH_LineTo 开头先调它("Coalesce a MoveTo point");IntGdiPolyline
宽笔分支构造临时路径时也调用。注意 :它只改 newStroke 与 pos,不写任何路径
记录 ------真正的 PT_MOVETO 由随后 start_new_stroke/PATH_LineTo 写入。
4.2 PATH_LineTo(PDC dc, INT x, INT y) --- 记录直线
c
BOOL FASTCALL PATH_LineTo (PDC dc, INT x, INT y);
流程 :锁路径 → point = (x, y) → 先试 PATH_MoveTo(若 DC 有 MoveTo 脏位则
直接完成登记)→ 否则若 pPath->newStroke:newStroke = FALSE,取 DC 当前点
(IntGetCurrentPositionEx + CoordLPtoDP)PATH_AddEntry(PT_MOVETO) 写子路径
起点 → add_log_points_new_stroke(dc, pPath, &point, 1, PT_LINETO, FALSE) 追加
一条 PT_LINETO 并推进当前点 → 解锁。
路径结果示例(MoveTo(10,10) → LineTo(20,30) → LineTo(40,50)):
i: 0 1 2
pt: (10,10) (20,30) (40,50)
fl: PT_MOVETO PT_LINETO PT_LINETO
4.3 PATH_Rectangle(PDC dc, INT x1, INT y1, INT x2, INT y2) --- 矩形
流程 :锁路径 → PATH_CheckRect(退化/兼容模式剔除;失败返回 TRUE)→ 生成
4 个角点,顺序为 (right,top) (left,top) (left,bottom) (right,bottom) →
若 DCPATH_CLOCKWISE 则 reverse_points(points, 4) → add_points(..., 4, PT_LINETO) → type[0] = PT_MOVETO(首点改 MOVETO)→ IntGdiCloseFigure 闭合 →
解锁。
路径结果 :[MOVETO (r,t), LINETO (l,t), LINETO (l,b), LINETO (r,b) | CLOSEFIGURE],共 4 条记录,整图闭合。
4.4 PATH_RoundRect(DC *dc, INT x1, INT y1, INT x2, INT y2, INT ell_width, INT ell_height) --- 圆角矩形
流程:
ell_width == 0 || ell_height == 0→ 退化为PATH_Rectangle。- 锁路径、
PATH_CheckRect。 - 把
(0,0,ell_width,ell_height)转设备坐标并 well-ordered,宽高夹取到矩形
尺寸以内 (min)。 - 计算圆角偏移:
xOffset = EngMulDiv(ell_width, 44771525, 200000000),即
w * (1 − 0.5522847) / 2(0.5522847 为三次贝塞尔逼近 1/4 圆弧的经典常量
k = 4/3·(√2−1));yOffset同理。半宽高用EngMulDiv(w, 1, 2)取四舍五入
的整数(注释特意强调不能直接整数除法)。 - 生成 16 个点 (注释引用 stackoverflow 的圆角矩形贝塞尔构造法):
points[0]:起点(right, top+ell_height);[1..3]:右上角贝塞尔 3 控制点;[4]:上边直线端点(left+ell_width, top);[5..7]:左上角贝塞尔;[8]:左边直线端点;[9..11]:左下角贝塞尔;[12]:下边直线端点;[13..15]:右下角贝塞尔(终点(right, bottom-ell_height),闭合回起点)。
- 顺时针则
reverse_points(16);add_points(..., 16, PT_BEZIERTO)后修正标志:
type[0]=PT_MOVETO; type[4]=type[8]=type[12]=PT_LINETO(四条直边),其余保持
PT_BEZIERTO(四角每 3 点一组);IntGdiCloseFigure。
路径结果 :MOVETO, (3×BEZIERTO, LINETO) × 4, CLOSEFIGURE,共 16 条。
4.5 PATH_Ellipse(PDC dc, INT x1, INT y1, INT x2, INT y2) --- 椭圆(4 段贝塞尔)
流程 :PATH_CheckRect(失败返回 TRUE)→ xRadius/yRadius = 半宽/半高 →
取 4 个切点 left/top/right/bottom(各边中点)→ 控制点偏移
xOffset = EngMulDiv(宽, 55428475, 200000000),即 w * 0.55428475 / 2
(= w·k/2,k=0.5522847)→ 构造 13 个点:
points[0] = right (起点:最右点)
points[1] = right 上移 yOffset (第 1 段控制点1)
points[2] = top 右移 xOffset (第 1 段控制点2)
points[3] = top (第 1 段终点 = 第 2 段起点)
points[4] = top 左移 xOffset
points[5] = left 上移 yOffset
points[6] = left
points[7] = left 下移 yOffset
points[8] = bottom 左移 xOffset
points[9] = bottom
points[10] = bottom 右移 xOffset
points[11] = right 下移 yOffset
points[12] = right (回到起点,闭合)
顺时针则反序;add_points(..., 13, PT_BEZIERTO),type[0]=PT_MOVETO;
IntGdiCloseFigure。共 13 条记录 = 4 段三次贝塞尔(首尾闭合点重复)。
4.6 PATH_DoArcPart(PPATH pPath, POINT corners[], double angleStart, double angleEnd, BYTE startEntryType)(static)
作用:把**不超过 90°(M_PI_2)**的一段圆弧用一段三次贝塞尔近似并追加到路径。
算法(经典圆弧贝塞尔公式):
halfAngle = (angleEnd - angleStart) / 2;
a = 4/3 · (1 − cos(halfAngle)) / sin(halfAngle); /* 控制点张量 */
p0 = (cos s, sin s); p3 = (cos e, sin e);
p1 = p0 − a·(−sin s, cos s) 的 y/x 叉乘形式:
xNorm[1] = xNorm[0] − a·yNorm[0]; yNorm[1] = yNorm[0] + a·xNorm[0];
xNorm[2] = xNorm[3] + a·yNorm[3]; yNorm[2] = yNorm[3] − a·xNorm[3];
- 角度差极小(
|halfAngle| ≤ 1e-8)时 4 个控制点退化为同一点(避免除零/抖动)。 - 4 个归一化控制点经
PATH_ScaleNormalizedPoint映射进corners框(椭圆框)。 startEntryType非零时首条记录用该类型(如 PT_MOVETO/PT_LINETO),否则假定
当前点即弧起点(start = !startEntryType跳过第 0 点)。add_points(pPath, points+start, 4−start, PT_BEZIERTO)批量写入。
使用方式 :PATH_Arc 每个象限段调用一次;IntGdiWidenPath 的 ROUND 端帽用
4 段 45° 弧拼出 180° 半圆(复用同一函数)。
4.7 PATH_Arc(...) --- 弧 / 弦 / 扇形 / 弧到
c
BOOL FASTCALL PATH_Arc(PDC dc, INT x1, INT y1, INT x2, INT y2,
INT xStart, INT yStart, INT xEnd, INT yEnd,
INT direction, INT lines);
参数 :(x1,y1)-(x2,y2) 为外接矩形;(xStart,yStart)/(xEnd,yEnd) 为起止
径点;direction 为 AD_CLOCKWISE/AD_COUNTERCLOCKWISE(0 时取
DCPATH_CLOCKWISE);lines 决定收尾方式:
| lines | 语义 | 收尾 |
|---|---|---|
| 0 | Arc(纯弧) | 无 |
-1 (GdiTypeArcTo) |
ArcTo | 仅更新当前点(若 newStroke 先写 MOVETO) |
1 (GdiTypeChord) |
Chord(弦) | IntGdiCloseFigure 闭合弦 |
2 (GdiTypePie) |
Pie(扇形) | 加中心点 `PT_LINETO |
流程:
- 锁路径;
x1==x2 || y1==y2(零宽/零高)直接返回 TRUE。 INTERNAL_LPTODP转换矩形与起止点,well-ordered 矩形(手动交换保证左上/右下)。PATH_NormalizePoint求起止归一化坐标 →angleStart/angleEnd = atan2(y,x)。- 角度校正 :顺时针要求
angleEnd落在angleStart之后(angleEnd <= angleStart,否则+= 2π);逆时针相反(>=,否则−= 2π)------保证扫角方向
与 direction 一致。 GM_COMPATIBLE下corners[1].x--/y--(不包含右/下边缘)。- arcto 且
newStroke:写 PT_MOVETO(当前点)开新子路径。 - 逐象限循环 (do-while,每段 ≤ 90°):首段起角 = angleStart,终角 = 下一个
象限边界(顺时针(floor(angleStart/π/2)+1)·π/2,逆时针(ceil−1)·π/2);后续
段每次 ±π/2;最后一段被 angleEnd 截断 (end = TRUE)。每段调
PATH_DoArcPart(..., start ? (arcto ? PT_LINETO : PT_MOVETO) : FALSE)------
即整条弧的首条记录为 MOVETO(arcto 时为 LINETO,因前面已有 MOVETO)。 - 收尾:arcto →
update_current_pos;chord →IntGdiCloseFigure;pie →
PATH_AddEntry(centre, PT_LINETO | PT_CLOSEFIGURE)(中心点连线并闭合)。
注意事项 :源码注释 FIXME------未校验所有错误返回、未尊重 newStroke(非 arcto
分支)。
4.8 PATH_PolyBezier(PDC dc, const POINT *pts, DWORD cbPoints) --- 整段贝塞尔(含起点)
c
BOOL FASTCALL PATH_PolyBezier (PDC dc, const POINT *pts, DWORD cbPoints);
add_log_points(dc, pPath, pts, cbPoints, PT_BEZIERTO, FALSE)(cbPoints 须为
3 的倍数:每 3 个点为一段贝塞尔的 3 个控制点,起点取上一记录)→
type[0] = PT_MOVETO(首点强制成为子路径起点 )→ 解锁。与 "To" 版本不同,
PolyBezier 的起点由输入 pts0 直接充当 MOVETO,不依赖 DC 当前点。
4.9 PATH_PolyBezierTo(PDC dc, const POINT *pts, DWORD cbPoints) --- 续接贝塞尔
add_log_points_new_stroke(dc, pPath, pts, cbPoints, PT_BEZIERTO, TRUE):
若 newStroke 先补 PT_MOVETO(当前点),再批量追加;bExtraPt=TRUE 时首点
覆盖为新笔画起点。由 line.c 的 IntGdiPolyBezierTo 在 PATH_IsPathOpen 时
调用(LineTo 同理经 IntGdiLineTo)。
4.10 PATH_PolyDraw(PDC dc, const POINT *pts, const BYTE *types, DWORD cbPoints) --- 混合绘制
作用 :PolyDraw 允许一次调用混合 MOVETO/LINETO/BEZIERTO(可带 CLOSEFIGURE),
按 types 数组逐点解释。
流程 :锁路径 → 要求 state == PATH_Open (否则 FALSE)→ 扫描已有记录找
lastmove(最后一个 PT_MOVETO 的索引)并保存 orig_pos、DC 当前点 cur_pos
→ 逐点 switch:
PT_MOVETO:newStroke = TRUE;pos = pts[i](转设备坐标);lastmove = numEntriesUsed。PT_LINETO/PT_LINETO|PT_CLOSEFIGURE:add_log_points_new_stroke(1 点, PT_LINETO, FALSE)。PT_BEZIERTO:须后续两点也是 BEZIERTO ((types[i+2] & ~PT_CLOSEFIGURE)
校验),一次add_log_points_new_stroke(3 点, PT_BEZIERTO, FALSE),i += 2;
否则视为非法落到 default。- default(非法类型/孤立的 BEZIERTO):回滚 ------
pos = orig_pos,
IntGdiMoveToEx(dc, cur_pos, ...)恢复 DC 当前点,返回 FALSE。
每步处理后若 types[i] & PT_CLOSEFIGURE:close_figure(pPath)(末点闭合)且
pos = pPoints[lastmove](当前点跳回子路径起点,后续 LINETO 从起点续接)。
4.11 PATH_PolylineTo(PDC dc, const POINT *pts, DWORD cbPoints) --- 续接折线
cbPoints < 1 → FALSE;若 newStroke 则 cbPoints--(首个点由 start_new_stroke
的 MOVETO 承担/覆盖);add_log_points_new_stroke(..., PT_LINETO, TRUE)。
由 line.c 的 IntGdiPolylineTo 在路径打开时调用。
4.12 PATH_PolyPolygon(PDC dc, const POINT* pts, const INT* counts, UINT polygons) --- 多多边形
流程 :校验每个 counts[poly] >= 2(<2 直接 FALSE)→ 汇总总点数 → 一次性
add_log_points(..., PT_LINETO, FALSE) 写入全部点 → 遍历每个多边形:type[0] = PT_MOVETO(首点开子路径)、type[counts[poly]-1] = PT_LINETO | PT_CLOSEFIGURE
(末点闭合)→ 解锁。即一个 PolyPolygon 产生 polygons 条闭合子路径。
4.13 PATH_PolyPolyline(PDC dc, const POINT* pts, const DWORD* counts, DWORD polylines) --- 多折线
逐点多边形循环:CoordLPtoDP 转设备坐标,PATH_AddEntry(pt, (point==0) ? PT_MOVETO : PT_LINETO)------每个子折线首点 MOVETO、其余 LINETO,不闭合。
4.14 PATH_ExtTextOut(...) 与字形轮廓(PATH_add_outline/PATH_BezierTo)
c
BOOL FASTCALL PATH_ExtTextOut(PDC dc, INT x, INT y, UINT flags, const RECTL *lprc,
LPCWSTR str, UINT count, const INT *dx);
作用 :BeginPath 下 TextOut/ExtTextOut(freetype.c 在 PATH_IsPathOpen 时
转发到本函数)把每个字符的原生字形轮廓 折成路径记录------这是"文字路径化"
(Path 上写字、字转区域)的实现。
流程:
- 锁路径;
state != PATH_Open→ FALSE;count == 0→ TRUE。 ETO_GLYPH_INDEX时ggo_flags |= GGO_GLYPH_INDEX。- 逐字符:
ftGdiGetGlyphOutline(dc, ch, GGO_NATIVE, &gm, 0, NULL, &identity, TRUE)先查询轮廓大小:- 返回
GDI_ERROR(位图字体无原生轮廓)→ continue (注释:对位图字体
ExtTextOut 不应失败,只是生成空路径;参考 test_emf_ExtTextOut_on_path)。 dwSize > 0→ 分配缓冲、再取轮廓,PATH_add_outline(dc, pPath, x+offset.x, y+offset.y, outline, dwSize)追加到路径。
- 返回
- 步进:
dx非空时按 dx 累加(ETO_PDY时 dx 成对、y 也推进),否则按
gm.gmCellIncX/gmCellIncY前进。
PATH_add_outline(PDC dc, PPATH pPath, INT x, INT y, TTPOLYGONHEADER *header, DWORD size)(static):
- 遍历
TTPOLYGONHEADER多边形块(校验dwType == TT_POLYGON_TYPE); - 每多边形首点
PT_MOVETO(x + int_from_fixed(pfxStart.x),y 取反 ------
FreeType 轮廓 y 轴向上,GDI 向下); - 遍历
TTPOLYCURVE:TT_PRIM_LINE:逐点 PT_LINETO;TT_PRIM_QSPLINE/TT_PRIM_CSPLINE:把"曲线起点(前一 POINTFX)+ 各控制点"
收集成点数组交PATH_BezierTo;
- 结束统一
IntGdiCloseFigure(每个字形轮廓自闭合)。
PATH_BezierTo(PPATH pPath, POINT *lppt, INT n) (static):把 n 个点构成的
任意阶样条折成三次贝塞尔序列:n2 → LINETO;n3 → 3 点 BEZIERTO;n>3 →
中点细分法(相邻控制点取中点切分,pt[2] = (lppt[i+1]+lppt[i+2])/2),逐段
add_points(3, PT_BEZIERTO)。
int_from_fixed(FIXED f) (static inline):f.fract >= 0x8000 ? f.value+1 : f.value------FIXED 定点数四舍五入取整。
4.15 记录示例:一次典型路径的数组内容
以 MM_TEXT 模式(设备坐标 == 逻辑坐标,免去 LP→DP 换算)为例,应用执行:
c
BeginPath(hdc);
MoveToEx(hdc, 10, 10, NULL);
LineTo(hdc, 100, 10);
PolyBezierTo(hdc, bez_pts /* 3 点 */, 3); // 续接一段三次贝塞尔
CloseFigure(hdc);
MoveToEx(hdc, 200, 200, NULL);
Ellipse(hdc, 200, 200, 300, 260);
EndPath(hdc);
记录过程中 PATH 数组逐步演化(numEntriesUsed、newStroke、pos 关键状态):
# 操作 pPoints 追加 pFlags 追加 newStroke pos
0 BeginPath --- --- TRUE DC 当前点
1 MoveToEx(10,10) --- --- TRUE (10,10)
2 LineTo(100,10) ① (10,10) PT_MOVETO FALSE (10,10)
② (100,10) PT_LINETO FALSE (100,10)
3 PolyBezierTo(3 点) ③ (bx1,by1) PT_BEZIERTO FALSE (bx3,by3)
④ (bx2,by2) PT_BEZIERTO
⑤ (bx3,by3) PT_BEZIERTO
4 CloseFigure --- [⑤] |= PT_CLOSEFIGURE TRUE (bx3,by3)
5 MoveToEx(200,200) --- --- TRUE (200,200)
6 Ellipse(200,200,300,260) ⑥..⑱ 13 个椭圆点 ⑥ PT_MOVETO TRUE (末点)
⑦..⑱ PT_BEZIERTO
[⑱] |= PT_CLOSEFIGURE
7 EndPath state: Open → Closed;flPath &= ~DCPATH_ACTIVE
最终数组形态(共 18 条记录,两条子路径):
i 0 1 2 3 4 5 6 7 ... 17
pt (10,10) (100,10) b1 b2 b3 (200,200) e1 ... e12
fl MOVETO LINETO BZ BZ BZ|CF MOVETO BZ ... BZ|CF
随后 StrokePath/FillPath 把该数组作为输入:子路径 1 是"直线 + 贝塞尔 + 闭合"
的开口图形(闭合由 CLOSEFIGURE 回填),子路径 2 是闭合椭圆。宽化时
IntGdiWidenPath 对每条子路径独立生成轮廓;填充时 PATH_PathToRegion 按
PT_MOVETO 切分出 2 个多边形。
5. 输出层(EndPath 之后)
5.1 PATH_AddFlatBezier(PPATH pPath, POINT *pt, BOOL closed) --- 贝塞尔折线化
c
BOOL FASTCALL PATH_AddFlatBezier (PPATH pPath, POINT *pt, BOOL closed);
贝塞尔转折线算法 :调用 GDI_Bezier(pt, 4, &no)(bezier.c)把以 pt[0] 为
起点、pt[1..3] 为控制点/终点的一段三次贝塞尔离散成 no 个折线点
(GDI_Bezier 内部递归细分直到平坦度阈值,输出数组含起点,TAG_BEZIER 池)→
从 i=1 起逐个 PATH_AddEntry(pts[i], PT_LINETO)(末点且 closed 时
PT_LINETO | PT_CLOSEFIGURE)→ ExFreePoolWithTag(pts, TAG_BEZIER)。
关键点 :贝塞尔段在路径中的起点是上一记录 (pt[0] = pPoints[srcpt-1]),
因此只把折线点从 i=1 写入;闭合标志取自该贝塞尔第 3 个控制点的
PT_CLOSEFIGURE 位(见 5.2)。
5.2 PATH_FlattenPath(PPATH pPath) --- 整路径转折线
c
PPATH FASTCALL PATH_FlattenPath (PPATH pPath);
流程 :PATH_CreatePath(pPath->numEntriesUsed) 建新路径 → 遍历原路径每条
记录(flag & ~PT_CLOSEFIGURE 分派):
PT_MOVETO/PT_LINETO:原样PATH_AddEntry(含原标志,保留 CLOSEFIGURE);PT_BEZIERTO:以pPoints[srcpt-1](上一记录)为起点、
closed = pFlags[srcpt+2] & PT_CLOSEFIGURE(本贝塞尔最后控制点的闭合位)调
PATH_AddFlatBezier,然后srcpt += 2跳过本段其余两个控制点;- 任何失败:解锁并
PATH_Delete新路径、返回 NULL。
结束后 newPath->state = pPath->state。返回的新路径已加锁且处于与源相同的
状态(Closed 路径平化后仍 Closed);源路径不变。
使用 :NtGdiFlattenPath(平化后替换 DC 路径)、NtGdiFillPath/StrokePath/ StrokeAndFillPath/PathToRegion(先平化再输出)、IntGdiWidenPath(宽化前必须
平化------轮廓计算只处理折线)。
5.3 PATH_PathToRegion(PPATH pPath, INT Mode, PREGION Rgn) --- 路径→区域
c
BOOL FASTCALL PATH_PathToRegion (PPATH pPath, INT nPolyFillMode, PREGION Rgn);
流程:
numEntriesUsed == 0→ FALSE。- 分配
counts数组(numEntriesUsed/2个 ULONG,足够容纳子路径数)------
pFlags[0]必须为 PT_MOVETO(ASSERT)。 - 扫描记录,遇到每个 PT_MOVETO 就把上一段长度记入
counts[polygons++]
(段长 = 当前 MOVETO 索引 − 上一段起点),最后补尾段(i > pos+1时)。 REGION_SetPolyPolygonRgn(Rgn, pPath->pPoints, counts, polygons, Mode)
(region.c)按Mode(ALTERNATE/WINDING 填充模式)把多边形组构造成区域;
失败打印 ERR。- 释放 counts,返回结果。路径本身不变。
注意 :counts 数组大小按 numEntriesUsed/2 分配(子路径数 ≤ 条目数/2),
有 ASSERT 校验。
5.4 PATH_FillPath / PATH_FillPathEx --- 填充
c
BOOL FASTCALL PATH_FillPath( PDC dc, PPATH pPath ); /* = FillPathEx(dc,pPath,NULL) */
BOOL FASTCALL PATH_FillPathEx(PDC dc, PPATH pPath, PBRUSH pbrFill);
流程(FillPathEx):
IntSysCreateRectpRgn(0,0,0,0)建临时区域(失败 → ERROR_NOT_ENOUGH_MEMORY)。PATH_PathToRegion(pPath, pdcattr->jFillMode, Rgn)------按 DC 填充模式
(ALTERNATE/WINDING)区域化;失败删除区域返回 FALSE。- 坐标基准切换 (关键技巧):路径点已是设备坐标 ,而
IntGdiFillRgn
按逻辑坐标 解释区域,故临时:- 保存
iMapMode、viewport/window 的 ext 与 org、世界变换(MatrixS2XForm); IntGdiSetMapMode(dc, MM_TEXT)、viewport/window org 清零;- 置
GM_ADVANCED后GreModifyWorldTransform(dc, &xform, MWT_IDENTITY)
把世界变换临时置为单位矩阵(注释:不能只靠 GM_COMPATIBLE 重置,实测不生效)。
- 保存
IntGdiFillRgn(dc, Rgn, pbrFill)------实际填充(bitblt.c,《分析_17》;
pbrFill 为 NULL 时用 DC 当前画刷)。- 按保存值逐一恢复映射模式、ext/org、世界变换(同样借助 GM_ADVANCED +
MWT_SET)。
PATH_FillPath 只是 FillPathEx(dc, pPath, NULL) 的薄封装(用 DC 画刷)。
源码注释带调侃:"You can play with this as long as you like, but if you break
Area.exe the purge will Begain on Path!!!"(Area.exe 是依赖 FillPath 的测试程序)。
5.5 PATH_StrokePath(DC *dc, PPATH pPath) --- 描边(完整流程)
c
BOOL FASTCALL PATH_StrokePath(DC *dc, PPATH pPath);
总流程分两大分支:
PATH_StrokePath(dc, pPath)
├─ 取 pbrLine = dc->dclevel.pbrLine
├─ IntIsEffectiveWidePen(pbrLine) ?(宽笔:宽度 > 1 或几何笔)
│ ├─ pNewPath = PATH_WidenPathEx(dc, pPath) // 轮廓化(5.6/5.7)
│ ├─ 临时 jFillMode = WINDING
│ ├─ PATH_FillPathEx(dc, pNewPath, pbrLine) // 用笔刷填充轮廓
│ ├─ 恢复 jFillMode;PATH_Delete(pNewPath);返回 TRUE
│ └─ (WidenPathEx 失败则回落细笔分支)
└─ 细笔分支(cosmetic/单位宽笔):
├─ 保存映射模式/viewport/window ext+org/世界变换
├─ 置 MM_TEXT + org 清零 + GM_ADVANCED + MWT_IDENTITY(理由同 FillPathEx)
├─ 分配折线缓冲 nAlloc = numEntriesUsed + 1 + 300(含贝塞尔展开余量)
├─ 逐条记录分派:
│ ├─ 校验:i==0 或上一记录 CLOSEFIGURE 后必须 PT_MOVETO
│ ├─ PT_MOVETO → 若 nLinePts>=2 先 IntGdiPolyline 刷出上一段;开新段
│ ├─ PT_LINETO → 追加到缓冲
│ ├─ PT_BEZIERTO → 校验连续 3 条;GDI_Bezier(pPoints+i-1, 4, &nBzrPts)
│ │ 折线化;按 nMinAlloc 计算必要时扩容(×2);
│ │ 拷入 pBzrPts[1..nBzrPts-1];i += 2
│ └─ default → 错误退出
├─ 遇 PT_CLOSEFIGURE → 追加 pLinePts[0](闭合回起点)
├─ 循环结束 nLinePts>=2 → IntGdiPolyline(dc, pLinePts, nLinePts)
└─ 恢复映射模式/世界变换;若移动过当前点,IntGetCurrentPositionEx +
IntDPtoLP + IntGdiMoveToEx 修正 DC 当前点(描边会推进当前位置)
细笔分支的输出对象 :IntGdiPolyline(line.c)→ 非空表面时经
IntLPtoDP + DC 原点偏移后,细笔走 IntEngPolyline → IntEngLineTo
(《分析_5》的线光栅化);若此时判定宽笔则会在 IntGdiPolyline 内递归 建临时
路径再走宽笔分支------但在 PATH_StrokePath 入口已排除宽笔,正常不会发生。
错误处理 :路径标志非法(如 CLOSEFIGURE 后非 MOVETO、贝塞尔不足 3 条、未知
标志)→ ERR + 释放缓冲 + 恢复状态 + 返回 FALSE。缓冲不足时按
nMinAlloc = nLinePts + (numEntriesUsed − i) + nBzrPts 预估并 ×2 重分配。
5.6 IntGdiWidenPath(PPATH pPath, UINT penWidth, UINT penStyle, FLOAT eMiterLimit) --- 宽化核心
c
PPATH FASTCALL IntGdiWidenPath(PPATH pPath, UINT penWidth, UINT penStyle, FLOAT eMiterLimit);
作用 :把闭合路径按"笔宽 + 端帽样式 + 连接样式"轮廓化 成一条新闭合路径
(该轮廓填充后即得宽笔描边效果)。返回的新路径 state = PATH_Closed 且已加锁;
失败返回 NULL。
流程:
- 提取
endcap = PS_ENDCAP_MASK & penStyle、joint = PS_JOIN_MASK & penStyle。 PATH_FlattenPath(pPath)平化(宽化只处理折线,贝塞尔必须先行离散)。- 笔宽内外分配:
penWidthIn = penWidth/2; penWidthOut = penWidth/2;若
In+Out < penWidth则Out++(奇数宽时外侧多 1 像素,保证总宽达标)。 - 切分子路径 :遍历平化路径,遇 PT_MOVETO 开新 stroke(
pStrokes数组逐次
扩容重分配 + memcpy 迁移;上一 stroke 置PATH_Closed,新 stroke 用
PATH_InitGdiPath+state=PATH_Open初始化,PT_LINETO 原样拷入;PT_BEZIERTO
理论上不应出现------已平化,命中打印 ERR)。 pNewPath = PATH_CreatePath(flat_path->numEntriesUsed)作为轮廓输出路径;
KeSaveFloatingPointState(&fpsave)保存浮点状态(后面大量 atan2/cos/sin 需要
FPU,且内核默认关闭浮点)。- 逐 stroke 轮廓化 :每个 stroke 建两条临时路径
pUpPath/pDownPath
(Init + Open),按法线偏移 生成上下两条边:- 端点(未闭合的 j0 或 jn-1) :取段角
theta = atan2(ya−yo, xa−xo)(j0 用点 0→1,jn-1 用点 n-2→n-1),按
endcap 处理:PS_ENDCAP_SQUARE:向"后"延伸半宽(cap_ext = −halfWidth·(cosθ,sinθ))
后,沿法线 ± 偏移各记一点(penWidthOut侧与penWidthIn侧)------方形帽;PS_ENDCAP_FLAT:仅沿法线 ± 偏移各记一点------平帽;PS_ENDCAP_ROUND(缺省):以端点为中心、penWidthIn/Out为半径的框,
用 4 段 45° 的PATH_DoArcPart(θ+π/2 → θ+3π/2)拼出半圆帽。
- 内部拐点(j>0 且 j<n-1,或闭合路径首尾) :取前驱/后继点,算入段角
theta = atan2(yo−ya, xo−xa)与转角
alpha = atan2(yb−yo, xb−xo) − theta(调整到 (−π, π]:>0 减 π,否则加 π);alpha == 0(共线)→ 跳过该点;- 若
PS_JOIN_MITER且eMiterLimit < |1/sin(alpha/2)|→ 降级为 BEVEL
(miter 尖角过长时按米特极限截断); alpha > 0:内侧=pUpPath、外侧=pDownPath;alpha < 0反之;- 内侧点 :沿入段法线偏移
penWidthIn一点 + 沿出段法线
(θ+α)偏移一点 → 2 个 PT_LINETO; - 外侧点 按 joint 三态:
PS_JOIN_MITER:miterWidth = |penWidthOut / cos(π/2 − |α|/2)|,
顶点 =xo + miterWidth·(cos(θ+α/2), sin(θ+α/2))→ 1 个 PT_LINETO
(尖角);PS_JOIN_BEVEL:入段法线偏移点 + 出段法线偏移点 → 2 个 PT_LINETO
(切角);PS_JOIN_ROUND(缺省):入段法线点、角平分线方向点
(θ+α/2)、出段法线点 → 3 个 PT_BEZIERTO (圆弧连接,保留贝塞尔
标志)。
- 开路径单段特例 (未闭合且正好 2 点,j==0):显式补上/下边缘点
(pPoints[1] ± penWidthOut/In·法线)到 pUpPath/pDownPath------没有内部拐点
来产生线段主体,必须靠端帽之间的这两点连接。
- 端点(未闭合的 j0 或 jn-1) :取段角
- 合并轮廓 :pUpPath 全部点按原标志拷入 pNewPath(k0 强制 PT_MOVETO);
pDownPath 先reverse_points再逆序拷入(k0 且原 stroke 闭合时
PT_MOVETO)------上下边反向串接成一条闭合轮廓。 - 清理:
PATH_DestroyGdiPath+ExFreePoolWithTag释放每个 stroke、
pUpPath、pDownPath。 - 收尾:
pNewPath->state = PATH_Closed、PATH_UnlockPath(pNewPath)、
KeRestoreFloatingPointState;释放pStrokes数组;删除平化路径
(先 Unlock 再PATH_Delete(flat_path->BaseObject.hHmgr))。
数学基础 :轮廓 = 骨架折线沿法线方向(θ+π/2)平移 ±penWidth/2 后,在拐角处
用 miter(角平分线延长)/bevel(切角)/round(贝塞尔圆弧)三种连接方式、端点处用
square/flat/round 三种端帽闭合。eMiterLimit 即 SetMiterLimit 设置的米特极限
(存于 dc->dclevel.laPath.eMiterLimit)。
5.7 PATH_WidenPath / PATH_WidenPathEx --- 宽化入口
c
static PPATH FASTCALL PATH_WidenPath(DC *dc); /* NtGdiWidenPath 用 */
PPATH FASTCALL PATH_WidenPathEx(DC *dc, PPATH pPath); /* StrokePath 宽笔分支用 */
PATH_WidenPathEx(dc, pPath) 流程:
pPath->state != PATH_Closed→ERROR_CAN_NOT_COMPLETE返回 NULL。GreGetObject(pdcattr->hpen, 0, NULL)查当前画笔对象大小(0 表示失败)。- 分配 EXTLOGPEN 缓冲并
GreGetObject取回;GDI_HANDLE_GET_TYPE(hpen)分派:GDI_OBJECT_TYPE_PEN:penStyle = ((LOGPEN*)elp)->lopnStyle;GDI_OBJECT_TYPE_EXTPEN:penStyle = elp->elpPenStyle;- 其他类型 →
ERROR_CAN_NOT_COMPLETE。
penWidth = elp->elpWidth;释放缓冲。- 化妆品笔拒绝 :EXTPEN 且
(PS_TYPE_MASK & penStyle) == PS_COSMETIC→
ERROR_CAN_NOT_COMPLETE(化妆品笔宽度恒为 1,宽化无意义)。 return IntGdiWidenPath(pPath, penWidth, penStyle, dc->dclevel.laPath.eMiterLimit)。
PATH_WidenPath(dc) :锁 DC 路径 → PATH_WidenPathEx(dc, pPath) → 解锁 →
返回新路径(NtGdiWidenPath 据此替换 DC 路径句柄)。
5.8 输出管线对照与错误码汇总
| 输出操作 | 预处理 | 核心路径 | 输出对象 | 消费后路径 |
|---|---|---|---|---|
FillPath |
FlattenPath | PathToRegion → FillPathEx → IntGdiFillRgn | 表面(画刷) | 删除 |
StrokePath(细笔) |
FlattenPath | 折线缓冲 → IntGdiPolyline | 表面(笔刷/线) | 删除 |
StrokePath(宽笔) |
FlattenPath | WidenPathEx → FillPathEx(WINDING) | 表面(笔刷) | 删除 |
StrokeAndFillPath |
FlattenPath | FillPath 成功后再 StrokePath | 表面 | 删除 |
WidenPath |
---(内部 Flatten) | WidenPathEx → IntGdiWidenPath | 新 PATH(替换 DC) | 原路径删除 |
FlattenPath |
--- | FlattenPath → 新 PATH | 新 PATH(替换 DC) | 原路径删除 |
PathToRegion |
FlattenPath | PathToRegion → REGION_SetPolyPolygonRgn | HRGN(交应用) | 删除 |
GetPath |
--- | memcpy + GdiPathDPtoLP | 用户缓冲 | 保留 |
错误码使用一览 (EngSetLastError):
| 错误码 | 触发场景 |
|---|---|
ERROR_NOT_ENOUGH_MEMORY |
PATH_CreatePath/FillPathEx/PATH_StrokePath 分配失败 |
ERROR_CAN_NOT_COMPLETE |
EndPath 状态错、CloseFigure 非 Open、WidenPathEx 非 Closed/化妆品笔/笔类型错、GetPath/PathToRegion 非 Closed |
ERROR_INVALID_HANDLE |
入口处 DC_LockDc 失败(部分入口用 INVALID_PARAMETER) |
ERROR_INVALID_PARAMETER |
GetPath 缓冲过小、DC 锁失败等 |
ERROR_OUTOFMEMORY |
WidenPathEx 的 elp 分配失败 |
ERROR_ARITHMETIC_OVERFLOW |
GetPath 的 DP→LP 转换溢出(27/32 位限制) |
6. NtGdi 系统调用入口(13 个)
path.c 共导出 13 个 NtGdi* 入口 (与《分析_31》第 2 节统计一致),全部遵循
统一模式:DC_LockDc(hDC)(失败 EngSetLastError(ERROR_INVALID_HANDLE/ ERROR_INVALID_PARAMETER))→ PATH_LockPath(dc->dclevel.hPath) → 状态判定 →
调内部 PATH_* 函数 → 逐级解锁。下表先总览,随后逐一展开:
| # | 入口 | 功能 | 状态要求 |
|---|---|---|---|
| 1 | NtGdiBeginPath |
开启路径记录 | --- |
| 2 | NtGdiEndPath |
结束记录(→ PATH_Closed) | PATH_Open + DCPATH_ACTIVE |
| 3 | NtGdiAbortPath |
放弃并删除路径 | --- |
| 4 | NtGdiCloseFigure |
闭合当前图(置 CLOSEFIGURE 位) | PATH_Open |
| 5 | NtGdiFillPath |
填充路径(平化→区域→IntGdiFillRgn) | PATH_Closed |
| 6 | NtGdiStrokePath |
描边路径(宽笔轮廓化/细笔折线) | PATH_Closed |
| 7 | NtGdiStrokeAndFillPath |
先填充再描边 | PATH_Closed |
| 8 | NtGdiWidenPath |
宽化路径并替换 DC 路径 | PATH_Closed |
| 9 | NtGdiFlattenPath |
平化路径并替换 DC 路径 | PATH_Closed |
| 10 | NtGdiPathToRegion |
路径转区域(返回 HRGN) | PATH_Closed |
| 11 | NtGdiGetPath |
取路径点+类型(设备→逻辑) | PATH_Closed |
| 12 | NtGdiGetMiterLimit |
取米特极限 | --- |
| 13 | NtGdiSetMiterLimit |
设米特极限 | --- |
6.1 NtGdiBeginPath(HDC hDC)
DC_LockDc;若DCPATH_ACTIVE且非DCPATH_SAVE(路径已开且不是 SaveDC
副本)→ 直接返回 TRUE(幂等,重复 BeginPath 无副作用)。- 若已有旧路径:非 SAVE →
PATH_Delete删除旧句柄;SAVE → 清
DCPATH_SAVE|DCPATH_ACTIVE并把hPath置 NULL(不动被保存的路径对象)。 PATH_CreatePath(NUM_ENTRIES_INITIAL=16)建新路径 → 立刻 置
DCPATH_ACTIVE并挂句柄("Set active ASAP!")→IntGetCurrentPositionEx+
IntLPtoDP把 DC 当前点写入pPath->pos(作为首个 stroke 的起点候选)→
解锁、返回 TRUE。
6.2 NtGdiEndPath(HDC hDC)
锁路径后判定:pPath->state != PATH_Open || !(flPath & DCPATH_ACTIVE) → 打印
TRACE、ERROR_CAN_NOT_COMPLETE、返回 FALSE;否则 state = PATH_Closed、
flPath &= ~DCPATH_ACTIVE、返回 TRUE。EndPath 是唯一把路径从 Open 变 Closed
的入口(宽化/平化的临时路径由内部直接置 Closed)。
6.3 NtGdiAbortPath(HDC hDC)
dc->dclevel.hPath 为空 → 直接返回 TRUE(无路径可放弃);否则
PATH_Delete(dc->dclevel.hPath)(失败返回 FALSE)→ hPath = 0、
flPath &= ~DCPATH_ACTIVE → TRUE。AbortPath 不要求路径处于任何特定状态 ,
记录中或已闭合的路径都可直接丢弃。
6.4 NtGdiCloseFigure(HDC hDC)
锁 DC → PATH_LockPath → 仅当 pPath->state == PATH_Open 时调
IntGdiCloseFigure(pPath)(末条记录 |= PT_CLOSEFIGURE、newStroke = TRUE)
并返回 TRUE;否则 ERROR_CAN_NOT_COMPLETE 返回 FALSE。注意 :CloseFigure
只是"标记"闭合------并不真正往路径里补一条回到起点的线段,闭合线由输出阶段
(StrokePath 的 pLinePts[0] 回填 / FillPath 的区域构造)隐式完成。
6.5 NtGdiFlattenPath(HDC hDC)
锁 DC → 锁路径 → 仅当 state == PATH_Closed 时 PATH_FlattenPath(pPath)
(否则 pNewPath 保持 NULL)→ 解锁原路径 → 若成功:PATH_Delete(hPath) 删除原
路径、hPath = pNewPath->BaseObject.hHmgr 换平化路径、解锁新路径、返回 TRUE;
否则 FALSE。平化是"就地替换":DC 路径句柄指向新的折线路径,原路径被删除。
6.6 NtGdiGetMiterLimit(HDC hdc, PDWORD pdwOut)
DC_LockDc → _SEH2_TRY + ProbeForWrite(pdwOut, sizeof(DWORD), 1) 探测用户
缓冲 → *pdwOut = pDc->dclevel.laPath.eMiterLimit(FLOAT 按 DWORD 位模式回传)
→ SEH 异常时 SetLastNtError 返回 FALSE → 解锁。注意 :返回值是
laPath.eMiterLimit 的位模式(调用方 gdi32 自己解释为 FLOAT)。
6.7 NtGdiSetMiterLimit(HDC hdc, DWORD dwNew, PDWORD pdwOut)
用 gxf_long worker(union)把 dwNew 的位模式存为 FLOAT:
pDc->dclevel.laPath.eMiterLimit = worker.f;pdwOut 非空时回写旧值
(worker1.f 转回 DWORD),同样 SEH 保护。该值随后被 PATH_WidenPathEx 传给
IntGdiWidenPath 用于 miter 极限判断。
6.8 NtGdiGetPath(HDC hDC, LPPOINT Points, LPBYTE Types, INT nSize)
两阶段(探测 → 拷贝):
_SEH2_TRY:ProbeForWrite(Points, nSize*sizeof(POINT), 4)与
ProbeForWrite(Types, nSize, 1)探测用户缓冲;异常返回 -1。- 锁 DC → 锁路径;
state != PATH_Closed→ERROR_CAN_NOT_COMPLETE跳 done。 - 三态:
nSize == 0:返回numEntriesUsed(应用先查大小再分配);nSize < numEntriesUsed:ERROR_INVALID_PARAMETER跳 done(缓冲太小);- 否则:
memcpy拷贝点与类型到用户缓冲 →GdiPathDPtoLP(dc, Points, numEntriesUsed)设备→逻辑 转换(失败ERROR_ARITHMETIC_OVERFLOW,
_SEH2_LEAVE保留部分结果)→ 返回numEntriesUsed。
- done:解锁路径与 DC,返回 ret(失败时 ret 保持 -1)。
返回值语义:-1 = 失败;≥0 = 拷贝的条目数(或 nSize==0 时所需条目数)。
6.9 NtGdiFillPath(HDC hDC)
流程 :锁 DC → 锁路径 → DC_vPrepareDCsForBlit 准备绘制(裁剪/鼠标隐藏合成,
见《分析_24》)→ 若 ulDirty_ & (DIRTY_LINE|DC_PEN_DIRTY) 则
DC_vUpdateLineBrush;若 DIRTY_FILL|DC_BRUSH_DIRTY 则 DC_vUpdateFillBrush
(确保画刷最新)→ PATH_FlattenPath(pPath) 平化:
- 平化路径
state != PATH_Closed(理论上不成立,源已 Closed)→
ERROR_CAN_NOT_COMPLETE; numEntriesUsed > 0→PATH_FillPath(dc, pNewPath)(区域化 + IntGdiFillRgn);- 空路径 → 直接 TRUE(无内容也算成功)。
随后双删 (平化路径 + 原路径)并 hPath = 0、清 DCPATH_ACTIVE、
DC_vFinishBlit、解锁,返回结果。FillPath 消费路径:调用后 DC 不再持有路径。
6.10 NtGdiStrokePath(HDC hDC)
与 6.9 同构,但只更新线画刷 (DIRTY_LINE|DC_PEN_DIRTY → DC_vUpdateLineBrush)
→ PATH_FlattenPath → state != PATH_Closed 报错;否则
PATH_StrokePath(dc, pNewPath)(宽笔→WidenPathEx 轮廓填充;细笔→IntGdiPolyline
折线)→ 双删、清路径、DC_vFinishBlit、解锁。
6.11 NtGdiStrokeAndFillPath(HDC hDC)
合并 6.9/6.10:更新填充与线两种画刷 → PATH_FlattenPath → 先
PATH_FillPath(pDc, pNewPath),成功后 再 PATH_StrokePath(pDc, pNewPath)
(同一份平化几何先填后描,保证轮廓贴合填充边界)→ 双删、清理、返回
bRet。空路径直接 TRUE。
6.12 NtGdiPathToRegion(HDC hDC) --- 返回 HRGN
流程 :锁 DC → 锁路径 → state != PATH_Closed → ERROR_CAN_NOT_COMPLETE
(路径保留,不删除)→ 否则:
REGION_AllocUserRgnWithHandle(1)分配带用户句柄 的区域(失败返回 NULL);
hrgnRval = Rgn->BaseObject.hHmgr。PATH_FlattenPath(pPath)平化(失败:删区域、返回 NULL)。PATH_PathToRegion(pNewPath, pdcattr->jFillMode, Rgn)------按 DC 填充模式区域化;
失败:删区域、hrgnRval = NULL;成功:REGION_UnlockRgn(Rgn)(把区域交给
用户)。- 双删路径、
hPath = NULL、清DCPATH_ACTIVE、解锁,返回hrgnRval。
与 FillPath 的区别 :PathToRegion 把路径几何物化为区域对象 返回给应用
(可继续用于 CombineRgn/SelectClipRgn/FrameRgn 等),而 FillPath 只是用画刷
填充一次即丢弃。
6.13 NtGdiWidenPath(HDC hDC)
锁 DC → PATH_WidenPath(pdc)(内部锁路径 + WidenPathEx)→ 成功则
PATH_Delete(pdc->dclevel.hPath) 删原路径、hPath = pPath->BaseObject.hHmgr
换上宽化轮廓路径 、返回 TRUE;失败返回 FALSE。宽化后路径保持 PATH_Closed,
应用可继续对轮廓路径 FillPath/GetPath------这是"粗笔画变成几何"的标准手法。
7. 调用链(mermaid)
7.1 路径记录链(BeginPath 期间图元如何进入路径)
渲染错误: Mermaid 渲染失败: Parse error on line 15: ...TH_AddEntry: pPointsn+pFlagsn] G -----------------------^ Expecting 'SQE', 'DOUBLECIRCLEEND', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'SQS'
7.2 输出链(EndPath 之后)
渲染错误: Mermaid 渲染失败: Parse error on line 20: ... St5[PATH_FillPathEx(WINDING, 笔刷) → IntG -----------------------^ Expecting 'SQE', 'DOUBLECIRCLEEND', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'PS'
7.3 宽笔描边内部链(PATH_StrokePath 宽笔分支展开)
渲染错误: Mermaid 渲染失败: Parse error on line 8: ...OVETO 切分子路径 pStrokes\[\]] C5 --> C6[每 -----------------------^ Expecting 'SQE', 'DOUBLECIRCLEEND', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'SQS'
8. 与《分析_18》《分析_31》及分册关系
| 项 | 说明 |
|---|---|
| 《分析_31》第 2 节 | 系统调用层总览:path.c 共 13 个 NtGdi* 入口(本文第 6 节逐一展开) |
| PATH 结构与 PATHOBJ | 《分析_18》(eng/pathobj.c):EXTPATHDATA 链表(ppdFirst/ppdLast/ppdCurrent)+ EPATHOBJ;本文 2.4/2.5 展开同一 PATH 的双表示 |
| 细笔描边输出 | 《分析_5》(eng/lineto.c 的 IntEngLineTo)------PATH_StrokePath 细笔分支经 IntGdiPolyline → IntEngPolyline 最终到达 |
| 填充输出 | 《分析_17》(bitblt.c 的 IntGdiFillRgn)------PATH_FillPathEx 的区域光栅化 |
| 裁剪合成 | 《分析_24》(MouseSafety/裁剪)------NtGdiFillPath/StrokePath 的 DC_vPrepareDCsForBlit |
| 贝塞尔折线化 | bezier.c 的 GDI_Bezier(TAG_BEZIER 池),path.c 的 PATH_AddFlatBezier/PATH_StrokePath 共用 |
| 画刷更新 | DC_vUpdateLineBrush/DC_vUpdateFillBrush(dirty 标志驱动),输出入口在绘制前调用 |
与《分析_18》的边界 :《分析_18》讲的是"面向驱动"的 PATHOBJ DDI 服务层
(eng/pathobj.c);本文讲的是"面向内核自身"的路径记录与输出层(ntgdi/path.c)。
两者共用同一个 PATH 结构(path.h),但数据面完全分离 :path.c 用
pPoints/pFlags 数组,pathobj.c 用 EXTPATHDATA 链表;全树无驱动调用后者。
9. 源码索引
| 文件 | 关键内容 |
|---|---|
| path.c(file:///d:/reactos/win32ss/gdi/ntgdi/path.c) | 本文全部函数:管理(CreatePath/DestroyGdiPath/Delete/Init/Assign/Save/Restore/Empty/AddEntry/ReserveEntries)、坐标(ScaleNormalizedPoint/NormalizePoint/CheckRect/GdiPathDPtoLP)、图元(MoveTo/LineTo/Rectangle/RoundRect/Ellipse/DoArcPart/Arc/PolyBezier(To)/PolyDraw/PolylineTo/PolyPolygon/PolyPolyline/ExtTextOut+add_outline+BezierTo)、输出(AddFlatBezier/FlattenPath/PathToRegion/FillPath(Ex)/StrokePath/IntGdiWidenPath/WidenPath(Ex))+ 13 个 NtGdi* 入口 |
| path.h(file:///d:/reactos/win32ss/gdi/ntgdi/path.h) | PATH 结构、DCPATH_* / GdiPathState / PATHTYPE_* / EXTPATHDATA / EPATHOBJ、PATH_* 声明与句柄宏(PATH_AllocPathWithHandle/PATH_LockPath/PATH_IsPathOpen) |
| eng/pathobj.c(file:///d:/reactos/win32ss/gdi/eng/pathobj.c) | PATHOBJ DDI 服务(EXTPATHDATA 链表的维护者,《分析_18》) |
| bezier.c(file:///d:/reactos/win32ss/gdi/ntgdi/bezier.c) | GDI_Bezier:三次贝塞尔递归细分折线化 |
| line.c(file:///d:/reactos/win32ss/gdi/ntgdi/line.c) | IntGdiPolyline/IntGdiPolyBezier(To)/IntGdiPolylineTo:路径打开时转发 PATH_*,否则光栅化(《分析_5》) |
| bitblt.c(file:///d:/reactos/win32ss/gdi/ntgdi/bitblt.c) | IntGdiFillRgn:区域填充(《分析_17》) |
| region.c(file:///d:/reactos/win32ss/gdi/ntgdi/region.c) | REGION_SetPolyPolygonRgn/REGION_AllocUserRgnWithHandle/IntSysCreateRectpRgn |
| coord.c(file:///d:/reactos/win32ss/gdi/ntgdi/coord.c) | IntGdiSetMapMode/GreModifyWorldTransform/MatrixS2XForm(FillPathEx/StrokePath 的临时 MM_TEXT 技巧) |
| wingdi.h(file:///d:/reactos/sdk/include/psdk/wingdi.h) | PT_MOVETO(6)/PT_LINETO(2)/PT_BEZIERTO(4)/PT_CLOSEFIGURE(1)、PS_ENDCAP_/PS_JOIN_ 掩码、GGO_NATIVE 等 |
关联模块 :arc.c(IntGdiArcInternal 转发 PATH_Arc)、fillshap.c
(Ellipse/RoundRect 转发 PATH_Ellipse/RoundRect)、freetype.c
(ftGdiGetGlyphOutline + ExtTextOut 转发 PATH_ExtTextOut)、dcobjs.c
(SaveDC/RestoreDC 调 PATH_SavePath/RestorePath)、pen.c(画笔对象,宽化取
EXTLOGPEN)。
9.1 path.c 函数行号索引(2026-08 快照)
| 函数 | 行号 | 函数 | 行号 |
|---|---|---|---|
PATH_CreatePath |
L34-L72 | PATH_DestroyGdiPath |
L78-L86 |
PATH_Delete |
L88-L103 | IntGdiCloseFigure |
L106-L119 |
GdiPathDPtoLP |
L123-L134 | PATH_InitGdiPath |
L140-L152 |
PATH_AssignGdiPath |
L164-L185 | PATH_SavePath |
L187-L207 |
PATH_RestorePath |
L209-L238 | PATH_EmptyPath |
L244-L252 |
PATH_AddEntry |
L260-L286 | PATH_ReserveEntries |
L294-L352 |
PATH_ScaleNormalizedPoint |
L361-L392 | PATH_NormalizePoint |
L399-L425 |
PATH_CheckRect |
L431-L459 | add_log_points |
L463-L486 |
add_points |
L490-L501 | reverse_points |
L504-L513 |
start_new_stroke |
L516-L526 | update_current_pos |
L529-L533 |
close_figure |
L536-L540 | add_log_points_new_stroke |
L543-L554 |
PATH_MoveTo |
L562-L582 | PATH_LineTo |
L591-L628 |
PATH_Rectangle |
L635-L676 | PATH_RoundRect |
L684-L786 |
PATH_Ellipse |
L791-L887 | PATH_DoArcPart |
L898-L951 |
PATH_Arc |
L963-L1136 | PATH_PolyBezierTo |
L1138-L1159 |
PATH_PolyBezier |
L1161-L1185 | PATH_PolyDraw |
L1187-L1269 |
PATH_PolylineTo |
L1271-L1296 | PATH_PolyPolygon |
L1298-L1346 |
PATH_PolyPolyline |
L1348-L1383 | PATH_AddFlatBezier |
L1388-L1410 |
PATH_FlattenPath |
L1417-L1453 | PATH_PathToRegion |
L1460-L1509 |
PATH_FillPath |
L1516-L1523 | PATH_FillPathEx |
L1525-L1606 |
PATH_StrokePath |
L1608-L1797 | IntGdiWidenPath |
L1801-L2174 |
PATH_WidenPath |
L2176-L2193 | PATH_WidenPathEx |
L2195-L2260 |
int_from_fixed |
L2262-L2265 | PATH_BezierTo |
L2272-L2314 |
PATH_add_outline |
L2316-L2409 | PATH_ExtTextOut |
L2414-L2511 |
NtGdiAbortPath |
L2518-L2546 | NtGdiBeginPath |
L2548-L2602 |
NtGdiCloseFigure |
L2604-L2641 | NtGdiEndPath |
L2643-L2683 |
NtGdiFillPath |
L2685-L2741 | NtGdiFlattenPath |
L2743-L2785 |
NtGdiGetMiterLimit |
L2787-L2818 | NtGdiGetPath |
L2820-L2903 |
NtGdiPathToRegion |
L2905-L2987 | NtGdiSetMiterLimit |
L2989-L3027 |
NtGdiStrokeAndFillPath |
L3029-L3086 | NtGdiStrokePath |
L3088-L3139 |
NtGdiWidenPath |
L3141-L3166 | (文件结尾 EOF) | L3168 |
10. 总结
path.c 是 ReactOS GDI 中"记录-回放"架构的完整实现:
- 记录面 :
BeginPath建 PATH(点+标志数组),期间所有图元经
PATH_AddEntry/add_log_points折算成统一表示------直线/贝塞尔/矩形/椭圆/圆弧
(贝塞尔近似)/文字轮廓,坐标一律先转设备坐标。 - 状态机 :
PATH_Null → PATH_Open → PATH_Closed,由
DCPATH_ACTIVE/state双重把关,保证输出前路径不可变、记录期不可输出。 - 输出面 :统一"先平化、再消费 "------
PATH_FlattenPath(GDI_Bezier 折线化)
是 Fill/Stroke/Widen/PathToRegion 的共同前置;填充走区域化 +IntGdiFillRgn,
细笔描边走IntGdiPolyline,宽笔描边走IntGdiWidenPath(法线偏移 + 端帽 +
连接 + 米特极限)再填充轮廓。 - 工程细节 :指数扩容保证摊还 O(1);临时 MM_TEXT + 单位世界变换规避
设备/逻辑坐标二次变换;SEH 探测保护所有用户缓冲;双删 + 句柄清零保证
DC 生命周期无悬挂路径。
本文档基于 ReactOS 源代码
win32ss/gdi/ntgdi/path.c、path.h及关联模块(bezier.c/line.c/bitblt.c/region.c/eng/pathobj.c)逐函数分析(2026 年 8 月)。