ReactOS 窗口系统分析(5):可见区域与窗口 DC — vis.c + windc.c

ReactOS 窗口系统分析(5):可见区域与窗口 DC --- vis.c + windc.c

本文是"窗口系统三大主线之窗口显示"系列的第 5 册(总册编号 _5),承接:

  • 第 1 册(window.c):窗口对象 WND 的创建与销毁;
  • 第 3 册(winpos.c):窗口位置、可见性与 Z 序;
  • 第 4 册(painting.c):更新区域(hrgnUpdate)与 WM_PAINT 绘制协议。

本册回答窗口显示链路中"画在哪、能画多少"两个问题:

  1. 可见区域(vis.c) :一个窗口实际露出多少面积(不被父窗口裁剪、不被兄弟/子窗口遮挡),以 GDI 区域对象(PREGION)形式表示;
  2. 窗口 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.cUserGetDesktopDC/桌面窗口)
  • win32ss/user/ntuser/winsta.cIntGetScreenDC/屏幕 DC)
  • win32ss/gdi/ntgdi/dclife.cIntGdiCreateDC/DC 生命周期)
  • win32ss/gdi/ntgdi/dc.h(DC 的 prgnVis/prgnRao/prgnClip
  • win32ss/gdi/ntgdi/cliprgn.cGdiSelectVisRgn/CLIPPING_UpdateGCRegion
  • win32ss/gdi/ntgdi/dcutil.c/dcstate.c/coord.c(DC 工具)
  • win32ss/gdi/eng/engwindow.cEngCreateWnd/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;
  • 下游锚点DceUpdateVisRgnGdiSelectVisRgn,把 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 链表 LEDcewindc.c 静态变量),系统内所有窗口 DC 都在这里登记;

  • DCE 按其 DCXFlags 分为三类(dce.hDCE_TYPE):

    类型 对应窗口类样式 生命周期 拥有者
    DCE_CACHE_DC 普通窗口(无 CS_OWNDC/CS_CLASSDC) 全局共享缓存,用完即还 无(ptiOwner 临时)
    DCE_CLASS_DC CS_CLASSDC(一类一 DC) 挂在 pcls->pdce,类卸载时销毁
    DCE_WINDOW_DC CS_OWNDC(一窗一 DC) 窗口销毁时清理 窗口所属线程
  • 缓存的复用判据hwndCurrent(DC 当前指向的窗口)+ DCXFlags & DCX_CACHECOMPAREMASK 完全一致才命中(windc.c L453-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)"的区域;
  • 驱动实际逐矩形绘制时枚举 CLIPOBJIntEngUpdateClipRegionWNDOBJ_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_ComputeVisibleRegionIntInvalidateWindows 中(hrgnClip 字段)
NtUserReleaseDC 不存在;用户态 ReleaseDCNtUserxReleaseDC(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 IntGetScreenDCScreenDeviceContext 一致

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 创建时绑定的是屏幕 PDEVEngpGetPDEV 按 "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)

分配

  1. ExAllocatePoolWithTag(PagedPool, sizeof(DCE), USERTAG_DCE) 分配 DCE 本体;
  2. DceCreateDisplayDC() 创建显示 DC(失败则回滚释放 DCE);
  3. DCECount++(全局计数,调试用);
  4. 初始化字段:hwndCurrent = Window ? UserHMGetHandle(Window) : NULLpwndOrg = pwndClip = WindowhrgnClip/hrgnClipPublic/hrgnSavedVis = NULLppiOwner = NULL
  5. InsertTailList(&LEDce, &pDce->List) 入链表;
  6. DCU_SetDcUndeletable(pDce->hDC):标记 DC 不可被应用 DeleteObject 删除(GDI 句柄保护,dcutil.c);
  7. 所有权
    • DCE_WINDOW_DC/DCE_CLASS_DCptiOwner = GetW32ThreadInfo()(DC 归属创建线程,进程清理时可回收);
    • 其他(缓存 DC)→ GreSetDCOwner(hDC, GDI_OBJ_HMGR_NONE)(无主),ptiOwner = NULL
  8. 类型标志
    • DCE_CACHE_DCDCXFlags = 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 == NULLGetDC(NULL) 取屏幕 DC):直接建桌面窗口矩形区域,不经过 vis.c(屏幕总是完全可见);
  • 常规DceGetVisRgnVIS_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)

释放流程:

  1. 状态检查DCX_DCEBUSY != (DCXFlags & (INDESTROY|DCEEMPTY|DCEBUSY)) → 返回 0(不在占用态,非法释放);
  2. EndPaint 恢复可见区域DceUpdateVisRgn(dce, dce->pwndOrg, dce->DCXFlags)(BeginPaint 时可见区域被 hrgnUpdate 收窄,EndPaint 恢复完整可见区域);
  3. 清附加裁剪(INTERSECT|EXCLUDE) && (CACHE || EndPaint)DceDeleteClipRgn
  4. 缓存清理 (仅 DCX_CACHE):
    • !DCX_NORESETATTRSIntGdiCleanDC(dce->hDC)(dcstate.c L73:DC_vCopyState(defaultDCstate, dc) 把 DC 复位到默认状态,删除 Path、prgnRao、prgnAPI;注释明确"prgnVis 不用清,UserGetDC 会立即更新");
    • DCX_DCEDIRTY:清空缓存项(hwndCurrent=0pwndOrg=pwndClip=NULLDCXFlags = DCX_CACHE|DCX_DCEEMPTY),因为脏项不再可靠;
    • GreSetDCOwner(hDC, GDI_OBJ_HMGR_NONE) 交还 GDI 所有权,ptiOwner=ppiOwner=NULL
  5. 返回 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_PARENTDCDCX_PARENTCLIP;非 CACHE 且非 CS_OWNDC 时:CS_CLASSDC→用/建类 DCE(pcls->pdce),否则→DCX_CACHE("廉价酒");再按窗口样式WS_CLIPSIBLINGSDCX_CLIPSIBLINGSWS_CLIPCHILDREN(非最小化)→DCX_CLIPCHILDREN;最小化且有类图标→强制 DCX_CACHE
