ReactOS 窗口系统分析(5):可见区域与窗口 DC --- vis.c + windc.c
本文是"窗口系统三大主线之窗口显示"系列的第 5 册(总册编号 _5),承接:
- 第 1 册(window.c):窗口对象 WND 的创建与销毁;
- 第 3 册(winpos.c):窗口位置、可见性与 Z 序;
- 第 4 册(painting.c):更新区域(hrgnUpdate)与 WM_PAINT 绘制协议。
本册回答窗口显示链路中"画在哪、能画多少"两个问题:
- 可见区域(vis.c) :一个窗口实际露出多少面积(不被父窗口裁剪、不被兄弟/子窗口遮挡),以 GDI 区域对象(
PREGION)形式表示;- 窗口 DC(windc.c) :应用
GetDC/BeginPaint拿到的 HDC 如何与窗口挂钩------通过 DCE(DC Entry,设备上下文缓存项) 这一 USER 与 GDI 之间的中介结构。涉及源码(均为本册核心):
win32ss/user/ntuser/vis.c(177 行)win32ss/user/ntuser/windc.c(1163 行)win32ss/user/ntuser/dce.h(DCE 结构)win32ss/user/ntuser/desktop.c(UserGetDesktopDC/桌面窗口)win32ss/user/ntuser/winsta.c(IntGetScreenDC/屏幕 DC)win32ss/gdi/ntgdi/dclife.c(IntGdiCreateDC/DC 生命周期)win32ss/gdi/ntgdi/dc.h(DC 的prgnVis/prgnRao/prgnClip)win32ss/gdi/ntgdi/cliprgn.c(GdiSelectVisRgn/CLIPPING_UpdateGCRegion)win32ss/gdi/ntgdi/dcutil.c/dcstate.c/coord.c(DC 工具)win32ss/gdi/eng/engwindow.c(EngCreateWnd/WNDOBJ 变化通知)win32ss/gdi/eng/clip.c(CLIPOBJ 区域装载)win32ss/user/user32/windows/dc.c(用户态ReleaseDC)
1. 概述
1.1 本册在窗口显示链路中的位置
窗口显示的完整链路(总览文档 4.x 节)为:
CreateWindowEx → IntCreateWindow(窗口对象)
→ ShowWindow / SetWindowPos(winpos.c:可见性、Z 序)
→ IntInvalidateRect / RedrawWindow(painting.c:累积更新区域 hrgnUpdate)
→ GetMessage 收到 WM_PAINT
→ BeginPaint(painting.c:IntBeginPaint)
└── UserGetDCEx(windc.c:取窗口 DC,DCE 出队/入队)
└── VIS_ComputeVisibleRegion(vis.c:合成可见区域 hrgnVis)
└── GdiSelectVisRgn(cliprgn.c:写入 DC->prgnVis)
└── CLIPPING_UpdateGCRegion(cliprgn.c:prgnVis∩prgnClip → prgnRao → CLIPOBJ)
→ 应用 GDI 绘制 → EndPaint(UserReleaseDC,DCE 回缓存)
→ 像素落在屏幕表面(PrimarySurface / 窗口表面)
本册两端各有一个"锚点":
- 上游锚点 :painting.c 的
IntBeginPaint调用UserGetDCEx(Window, Window->hrgnUpdate, DCX_INTERSECTRGN | DCX_USESTYLE),把"更新区域"作为裁剪区域传给窗口 DC; - 下游锚点 :
DceUpdateVisRgn调GdiSelectVisRgn,把 vis.c 合成的可见区域灌入 GDI 的 DC 对象(prgnVis),从此 GDI 的绘制裁剪完全由 USER 的可见区域驱动。
1.2 一条 GetDC 请求的完整旅程(ASCII 架构图)
以应用调用 GetDC(hWnd)(hWnd 非空)为例:
应用线程
│ GetDC(hWnd) (user32.spec 直通系统调用)
▼
NtUserGetDC(windc.c L1056)
│ UserEnterExclusive() ── 加 USER 锁
▼
NtUserGetDCEx(windc.c L1016)
│ UserGetWindowObject(hWnd) ── 句柄 → PWND
▼
UserGetDCEx(windc.c L344) ★ 本册核心分发器
├── 标志规范化(DCX_USESTYLE → DCX_CLIPCHILDREN / CLIPSIBLINGS / PARENTCLIP / CACHE)
├── 缓存表扫描(LEDce 全局链表):
│ ├── 命中空闲 CACHE 项(hwndCurrent+DCXFlags 匹配)→ 复用
│ └── 未命中 → DceAllocDCE(NULL, DCE_CACHE_DC)(windc.c L85)
├── Dce->DCXFlags = Flags | DCX_DCEBUSY(标记占用,移链表头部 = LRU)
├── ClipRegion 处理(HRGN_WINDOW / hrgnUpdate / 调用者区域)
├── DceSetDrawable(windc.c L144): GreSetDCOrg(hDC, rcClient.left, rcClient.top)
└── DceUpdateVisRgn(windc.c L190)
├── DceGetVisRgn → VIS_ComputeVisibleRegion(vis.c L12) ★ 可见区域合成
├── (可选)与 Dce->hrgnClip 求 RGN_AND / RGN_DIFF
└── GdiSelectVisRgn(cliprgn.c L59)→ DC->prgnVis ← 平移到 DC 原点
└── IntEngWindowChanged(Window, WOC_RGN_CLIENT)(engwindow.c L97)
└── IntEngWndUpdateClipObj → 驱动 WNDOBJ 裁剪
▼
返回 Dce->hDC(应用拿到的 HDC,裁剪 = 可见区域 ∩ 更新区域)
│
│ 应用用 HDC 调用 GDI 函数(TextOut、FillRect、BitBlt...)
│ └── GDI 引擎在绘制准备阶段:CLIPPING_UpdateGCRegion(cliprgn.c L741)
│ prgnRao = prgnVis ∩ prgnAPI(prgnClip ∩ prgnMeta)
│ → IntEngUpdateClipRegion → CLIPOBJ → 驱动逐矩形裁剪绘制
▼
像素最终写入 ppdev 的屏幕表面(PrimarySurface),或经窗口表面合成到屏幕
│
│ ReleaseDC(hWnd, hDC)(user32 dc.c L39)
▼
NtUserxReleaseDC(simplecall 单参数快速路径)
│ ONEPARAM_ROUTINE_RELEASEDC → UserReleaseDC(windc.c L960)
▼
DceReleaseDC(windc.c L278)
├── EndPaint 时恢复可见区域(DceUpdateVisRgn)
├── IntGdiCleanDC(复位 DC 状态,为下一个用户做准备)
├── GreSetDCOwner(GDI_OBJ_HMGR_NONE)(交还所有权)
└── DCXFlags = DCX_CACHE | DCX_DCEEMPTY(回缓存池)
关键结论:窗口 DC = 一个永远指向屏幕设备(DISPLAY PDEV)的 HDC + 一份随窗口变化的可见区域 + 一份随调用变化的附加裁剪。USER 不"创建"绘制表面,而是把"当前窗口能画到哪里"计算出来塞给 GDI,由 GDI 决定像素落点。
2. 设计动机
2.1 为什么要缓存窗口 DC(DCE 缓存)
HDC 是 GDI 句柄表对象,每个 DC 携带大量状态(字体、画刷、笔、映射模式、裁剪区域、原点...),创建/销毁成本高。而窗口绘制是高频操作:每次 WM_PAINT、每次鼠标拖动重绘、每次 WM_ERASEBKGND 都要拿 DC。若每次都新建 DC,性能不可接受。
ReactOS(沿袭 Windows 设计)采用 DCE(Device Context Entry)缓存:
-
全局一张 DCE 链表
LEDce(windc.c静态变量),系统内所有窗口 DC 都在这里登记; -
DCE 按其
DCXFlags分为三类(dce.h的DCE_TYPE):类型 对应窗口类样式 生命周期 拥有者 DCE_CACHE_DC普通窗口(无 CS_OWNDC/CS_CLASSDC) 全局共享缓存,用完即还 无( ptiOwner临时)DCE_CLASS_DCCS_CLASSDC(一类一 DC)挂在 pcls->pdce,类卸载时销毁类 DCE_WINDOW_DCCS_OWNDC(一窗一 DC)窗口销毁时清理 窗口所属线程 -
缓存的复用判据 :
hwndCurrent(DC 当前指向的窗口)+DCXFlags & DCX_CACHECOMPAREMASK完全一致才命中(windc.cL453-L485)。掩码只比较与裁剪相关的位,忽略 INTERSECT/EXCLUDE 等一次性标志,避免缓存项碎片化。
注释中有一句非常传神的自嘲(windc.c L446、L450):
"Window nz, check to see if we still own this or it is just cheap wine tonight."
"Ah~ Not Powned! Forced to be cheap~"
即:如果窗口不属于当前线程(无法"私有拥有"),就退化为"廉价酒"------用共享缓存 DC。
2.2 为什么可见区域要独立于窗口 DC 计算(vis.c 的职责)
一个窗口的"可见性"是几何合成 问题:窗口矩形减去被上层兄弟遮挡的部分、被父客户区裁剪的部分、被自己的 SetWindowRgn 形状区域裁掉的部分,还要减去(若 WS_CLIPCHILDREN)被子窗口覆盖的部分。这个结果:
- 与"谁在绘制"无关(任何拿到该窗口 DC 的人都应该只能画可见部分);
- 与"绘制什么"无关(不关心更新区域);
- 变化频繁(窗口一移动/一改变 Z 序/一出现新窗口,所有受影响窗口的可见区域都要重算)。
因此 ReactOS 把它做成独立模块 vis.c,提供纯函数 VIS_ComputeVisibleRegion,由多个调用方共享:
| 调用方 | 场景 |
|---|---|
DceGetVisRgn(windc.c L72) |
GetDC/BeginPaint 时计算 DC 可见区域 |
IntEngWndUpdateClipObj(engwindow.c L51) |
驱动通过 EngCreateWnd 注册 WNDOBJ 后,维护驱动侧裁剪 |
co_WinPosSetWindowPos(winpos.c) |
窗口移动后判断新旧可见区域差(搬移像素、标记脏区域) |
2.3 为什么 GDI 侧还要一份"可见区域"(prgnVis/prgnRao)
USER 算出的可见区域是屏幕坐标 下的区域。GDI 的 DC 有自己的原点 (ptlDCOrig,窗口 DC 的原点 = 客户区左上角屏幕坐标),绘制时所有坐标先减去原点。因此:
GdiSelectVisRgn(cliprgn.c L59)把 USER 区域拷贝进dc->prgnVis后立即REGION_bOffsetRgn(-ptlDCOrig)平移到 DC 空间;- 绘制前
CLIPPING_UpdateGCRegion(cliprgn.c L741)再把prgnVis与应用裁剪(prgnClip)、Meta 区域相交得到prgnRao,作为"真正允许光栅化(Raster Ops Allowed)"的区域; - 驱动实际逐矩形绘制时枚举
CLIPOBJ(IntEngUpdateClipRegion→WNDOBJ_bEnum/CLIPOBJ_bEnum)。
一句话:USER 管"窗口能露多少",GDI 管"DC 能画多少" ,中间由 prgnVis 桥接,prgnRao 是最终裁决。
2.4 为什么屏幕 DC 要常驻(IntGetScreenDC)
系统内部(光标绘制、鼠标指针移动、桌面背景)需要一块不属于任何窗口 的 DC 直接操作屏幕。co_IntInitializeDesktopGraphics(winsta.c L262)在显示驱动初始化成功后创建一次:
c
ScreenDeviceContext = IntGdiCreateDC(&DriverName, NULL, NULL, NULL, FALSE);
GreSetDCOwner(ScreenDeviceContext, GDI_OBJ_HMGR_PUBLIC);
之后 IntGetScreenDC()(winsta.c L382)只返回这个全局 ScreenDeviceContext,供 msgqueue.c 的光标绘制(GreSetPointerShape/GreMovePointer)等高频路径直接使用,避免反复创建。
2.5 三个"不存在的函数"澄清
阅读源码时需要注意,本册任务清单中的几个名字在当前 ReactOS 主干代码中并不存在(说明 ReactOS 的实现与 Windows 有差异):
| 任务清单中的名字 | 实际情况 |
|---|---|
DceUpdateHwndRgn |
不存在;窗口形状区域(SetWindowRgn)的处理内联在 VIS_ComputeVisibleRegion 与 IntInvalidateWindows 中(hrgnClip 字段) |
NtUserReleaseDC |
不存在;用户态 ReleaseDC 经 NtUserxReleaseDC(simplecall.c 单参数快速路径)直接调 UserReleaseDC(windc.c L960) |
UserCreateWindowSurface |
不存在;ReactOS 的窗口没有独立 GDI 表面(Windows 的窗口表面由 DWM 引入),窗口 DC 直接画在屏幕 PDEV 表面,靠可见区域裁剪保证正确性 |
NtUserGetWindowDisplayAffinity |
不存在;GetWindowDisplayAffinity/SetWindowDisplayAffinity 仅存在于 user32_vista 中,是 FIXME stub(win.c L1704/L1721) |
2.6 与 Windows 实现的差异对照
| 方面 | Windows(经典路径) | ReactOS | 影响 |
|---|---|---|---|
| 窗口表面 | 窗口有独立 hsurf(窗口表面),GDI 画到窗口表面,合成器/搬移器负责上屏 |
无独立表面,窗口 DC 直接绑定屏幕 PDEV 表面 | ReactOS 无 DWM 合成,靠可见区域裁剪保证不越界;窗口移动时用 BitBlt 搬移 |
| DCE 缓存 | 全局 DCE 缓存 + 类/窗口私有 DC | 同(LEDce + pcls->pdce + 私有 DCE) |
机制一致 |
| 可见区域 | Vis__xxx 系列(Windows 内部 vis.c) |
收敛为 VIS_ComputeVisibleRegion 单一入口 |
语义一致,实现精简 |
NtUserReleaseDC |
存在独立系统调用 | 无;走 NtUserxReleaseDC(simplecall 快速路径) |
行为一致,入口不同 |
GetWindowDisplayAffinity |
Vista+ 完整实现 | 仅 user32_vista stub(FIXME) |
功能缺失 |
| 屏幕 DC | GetDC(NULL) 返回屏幕 DC |
IntGetScreenDC(ScreenDeviceContext) |
一致 |
3. 核心数据结构
3.1 DCE(DC Entry)--- dce.h L17-L32
c
typedef struct tagDCE
{
LIST_ENTRY List; // 挂入全局链表 LEDce
HDC hDC; // 真正的 GDI DC 句柄(DISPLAY 设备 DC)
HWND hwndCurrent; // 当前关联窗口句柄(缓存匹配键)
PWND pwndOrg; // 原点窗口(DC 原点/可见区域按它计算)
PWND pwndClip; // 裁剪窗口
PWND pwndRedirect; // 重定向窗口(分层窗口等,未广泛使用)
HRGN hrgnClip; // 附加裁剪区域(INTERSECT/EXCLUDE/更新区域)
HRGN hrgnClipPublic;// 公开副本(供回调/测试)
HRGN hrgnSavedVis; // 保存的可见区域(EndPaint 恢复用)
DWORD DCXFlags; // 状态/类型标志(见下)
PTHREADINFO ptiOwner; // 拥有线程(缓存 DC 为临时拥有者)
PPROCESSINFO ppiOwner; // 拥有进程(DCX_PROCESSOWNED 时)
struct _MONITOR* pMonitor; // 所属监视器(多显示器判断)
} DCE, *PDCE;
DCE 的三个指针窗口:
hwndCurrent:DCE 现在"挂在"哪个窗口上(缓存命中判据);pwndOrg:可见区域/DC 原点以哪个窗口为基准(GetDC 的窗口);pwndClip:裁剪以哪个窗口为基准(通常与 pwndOrg 相同)。
正常情况下三者指向同一窗口;DCX_PARENTCLIP 场景下 pwndClip 会指向父窗口(DceUpdateVisRgn 的 parent 分支)。
3.2 DCE 类型与 DCX 标志 --- dce.h L10-L15、L34-L42
c
typedef enum
{
DCE_CACHE_DC, /* 缓存 DC(普通窗口,共享) */
DCE_CLASS_DC, /* 类 DC(CS_CLASSDC) */
DCE_WINDOW_DC /* 窗口 DC(CS_OWNDC) */
} DCE_TYPE;
内部 DCX 标志(公共 DCX_* 在 psdk/winuser.h):
| 标志 | 值 | 含义 |
|---|---|---|
DCX_DCEEMPTY |
0x00000800 | 缓存项空闲(无人占用) |
DCX_DCEBUSY |
0x00001000 | 缓存项被占用(GetDC 后 ReleaseDC 前) |
DCX_DCEDIRTY |
0x00002000 | 可见区域过期,下次使用需重算 |
DCX_LAYEREDWIN |
0x00004000 | 分层窗口 DC |
DCX_DCPOWNED |
0x00008000 | 进程私有 DC(遗留) |
DCX_NOCLIPCHILDREN |
0x00080000 | 不裁剪子窗口(显式请求) |
DCX_NORECOMPUTE |
0x00100000 | 不重算可见区域(遗留) |
DCX_INDESTROY |
0x00400000 | DCE 正在销毁 |
缓存比较掩码(windc.c L20-L23):
c
#define DCX_CACHECOMPAREMASK (DCX_CLIPSIBLINGS | DCX_CLIPCHILDREN | \
DCX_NORESETATTRS | DCX_LOCKWINDOWUPDATE | \
DCX_LAYEREDWIN | DCX_CACHE | DCX_WINDOW | \
DCX_PARENTCLIP)
只有影响"裁剪语义"的位参与缓存匹配;DCX_INTERSECTRGN/DCX_EXCLUDERGN/DCX_KEEPCLIPRGN 是一次性操作位,不入掩码。
3.3 DC 中的区域三兄弟 --- dc.h L95-L137
c
typedef struct _DC
{
...
PDC_ATTR pdcattr; // DC 属性(用户态可见部分,含 VisRectRegion)
DCLEVEL dclevel; // 当前状态层(SaveDC/RestoreDC 栈)
...
RECTL erclClip; // 裁剪外接矩形(供驱动)
POINTL ptlDCOrig; // DC 原点(窗口 DC = 客户区/窗口左上角屏幕坐标)
RECTL erclWindow; // 窗口矩形(DC 的"窗口"范围)
RECTL erclBounds; // 绘制范围
...
PREGION prgnAPI; // 应用裁剪(SelectClipRgn 结果)
_Notnull_ PREGION prgnVis; // 可见区域(USER 提供,永不为 NULL)
PREGION prgnRao; // Raster-Ops Allowed:最终可绘制区域 = prgnVis ∩ prgnAPI
...
XCLIPOBJ co; // CLIPOBJ(驱动枚举矩形用)
} DC;
三个区域的关系(在 CLIPPING_UpdateGCRegion 中体现,cliprgn.c L741-L853):
prgnVis (USER 的可见区域,DC 坐标,经 GdiSelectVisRgn 载入)
∩
prgnAPI (应用裁剪 = SelectClipRgn 的 prgnClip ∩ 元区域 prgnMeta)
=
prgnRao (最终光栅化允许区域 → IntEngUpdateClipRegion → CLIPOBJ)
注意 dc->dclevel.prgnClip 是"应用选择的活动裁剪区域"(SelectClipRgn 设置),dc->prgnClip(如果存在)在 IntSelectClipRgn 中作为中间产物;而 DCE 的 hrgnClip 是 USER 侧的附加裁剪,两者通过 GdiSelectVisRgn 之前的合并(DceUpdateVisRgn 中的 RGN_AND/RGN_DIFF)发生联系。
3.4 WNDOBJ / EWNDOBJ --- engobjects.h L42-L105、engwindow.c
驱动如果想"跟随窗口变化",通过 EngCreateWnd(pso, hWnd, pfn, fl, iPixelFormat) 注册一个 WNDOBJ。ReactOS 的内部扩展结构:
c
typedef struct _EWNDOBJ // = XCLIPOBJ + 扩展字段
{
CLIPOBJ; // 公共部分:rclBounds、iDComplexity、Rects...
RWNDOBJ; // pvConsumer、rclClient、psoOwner
struct _REGION *pClipRgn; // 指向 prgnRao 或 prgnVis
RECTL rclClipRgn;
RECTL* Rects; // 矩形缓冲(区域数据)
...
DWORD RectCount;
PVOID pDDA;
/* 扩展部分 */
HWND Hwnd; // 关联窗口句柄
WNDOBJCHANGEPROC ChangeProc; // 变化回调
FLONG Flags; // 关心哪些变化(WO_RGN_CLIENT 等)
int PixelFormat; // OpenGL 像素格式
} EWNDOBJ;
WNDOBJ 通过窗口属性挂接:UserSetProp(Window, AtomWndObj, Clip, TRUE)(engwindow.c L206),AtomWndObj 是全局原子 "SysWNDO"(ntuser.c L66)。USER 的可见区域变化 → IntEngWindowChanged → 更新 WNDOBJ 的 CLIPOBJ → 调用驱动的 ChangeProc。
3.5 DCE 缓存表 --- LEDce
LEDce(静态 LIST_ENTRY,windc.c L17,InitDCEImpl 初始化)
│
├── DCE_A(空闲,DCX_CACHE|DCX_DCEEMPTY)
├── DCE_B(占用,DCX_DCEBUSY,hwndCurrent=W1)── 使用中
├── DCE_C(类 DC,挂 pcls->pdce,CS_CLASSDC)
└── DCE_D(窗口 DC,CS_OWNDC,窗口 W2 私有)
- 入队 :
DceAllocDCE尾插(InsertTailList);UserGetDCEx命中占用时移到头部(RemoveEntryList+InsertHeadList,即"最近使用提升"); - 出队 :
DceFreeDCE从链表摘除并释放内存; - 遍历:所有函数都线性扫描链表(DCE 数量级小,可接受)。
3.6 窗口表面(SURFACE)与屏幕 PDEV
ReactOS 的窗口没有独立表面 。窗口 DC 通过 DceCreateDisplayDC 创建时绑定的是屏幕 PDEV (EngpGetPDEV 按 "DISPLAY" 查找,dclife.c L658):
GreOpenDCW("DISPLAY")
→ EngpGetPDEV → PPDEVOBJ(含 PDEVOBJ_pSurface:屏幕主表面)
→ DC_AllocDcWithHandle + DC_vInitDc(dclevel.pSurface = ppdev 的表面)
因此窗口 DC 的 dclevel.pSurface 就是屏幕表面(PDEVOBJ_pSurface(ppdev),dclife.c L483)。窗口的"画布"即屏幕,正确性完全依赖可见区域裁剪------这正是本册两大主题绑在一起的根本原因。
(Windows Vista+ 的 DWM 合成器引入了独立的窗口表面 CreateWindowSurface;ReactOS 走经典路径,无此概念。)
4. vis.c 详解:可见区域合成
4.1 VIS_ComputeVisibleRegion --- vis.c L12-L142(核心函数)
c
PREGION FASTCALL
VIS_ComputeVisibleRegion(
PWND Wnd,
BOOLEAN ClientArea, // TRUE:以客户区为初始区域;FALSE:以窗口矩形为初始区域
BOOLEAN ClipChildren, // TRUE:扣除子窗口(WS_CLIPCHILDREN 语义)
BOOLEAN ClipSiblings) // TRUE:扣除上层兄弟(GetDC 的强制语义)
算法分四步:
第一步:前提检查与初始区域
c
if (!Wnd || !(Wnd->style & WS_VISIBLE)) return NULL;
VisRgn = ClientArea ? rcClient : rcWindow; // 初始 = 窗口自身范围
窗口不可见或已销毁 → 返回 NULL(调用方 DceGetVisRgn 会兜底创建空区域)。
第二步:沿父链向上裁剪与扣兄弟(核心循环,L44-L99)
c
PreviousWindow = Wnd;
CurrentWindow = Wnd->spwndParent;
while (CurrentWindow)
{
1) 校验父窗口(VerifyWnd);父不可见 → 返回 NULL(整个区域作废)
2) VisRgn ∩= CurrentWindow->rcClient // 被父客户区裁剪
3) 若 (PreviousWindow->style & WS_CLIPSIBLINGS) // 上一级窗口声明了"裁兄弟"
或 (PreviousWindow == Wnd && ClipSiblings) // 或本次调用要求裁兄弟
{
遍历父窗口子链 spwndChild → 直到 PreviousWindow 为止:
跳过不可见 / WS_EX_TRANSPARENT 的兄弟
兄弟区域 = 该兄弟 rcWindow(与其 hrgnClip 形状区域求交)
VisRgn -= 兄弟区域(RGN_DIFF)
}
4) PreviousWindow = CurrentWindow; CurrentWindow = 父的父; // 上溯一级
}
关键细节:
- Z 序与遍历方向 :
spwndChild指向最底层(最老)的子窗口,spwndNext指向更新(更上层)的兄弟。遍历从spwndChild走到PreviousWindow,意味着只扣除"比我新的兄弟"------比我老的兄弟在我之下,不遮挡我,不能扣。 - WS_EX_TRANSPARENT:透明窗口(如鼠标穿透层)不参与遮挡,跳过。
- 窗口形状(SetWindowRgn) :兄弟若有
hrgnClip形状区域,先把兄弟矩形平移到其窗口原点、与形状区域求交、再平移回来,得到真实的兄弟"遮挡面积"(vis.c L79-L89)。 - 父的可见性:任何一个祖先不可见,整个区域立即作废(L56-L61)。父窗口可见性对子窗口是"与"关系。
第三步:扣除子窗口(ClipChildren,L101-L127)
c
if (ClipChildren)
遍历 Wnd->spwndChild 子链:
跳过不可见 / WS_EX_TRANSPARENT 的子
VisRgn -= 子窗口区域(同样处理子的 hrgnClip 形状)
第四步:本窗口形状区域求交(L129-L139)
c
if (Wnd->hrgnClip && !(Wnd->style & WS_MINIMIZE))
VisRgn ∩= Wnd->hrgnClip(先平移到窗口原点求交再平移回屏幕坐标)
最小化窗口跳过形状裁剪(图标绘制需要完整窗口矩形)。
返回值 :屏幕坐标下的 PREGION(区域对象所有权交给调用方,由调用方 REGION_Delete)。
4.2 合成算法图解
屏幕坐标系(Y 向下)
┌────────────── 父窗口(P)客户区 ────────────────────┐
│ │
│ ┌───────── 兄弟窗口 S2(在 W 之上,新)────────┐ │
│ │ │ │
│ │ ┌────── W(本窗口)───────┐ │ │
│ │ │ A区(被 S2 遮挡) │ │ │
│ │ ├─────────────────────────┤ │ │
│ │ │ B区(可见) │ │ │
│ │ │ ┌──── 子窗口 C ────┐│ │ │
│ │ │ │ C区(被子遮挡) ││ │ │
│ │ │ └─────────────────┘│ │ │
│ └────────┴─────────────────────────┴──────────┘ │
│ │
│ ┌───────── 兄弟窗口 S1(在 W 之下,老)───────┐ │
│ │ (不遮挡 W,保留) │ │
│ └─────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────┘
VIS_ComputeVisibleRegion(W, ClientArea=TRUE, ClipChildren=TRUE, ClipSiblings=TRUE)
初始 = W.rcClient
∩父客户区(父裁剪)
−S2 区域(比 W 新的兄弟) → 扣掉 A
−C 区域(ClipChildren,子窗口) → 扣掉 C
∩W.hrgnClip(窗口形状,若有)
结果 = 仅 B 区
4.3 co_VIS_WindowLayoutChanged --- vis.c L144-L175
窗口布局变化(移动/缩放)后,新暴露出来的区域需要让父窗口重绘:
c
VOID FASTCALL
co_VIS_WindowLayoutChanged(PWND Wnd, PREGION NewlyExposed)
{
父窗口存在时:
TempRgn = NewlyExposed(拷贝)
平移到父客户区坐标(Wnd.rcWindow − Parent.rcClient)
父窗口 RedrawWindow( TempRgn, RDW_FRAME|RDW_ERASE|RDW_INVALIDATE|RDW_ALLCHILDREN )
}
与 winpos.c 的搬移逻辑配合:窗口移动后,旧位置让出的"新暴露"区域立即让父窗口及其子窗口重绘。注意 ASSERT_REFS_CO(Wnd) 断言窗口引用计数正确(co 前缀表示"调用者已持有引用")。
4.4 vis.h 说明
vis.h 只导出两个函数(vis.h L12-L13):
c
PREGION FASTCALL VIS_ComputeVisibleRegion(PWND Window, BOOLEAN ClientArea, BOOLEAN ClipChildren, BOOLEAN ClipSiblings);
VOID FASTCALL co_VIS_WindowLayoutChanged(PWND Window, PREGION UncoveredRgn);
注:早期 ReactOS 版本曾计划/存在
Vis__xxx系列辅助函数(Vis__CreateVisibleRegion等),当前主干已收敛为上述单一入口。本分析以实际源码为准。
5. windc.c 详解:窗口 DC 管理(逐函数)
windc.c 共 1163 行,调试通道 UserDce。以下按代码顺序逐函数展开。
5.1 InitDCEImpl --- L27-L34
c
CODE_SEG("INIT")
NTSTATUS NTAPI InitDCEImpl(VOID)
{
InitializeListHead(&LEDce);
return STATUS_SUCCESS;
}
USER 初始化阶段(InitUserImpl)调用,初始化全局 DCE 链表头。CODE_SEG("INIT") 表示该函数在初始化后丢弃(分页守护段)。
5.2 DceCreateDisplayDC --- L39-L48
c
HDC FASTCALL DceCreateDisplayDC(VOID)
{
UNICODE_STRING DriverName = RTL_CONSTANT_STRING(L"DISPLAY");
if (!co_IntGraphicsCheck(TRUE))
KeBugCheckEx(VIDEO_DRIVER_INIT_FAILURE, 0, 0, 0, USER_VERSION);
return IntGdiCreateDC(&DriverName, NULL, NULL, NULL, FALSE);
}
co_IntGraphicsCheck(TRUE):确认图形子系统可用,否则 KeBugCheckEx(VIDEO_DRIVER_INIT_FAILURE)(视频驱动初始化失败蓝屏);IntGdiCreateDC(L"DISPLAY")→GreOpenDCW(dclife.c L640)→EngpGetPDEV找到屏幕 PDEV → 分配 DC(DC_AllocDcWithHandle)+ 初始化(DC_vInitDc);- 结果是一个绑定屏幕设备的普通 DC(DCTYPE_DIRECT)。
5.3 DceGetDceFromDC --- L51-L68
按 HDC 句柄线性扫描 LEDce,返回匹配的 DCE*。供外部(测试、调试、UserGethWnd 等)反查 DCE。
5.4 DceGetVisRgn --- L70-L83
c
static PREGION FASTCALL DceGetVisRgn(PWND Window, ULONG Flags, HWND hWndChild, ULONG CFlags)
{
Rgn = VIS_ComputeVisibleRegion( Window,
0 == (Flags & DCX_WINDOW), // ClientArea
0 != (Flags & DCX_CLIPCHILDREN),
0 != (Flags & DCX_CLIPSIBLINGS));
if (!Rgn) Rgn = IntSysCreateRectpRgn(0, 0, 0, 0); // 兜底空区域
return Rgn;
}
把 DCX 标志翻译成 VIS_ComputeVisibleRegion 的三个布尔参数。hWndChild/CFlags 目前未使用(保留给 PARENTCLIP 场景的扩展)。VIS_ComputeVisibleRegion 可能返回 NULL(窗口不可见),此处兜底创建空矩形区域,保证调用方永远拿到非空区域。
5.5 DceAllocDCE --- L85-L142
c
PDCE FASTCALL DceAllocDCE(PWND Window OPTIONAL, DCE_TYPE Type)
分配:
ExAllocatePoolWithTag(PagedPool, sizeof(DCE), USERTAG_DCE)分配 DCE 本体;DceCreateDisplayDC()创建显示 DC(失败则回滚释放 DCE);DCECount++(全局计数,调试用);- 初始化字段:
hwndCurrent = Window ? UserHMGetHandle(Window) : NULL,pwndOrg = pwndClip = Window,hrgnClip/hrgnClipPublic/hrgnSavedVis = NULL,ppiOwner = NULL; InsertTailList(&LEDce, &pDce->List)入链表;DCU_SetDcUndeletable(pDce->hDC):标记 DC 不可被应用DeleteObject删除(GDI 句柄保护,dcutil.c);- 所有权 :
DCE_WINDOW_DC/DCE_CLASS_DC→ptiOwner = GetW32ThreadInfo()(DC 归属创建线程,进程清理时可回收);- 其他(缓存 DC)→
GreSetDCOwner(hDC, GDI_OBJ_HMGR_NONE)(无主),ptiOwner = NULL;
- 类型标志 :
DCE_CACHE_DC→DCXFlags = DCX_CACHE | DCX_DCEEMPTY(空闲缓存项);- 其他 →
DCXFlags = DCX_DCEBUSY(立即占用),且DCE_WINDOW_DC时按窗口样式补DCX_CLIPCHILDREN(WS_CLIPCHILDREN)/DCX_CLIPSIBLINGS(WS_CLIPSIBLINGS)。
5.6 DceSetDrawable --- L144-L166
c
static VOID APIENTRY DceSetDrawable(PWND Window OPTIONAL, HDC hDC, ULONG Flags, BOOL SetClipOrigin)
{
if (Window)
rect = (Flags & DCX_WINDOW) ? Window->rcWindow : Window->rcClient;
GreSetDCOrg(hDC, rect.left, rect.top, &rect);
}
把 DC 原点设为窗口(客户区或整个窗口)的左上角屏幕坐标,同时把 erclWindow 设为对应矩形。这是"窗口 DC"的语义核心:DC 的原点决定了 GDI 坐标 → 屏幕坐标的换算。
GreSetDCOrg(coord.c L1356)内部:ptlDCOrig = (x,y)、ptlFillOrigin = 画刷原点 + (x,y)、erclWindow = *Rect。
5.7 DceDeleteClipRgn --- L169-L188
c
static VOID FASTCALL DceDeleteClipRgn(DCE* Dce)
{
Dce->DCXFlags &= ~(DCX_EXCLUDERGN | DCX_INTERSECTRGN);
if (Dce->DCXFlags & DCX_KEEPCLIPRGN) Dce->DCXFlags &= ~DCX_KEEPCLIPRGN;
else if (Dce->hrgnClip != NULL) GreDeleteObject(Dce->hrgnClip);
Dce->hrgnClip = NULL;
Dce->DCXFlags |= DCX_DCEDIRTY; // 标记可见区域过期
IntGdiSetHookFlags(Dce->hDC, DCHF_INVALIDATEVISRGN); // 通知 GDI:vis 需重算
}
释放附加裁剪区域:
DCX_KEEPCLIPRGN语义:调用者(如IntBeginPaint)把hrgnUpdate交给 DC 但保留所有权(BeginPaint 后 hrgnUpdate 归应用/进程),所以只清标志不删区域;- 否则
GreDeleteObject(hrgnClip); - 清
DCX_INTERSECTRGN|DCX_EXCLUDERGN(一次性的); - 置
DCX_DCEDIRTY+IntGdiSetHookFlags(DCHF_INVALIDATEVISRGN)(dcutil.c L422:置DC_DIRTY_RAO,让 GDI 绘制前重算 GC 区域)。
5.8 DceUpdateVisRgn --- L190-L276(核心)
c
VOID FASTCALL DceUpdateVisRgn(DCE *Dce, PWND Window, ULONG Flags)
计算可见区域(三路分支):
c
if (Flags & DCX_PARENTCLIP)
{
Parent = Window->spwndParent;
if (!Parent) goto noparent; // 无父:跳过(区域保持 NULL?)
// 按父窗口计算可见区域(子窗口的 DC 借用父的可见性)
if (Parent->style & WS_CLIPSIBLINGS)
DcxFlags = DCX_CLIPSIBLINGS | (Flags & ~(DCX_CLIPCHILDREN | DCX_WINDOW));
else
DcxFlags = Flags & ~(DCX_CLIPSIBLINGS | DCX_CLIPCHILDREN | DCX_WINDOW);
RgnVisible = DceGetVisRgn(Parent, DcxFlags, UserHMGetHandle(Window), Flags);
}
else if (Window == NULL)
{
// 无窗口(GetDC(NULL)):可见区域 = 桌面窗口矩形
DesktopWindow = UserGetWindowObject(IntGetDesktopWindow());
if (DesktopWindow) RgnVisible = IntSysCreateRectpRgnIndirect(&DesktopWindow->rcWindow);
}
else
{
RgnVisible = DceGetVisRgn(Window, Flags, 0, 0); // 常规:VIS_ComputeVisibleRegion
}
- DCX_PARENTCLIP (CS_PARENTDC 类样式/GetDCEx 显式指定):窗口使用父窗口的客户区坐标系 绘制(子窗口内容直接画在父表面),可见区域按父窗口计算;父有
WS_CLIPSIBLINGS时保持裁兄弟语义; - Window == NULL (
GetDC(NULL)取屏幕 DC):直接建桌面窗口矩形区域,不经过 vis.c(屏幕总是完全可见); - 常规 :
DceGetVisRgn→VIS_ComputeVisibleRegion。
附加裁剪(INTERSECT/EXCLUDE):
c
if (Flags & DCX_INTERSECTRGN) // GetDCEx(ClipRegion) / BeginPaint(hrgnUpdate)
{
RgnClip = REGION_LockRgn(Dce->hrgnClip);
if (RgnClip) { VisRgn ∩= RgnClip; REGION_UnlockRgn; }
else { 释放 VisRgn;VisRgn = 空区域 } // 无裁剪区域则什么都不画
}
else if ((Flags & DCX_EXCLUDERGN) && Dce->hrgnClip != NULL)
{
VisRgn −= Dce->hrgnClip(RGN_DIFF) // ExcludeClipRect 语义
}
落盘与通知:
c
Dce->DCXFlags &= ~DCX_DCEDIRTY;
GdiSelectVisRgn(Dce->hDC, RgnVisible); // 写入 DC->prgnVis(平移至 DC 原点)
if (Window) IntEngWindowChanged(Window, WOC_RGN_CLIENT); // 通知驱动 WNDOBJ
REGION_Delete(RgnVisible); // 区域已拷贝,释放临时区域
GdiSelectVisRgn(cliprgn.c L59):
c
dc->fs |= DC_DIRTY_RAO;
REGION_bCopy(dc->prgnVis, prgn);
REGION_bOffsetRgn(dc->prgnVis, -dc->ptlDCOrig.x, -dc->ptlDCOrig.y);
注意 prgnVis 的"屏幕坐标 → DC 坐标"平移在这里完成。
IntEngWindowChanged(WOC_RGN_CLIENT)(engwindow.c L97)→ IntEngWndUpdateClipObj(engwindow.c L51):再次调用 VIS_ComputeVisibleRegion(Window, TRUE, TRUE, TRUE) 填充 WNDOBJ 的 CLIPOBJ(驱动侧),并 Clip->iUniq++ 递增版本号。
5.9 DceReleaseDC --- L278-L341
c
static INT FASTCALL DceReleaseDC(DCE* dce, BOOL EndPaint)
释放流程:
- 状态检查 :
DCX_DCEBUSY != (DCXFlags & (INDESTROY|DCEEMPTY|DCEBUSY))→ 返回 0(不在占用态,非法释放); - EndPaint 恢复可见区域 :
DceUpdateVisRgn(dce, dce->pwndOrg, dce->DCXFlags)(BeginPaint 时可见区域被 hrgnUpdate 收窄,EndPaint 恢复完整可见区域); - 清附加裁剪 :
(INTERSECT|EXCLUDE) && (CACHE || EndPaint)→DceDeleteClipRgn; - 缓存清理 (仅
DCX_CACHE):!DCX_NORESETATTRS→IntGdiCleanDC(dce->hDC)(dcstate.c L73:DC_vCopyState(defaultDCstate, dc)把 DC 复位到默认状态,删除 Path、prgnRao、prgnAPI;注释明确"prgnVis 不用清,UserGetDC 会立即更新");- 若
DCX_DCEDIRTY:清空缓存项(hwndCurrent=0、pwndOrg=pwndClip=NULL、DCXFlags = DCX_CACHE|DCX_DCEEMPTY),因为脏项不再可靠; GreSetDCOwner(hDC, GDI_OBJ_HMGR_NONE)交还 GDI 所有权,ptiOwner=ppiOwner=NULL;
- 返回 1(成功释放)。
被
#if 0禁用的"缓存超过 32 个就清理"逻辑(L324-L338)保留在代码中作为研究注释:当DCECache > 32时遍历链表释放所有非 BUSY 缓存项。目前未启用。
5.10 UserGetDCEx --- L344-L666(核心分发器,全书最长函数)
签名:
c
HDC FASTCALL UserGetDCEx(PWND Wnd OPTIONAL, HANDLE ClipRegion, ULONG Flags)
阶段一:标志规范化(L356-L451)
| 条件 | 动作 |
|---|---|
Wnd == NULL |
清 DCX_USESTYLE,置 DCX_CACHE(屏幕 DC 走缓存) |
DCX_PARENTCLIP |
强制 DCX_CACHE(父裁剪语义只支持缓存 DC) |
DCX_USESTYLE 且非 DCX_WINDOW |
按类样式 决策:CS_PARENTDC→DCX_PARENTCLIP;非 CACHE 且非 CS_OWNDC 时:CS_CLASSDC→用/建类 DCE(pcls->pdce),否则→DCX_CACHE("廉价酒");再按窗口样式 补 WS_CLIPSIBLINGS→DCX_CLIPSIBLINGS、WS_CLIPCHILDREN(非最小化)→DCX_CLIPCHILDREN;最小化且有类图标→强制 DCX_CACHE |
DCX_USESTYLE 且 DCX_WINDOW |
只补 WS_CLIPSIBLINGS→DCX_CLIPSIBLINGS,强制 DCX_CACHE |
DCX_WINDOW |
清 DCX_CLIPCHILDREN(画整个窗口时不能裁子) |
DCX_NOCLIPCHILDREN |
置 DCX_CACHE,清 `DCX_PARENTCLIP |
非 WS_CHILD 或无父 |
清 DCX_PARENTCLIP,置 DCX_CLIPSIBLINGS(顶级窗口必裁兄弟) |
| `DCX_CLIPSIBLINGS | DCX_CLIPCHILDREN` |
DCX_PARENTCLIP 且父子都可见 |
清 DCX_CLIPCHILDREN,父有 WS_CLIPSIBLINGS 则补 |
非 DCX_CACHE 且 Wnd->head.pti != 当前线程 |
强制 DCX_CACHE(跨线程不能私有) |
规范化后 DcxFlags = Flags & DCX_CACHECOMPAREMASK 作为缓存匹配键。
阶段二:取/建 DCE(L453-L542)
缓存路径(DCX_CACHE):
c
KeEnterCriticalRegion();
遍历 LEDce:
若 (Dce->DCXFlags & (DCX_CACHE|DCX_DCEBUSY)) == DCX_CACHE // 空闲缓存项
DceUnused = Dce;
if (Dce->DCXFlags & DCX_DCEEMPTY) DceEmpty = Dce;
else if (Dce->hwndCurrent == 目标句柄 && (Dce->DCXFlags & MASK) == DcxFlags)
{ UpdateClipOrigin = TRUE; break; } // 完全命中
KeLeaveCriticalRegion();
Dce = DceEmpty ? DceEmpty : DceUnused; // 优先复用空项
if (!Dce) Dce = DceAllocDCE(NULL, DCE_CACHE_DC); // 都没有就新建
Dce->hwndCurrent = 句柄; Dce->pwndOrg = Dce->pwndClip = Wnd;
私有路径(CS_OWNDC/CS_CLASSDC,非 CACHE):
c
遍历 LEDce:
跳过 DCX_CACHE 项;
若 Dce->hwndCurrent == 目标句柄:
若类样式从 CS_CLASSDC 切到 CS_OWNDC 且 Dce 是旧类 DCE → 跳过(防陈旧)
bUpdateVisRgn = FALSE; // 私有 DC 可见区域由自身维护,不必每次重算
break;
若 Dce->hDC == hDC → break;
若 !Dce → return NULL(私有 DC 必须已存在,如类 DC 未建)
若 (Flags 带 INTERSECT/EXCLUDE) 且 (Dce 带 INTERSECT/EXCLUDE) → DceDeleteClipRgn(重来)
阶段三:占用与更新(L543-L665)
c
if (!GreIsHandleValid(Dce->hDC)) { 重建 hDC = DceCreateDisplayDC(); } // 防御
Dce->DCXFlags = Flags | DCX_DCEBUSY; // 标记占用
RemoveEntryList(&Dce->List); InsertHeadList(&LEDce); // LRU 提升(注释:防 wine dce 测试随机错误,rev 6691)
if ((Flags & DCX_INTERSECTUPDATE) && !ClipRegion) // BeginPaint 语义
{
Flags |= DCX_INTERSECTRGN | DCX_KEEPCLIPRGN; // 附加裁剪 = hrgnUpdate,且保留所有权
ClipRegion = Wnd->hrgnUpdate;
bUpdateVisRgn = TRUE;
}
if (ClipRegion == HRGN_WINDOW) // 特殊标记:以窗口矩形为裁剪
Dce->hrgnClip = NtGdiCreateRectRgn(rcClient 或 rcWindow 矩形);
else if (ClipRegion != NULL)
Dce->hrgnClip = ClipRegion; // 调用方区域(GetDCEx 的 hrgnClip 参数)
if (IntGdiSetHookFlags(Dce->hDC, DCHF_VALIDATEVISRGN)) bUpdateVisRgn = TRUE;
DceSetDrawable(Wnd, Dce->hDC, Flags, UpdateClipOrigin); // 设 DC 原点
if (bUpdateVisRgn) DceUpdateVisRgn(Dce, Wnd, Flags); // 算可见区域
LockWindowUpdate 支持(L616-L640) :若桌面处于 LockWindowUpdate 状态(pdeskLock->spwndLockUpdate),且本 DC 的目标窗口是该锁定窗口或其子窗口、且未带 DCX_LOCKWINDOWUPDATE,则把可见区域置空------锁定窗口的绘制被整体丢弃(拖动滚动条时的经典语义)。
收尾:
c
if (DCX_CACHE) { GreSetDCOwner(hDC, GDI_OBJ_HMGR_POWNED); ptiOwner = 当前线程; } // 临时拥有
if (Wnd 是 WS_EX_LAYOUTRTL 且 !DCX_KEEPLAYOUT) NtGdiSetLayout(hDC, -1, LAYOUT_RTL); // 希伯来/阿拉伯镜像
if (DCX_PROCESSOWNED) { ppi->W32PF_flags |= W32PF_OWNDCCLEANUP; ptiOwner=NULL; ppiOwner=ppi; }
return Dce->hDC;
5.11 NtUserGetDCEx --- L1016-L1036
c
HDC APIENTRY NtUserGetDCEx(HWND hWnd OPTIONAL, HANDLE ClipRegion, ULONG Flags)
{
UserEnterExclusive();
if (hWnd && !(Wnd = UserGetWindowObject(hWnd))) goto Exit; // 非法句柄 → NULL
Ret = UserGetDCEx(Wnd, ClipRegion, Flags);
Exit:
UserLeave();
return Ret;
}
系统调用入口:加 USER 独占锁(UserEnterExclusive),句柄→对象,转发内核实现。UserGetWindowObject 失败(窗口已销毁)返回 NULL 而不蓝屏。
5.12 NtUserGetDC --- L1056-L1062
c
HDC APIENTRY NtUserGetDC(HWND hWnd)
{
return NtUserGetDCEx(hWnd, NULL,
NULL == hWnd ? DCX_CACHE | DCX_WINDOW : DCX_USESTYLE);
}
GetDC(hWnd) 的两个语义:
- hWnd == NULL (屏幕 DC):
DCX_CACHE|DCX_WINDOW------走缓存,可见区域=桌面窗口矩形(DceUpdateVisRgn的 Window==NULL 分支); - hWnd 非空 :
DCX_USESTYLE------按窗口/类样式决策一切(见 5.10)。
5.13 NtUserGetWindowDC --- L1050-L1054
c
HDC APIENTRY NtUserGetWindowDC(HWND hWnd)
{
return NtUserGetDCEx(hWnd, 0, DCX_USESTYLE | DCX_WINDOW);
}
整个窗口(含非客户区)的 DC:DCX_WINDOW 使原点=窗口左上角、可见区域按 rcWindow 计算、且不裁子窗口(DCX_WINDOW 清 DCX_CLIPCHILDREN)。
5.14 UserReleaseDC --- L960-L987 与用户态 ReleaseDC
c
INT FASTCALL UserReleaseDC(PWND Window, HDC hDc, BOOL EndPaint)
{
遍历 LEDce 找 dce->hDC == hDc;
if (Hit && (dce->DCXFlags & DCX_DCEBUSY))
nRet = DceReleaseDC(dce, EndPaint);
return nRet; // 1=成功释放,0=失败
}
用户态(user32/windows/dc.c L39-L50):
c
int WINAPI ReleaseDC(HWND hWnd, HDC hDC)
{
if (!hDC) return FALSE;
GdiReleaseDC(hDC); // 释放本线程 GDI 局部表引用(UserReleaseDC 的"GDI 半边")
return NtUserxReleaseDC(hDC); // 单参数快速系统调用
}
NtUserxReleaseDC 是 simplecall.c 的 ONEPARAM_ROUTINE_RELEASEDC(L306-L308)→ UserReleaseDC(NULL, hDC, FALSE)。注意:这里 Window 传 NULL 也没关系 ,UserReleaseDC 只看 hDC 找 DCE。
NtGdiDeleteObjectApp/IntGdiDeleteDC(dclife.c L892)遇到永久 DC(DC_PERMANANT)时也会回调UserReleaseDC(NULL, hDC, FALSE)尝试按窗口 DC 释放("No, you naughty application!"),失败才强制IntGdiDeleteDC(hDC, TRUE)。
5.15 DceFreeDCE --- L671-L717
c
void FASTCALL DceFreeDCE(PDCE pdce, BOOLEAN Force)
{
pdce->DCXFlags |= DCX_INDESTROY; // 进入销毁态
if (Force && 所有权 != POWNED) // 强制时把 GDI 所有权拿回 POWNED
GreSetDCOwner(hDC, GDI_OBJ_HMGR_POWNED); //(否则 PUBLIC → POWNED)
if (!Hit) IntGdiDeleteDC(pdce->hDC, TRUE); // 删除 GDI DC
if (hrgnClip && !(DCX_KEEPCLIPRGN)) GreDeleteObject(hrgnClip);
RemoveEntryList(&pdce->List);
ExFreePoolWithTag(pdce, USERTAG_DCE);
DCECount--;
}
DCE 销毁的完整反过程:摘链表、删 GDI DC、删附加区域、释放内存。DCX_INDESTROY 让 IntWindowFromDC/UserReleaseDC 等并发访问安全地忽略该 DCE。
5.16 DceFreeWindowDCE --- L724-L802(窗口销毁清理)
窗口销毁时(co_UserFreeWindow → DceFreeWindowDCE)逐项处理挂在该窗口上的 DCE:
| DCE 类型 | 处理 |
|---|---|
类 DCE(CS_CLASSDC) |
DceDeleteClipRgn + DceUpdateVisRgn 复位可见区域 → 降级为缓存项(`DCX_DCEEMPTY |
窗口 DCE(CS_OWNDC) |
非类 DCE → DceFreeDCE(pDCE, FALSE) 直接销毁 |
| 共享缓存 DCE(占用中) | ERR("[%p] GetDC() without ReleaseDC()!\n")(应用泄漏警告)+ DceReleaseDC(pDCE, FALSE) 强制回收 |
| 共享缓存 DCE(空闲) | 复位 DCX_DCEEMPTY、hwndCurrent=0 |
这解释了常见的 GetDC() without ReleaseDC() 错误日志来源------应用拿了 DC 没还,窗口销毁时由系统兜底。
5.17 DceFreeClassDCE / DceFreeThreadDCE / DceEmptyCache
DceFreeClassDCE(pdceClass)(L804-L820):类卸载(UserDestroyClass)时销毁类 DCE;DceFreeThreadDCE(pti)(L822-L841):线程退出清理(ExitThreadCallback)时销毁该线程拥有的缓存 DCE;DceEmptyCache()(L843-L856):遍历清空全部 DCE(系统关闭/测试用)。
5.18 DceResetActiveDCEs --- L858-L934
窗口移动后(winpos.c 调用)修正所有关联 DCE 的坐标基准:
c
对每个非空/非销毁 DCE:
CurrentWindow = 由 hwndCurrent 反查(或参数 Window)
if (Window == CurrentWindow || IntIsChildWindow(Window, CurrentWindow))
{
// 平移 DC 原点(DCX_WINDOW → rcWindow 左上角,否则 rcClient 左上角)
Delta = 新原点 − dc->ptlDCOrig;
dc->ptlDCOrig = 新原点;
if (dc->dclevel.prgnClip) { REGION_bOffsetRgn(prgnClip, Delta); dc->fs |= DC_DIRTY_RAO; }
if (pDCE->hrgnClip) NtGdiOffsetRgn(hrgnClip, Delta);
DceUpdateVisRgn(pDCE, CurrentWindow, pDCE->DCXFlags); // 重算可见区域
IntGdiSetHookFlags(pDCE->hDC, DCHF_VALIDATEVISRGN);
}
窗口一动,原点、裁剪、可见区域三者同步平移重算。
5.19 IntWindowFromDC / UserGethWnd
c
HWND FASTCALL IntWindowFromDC(HDC hDc) // L936-L958
{
遍历 LEDce:dce->hDC == hDc → 返回 dce->hwndCurrent(销毁中返回 NULL)
}
HWND FASTCALL UserGethWnd(HDC hdc, PWNDOBJ *pwndo) // L995-L1014
{
hWnd = IntWindowFromDC(hdc);
if (hWnd 有效) {
Clip = (EWNDOBJ*)UserGetProp(Wnd, AtomWndObj, TRUE); // 窗口属性里的 WNDOBJ
if (Clip && Clip->Hwnd == hWnd) *pwndo = (PWNDOBJ)Clip;
}
return hWnd;
}
WindowFromDC(user32)→ NtUserxWindowFromDC → IntWindowFromDC:HDC → 窗口句柄的反查路径(WINDOWFROMDC 快速调用,simplecall.c L240)。
5.20 NtUserSelectPalette --- L1075-L1087
SelectPalette 的系统调用:加 USER 锁后转发 GdiSelectPalette(dcobjs.c),调色板选择对窗口 DC 与普通 DC 一视同仁。
5.21 DceLockWindowUpdate / DceUnlockWindowUpdate --- L1093-L1161
LockWindowUpdate(拖动锁定)的 DCE 面:
- 加锁 (
DceLockWindowUpdate(Window)):遍历所有 DCE,凡目标窗口是该锁定窗口或其后代、且未带DCX_LOCKWINDOWUPDATE的,立即把可见区域置空(GdiSelectVisRgn(hDC, 空区域))------锁定期间这些 DC 的绘制全部丢弃; - 解锁 (
DceUnlockWindowUpdate):遍历所有 DCE 置DCX_DCEDIRTY+IntGdiSetHookFlags(DCHF_INVALIDATEVISRGN)------下次使用时重算可见区域。
配合 UserGetDCEx 内的加锁检查(L616-L640)形成完整闭环。
6. 可见区域合成算法专题
6.1 参与合成的要素汇总
| 要素 | 数据来源 | 运算 | 位置 |
|---|---|---|---|
| 窗口自身范围 | Wnd->rcWindow / rcClient |
初始区域 | vis.c L29-L36 |
| 祖先客户区裁剪 | 每级父的 rcClient |
RGN_AND | vis.c L63-L65 |
| 上层兄弟遮挡 | 兄弟 rcWindow(±hrgnClip 形状) |
RGN_DIFF | vis.c L67-L95 |
| 子窗口遮挡 | 子 rcWindow(±hrgnClip 形状) |
RGN_DIFF | vis.c L101-L127 |
| 窗口形状 | Wnd->hrgnClip(SetWindowRgn) |
RGN_AND | vis.c L129-L139 |
| 祖先不可见 | 每级父的 style & WS_VISIBLE |
整体作废 | vis.c L56-L61 |
| 透明窗口 | WS_EX_TRANSPARENT |
跳过不扣 | vis.c L75、L107 |
| 最小化 | WS_MINIMIZE |
跳过形状裁剪 | vis.c L79、L111、L129 |
6.2 遮挡关系判定(Z 序语义)
- 兄弟遮挡只扣"比自己新 "的兄弟:子链遍历到
PreviousWindow即停(vis.c L71-L72); - 是否扣兄弟取决于被裁剪窗口的
WS_CLIPSIBLINGS样式或调用方的强制ClipSiblings标志(GetDCEx的DCX_CLIPSIBLINGS、GetDC对非子窗口的强制); - 是否扣子取决于调用方 的
ClipChildren(WS_CLIPCHILDREN样式 →UserGetDCEx置DCX_CLIPCHILDREN→ vis.c 第三阶段); - 例:父窗口有
WS_CLIPCHILDREN时,父的 DC 会自动把子窗口区域抠掉(否则子窗口区域会被父画脏)。
6.3 区域运算的 GDI 实现
IntGdiCombineRgn(Dst, Src1, Src2, Mode) 是内核区域合并(region.c),支持 RGN_AND/RGN_DIFF/RGN_OR/RGN_COPY。vis.c 全程使用它 + REGION_bOffsetRgn(区域平移)完成几何运算。IntSysCreateRectpRgnIndirect(region.h L93 宏)按 RECTL 建区域。
6.4 区域结果的去向(三份拷贝)
一次 GetDC 计算出的可见区域最终有三份消费者:
- DC->prgnVis (cliprgn.c
GdiSelectVisRgn拷贝 + 平移)------GDI 绘制裁剪基础; - WNDOBJ->CLIPOBJ (engwindow.c
IntEngWndUpdateClipObj重新计算 +IntEngUpdateClipRegion装载)------驱动 DirectDraw/OpenGL 等使用; - winpos.c 的搬移判断 (
co_WinPosSetWindowPos比较移动前后的可见区域)------决定哪些像素可搬、哪些需要重画。
6.5 可见区域的失效与重算时机
可见区域不是常驻字段(WND 结构没有 hrgnVis 成员),而是每次 GetDC 时按需计算 的派生量。它的"失效"通过 DCE 的 DCX_DCEDIRTY 位 + GDI DC 的 DC_DIRTY_RAO 位表达:
| 失效事件 | 置位动作 | 重算动作 |
|---|---|---|
DceDeleteClipRgn(清理附加裁剪) |
DCX_DCEDIRTY + DCHF_INVALIDATEVISRGN → DC_DIRTY_RAO |
下次 UserGetDCEx 中 DceUpdateVisRgn |
DceUnlockWindowUpdate(解锁拖动) |
所有 DCE 置 DCX_DCEDIRTY |
同上 |
| 窗口移动/缩放 | DceResetActiveDCEs 直接调 DceUpdateVisRgn |
立即重算 |
GetDCEx 传入新 ClipRegion |
bUpdateVisRgn = TRUE |
立即重算 |
IntGdiSetHookFlags(DCHF_VALIDATEVISRGN) 返回真 |
--- | 立即重算 |
绘制时发现 DC_DIRTY_RAO |
--- | CLIPPING_UpdateGCRegion 重新合成 prgnRao |
由于 GdiSelectVisRgn 每次都整体拷贝 区域(REGION_bCopy),可见区域没有增量更新问题------USER 每次合成完整区域,GDI 直接替换。这是"按需全量计算"模式,正确性简单、性能靠缓存(DCE 复用 + 同窗口同标志跳过重算)弥补。
7. 窗口 DC 生命周期
7.1 状态机
DceAllocDCE(NULL, DCE_CACHE_DC)
│
▼
┌───── DCX_CACHE | DCX_DCEEMPTY(空闲缓存)◄──────┐
│ │ │
│ UserGetDCEx(命中/新建) │
│ ▼ │
│ DCX_CACHE | DCX_DCEBUSY(占用,临时拥有) │
│ │ │
│ ReleaseDC → DceReleaseDC │
│ │ │
│ ┌────────────┴────────────┐ │
│ │ IntGdiCleanDC 复位状态 │ │
│ │ GreSetDCOwner(NONE) │ │
│ └────────────┬────────────┘ │
└────────────────┘ │
│
DceFreeDCE(窗口销毁/线程退出/类卸载/EmptyCache) │
│
DceAllocDCE(Wnd, DCE_CLASS_DC / DCE_WINDOW_DC)(私有) │
│ │
▼ │
DCX_DCEBUSY(常驻,随窗口/类生命周期) │
│ │
└── DceFreeWindowDCE / DceFreeClassDCE ────────────┘
7.2 生命周期事件表
| 事件 | 处理函数 | 说明 |
|---|---|---|
| 系统初始化 | InitDCEImpl |
初始化链表 |
| GetDC/GetDCEx/GetWindowDC | NtUserGetDC* → UserGetDCEx |
取 DCE、设原点、算可见区域 |
| BeginPaint | IntBeginPaint → UserGetDCEx(INTERSECTUPDATE) |
可见区域 ∩ 更新区域 |
| 绘制结束 | ReleaseDC/EndPaint → UserReleaseDC → DceReleaseDC |
复位 DC、回缓存 |
| 窗口移动/缩放 | DceResetActiveDCEs |
平移原点/裁剪、重算可见区域 |
| 窗口销毁 | DceFreeWindowDCE |
私有 DCE 销毁、缓存 DCE 复位(含泄漏告警) |
| 窗口类卸载 | DceFreeClassDCE |
销毁类 DCE |
| 线程退出 | DceFreeThreadDCE |
销毁线程缓存 DCE |
| LockWindowUpdate | DceLockWindowUpdate/DceUnlockWindowUpdate |
置空/重置可见区域 |
| 系统关闭 | DceEmptyCache |
全部销毁 |
7.3 典型场景回放
场景 A:普通窗口 WM_PAINT(最频繁)
IntBeginPaint(painting.c L1546)
UserGetDCEx(W, W->hrgnUpdate, DCX_INTERSECTRGN|DCX_USESTYLE)
→ 命中/新建缓存 DCE
→ hrgnClip = hrgnUpdate(KEEPCLIPRGN:所有权留给进程)
→ DceUpdateVisRgn:VIS_ComputeVisibleRegion(W) ∩ hrgnUpdate
→ GdiSelectVisRgn
应用绘制(GDI 以 prgnVis 为最终裁剪)
IntEndPaint(painting.c L1608)
UserReleaseDC(W, hdc, TRUE)
→ DceReleaseDC(EndPaint=TRUE)
→ DceUpdateVisRgn 恢复完整可见区域
→ DceDeleteClipRgn(清 hrgnUpdate 引用,保留区域对象)
→ IntGdiCleanDC + 回缓存
场景 B:GetDC(NULL) 屏幕 DC
NtUserGetDC(NULL)
→ DCX_CACHE|DCX_WINDOW → Wnd=NULL 分支
→ DceUpdateVisRgn:区域 = 桌面窗口 rcWindow(整屏)
→ 缓存复用(下次 GetDC(NULL) 命中同一 DCE)
场景 C:GetWindowDC 画非客户区
NtUserGetWindowDC → DCX_USESTYLE|DCX_WINDOW
→ 原点 = rcWindow.left/top,可见区域按整个窗口矩形合成
→ 不裁子窗口
场景 D:CS_OWNDC 窗口(一窗一 DC)
UserGetDCEx(非 CACHE 路径)
→ 扫描非缓存 DCE:hwndCurrent 匹配即复用(bUpdateVisRgn=FALSE)
→ 首次:DceAllocDCE(W, DCE_WINDOW_DC)(ptiOwner=创建线程)
→ 每次 GetDC 复用同一 HDC,bUpdateVisRgn=FALSE 省掉一次区域合成
→ 窗口销毁:DceFreeWindowDCE → DceFreeDCE(FALSE)
场景 E:CS_CLASSDC 窗口(一类一 DC)
UserGetDCEx(非 CACHE 路径)
→ pcls->pdce 不存在则 DceAllocDCE(W, DCE_CLASS_DC),挂到 pcls->pdce
→ 所有同类窗口共享该 DC(坐标按各窗口分别 SetDrawable + UpdateVisRgn)
→ 类卸载:DceFreeClassDCE
场景 F:LockWindowUpdate 拖动锁定
LockWindowUpdate(hWndLock)(winpos.c 拖动滚动条)
→ DceLockWindowUpdate(hWndLock):所有指向锁定窗口/其子的 DCE 可见区域置空
→ 期间任何绘制被 GDI 丢弃(prgnVis 为空)
→ 新 GetDC 也走 UserGetDCEx 内的加锁检查(同样置空)
→ LockWindowUpdate(NULL):DceUnlockWindowUpdate 全部标记 DIRTY
→ 下次使用重算可见区域,恢复绘制
7.4 系统内部调用方全景
除应用通过 Win32 API 取 DC 外,win32k 内部大量子系统直接调用 UserGetDCEx(本册函数的内核级复用):
| 调用方 | 位置 | 用途 |
|---|---|---|
IntBeginPaint |
painting.c L1546 | WM_PAINT 绘制(INTERSECTUPDATE) |
IntDefWindowProc |
defwnd.c L421/L1318 | WM_ERASEBKGND、WM_ICONERASEBKGND 擦背景 |
IntDefWindowProc |
defwnd.c L1304 | WM_PAINTICON 用 `DCX_WINDOW |
MENU_DrawMenuBar 等 |
menu.c L3161/L3163/L3381/L3382 | 菜单栏/弹出菜单绘制(菜单 DC) |
| 菜单位图测量 | menu.c L2858/L5585 | UserGetDCEx(NULL,0,DCX_USESTYLE) 屏幕 DC 量尺寸 |
IntCreateCaret |
caret.c L29 | 插入符位图绘制 |
OpenClipboard 关闭重绘 |
clipboard.c L166/L266 | 剪贴板查看者窗口 DC |
IntUpdateLayeredWindow |
layered.c L166 | 分层窗口合成 DC |
| 输入状态显示 | keyboard.c L761 | 键盘指示(NUM/CAPS 灯) |
ForceNCPaintErase |
winpos.c L1739 | 非客户区强制擦除 |
| 窗口搬移 BitBlt | winpos.c L2136 | 移动时位块搬移(`DCX_WINDOW |
这些调用统一享受 DCE 缓存:系统内部取 DC 与应用取 DC 走同一条路径,这正是 DCE 设计的目的------"所有窗口绘制共享一套裁剪与缓存语义"。
8. 窗口表面创建与 GDI 关联
8.1 窗口 DC 与 GDI DC 的绑定(dclife.c)
DCE 分配时调用 DceCreateDisplayDC(windc.c L39)→ IntGdiCreateDC(dclife.c L1039):
c
HDC FASTCALL IntGdiCreateDC(
PUNICODE_STRING Driver, // L"DISPLAY"
PUNICODE_STRING pustrDevice,// NULL
PVOID pUMdhpdev, PVOID pdmInit, BOOL CreateAsIC)
{
hdc = GreOpenDCW(pustrDevice, pdmInit, NULL,
CreateAsIC ? DCTYPE_INFO : DCTYPE_DIRECT,
TRUE, NULL, NULL, pUMdhpdev);
return hdc;
}
GreOpenDCW(dclife.c L638)的绑定序列:
EngpGetPDEV(pustrDevice):按设备名找 PDEVOBJ("DISPLAY" → 主显示设备,或 NULL → 默认设备);DC_AllocDcWithHandle(GDILoObjType_LO_DC_TYPE):GDI 句柄表分配 DC 对象(GDIOBJ_AllocateObject,Lookaside 池);DC_vInitDc(pdc, iType, ppdev)(dclife.c L129):初始化 PDEV 引用、dclevel.pSurface = PDEVOBJ_pSurface(ppdev)(屏幕表面 )、IntEngInitClipObj(&pdc->co)、IntSetDefaultRegion(pdc)(prgnVis = 表面大小矩形,dcutil.c L350)、dclevel.sizl、图形能力位等;DC_InitHack:默认 DC 状态(defaultDCstate)+prgnDefault+ 默认字体;DC_bAllocDcAttr:分配 DC 属性(dcattr,含用户态可见的VisRectRegion)。
至此 HDC 完全可绘制:表面=屏幕、裁剪=整屏、状态=默认。窗口语义(原点、可见区域、附加裁剪)随后由 windc.c 的 DceSetDrawable+DceUpdateVisRgn 灌入。
8.2 位图兼容性检查 DC_bIsBitmapCompatible --- dcobjs.c L314
SelectObject(hdc, hbmp) 时(dcobjs.c NtGdiSelectBitmap L343)必须确认位图与 DC 兼容:
c
BOOL NTAPI DC_bIsBitmapCompatible(PDC pdc, PSURFACE psurf)
{
if (!(psurf->flags & API_BITMAP)) return FALSE; // 只接受 API 位图
if (psurf->hSecure != NULL) return TRUE; // DIB Section 永远兼容
if (psurf->SurfObj.hdev == (HDEV)pdc->ppdev) return TRUE; // 同 PDEV
cBitsPixel = 位图色深;
return (cBitsPixel == 1) || (cBitsPixel == pdc->ppdev->gdiinfo.cBitsPixel); // 同色深
}
窗口 DC 是显示 DC,SelectObject 位图时经此检查;失败则返回 NULL 并设 ERROR_INVALID_PARAMETER。窗口 DC 通常不直接选位图(那是内存 DC 的事),此检查保证跨 DC 类型的操作安全。
8.3 DC 清理 DC_vCleanup --- dclife.c L357
DC 对象销毁时(句柄表回收)释放全部资源:
c
DC_vCleanup(PVOID ObjectBody)
{
DC_vFreeDcAttr(pdc); // 释放 DC 属性
DC_vRestoreDC(pdc, 1); // 回滚所有 SaveDC 层
DC_vSelectSurface(pdc, NULL); // 释放表面引用(屏幕表面)
DC_vSelectFillBrush / LineBrush / Palette(NULL) // 释放画刷/笔/调色板
EBRUSHOBJ_vCleanup × 4 // 填充/线/文本/背景画刷
LFONT_ShareUnlockFont // 字体
REGION_Delete(prgnClip / prgnMeta / prgnVis / prgnRao / prgnAPI) // ★ 区域全清
IntEngFreeClipResources(&pdc->co); // CLIPOBJ
PATH_Delete(hPath);
SURFACE_ShareUnlockSurface(pSurface);
PDEVOBJ_vRelease(ppdev); // 释放 PDEV 引用
}
注意 prgnVis/prgnRao/prgnAPI/prgnClip 全部在 DC 销毁时释放------这印证了 5.8 中 GdiSelectVisRgn 只是"拷贝"区域数据(REGION_bCopy),DCE 侧计算出的临时区域由 windc.c 自行 REGION_Delete,双方所有权清晰。
8.4 驱动侧窗口对象 WNDOBJ(engwindow.c)
驱动需要感知窗口裁剪变化时(OpenGL/DDI 加速路径):
EngCreateWnd(pso, hWnd, pfn, fl, iPixelFormat) (engwindow.c L143)
├── UserGetWindowObject(hWnd)
├── EngAllocMem(EWNDOBJ) + IntEngInitClipObj
├── IntEngWndUpdateClipObj(Clip, Window)
│ ├── VIS_ComputeVisibleRegion(Window, TRUE, TRUE, TRUE) // ★ 再次合成
│ ├── IntEngUpdateClipRegion(CLIPOBJ, nCount, Buffer, rcBound) // 装载矩形
│ └── Clip->rclClient = rcClient; Clip->iUniq++
├── Clip->Hwnd / ChangeProc / Flags / PixelFormat
└── UserSetProp(Window, AtomWndObj, Clip) // 挂到窗口属性
之后每次可见区域变化:
DceUpdateVisRgn → IntEngWindowChanged(Wnd, WOC_RGN_CLIENT)(engwindow.c L97)
→ IntEngWndUpdateClipObj(重算 CLIPOBJ)
→ IntEngWndCallChangeProc(调用驱动 pfn,engwindow.c L18)
└── flChanged & Clip->Flags 过滤后:ChangeProc(WNDOBJ*, flChanged)
→ 附加 WOC_CHANGED 通知(HACK,兼容驱动习惯)
窗口销毁时 IntEngWindowChanged(Window, WOC_DELETE)(window.c L3005 调用),驱动可据此释放资源;EngDeleteWnd 摘属性并释放(engwindow.c L222)。
IntEngUpdateClipRegion(eng/clip.c L173)按矩形数选择数据布局:
count > 1:分配ENUMRECTS数组,iDComplexity = DC_COMPLEX,iFComplexity = FC_RECT4/FC_COMPLEX;count <= 1:直接使用内嵌rclBounds,DC_RECT/DC_TRIVIAL。
驱动通过 WNDOBJ_bEnum/WNDOBJ_cEnumStart(engwindow.c L258/L273)逐矩形枚举。
9. 屏幕 DC 与桌面窗口
9.1 屏幕 DC 的创建(winsta.c co_IntInitializeDesktopGraphics L262)
显示驱动初始化(InitVideo 之后)时:
c
lRet = PDEVOBJ_lChangeDisplaySettings(NULL, NULL, NULL, &gpmdev, TRUE); // 切换显示模式
if (失败 && !gbBaseVideo) { gbBaseVideo = TRUE; 重试 } // 回退基础视频
ScreenDeviceContext = IntGdiCreateDC(&DriverName, NULL, NULL, NULL, FALSE); // L285
if (!ScreenDeviceContext) { IntDestroyPrimarySurface(); return FALSE; }
GreSetDCOwner(ScreenDeviceContext, GDI_OBJ_HMGR_PUBLIC); // 公开所有权:任何进程可访问
IntCreatePrimarySurface(); // 创建屏幕主表面
hSystemBM = NtGdiCreateCompatibleDC(ScreenDeviceContext); // 系统内存 DC(位图搬移用)
NtGdiSelectFont(hSystemBM, SYSTEM_FONT);
GreSetDCOwner(hSystemBM, GDI_OBJ_HMGR_PUBLIC);
InitMetrics(); // 系统度量(屏幕尺寸等)
关键点 :ScreenDeviceContext 的 GDI 所有权设为 GDI_OBJ_HMGR_PUBLIC------它是跨进程共享的"系统 DC",任何进程的 USER 代码都能用它操作屏幕(光标、桌面背景)。
9.2 IntGetScreenDC --- winsta.c L381-L385
c
HDC FASTCALL IntGetScreenDC(VOID) { return ScreenDeviceContext; }
高频调用方(msgqueue.c):
- 光标显示/隐藏:
GreSetPointerShape(hdcScreen, ...)(L144)、GreMovePointer(hdcScreen, -1, -1)隐藏(L157); - 光标跟随鼠标移动:
GreMovePointer(hdcScreen, pt.x, pt.y)(L687、L726); - 会话切换隐藏光标(L2382)。
9.3 UserGetDesktopDC --- desktop.c L1586
c
HDC FASTCALL UserGetDesktopDC(ULONG DcType, BOOL bAltDc, BOOL ValidatehWnd)
{
UserEnterExclusive(); // GDI/DX 可能调用,需 USER 锁
if (DcType == DCTYPE_DIRECT)
{
DesktopObject = UserGetDesktopWindow(); // 桌面窗口对象
DesktopHDC = (HDC)UserGetWindowDC(DesktopObject); // ★ 走标准窗口 DC 路径
}
else
{
pMonitor = UserGetPrimaryMonitor();
DesktopHDC = IntGdiCreateDisplayDC(pMonitor->hDev, DcType, bAltDc); // 直接建显示 DC
}
UserLeave();
return DesktopHDC;
}
DCTYPE_DIRECT:桌面窗口的窗口 DC(原点=屏幕原点、可见区域=整屏),走完整 DCE 机制;- 其他(MEMORY/INFO):直接用显示器 PDEV 建显示 DC(
IntGdiCreateDisplayDC,dclife.c L1062,当前是UNIMPLEMENTED的简化实现------实际上DcType==DCTYPE_MEMORY走NtGdiCreateCompatibleDC(NULL),否则IntGdiCreateDC(NULL,NULL,NULL,NULL,(DcType==DCTYPE_INFO)))。
9.4 桌面窗口与桌面 DC
IntGetDesktopWindow(desktop.c L1391):活动桌面的pdo->DesktopWindow(句柄);UserGetDesktopWindow(desktop.c L1402):对应 PWND 对象;- 桌面窗口是窗口树的根(
spwndParent == NULL,IntIsDesktopWindow宏),类DesktopWindowClass,覆盖整个屏幕(rcWindow= 屏幕尺寸); DceUpdateVisRgn的Window==NULL分支把可见区域定为桌面窗口矩形------所以GetDC(NULL)等价于"桌面 DC"。
9.5 IntPaintDesktop --- desktop.c L1849
用给定 HDC 绘制桌面背景(壁纸/纯色):
c
IntPaintDesktop(HDC hDC)
{
GdiGetClipBox(hDC, &Rect);
WndDesktop = 桌面窗口对象;
if (有壁纸 gspv.hbmWallpaper) 按 wmFit/wmFill/wmCenter/wmTile 模式缩放平铺绘制;
else PatBlt 类背景画刷(或 Safe Mode 提示);
}
由 co_IntInitializeDesktopGraphics 启动时(winsta.c L361)、显示切换、WM_DISPLAYCHANGE(desktop.c L1484/L2819)触发。桌面 DC 的三条获取途径在此统一:IntGetScreenDC(系统)、UserGetDesktopDC(DCE 机制)、直接 IntGdiCreateDC(多显示器遍历 L357)。
10. 调用链全景(mermaid)
10.1 GetDC / BeginPaint 调用链
#mermaid-svg-XsnJ78kCgFbqP3hW{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-XsnJ78kCgFbqP3hW .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-XsnJ78kCgFbqP3hW .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-XsnJ78kCgFbqP3hW .error-icon{fill:#552222;}#mermaid-svg-XsnJ78kCgFbqP3hW .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-XsnJ78kCgFbqP3hW .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-XsnJ78kCgFbqP3hW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-XsnJ78kCgFbqP3hW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-XsnJ78kCgFbqP3hW .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-XsnJ78kCgFbqP3hW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-XsnJ78kCgFbqP3hW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-XsnJ78kCgFbqP3hW .marker{fill:#333333;stroke:#333333;}#mermaid-svg-XsnJ78kCgFbqP3hW .marker.cross{stroke:#333333;}#mermaid-svg-XsnJ78kCgFbqP3hW svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-XsnJ78kCgFbqP3hW p{margin:0;}#mermaid-svg-XsnJ78kCgFbqP3hW .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-XsnJ78kCgFbqP3hW .cluster-label text{fill:#333;}#mermaid-svg-XsnJ78kCgFbqP3hW .cluster-label span{color:#333;}#mermaid-svg-XsnJ78kCgFbqP3hW .cluster-label span p{background-color:transparent;}#mermaid-svg-XsnJ78kCgFbqP3hW .label text,#mermaid-svg-XsnJ78kCgFbqP3hW span{fill:#333;color:#333;}#mermaid-svg-XsnJ78kCgFbqP3hW .node rect,#mermaid-svg-XsnJ78kCgFbqP3hW .node circle,#mermaid-svg-XsnJ78kCgFbqP3hW .node ellipse,#mermaid-svg-XsnJ78kCgFbqP3hW .node polygon,#mermaid-svg-XsnJ78kCgFbqP3hW .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-XsnJ78kCgFbqP3hW .rough-node .label text,#mermaid-svg-XsnJ78kCgFbqP3hW .node .label text,#mermaid-svg-XsnJ78kCgFbqP3hW .image-shape .label,#mermaid-svg-XsnJ78kCgFbqP3hW .icon-shape .label{text-anchor:middle;}#mermaid-svg-XsnJ78kCgFbqP3hW .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-XsnJ78kCgFbqP3hW .rough-node .label,#mermaid-svg-XsnJ78kCgFbqP3hW .node .label,#mermaid-svg-XsnJ78kCgFbqP3hW .image-shape .label,#mermaid-svg-XsnJ78kCgFbqP3hW .icon-shape .label{text-align:center;}#mermaid-svg-XsnJ78kCgFbqP3hW .node.clickable{cursor:pointer;}#mermaid-svg-XsnJ78kCgFbqP3hW .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-XsnJ78kCgFbqP3hW .arrowheadPath{fill:#333333;}#mermaid-svg-XsnJ78kCgFbqP3hW .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-XsnJ78kCgFbqP3hW .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-XsnJ78kCgFbqP3hW .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XsnJ78kCgFbqP3hW .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-XsnJ78kCgFbqP3hW .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XsnJ78kCgFbqP3hW .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-XsnJ78kCgFbqP3hW .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-XsnJ78kCgFbqP3hW .cluster text{fill:#333;}#mermaid-svg-XsnJ78kCgFbqP3hW .cluster span{color:#333;}#mermaid-svg-XsnJ78kCgFbqP3hW 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-XsnJ78kCgFbqP3hW .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-XsnJ78kCgFbqP3hW rect.text{fill:none;stroke-width:0;}#mermaid-svg-XsnJ78kCgFbqP3hW .icon-shape,#mermaid-svg-XsnJ78kCgFbqP3hW .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XsnJ78kCgFbqP3hW .icon-shape p,#mermaid-svg-XsnJ78kCgFbqP3hW .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-XsnJ78kCgFbqP3hW .icon-shape .label rect,#mermaid-svg-XsnJ78kCgFbqP3hW .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XsnJ78kCgFbqP3hW .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-XsnJ78kCgFbqP3hW .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-XsnJ78kCgFbqP3hW :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} GetDC hWnd
BeginPaint
DCX_INTERSECTUPDATE
标志规范化
未命中
命中
RGN_AND/RGN_DIFF
重新合成
应用
user32.spec 直通
NtUserBeginPaint painting.c
NtUserGetDC windc.c L1056
NtUserGetDCEx windc.c L1016
IntBeginPaint painting.c L1506
UserGetDCEx windc.c L344
UserGetDCEx windc.c L344
DCX_USESTYLE/CLIPCHILDREN/CLIPSIBLINGS/PARENTCLIP
扫描 LEDce 缓存表
DceAllocDCE windc.c L85
DceCreateDisplayDC L39
IntGdiCreateDC dclife.c L1039
GreOpenDCW dclife.c L638
EngpGetPDEV 屏幕PDEV
DceSetDrawable L144 → GreSetDCOrg coord.c L1356
DceUpdateVisRgn windc.c L190
DceGetVisRgn L72
VIS_ComputeVisibleRegion vis.c L12
IntGdiCombineRgn region.c
GdiSelectVisRgn cliprgn.c L59
DC->prgnVis 平移至DC原点
IntEngWindowChanged engwindow.c L97
IntEngWndUpdateClipObj L51
VIS_ComputeVisibleRegion
IntEngUpdateClipRegion eng/clip.c L173
驱动 ChangeProc 通知
绘制准备 CLIPPING_UpdateGCRegion cliprgn.c L741
prgnRao = prgnVis ∩ prgnAPI
驱动逐矩形光栅化 → 屏幕表面
10.2 ReleaseDC / EndPaint 调用链
渲染错误: Mermaid 渲染失败: Parse error on line 15: ... CACHE回缓存 DCX_CACHE\|DCX_DCEEMPTY -----------------------^ Expecting 'SQE', 'TAGEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'PIPE'
10.3 窗口移动时的 DCE 联动(winpos × windc)
渲染错误: Mermaid 渲染失败: Parse error on line 3: ...erGetDCEx DCX_WINDOW|CACHE|INTERSECTRGN -----------------------^ Expecting 'SQE', 'TAGEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'PIPE'
11. 与周边模块的关系
11.1 与 painting.c(第 4 册)的关系
painting.c 是 windc.c 的最大调用方:
| painting.c 位置 | 调用 | 语义 |
|---|---|---|
IntBeginPaint L1546 |
`UserGetDCEx(W, hrgnUpdate, DCX_INTERSECTRGN | DCX_USESTYLE)` |
IntEndPaint L1608 |
UserReleaseDC(W, hdc, TRUE) |
归还 DC 并恢复可见区域 |
IntInvalidateWindows L684 |
区域与 hrgnClip/rcWindow 求交 |
更新区域同样受窗口形状约束 |
关键衔接点:更新区域(第 4 册)负责"哪些要重画",可见区域(本册)负责"哪些能画" 。IntBeginPaint 用 DCX_KEEPCLIPRGN 把 hrgnUpdate 借给 DC(所有权留在窗口,BeginPaint 后窗口置 hrgnUpdate = NULL,区域归属应用进程,EndPaint 后由应用 DeleteObject)。
11.2 与 winpos.c(第 3 册)的关系
- 首次显示 :
co_WinPosShowWindow使窗口可见后,后续 WM_PAINT 通过 DCE 机制首次为窗口计算可见区域(无独立"创建窗口表面"步骤,见 2.5); - 移动/缩放 :
- 移动前计算新旧可见区域差(
VIS_ComputeVisibleRegion+IntGdiCombineRgn); - 位块搬移用窗口 DC + 区域裁剪实现(winpos.c L2136-L2152 的"小把戏":区域不能直接 BitBlt,就把它设为裁剪区再搬外接矩形);
DceResetActiveDCEs(winpos.c 调用)同步修正所有受影响的 DCE;IntEngWindowChanged(Wnd, WOC_RGN_CLIENT)(winpos.c L2441)通知驱动;co_VIS_WindowLayoutChanged(vis.c L144)让父窗口重绘新暴露区;
- 移动前计算新旧可见区域差(
- Z 序变化:兄弟关系变化 → 下次合成可见区域时遮挡关系自动更新。
11.3 与 GDI dclife.c / cliprgn.c / dcutil.c 的关系
| GDI 侧函数 | 被 windc.c/vis.c 使用处 | 职责 |
|---|---|---|
IntGdiCreateDC(dclife.c L1039) |
DceCreateDisplayDC |
创建显示 DC |
IntGdiDeleteDC(dclife.c L892) |
DceFreeDCE |
删除 DC(含永久 DC 的 UserReleaseDC 回调) |
GreSetDCOwner(dclife.c L455) |
DceAllocDCE/DceReleaseDC/DceFreeDCE |
DC 所有权(进程私有/公开/无主) |
GreSetDCOrg(coord.c L1356) |
DceSetDrawable |
设 DC 原点与窗口矩形 |
GdiSelectVisRgn(cliprgn.c L59) |
DceUpdateVisRgn |
拷贝可见区域到 prgnVis |
CLIPPING_UpdateGCRegion(cliprgn.c L741) |
绘制准备(DC_DIRTY_RAO 时) | prgnVis∩prgnAPI → prgnRao → CLIPOBJ |
IntGdiSetHookFlags(dcutil.c L422) |
DceDeleteClipRgn/DceUnlockWindowUpdate |
置/清 DC_DIRTY_RAO(vis 重算请求) |
IntGdiCleanDC(dcstate.c L73) |
DceReleaseDC |
DC 状态复位到 defaultDCstate |
IntSetDefaultRegion(dcutil.c L350) |
DC_vInitDc/GdiSelectVisRgn(NULL) |
prgnVis = 表面大小 |
11.4 与 engwindow.c / clip.c 的关系
IntEngWindowChanged 由 windc.c(L270)与 winpos.c(L2441)触发,维护驱动侧 WNDOBJ 的 CLIPOBJ 并调用驱动的 ChangeProc。这是"USER 可见区域"向"DDI 驱动裁剪"的桥。EngCreateWnd 是驱动注册入口,AtomWndObj("SysWNDO")属性把 EWNDOBJ 挂到窗口。
11.5 与 winsta.c / desktop.c(第 18 册)的关系
ScreenDeviceContext(desktop.c L53 全局):屏幕 DC 的根,co_IntInitializeDesktopGraphics创建(winsta.c L285),IntGetScreenDC返回(winsta.c L382);UserGetDesktopDC(desktop.c L1586):桌面窗口 DC 的对外接口;- 桌面切换(
co_IntShowDesktop/IntHideDesktop):桌面窗口的可见性驱动整棵窗口树可见区域的连锁变化。
12. 可见区域与 DC 裁剪协作小结
把本册两大主题串成一张"职责对照表":
| 层面 | 区域 | 谁计算 | 谁消费 | 坐标空间 |
|---|---|---|---|---|
| USER 可见性 | hrgnVis 概念(vis.c 结果) |
VIS_ComputeVisibleRegion |
DceGetVisRgn/WNDOBJ/winpos |
屏幕坐标 |
| DCE 附加裁剪 | Dce->hrgnClip |
UserGetDCEx(ClipRegion 参数/INTERSECTUPDATE) |
DceUpdateVisRgn 求交/求差 |
屏幕坐标 |
| GDI 可见区域 | DC->prgnVis |
GdiSelectVisRgn(拷贝+平移) |
绘制裁剪 | DC 坐标 |
| GDI 应用裁剪 | DC->dclevel.prgnClip/prgnAPI |
IntSelectClipRgn(SelectClipRgn) |
CLIPPING_UpdateGCRegion |
DC 坐标 |
| 最终可绘制 | DC->prgnRao |
CLIPPING_UpdateGCRegion |
驱动(CLIPOBJ/枚举) | DC 坐标 |
一句话总结 :vis.c 用区域差运算回答"窗口露多少",windc.c 用 DCE 缓存回答"画在哪块 DC 上",cliprgn.c 把两者合成 GDI 的最终裁剪------三者合起来,就是"窗口显示"中最底层的几何保障:一个窗口无论如何都不会画到别人脸上,也不会把像素画到屏幕之外。
13. 源码索引
13.1 本册核心文件与行号
| 文件 | 关键符号 | 行号 |
|---|---|---|
win32ss/user/ntuser/vis.c |
VIS_ComputeVisibleRegion |
L12 |
co_VIS_WindowLayoutChanged |
L144 | |
win32ss/user/ntuser/vis.h |
两个函数声明 | L12-L13 |
win32ss/user/ntuser/windc.c |
InitDCEImpl |
L27 |
DceCreateDisplayDC |
L39 | |
DceGetDceFromDC |
L51 | |
DceGetVisRgn |
L70 | |
DceAllocDCE |
L85 | |
DceSetDrawable |
L144 | |
DceDeleteClipRgn |
L169 | |
DceUpdateVisRgn |
L190 | |
DceReleaseDC |
L278 | |
UserGetDCEx |
L344 | |
DceFreeDCE |
L671 | |
DceFreeWindowDCE |
L724 | |
DceFreeClassDCE |
L804 | |
DceFreeThreadDCE |
L822 | |
DceEmptyCache |
L843 | |
DceResetActiveDCEs |
L858 | |
IntWindowFromDC |
L936 | |
UserReleaseDC |
L960 | |
UserGetWindowDC |
L989 | |
UserGethWnd |
L995 | |
NtUserGetDCEx |
L1016 | |
NtUserGetWindowDC |
L1050 | |
NtUserGetDC |
L1056 | |
NtUserSelectPalette |
L1075 | |
DceLockWindowUpdate |
L1093 | |
DceUnlockWindowUpdate |
L1143 | |
win32ss/user/ntuser/dce.h |
DCE 结构 / DCE_TYPE / DCX 内部标志 | L10-L57 |
13.2 关联文件
| 文件 | 关键符号 | 行号 |
|---|---|---|
win32ss/gdi/ntgdi/dclife.c |
IntGdiCreateDC |
L1039 |
GreOpenDCW |
L638 | |
DC_AllocDcWithHandle |
L67 | |
DC_vInitDc |
L129 | |
DC_vCleanup |
L357 | |
GreSetDCOwner/DC_vSetOwner |
L455/L414 | |
IntGdiDeleteDC |
L892 | |
IntGdiCreateDisplayDC |
L1062 | |
win32ss/gdi/ntgdi/dc.h |
DC 结构(prgnVis/prgnRao/prgnAPI) | L95-L137 |
DC_LockDc/DC_vSelectSurface |
L218/L246 | |
win32ss/gdi/ntgdi/dcobjs.c |
DC_bIsBitmapCompatible |
L314 |
NtGdiSelectBitmap |
L343 | |
win32ss/gdi/ntgdi/cliprgn.c |
GdiSelectVisRgn |
L59 |
IntGdiReleaseRaoRgn/IntGdiReleaseVisRgn |
L18/L28 | |
CLIPPING_UpdateGCRegion |
L741 | |
win32ss/gdi/ntgdi/dcutil.c |
IntSetDefaultRegion |
L350 |
IntGdiSetHookFlags |
L422 | |
win32ss/gdi/ntgdi/dcstate.c |
IntGdiCleanDC |
L73 |
win32ss/gdi/ntgdi/coord.c |
GreSetDCOrg |
L1356 |
win32ss/gdi/eng/engwindow.c |
EngCreateWnd |
L143 |
IntEngWindowChanged |
L97 | |
IntEngWndUpdateClipObj |
L51 | |
IntEngWndCallChangeProc |
L18 | |
EngDeleteWnd/WNDOBJ_bEnum/WNDOBJ_cEnumStart/WNDOBJ_vSetConsumer |
L222/L258/L273/L290 | |
win32ss/gdi/eng/clip.c |
IntEngUpdateClipRegion/IntEngInitClipObj/IntEngFreeClipResources |
L173/L158/L164 |
win32ss/gdi/eng/engobjects.h |
EWNDOBJ/XCLIPOBJ 结构 | L42-L105 |
win32ss/user/ntuser/winsta.c |
co_IntInitializeDesktopGraphics |
L262 |
IntGetScreenDC |
L382 | |
win32ss/user/ntuser/desktop.c |
UserGetDesktopDC |
L1586 |
IntGetDesktopWindow/UserGetDesktopWindow |
L1391/L1402 | |
IntPaintDesktop |
L1849 | |
win32ss/user/ntuser/painting.c |
IntBeginPaint/IntEndPaint |
L1506/L1602 |
win32ss/user/ntuser/winpos.c |
ForceNCPaintErase 的 UserGetDCEx | L1739 |
| 移动搬移的 UserGetDCEx+BitBlt | L2136 | |
win32ss/user/ntuser/simplecall.c |
ONEPARAM_ROUTINE_RELEASEDC/WINDOWFROMDC |
L306/L240 |
win32ss/user/user32/windows/dc.c |
ReleaseDC/WindowFromDC |
L39/L58 |
win32ss/user/user32/user32.spec |
GetDC/GetDCEx/GetWindowDC 直通 | L283/L284/L396 |
13.3 调试提示
- 调试通道:
UserDce(windc.cDBG_DEFAULT_CHANNEL(UserDce))、UserWinpos(vis.c)、GdiClipRgn(cliprgn.c)、EngWnd(engwindow.c); - 观察 DCE 分配/释放:
TRACE("Alloc DCE's! %d\n", DCECount)(windc.c L101)、TRACE("Freed DCE's! %d \n", DCECount)(L716); - 应用泄漏 DC:serial 中出现
err: [%p] GetDC() without ReleaseDC()!(windc.c L793)即窗口销毁时仍有未释放的缓存 DC; - 可见区域问题:打开
UserWinpos通道可见VIS_ComputeVisibleRegion路径;GdiClipRgn通道可见 CLIPOBJ 矩形枚举; - 常见故障排查:"画不出来"→ 先查
prgnVis是否为空(LockWindowUpdate、窗口不可见、父链断);"画到别处"→ 查ptlDCOrig(原点错误,DceSetDrawable/DceResetActiveDCEs未生效)。
本文基于 ReactOS 源代码(截至 2026 年 8 月)逐函数分析,所有行号均对应实际源码。
核心源码:
win32ss/user/ntuser/vis.c、win32ss/user/ntuser/windc.c、win32ss/user/ntuser/dce.h配套:总览《ReactOS 窗口系统架构分析》4.3 节;下一册 _6 将分析非客户区绘制(nonclient.c)。