DCX_USESTYLEDCX_WINDOW 只补 WS_CLIPSIBLINGSDCX_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_CACHEWnd->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_WINDOWDCX_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_INDESTROYIntWindowFromDC/UserReleaseDC 等并发访问安全地忽略该 DCE。

5.16 DceFreeWindowDCE --- L724-L802(窗口销毁清理)

窗口销毁时(co_UserFreeWindowDceFreeWindowDCE)逐项处理挂在该窗口上的 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_DCEEMPTYhwndCurrent=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)→ NtUserxWindowFromDCIntWindowFromDC: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 标志(GetDCExDCX_CLIPSIBLINGSGetDC 对非子窗口的强制);
  • 是否扣子取决于调用方ClipChildrenWS_CLIPCHILDREN 样式 → UserGetDCExDCX_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 计算出的可见区域最终有三份消费者:

  1. DC->prgnVis (cliprgn.c GdiSelectVisRgn 拷贝 + 平移)------GDI 绘制裁剪基础;
  2. WNDOBJ->CLIPOBJ (engwindow.c IntEngWndUpdateClipObj 重新计算 + IntEngUpdateClipRegion 装载)------驱动 DirectDraw/OpenGL 等使用;
  3. winpos.c 的搬移判断co_WinPosSetWindowPos 比较移动前后的可见区域)------决定哪些像素可搬、哪些需要重画。

6.5 可见区域的失效与重算时机

可见区域不是常驻字段(WND 结构没有 hrgnVis 成员),而是每次 GetDC 时按需计算 的派生量。它的"失效"通过 DCE 的 DCX_DCEDIRTY 位 + GDI DC 的 DC_DIRTY_RAO 位表达:

失效事件 置位动作 重算动作
DceDeleteClipRgn(清理附加裁剪) DCX_DCEDIRTY + DCHF_INVALIDATEVISRGNDC_DIRTY_RAO 下次 UserGetDCExDceUpdateVisRgn
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 IntBeginPaintUserGetDCEx(INTERSECTUPDATE) 可见区域 ∩ 更新区域
绘制结束 ReleaseDC/EndPaintUserReleaseDCDceReleaseDC 复位 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)的绑定序列:

  1. EngpGetPDEV(pustrDevice):按设备名找 PDEVOBJ("DISPLAY" → 主显示设备,或 NULL → 默认设备);
  2. DC_AllocDcWithHandle(GDILoObjType_LO_DC_TYPE):GDI 句柄表分配 DC 对象(GDIOBJ_AllocateObject,Lookaside 池);
  3. 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、图形能力位等;
  4. DC_InitHack:默认 DC 状态(defaultDCstate)+ prgnDefault + 默认字体;
  5. 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_COMPLEXiFComplexity = FC_RECT4/FC_COMPLEX
  • count <= 1:直接使用内嵌 rclBoundsDC_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_MEMORYNtGdiCreateCompatibleDC(NULL),否则 IntGdiCreateDC(NULL,NULL,NULL,NULL,(DcType==DCTYPE_INFO)))。

9.4 桌面窗口与桌面 DC

  • IntGetDesktopWindow(desktop.c L1391):活动桌面的 pdo->DesktopWindow(句柄);
  • UserGetDesktopWindow(desktop.c L1402):对应 PWND 对象;
  • 桌面窗口是窗口树的根(spwndParent == NULLIntIsDesktopWindow 宏),类 DesktopWindowClass,覆盖整个屏幕(rcWindow = 屏幕尺寸);
  • DceUpdateVisRgnWindow==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 册)负责"哪些要重画",可见区域(本册)负责"哪些能画"IntBeginPaintDCX_KEEPCLIPRGNhrgnUpdate 借给 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.c DBG_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.cwin32ss/user/ntuser/windc.cwin32ss/user/ntuser/dce.h

配套:总览《ReactOS 窗口系统架构分析》4.3 节;下一册 _6 将分析非客户区绘制(nonclient.c)。

相关推荐
OPEN-F1 小时前
Python进阶教程:装饰器与生成器深入
开发语言·python
千里码aicood2 小时前
基于Python的电商数据采集系统(大数据+爬虫)
开发语言·python
_Narcissus_3 小时前
动态规划初探(含完整代码实现)
c语言·数据结构·c++·算法·动态规划·背包问题·最短路径
陈皮波比茶3 小时前
Python项目实战-网络机器人(爬虫)
开发语言·网络·爬虫·python·机器人
Cerman3 小时前
QEMU启动OVMF shell超时分析
c语言·windows·经验分享
法哥20124 小时前
用 C++ 控制 CMW100,到底在干什么?
开发语言·c++
QH139292318804 小时前
NVIDIA H200 H300服务器
运维·服务器·开发语言·网络·信息与通信
OPEN-F4 小时前
Python进阶教程:单元测试与代码质量
开发语言·python·单元测试
阿里嘎多学长4 小时前
2026-08-20 GitHub 热点项目精选
开发语言·程序员·github·代码托管