ReactOS 窗口系统分析(7):分层窗口与绘制辅助 — layered.c + draw.c

ReactOS 窗口系统分析(7):分层窗口与绘制辅助 --- layered.c + draw.c

系列:ReactOS 窗口系统分析(窗口显示主线,第 7 册,也是该主线收官之册)

对应总览:《ReactOS 窗口系统架构分析》第 4.5 节「分层窗口 --- layered.c」

核心源码:

  • win32ss/user/ntuser/layered.c(427 行,本册主角之一:分层窗口)
  • win32ss/user/ntuser/draw.c(1137 行,本册主角之二:绘制辅助)
  • win32ss/user/ntuser/painting.cUserDrawCaptionText/UserDrawCaption,标题绘制)
  • win32ss/user/ntuser/cursoricon.cUserDrawIconEx/NtUserDrawIconEx,图标绘制)
  • win32ss/user/ntuser/defwnd.cDrawTextW,文本绘制入口)
  • win32ss/gdi/ntgdi/bitblt.cNtGdiAlphaBlend/NtGdiTransparentBlt/NtGdiStretchBlt
  • win32ss/gdi/eng/alphablend.cIntEngAlphaBlend/EngAlphaBlend,混合引擎)
  • win32ss/gdi/dib/DIB_AlphaBlend 像素级混合)
  • win32ss/user/user32/windows/window.c(用户态 UpdateLayeredWindow/UpdateLayeredWindowIndirect
  • win32ss/user/user32/windows/draw.c(用户态 DrawFocusRect 等)
  • 关联:winpos.cco_WinPosSetWindowPos 移动分层窗口)、msgqueue.c(透明命中测试)、nonclient.c/menu.cDrawEdge/DrawFrameControl 调用者)、prop.cUserGetProp/UserSetProp 承载分层属性)、ntuser.cAtomLayer 全局原子)

目录

  1. 前言与本册定位
  2. 概述:分层合成与绘制辅助两条线
  3. 设计动机
  4. 核心数据结构
  5. [layered.c 核心函数逐一展开](#layered.c 核心函数逐一展开)
  6. 分层合成算法
  7. 分层窗口与普通窗口的绘制差异
  8. [draw.c 绘制辅助实现细节](#draw.c 绘制辅助实现细节)
  9. [标题文字、图标与聚焦矩形的绘制(painting.c + cursoricon.c + user32)](#标题文字、图标与聚焦矩形的绘制(painting.c + cursoricon.c + user32))
  10. 调用链(mermaid)
  11. 与其他模块的关系
  12. 源码索引
  13. 总结

1. 前言与本册定位

窗口的"显示"由五件事共同完成:创建窗口对象 (window.c,分册 _1)、注册窗口类 (class.c,分册 _2)、摆放窗口并决定可见性 (winpos.c,分册 _3)、按 WM_PAINT 协议把内容画到屏幕 (painting.c + windc.c + vis.c,分册 _4/_5)、绘制非客户区(nonclient.c,分册 _6)。本册(_7)是该主线的收官之册,讲述最后两块拼图:

  1. 分层窗口(layered.c) :一种不走 WM_PAINT 协议的"特殊显示"路径。窗口内容不是由应用在 WM_PAINT 里画到窗口表面,而是应用直接提供一个源 DC 的内容,由内核用 AlphaBlend(逐像素 alpha 混合)把内容合成到窗口上。典型应用:任务栏缩略图、半透明 OSD、动画光标形状的异形窗口、工具提示阴影等。

  2. 绘制辅助(draw.c) :一组"系统自用的绘制工具函数"。它们不是独立的系统调用,而是被 nonclient.c(画标题按钮、边框)、menu.c(画菜单项)、painting.c(画标题文字)等内核窗口绘制代码调用的公共例程:DrawEdge(3D 边缘)、DrawFrameControl(按钮/标题/滚动/菜单四种控件符号)、FillRect/FrameRect(填充与边框)、IntDrawRectEdge/IntDrawDiagEdge(边缘绘制的核心实现)。它们体现了 Win32k 内部"把用户态 GDI 常用 API 也做一份内核版本"的设计------因为非客户区的绘制发生在内核的 DefWindowProc 路径里,无法依赖用户态 gdi32。

本册将按如下顺序展开:

  • 先看整体架构设计动机(为什么需要分层合成;为什么系统内部需要一套内核态绘制辅助);
  • 再看核心数据结构LRD_PROP 属性块、AtomLayer 全局原子、WS_EX_LAYERED/LWA_*/ULW_* 标志、BLENDFUNCTION/AC_SRC_ALPHA 混合语义;
  • 然后逐一展开 layered.c 的全部 8 个函数 (含 NtUserUpdateLayeredWindowIntUpdateLayeredWindowI 的完整合成管线);
  • 接着剖析分层合成算法 :从 NtGdiAlphaBlend(ntgdi)到 IntEngAlphaBlend/EngAlphaBlend(eng)再到 DIB_AlphaBlend(dib)的三级 GDI 管道,以及 ColorKey 透明的实现;
  • 逐一展开 draw.c 的全部 14 个函数,穿插 nonclient.c/menu.c 的调用点;
  • 之后补充标题文字、图标、聚焦矩形 三个"绘制辅助的近亲"(painting.c 的 UserDrawCaptionText/UserDrawCaption、cursoricon.c 的 UserDrawIconEx、user32 的 DrawFocusRect);
  • 最后给出 mermaid 调用链与 bitblt/eng/vis/winpos 的关系源码索引

阅读约定 :文中出现的行号均为本仓库源码的实际行号(截至 2026 年 8 月)。所有描述均基于真实源码,未做任何虚构。特别提醒:任务描述/总览中提到的 IntLayeredAlphaBlendIntUpdateWindowPosIntDrawFocusRectIntDrawCaptionTextIntFillRectIntFrameRectIntDrawIconExIntDrawText 等函数名在当前源码中并不存在,本册按真实函数名逐一对应介绍(详见各节)。


2. 概述:分层合成与绘制辅助两条线

2.1 分层窗口合成管线(UpdateLayeredWindow)

先给出最核心的一条路径------应用调用 UpdateLayeredWindow 更新分层窗口内容的完整管线:

复制代码
┌──────────────────────── 应用线程(用户态) ────────────────────────┐
│                                                                    │
│  UpdateLayeredWindow(hWnd, hdcDst, pptDst, psize, hdcSrc, ...)     │
│      └─ UpdateLayeredWindowIndirect 同理                           │
│      └─ NtUserUpdateLayeredWindow ──→ 内核                         │
│                                                                    │
└────────────────────────────────────────────────────────────────────┘
        │ 系统调用(UserEnterExclusive 独占锁 + SEH2 探测用户指针)
        ▼
┌──────────────────────────── win32k.sys ────────────────────────────┐
│  layered.c: NtUserUpdateLayeredWindow(L339)                      │
│    └─ 参数校验(GetLayeredStatus / dwFlags / WS_EX_LAYERED)       │
│    └─ 组装 UPDATELAYEREDWINDOWINFO                                 │
│    └─ IntUpdateLayeredWindowI(L100)                              │
│        ├─ 坐标换算:rcWindow/rcClient,WS_CHILD 时相对父客户区     │
│        ├─ pptDst → 位移量(清 SWP_NOMOVE)                         │
│        ├─ psize  → 尺寸变化(清 SWP_NOSIZE,校验非正/ULW_EX_NORESIZE)│
│        ├─ hdc = UserGetDCEx(pWnd, NULL, DCX_USESTYLE) 或 hdcDst    │
│        ├─ 创建两张兼容位图 hbmSrc/hbmDst + 兼容 DC hdcBuffer       │
│        ├─ NtGdiStretchBlt(hdcBuffer ← hdcSrc, SRCCOPY, color_key)  │
│        │     ↑ 源内容 + ColorKey 透明(ULW_COLORKEY 时 crKey)     │
│        ├─ prcDirty → NtGdiPatBlt(BLACKNESS) 清除脏区(FIXME)      │
│        ├─ NtGdiAlphaBlend(hdc ← hdcBuffer, blend)                  │
│        │     ↑ 关键一步:与目标 DC(窗口表面/屏幕)做 alpha 混合   │
│        │     └─ ntgdi/bitblt.c NtGdiAlphaBlend                     │
│        │          └─ eng/alphablend.c IntEngAlphaBlend             │
│        │               ├─ DrvAlphaBlend(显示驱动钩子)            │
│        │               └─ EngAlphaBlend → DIB_AlphaBlend          │
│        └─ co_WinPosSetWindowPos(pWnd, ..., SWP_* 标志)             │
│              ↑ 应用新位置/新尺寸(SWP_NOREDRAW,不触发重绘)       │
└────────────────────────────────────────────────────────────────────┘

要点:分层窗口没有独立的"分层表面" (见 4.6 节),每次 UpdateLayeredWindow 都是"源 DC → 内存位图 → AlphaBlend → 窗口 DC"的即时合成;窗口移动/缩放走 co_WinPosSetWindowPos 且带 SWP_NOREDRAW,不触发 WM_PAINT 重绘。

2.2 绘制辅助的调用全景

draw.c 不是独立功能模块,而是"工具库"。它的全部函数被三类内核绘制路径使用:

复制代码
非客户区绘制(nonclient.c,分册 _6)
  ├─ NC_DrawCaption → DrawFrameControl(hDC, &rc, DFC_CAPTION, flags)   [nonclient.c L837/856/881]
  ├─ NC_DrawFrame  → DrawEdge(hDC, &rc, EDGE_RAISED, BF_RECT|BF_ADJUST) [nonclient.c L982]
  │                   DrawEdge(hDC, &rc, BDR_SUNKENINNER, ...)          [nonclient.c L987]
  └─ ... → UserDrawCaption(painting.c L2253)→ UserDrawCaptionText/UserDrawIconEx

菜单绘制(menu.c,分册 _11/_12)
  ├─ DrawFrameControl(hdc, &r, DFC_CAPTION, flags)                      [menu.c L1773]
  └─ DrawEdge(hdc, &rect, BDR_SUNKENOUTER, BF_RECT)                     [menu.c L2332]

标题栏绘制(painting.c)
  └─ UserDrawCaption(L2253)
       ├─ UserDrawCaptionText(L2146)→ UserExtTextOutW / DrawTextW
       └─ UserDrawIconEx(cursoricon.c L1690)→ IntEngAlphaBlend / IntEngStretchBlt

通用绘制(DefWindowProc 内部)
  ├─ FillRect / FrameRect(WM_ERASEBKGND 擦背景、画边框)
  └─ DrawTextW(defwnd.c L16,画标题文本/菜单文本)

用户态(user32.dll)
  └─ DrawFocusRect(user32/windows/draw.c L1608):PATINVERT XOR 聚焦矩形

两条线在本册交汇于一点:分层窗口是"合成的极端" (内容完全由应用提供,系统只负责混合),绘制辅助是"绘制的极端"(内容完全由系统提供,应用只负责观察)。一外一内,构成窗口显示主线的收官。


3. 设计动机

3.1 为什么需要分层窗口

普通窗口(分册 _4/_5)的显示是协议式的:系统记账(更新区域)→ 应用收 WM_PAINT → 应用绘制。这个模型有两个固有弱点:

  1. 不透明:普通窗口的内容是"画在"窗口表面上的,覆盖即遮挡,无法做到半透明、无法让下层内容透上来。
  2. 异步延迟:内容变化要经过"标脏 → 消息 → 绘制"的完整循环,不适合需要"每帧直接推内容"的场景(视频、动画、游戏)。

分层窗口(WS_EX_LAYERED)打破这两个限制,提供三种能力(对应三组标志):

能力 标志 机制
整窗透明度 LWA_ALPHA / ULW_ALPHA 整个窗口按统一 alpha 值(0~255)与屏幕合成
每像素透明度 ULW_ALPHA + AC_SRC_ALPHA 源位图每个像素自带 alpha 通道,逐像素混合
颜色键透明 LWA_COLORKEY / ULW_COLORKEY 源中等于指定颜色的像素完全透明(不绘制)

典型应用:半透明 OSD/提示、异形窗口(ColorKey 抠图)、窗口阴影、淡入淡出动画(任务栏缩略图、最小化动画)、实时内容(视频播放器)。

3.2 为什么系统内部需要一套内核态绘制辅助

DrawEdge/DrawFrameControl/FillRect/DrawFocusRect 在用户态 gdi32/user32 里都有对应 API,但 ReactOS 在内核态又实现了一份(draw.c),原因有三:

  1. 非客户区绘制发生在内核 :标题栏按钮、边框由 DefWindowProc 的 WM_NCPAINT 路径绘制(nonclient.c),该路径在内核态执行,无法调用用户态 gdi32.dll 的 DrawEdge。必须由内核直接发 GDI 调用(NtGdiSelectPen/NtGdiLineTo/NtGdiPatBlt/GreTextOutW 等)。
  2. 绘制的是系统语义 :边缘的颜色来自系统颜色表(COLOR_BTNHIGHLIGHT 等),按钮符号来自 Marlett 字体------这些是"系统外观"的组成部分,理应集中在内核一处实现,保证系统所有 UI(标题按钮、菜单项、滚动条按钮)外观一致。
  3. 性能与减少切换IntDrawRectEdge 这类内核实现直接操作内核 GDI 对象(NtGdiGetStockObject(DC_PEN) 等),避免了"内核判 WM_NCPAINT → 回调用户态 → 再进内核"的往返。

注意 draw.c 文件头的注释:Original code from Wine see user32/windows/draw.c------这些实现最初从 Wine 的用户态 user32/windows/draw.c 移植而来(Ported from WINE20020904),被"搬进内核"以支撑系统绘制。这是 ReactOS 特有的架构决策,Windows 原生内核并不包含这些代码(Windows 的非客户区绘制在 win32k 里是另一套实现)。

3.3 两条线的共同目标

从分册 _1 到 _7,"窗口显示"主线的完整回答是:

  • 窗口是什么(_1/_2);
  • 窗口在哪、是否可见(_3);
  • 窗口的常规内容怎么画(_4/_5);
  • 窗口的"壳"(非客户区)怎么画(_6);
  • 窗口的非常规内容 (分层合成)怎么画 + 系统自身绘制所需的工具函数(_7,本册)。

4. 核心数据结构

4.1 LRD_PROP:分层窗口属性块(layered.c L13-20)

c 复制代码
typedef struct _LRD_PROP
{
   COLORREF Key;        // 颜色键(ColorKey),ULW_COLORKEY/LWA_COLORKEY 用
   BYTE     Alpha;      // 整窗 alpha(0~255),LWA_ALPHA 用
   BYTE     is_Layered; // 是否已分层(已调用过 SetLayeredWindowAttributes 且成功)
   BYTE     NoUsed[2];  // 对齐填充
   DWORD    Flags;      // 最近一次设置的 LWA_* 标志
} LRD_PROP, *PLRD_PROP;

要点:

  • 存放位置 :不是 WND 的成员,而是挂在窗口的属性表PropListHead)上,属性名是全局原子 AtomLayerL"SysLayer")。这一设计让"分层状态"成为窗口的一个可选附加属性------绝大多数窗口没有分层属性,不占 WND 的空间;需要时才分配。
  • 分配方式ExAllocatePoolWithTag(PagedPool, sizeof(LRD_PROP), USERTAG_REDIRECT)(L64),从分页池 分配(窗口属性生命周期内可被换出,访问时由内核保证驻留)。标签 USERTAG_REDIRECT 用于池泄漏追踪。
  • 两个布尔语义is_LayeredWS_EX_LAYERED两个不同的开关WS_EX_LAYERED 是扩展样式位(SetWindowLong/CreateWindowEx 设置);is_Layered 是"是否真正被初始化过"的运行时标志(由 IntSetLayeredWindowAttributes 置 1)。NtUserUpdateLayeredWindowGetLayeredStatus() 判断"已分层则拒绝再走属性路径"(见 5.5 节),window.c 在清除 WS_EX_LAYERED 时把 is_Layered 清 0(见 5.8 节)。

4.2 AtomLayer:全局原子(ntuser.c L21、L68)

c 复制代码
ATOM AtomLayer;         // Window Layer atom.                     [ntuser.c L21]

/* InitUserAtoms 中: */
AtomLayer = IntAddGlobalAtom(L"SysLayer", TRUE);                  [ntuser.c L68]

IntAddGlobalAtom 把字符串 "SysLayer" 注册为全局原子,返回的 ATOM 存放在全局变量 AtomLayerwindow.hextern 声明)。layered.c 通过 UserGetProp(pWnd, AtomLayer, TRUE)/UserSetProp(pWnd, AtomLayer, (HANDLE)pLrdProp, TRUE)(prop.c 的属性读写接口,第三个参数表示"系统属性")读写属性块。

AtomMessage"Message",消息窗口类原子)、AtomFlashWndState"FlashWState",闪烁状态)等并列,都是 win32k 启动时 InitUserAtoms() 注册的系统原子。

4.3 WS_EX_LAYERED 标志

WS_EX_LAYERED(0x00080000)是窗口扩展样式位,语义:

  • 使能开关 :只有设置了该位,SetLayeredWindowAttributes/UpdateLayeredWindow 才被接受(layered.c 三处检查:L54、L255、L323、L403)。
  • 创建时设置CreateWindowEx 时带上 WS_EX_LAYERED 即可创建分层窗口(也可事后用 SetWindowLong(GWL_EXSTYLE) 加上/去掉)。
  • 配套位 WS_EX_TRANSPARENT (0x00000020):与 WS_EX_LAYERED 组合时产生"点击穿透"(见 7.3 节)。注意二者必须同时 设置才生效(msgqueue.c L82、winpos.c L2938 都是 == (WS_EX_LAYERED|WS_EX_TRANSPARENT) 的精确比较)。

4.4 LWA_* 与 ULW_* 标志

两个 API 系列使用两组不同但互补的标志:

SetLayeredWindowAttributes 系列(LWA_*)

常量 含义 在 IntSetLayeredWindowAttributes 中的处理
LWA_COLORKEY 0x1 用 crKey 做颜色键透明 只存入 pLrdProp->Key(L78),不校验标志本身
LWA_ALPHA 0x2 用 bAlpha 做整窗 alpha 有则写 Alpha(L80-83),无则"若未分层则 Alpha=0"(L86)

实现细节:IntSetLayeredWindowAttributesif (dwFlags & LWA_ALPHA) pLrdProp->Alpha = bAlpha; else { if (!pLrdProp->is_Layered) pLrdProp->Alpha = 0; }------即只认 LWA_ALPHALWA_COLORKEY 不显式分支(Key 总是被无条件写入 L78)。pLrdProp->Flags = dwFlags 原样保存调用者给的标志供 GetLayeredWindowAttributes 查询。

UpdateLayeredWindow 系列(ULW_*)

常量 含义
ULW_COLORKEY 0x1 用 crKey 做颜色键透明(传给 StretchBlt 的 color_key)
ULW_ALPHA 0x2 用 pblend(BLENDFUNCTION)做混合(含 AC_SRC_ALPHA 每像素 alpha)
ULW_OPAQUE 0x4 不透明更新(无 alpha、无 ColorKey)
ULW_EX_NORESIZE 0x8 UpdateLayeredWindowIndirect 可用:禁止隐式改变窗口尺寸

NtUserUpdateLayeredWindow 的参数校验(L402)正是用 dwFlags & ~(ULW_COLORKEY | ULW_ALPHA | ULW_OPAQUE | ULW_EX_NORESIZE) 来拒绝未知标志。

4.5 BLENDFUNCTION 与 AC_SRC_ALPHA

c 复制代码
typedef struct _BLENDFUNCTION {
    BYTE BlendOp;             // AC_SRC_OVER = 0(源覆盖到目标)
    BYTE BlendFlags;          // 必须为 0
    BYTE SourceConstantAlpha; // 0~255,整窗 alpha 系数
    BYTE AlphaFormat;         // 0 = 无每像素 alpha;AC_SRC_ALPHA = 1 = 源有 alpha 通道
} BLENDFUNCTION;

layered.c 中默认混合参数(L155 与 L357 各一处):

c 复制代码
BLENDFUNCTION blend = { AC_SRC_OVER, 0, 255, 0 };
  • BlendOp = AC_SRC_OVER:标准"源覆盖在目标上"的 alpha 混合;
  • SourceConstantAlpha = 255:默认完全不透明,实际 alpha 由源位图决定;
  • AlphaFormat = 0:默认无每像素 alpha。

ULW_ALPHA 时用调用者传入的 *pblend 覆盖默认值(L202-206),AlphaFormat = AC_SRC_ALPHA 即启用每像素 alpha。合成算法的具体数学见 6.2 节。

4.6 重要澄清:ReactOS 没有"独立分层表面"

总览文档 4.5 节写道"layered.c 维护分层表面(hSection/映射)"------这是对 Windows 实现 (以及 ReactOS 未来计划)的描述,当前 ReactOS 源码中并不存在独立的 hSection 分层表面 。真实实现(IntUpdateLayeredWindowI,L100-233)是:

  1. 每次调用时用 NtGdiCreateCompatibleBitmap(info->hdcSrc, w, h) 创建两张临时内存位图(hbmSrc/hbmDst);
  2. 源内容经 NtGdiStretchBlt 拷入 hbmSrc(处理 ColorKey);
  3. NtGdiAlphaBlend 把 hbmSrc 混合到目标 DC 当前表面hdcDst 或窗口 DC 的可见表面);
  4. 临时位图/DC 全部删除。

也就是说,ReactOS 的分层窗口是**"内存位图即时合成"**模型:内容不驻留,合成即完成。优点是实现简单、与现有 GDI 管线完全兼容;缺点是没有持久表面,无法由系统侧做"增量重合成",每次更新都是全量重新 AlphaBlend(prcDirty 的脏区优化也仅是"合成后把脏区涂黑"这一 FIXME 级别的占位,见 5.6 节)。

这一差异值得记住:阅读其他文档(包括本系列总览)若看到"分层表面/hSection"字样,指的是 Windows 概念而非当前 ReactOS 代码。

4.7 UPDATELAYEREDWINDOWINFO:参数聚合结构

NtUserUpdateLayeredWindow(L339-425)把 10 个散参数组装成 UPDATELAYEREDWINDOWINFO info(L410-419)再传给 IntUpdateLayeredWindowI

c 复制代码
info.cbSize   = sizeof(info);
info.hdcDst   = hdcDst;
info.pptDst   = pptDst ? &Dst : NULL;      // 新位置(屏幕坐标)
info.psize    = &Size;                     // 新尺寸(必填!见 5.5 节)
info.hdcSrc   = hdcSrc;                    // 源 DC(可为 NULL = 只移动/缩放)
info.pptSrc   = pptSrc ? &Src : NULL;      // 源内容偏移
info.crKey    = crKey;                     // ColorKey
info.pblend   = &blend;                    // 混合参数
info.dwFlags  = dwFlags;                   // ULW_*
info.prcDirty = prcDirty ? &Dirty : NULL;  // 脏矩形(FIXME 级)

注意 info.psize = &Size 无条件赋值------因为 ProbeForRead(psize, sizeof(SIZE), 1)(L381)要求 psize 非空UpdateLayeredWindowpsize 参数不可为 NULL(Windows 文档也要求必须给出尺寸)。若调用者传 NULL 会在 SEH2 中触发异常并被转换为 ERROR_INVALID_PARAMETER(L396)。


5. layered.c 核心函数逐一展开

本节逐一展开 layered.c 的全部 8 个函数。调用关系全景:

复制代码
NtUserSetLayeredWindowAttributes (L310) ──→ IntSetLayeredWindowAttributes (L45)
NtUserGetLayeredWindowAttributes (L239) ──→ (读 LRD_PROP)
NtUserUpdateLayeredWindow   (L339) ──→ IntUpdateLayeredWindowI (L100) ──→ co_WinPosSetWindowPos
GetLayeredStatus (L22) / SetLayeredStatus (L33) ← 被 NtUserUpdateLayeredWindow / window.c 使用

5.1 GetLayeredStatus / SetLayeredStatus(L22-43)

c 复制代码
BOOL FASTCALL GetLayeredStatus(PWND pWnd)
{
   PLRD_PROP pLrdProp = UserGetProp(pWnd, AtomLayer, TRUE);
   if (pLrdProp) return pLrdProp->is_Layered;
   return FALSE;
}

BOOL FASTCALL SetLayeredStatus(PWND pWnd, BYTE set)
{
   PLRD_PROP pLrdProp = UserGetProp(pWnd, AtomLayer, TRUE);
   if (pLrdProp) { pLrdProp->is_Layered = set; return TRUE; }
   return FALSE;
}
  • GetLayeredStatus :按原子取属性块,返回 is_Layered;无属性块返回 FALSE。被 NtUserUpdateLayeredWindow(L401)用来拒绝"已分层窗口再次 UpdateLayeredWindow"。
  • SetLayeredStatus :写 is_Layered。被 window.c L3936 用来在清除 WS_EX_LAYERED 时复位分层状态(见 5.8 节)。
  • 两个函数都只做"读/写一个字节",不分配内存------分配只在 IntSetLayeredWindowAttributes 中发生。声明在 userfuncs.h L167-168。

5.2 IntSetLayeredWindowAttributes(L45-98)

这是 SetLayeredWindowAttributes 的内核实现(也被内部其他路径调用),完整逻辑:

第一步:校验扩展样式(L54-58)

c 复制代码
if (!(pWnd->ExStyle & WS_EX_LAYERED)) {
   ERR("Not a Layered Window!\n");
   return FALSE;
}

窗口必须带 WS_EX_LAYERED,否则失败。

第二步:取或建属性块(L60-72)

c 复制代码
pLrdProp = UserGetProp(pWnd, AtomLayer, TRUE);
if (!pLrdProp) {
   pLrdProp = ExAllocatePoolWithTag(PagedPool, sizeof(LRD_PROP), USERTAG_REDIRECT);
   if (pLrdProp == NULL) { ERR(...); return FALSE; }
   RtlZeroMemory(pLrdProp, sizeof(LRD_PROP));
   UserSetProp(pWnd, AtomLayer, (HANDLE)pLrdProp, TRUE);
}

首次调用时分配并挂载属性;后续调用复用。这是 LRD_PROP 生命周期中唯一的分配点。

第三步:写属性(L74-95)

c 复制代码
was_Layered = pLrdProp->is_Layered;
pLrdProp->Key = crKey;                       // 总是写入颜色键
if (dwFlags & LWA_ALPHA)
   pLrdProp->Alpha = bAlpha;                 // 带 LWA_ALPHA 才写 alpha
else
   if (!pLrdProp->is_Layered) pLrdProp->Alpha = 0;  // 首设且无 alpha → 0
pLrdProp->Flags = dwFlags;
pLrdProp->is_Layered = 1;                    // 置"已分层"
if (!was_Layered)
   co_UserRedrawWindow(pWnd, NULL, NULL, RDW_INVALIDATE | RDW_ERASE | RDW_FRAME);

关键行为:

  • 首设触发整窗重绘was_Layered 为 0 说明这是第一次设置分层属性,窗口此前是普通窗口,需要 co_UserRedrawWindow(RDW_INVALIDATE | RDW_ERASE | RDW_FRAME)(painting.c 的内部重绘入口)把整个窗口(含非客户区,RDW_FRAME)标脏重画------因为分层生效后窗口外观(半透明/ColorKey)会立即改变。再次设置则不重绘(分层状态下连续调整 alpha 由合成路径负责)。
  • FIXME 遗留 :L96 注释 // FIXME: Now set some bits to the Window DC!!!!------Windows 会在窗口 DC 上设置透明位,ReactOS 未实现,仅靠属性块记录。

返回值:成功 TRUE(含"分配失败"以外的所有路径)。

5.3 NtUserSetLayeredWindowAttributes(L310-334)

c 复制代码
BOOL APIENTRY NtUserSetLayeredWindowAttributes(HWND hwnd, COLORREF crKey, BYTE bAlpha, DWORD dwFlags)
{
   PWND pWnd;
   BOOL Ret = FALSE;
   TRACE("Enter NtUserSetLayeredWindowAttributes\n");
   UserEnterExclusive();
   if (!(pWnd = UserGetWindowObject(hwnd)) || !(pWnd->ExStyle & WS_EX_LAYERED)) {
      ERR("Not a Layered Window!\n");
      goto Exit;
   }
   Ret = IntSetLayeredWindowAttributes(pWnd, crKey, bAlpha, dwFlags);
Exit:
   UserLeave();
   return Ret;
}
  • 系统调用包装:UserEnterExclusive()独占锁 (写路径,防止与并发合成竞争属性块),UserGetWindowObject(hwnd) 句柄转对象(无效句柄 → FALSE)。
  • WS_EX_LAYERED 检查在此再做一次(内核入口自校验,不信任用户态)。
  • 用户态入口 SetLayeredWindowAttributesuser32.spec L660 直接映射到本系统调用,无额外包装。

5.4 NtUserGetLayeredWindowAttributes(L239-305)

c 复制代码
BOOL APIENTRY NtUserGetLayeredWindowAttributes(HWND hwnd, COLORREF *pcrKey, BYTE *pbAlpha, DWORD *pdwFlags)

流程:

  1. UserEnterExclusive()UserGetWindowObject + WS_EX_LAYERED 检查(L254-255),失败 goto Exit
  2. UserGetProp(pWnd, AtomLayer, TRUE) 取属性块;无属性块或 is_Layered == 0(L269)都视为"未分层",退出返回 FALSE。
  3. _SEH2_TRY 内用 ProbeForWrite 逐个探测并写回三个输出参数(L274-297):
c 复制代码
if (pcrKey)  { ProbeForWrite(pcrKey, sizeof(*pcrKey), 1);  *pcrKey  = pLrdProp->Key;   }
if (pbAlpha) { ProbeForWrite(pbAlpha, sizeof(*pbAlpha), 1); *pbAlpha = pLrdProp->Alpha; }
if (pdwFlags){ ProbeForWrite(pdwFlags, sizeof(*pdwFlags), 1); *pdwFlags = pLrdProp->Flags; }
  1. _SEH2_EXCEPT 捕获访问违规:SetLastNtError(exception code) 并返回 FALSE(写用户地址可能因页面无效而异常,SEH 是内核访问用户缓冲的标准保护手段)。
  2. 全成功 Ret = TRUEUserLeave() 返回。

返回值语义:TRUE = 窗口是分层窗口且返回了属性;FALSE = 窗口无效/非分层/无属性(此时三个输出参数内容不变)。user32.spec L322 把用户态 GetLayeredWindowAttributes 直接映射到此系统调用。

5.5 NtUserUpdateLayeredWindow(L339-425)

UpdateLayeredWindow 的系统调用本体,10 个参数。分四步:

第一步:SEH2 拷贝用户参数(L369-399)

c 复制代码
_SEH2_TRY {
   if (pptDst) { ProbeForRead(pptDst, sizeof(POINT), 1); Dst = *pptDst; }
   if (pptSrc) { ProbeForRead(pptSrc, sizeof(POINT), 1); Src = *pptSrc; }
   ProbeForRead(psize, sizeof(SIZE), 1);  Size = *psize;      // psize 必须非空!
   if (pblend) { ProbeForRead(pblend, sizeof(BLENDFUNCTION), 1); blend = *pblend; }
   if (prcDirty){ ProbeForRead(prcDirty, sizeof(RECT), 1); Dirty = *prcDirty; }
} _SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER) {
   EngSetLastError(ERROR_INVALID_PARAMETER);
   _SEH2_YIELD(goto Exit);
}

所有指针参数都用 ProbeForRead 探测后再拷入栈上局部变量(Dst/Src/Size/Dirty/blend),异常统一转 ERROR_INVALID_PARAMETER。注意默认 blend = { AC_SRC_OVER, 0, 255, 0 } 在函数开头初始化(L357),pblend 为 NULL 时用默认值。

第二步:参数语义校验(L401-408)

c 复制代码
if ( GetLayeredStatus(pWnd) ||
     dwFlags & ~(ULW_COLORKEY | ULW_ALPHA | ULW_OPAQUE | ULW_EX_NORESIZE) ||
     !(pWnd->ExStyle & WS_EX_LAYERED) )
{
   ERR("Layered Window Invalid Parameters\n");
   EngSetLastError(ERROR_INVALID_PARAMETER);
   goto Exit;
}

三个拒绝条件:

  1. GetLayeredStatus(pWnd) 为真------窗口已经被 SetLayeredWindowAttributes 初始化过 (is_Layered == 1),此时不允许再用 UpdateLayeredWindow(两种分层风格互斥,Windows 语义同)。
  2. dwFlags 含未知位(超出四个 ULW_* 的并集)。
  3. 窗口没有 WS_EX_LAYERED

第三步:组装 info 并调用(L410-420)

c 复制代码
info.cbSize   = sizeof(info);
info.hdcDst   = hdcDst;
info.pptDst   = pptDst ? &Dst : NULL;
info.psize    = &Size;
info.hdcSrc   = hdcSrc;
info.pptSrc   = pptSrc ? &Src : NULL;
info.crKey    = crKey;
info.pblend   = &blend;
info.dwFlags  = dwFlags;
info.prcDirty = prcDirty ? &Dirty : NULL;
Ret = IntUpdateLayeredWindowI(pWnd, &info);

第四步 :TRACE 记录结果,UserLeave()

用户态入口 UpdateLayeredWindow(user32/windows/window.c L1751-1777):

  • 拒绝 ULW_EX_NORESIZE(该标志仅 UpdateLayeredWindowIndirect 合法,L1762-1766);
  • NtUserUpdateLayeredWindow(hwnd, hdcDst, pptDst, psize, hdcSrc, pptSrc, crKey, pbl, dwFlags, NULL)(prcDirty 恒为 NULL)。

UpdateLayeredWindowIndirect(L1782-1801):校验 info->cbSize == sizeof(*info) 后把 UPDATELAYEREDWINDOWINFO 拆开转发,prcDirtyinfo->prcDirty

5.6 IntUpdateLayeredWindowI(L100-233):合成管线核心

这是 layered.c 的灵魂函数,同时也是**"分层窗口移动/缩放"的实际执行者**(通过尾部的 co_WinPosSetWindowPos)。分五步展开。

第一步:计算新矩形(L104-149)

c 复制代码
Window = pWnd->rcWindow;
Client = pWnd->rcClient;
Parent = pWnd->spwndParent;
if (pWnd->style & WS_CHILD && Parent) {
   RECTL_vOffsetRect(&Window, -Parent->rcClient.left, -Parent->rcClient.top);
   RECTL_vOffsetRect(&Client, -Parent->rcClient.left, -Parent->rcClient.top);
}
  • 取出窗口矩形与客户区矩形(屏幕坐标);
  • 子窗口WS_CHILD 且有父)时先把两个矩形平移为父客户区相对坐标 (减 Parent->rcClient.left/top)------因为 UpdateLayeredWindowpptDst 对子窗口而言是父客户区坐标。平移后再算偏移(见下)。
c 复制代码
if (info->pptDst) {
   Offset.cx = info->pptDst->x - Window.left;
   Offset.cy = info->pptDst->y - Window.top;
   RECTL_vOffsetRect(&Client, Offset.cx, Offset.cy);
   RECTL_vOffsetRect(&Window, Offset.cx, Offset.cy);
   flags &= ~SWP_NOMOVE;                     // 需要移动
}
  • pptDst 给出新位置 → 位移量 = 新位置 − 当前窗口左上角 → 两个矩形整体平移 → 清除 SWP_NOMOVE(告诉尾部 SetWindowPos 要动)。
c 复制代码
if (info->psize) {
   Offset.cx = info->psize->cx - (Window.right  - Window.left);
   Offset.cy = info->psize->cy - (Window.bottom - Window.top);
   if (info->psize->cx <= 0 || info->psize->cy <= 0) { ... ERROR_INVALID_PARAMETER; return FALSE; }
   if ((info->dwFlags & ULW_EX_NORESIZE) && (Offset.cx || Offset.cy)) { ... ERROR_INCORRECT_SIZE; return FALSE; }
   Client.right  += Offset.cx;
   Client.bottom += Offset.cy;
   Window.right  += Offset.cx;
   Window.bottom += Offset.cy;
   flags &= ~SWP_NOSIZE;                     // 需要缩放
}
  • psize 给出新尺寸 → 尺寸差 = 新宽高 − 当前宽高;
  • 尺寸必须为正(<= 0ERROR_INVALID_PARAMETER);
  • ULW_EX_NORESIZE 且尺寸有变化 → 报 ERROR_INCORRECT_SIZE("只许位置变、不许大小变",供 UpdateLayeredWindowIndirect 的调用者约束行为);
  • 矩形扩展后清除 SWP_NOSIZE

第二步:准备目标 DC 与位图(L151-179)

c 复制代码
if (info->hdcSrc) {
   HDC hdc, hdcBuffer;
   BLENDFUNCTION blend = { AC_SRC_OVER, 0, 255, 0 };
   COLORREF color_key = (info->dwFlags & ULW_COLORKEY) ? info->crKey : CLR_INVALID;

   Rect = Window;
   RECTL_vOffsetRect(&Rect, -Window.left, -Window.top);     // 转成"窗口内坐标"

   if (!info->hdcDst) hdc = UserGetDCEx(pWnd, NULL, DCX_USESTYLE);
   else hdc = info->hdcDst;

   hbmSrc = NtGdiCreateCompatibleBitmap(info->hdcSrc, Rect.right - Rect.left, Rect.bottom - Rect.top);
   hbmDst = NtGdiCreateCompatibleBitmap(info->hdcSrc, Rect.right - Rect.left, Rect.bottom - Rect.top);
   GreGetObject(hbmSrc, sizeof(DIBSECTION), &dibs);
   hdcBuffer = NtGdiCreateCompatibleDC(hdc);
   hOldBitmap  = (HBITMAP)NtGdiSelectBitmap(hdcBuffer, hbmSrc);
   hOldBitmap1 = (HBITMAP)NtGdiSelectBitmap(hdc, hbmDst);
  • hdcSrc 为 NULL 时跳过整个合成(ret = TRUE,仅走"移动/缩放"路径,L228-229);
  • hdcDst 缺省时用 UserGetDCEx(pWnd, NULL, DCX_USESTYLE)窗口 DCDCX_USESTYLE:按窗口类样式创建 DC)------合成目标是窗口表面(配合可见区域裁剪,见 11 节);
  • info->hdcSrc 为模板创建两张兼容位图(hbmSrc 承接源内容、hbmDst 作合成目标)与一个兼容 DC(hdcBuffer);
  • GreGetObject(hbmSrc, sizeof(DIBSECTION), &dibs) 检查源位图格式(TRACE 打印 biBitCount)------若源是 32bpp 带 alpha 的 DIB,AlphaBlend 时每像素 alpha 才有效(对应 AC_SRC_ALPHA)。

第三步:源内容搬运 + ColorKey 透明(L181-199)

c 复制代码
NtGdiStretchBlt( hdcBuffer, Rect.left, Rect.top, Rect.right-Rect.left, Rect.bottom-Rect.top,
                 info->hdcSrc,
                 Rect.left + (info->pptSrc ? info->pptSrc->x : 0),
                 Rect.top  + (info->pptSrc ? info->pptSrc->y : 0),
                 Rect.right-Rect.left, Rect.bottom-Rect.top,
                 SRCCOPY, color_key );

if (info->prcDirty) {
   ERR("prcDirty\n");
   RECTL_bIntersectRect(&Rect, &Rect, info->prcDirty);
   NtGdiPatBlt(hdc, Rect.left, Rect.top, Rect.right-Rect.left, Rect.bottom-Rect.top, BLACKNESS);
}
  • NtGdiStretchBltcolor_key 参数 (第 12 参 crBackColorSRCCOPY 三元光栅操作)把源内容(含 pptSrc 偏移)拷入 hdcBuffer/hbmSrcColorKey 透明就是在这条 StretchBlt 里实现的 (见 6.4 节:与源颜色相等的像素以 crBackColor 填充,CLR_INVALID 表示不用颜色键)。
  • prcDirty 处理:与窗口矩形求交后把目标 DC 的对应区域用 BLACKNESS 涂黑 。注释 // Need to test this, Dirty before or after StretchBlt? 表明顺序未定论;ERR("prcDirty\n") 说明该路径目前只是占位级实现。

第四步:AlphaBlend 合成(L202-219)

c 复制代码
if (info->dwFlags & ULW_ALPHA) {
   blend = *info->pblend;                     // 采用调用者混合参数(可含 AC_SRC_ALPHA)
}
ret = NtGdiAlphaBlend( hdc, Rect.left, Rect.top, Rect.right-Rect.left, Rect.bottom-Rect.top,
                       hdcBuffer,
                       Rect.left + (info->pptSrc ? info->pptSrc->x : 0),
                       Rect.top  + (info->pptSrc ? info->pptSrc->y : 0),
                       Rect.right-Rect.left, Rect.bottom-Rect.top,
                       blend, 0);
  • 默认(无 ULW_ALPHA)用 {AC_SRC_OVER, 0, 255, 0}(不透明拷贝语义);带 ULW_ALPHA 用调用者 pblendULW_ALPHA + AC_SRC_ALPHA = 每像素 alpha;ULW_ALPHA 单独 = 整窗 SourceConstantAlpha)。
  • NtGdiAlphaBlend 的最后一个参数 hcmXform 传 0(颜色变换句柄,未用)。
  • 合成目标 hdc 当前选中的是 hbmDst(第二步已选入)------合成先发生在内存位图 hbmDst 上 ,随后该位图随 DC 一起被释放;但注意 hdc 若是 UserGetDCEx 返回的窗口 DC,其表面就是窗口表面,hbmDst 被选入后合成实际写入位图而非窗口表面,随后的 NtGdiSelectBitmap(hdc, hOldBitmap1) 还原后,位图内容并未再贴回窗口表面。这一"hbmDst 临时位图"的使用目前更多是结构占位(对应 Windows 中的分层表面雏形),真正的屏幕呈现由窗口 DC 路径的可见区域裁剪保证。

第五步:清理 + 应用新位置(L221-232)

c 复制代码
NtGdiSelectBitmap(hdc, hOldBitmap1);
NtGdiSelectBitmap(hdcBuffer, hOldBitmap);
if (hbmSrc) GreDeleteObject(hbmSrc);
if (hbmDst) GreDeleteObject(hbmDst);
if (hdcBuffer) IntGdiDeleteDC(hdcBuffer, FALSE);
if (!info->hdcDst) UserReleaseDC(pWnd, hdc, FALSE);

co_WinPosSetWindowPos(pWnd, 0, Window.left, Window.top,
                      Window.right - Window.left, Window.bottom - Window.top, flags);
return ret;
  • 还原选区、删除两张位图、删除兼容 DC(IntGdiDeleteDC)、释放窗口 DC;
  • 尾部 co_WinPosSetWindowPos(pWnd, 0, x, y, w, h, flags)(winpos.c 的内核版 SetWindowPos):flags = SWP_NOSIZE | SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOREDRAW(L108,按第一步的结果去掉 SWP_NOMOVE/SWP_NOSIZE)。这就是分层窗口移动/缩放的实现 ------通过 SetWindowPos 更新 rcWindow,且 SWP_NOREDRAW 保证不触发重绘(内容由应用自己推)。

5.7 分层窗口移动:co_WinPosSetWindowPos 与 flags

任务描述中提到的"IntUpdateWindowPos(分层窗口移动)"在源码中对应 IntUpdateLayeredWindowI 尾部的 co_WinPosSetWindowPos 调用,并没有独立函数。要点:

标志位 是否置位 含义
SWP_NOSIZE 初始置位;有 psize 时清除 不改变大小 / 按 psize 改变
SWP_NOMOVE 初始置位;有 pptDst 时清除 不移动 / 按 pptDst 移动
SWP_NOZORDER 恒置位 不改变 Z 序(分层窗口内容更新不应影响层级)
SWP_NOACTIVATE 恒置位 不激活(不抢焦点)
SWP_NOREDRAW 恒置位 不重绘(内容由合成路径直接提供)

移动分层窗口(UpdateLayeredWindow(hWnd, NULL, &pt, NULL, NULL, NULL, 0, NULL, 0) 或带 psize 缩放)因此完全绕开 WM_PAINT:winpos.c 只更新矩形并让 vis.c 重新计算可见区域,可见区域变化导致的下层窗口重绘由 winpos 常规路径处理(分册 _3/_4),而分层窗口自身的内容不变。

5.8 分层状态清理(window.c L3934-3937)

NtUserSetWindowLong(hWnd, GWL_EXSTYLE, ...) 修改扩展样式时,若新样式去掉了 WS_EX_LAYERED,会把运行时分层标志一并复位:

c 复制代码
if (!(Window->ExStyle & WS_EX_LAYERED)) {
   SetLayeredStatus(Window, 0);
}
Window->ExStyle = (DWORD)Style.styleNew;

注意判断用的是 ExStyle(此时 Window->ExStyle 尚未被 styleNew 覆盖):旧样式无 WS_EX_LAYERED 则强制 is_Layered = 0。这保证"先 SetLayeredWindowAttributes 再去掉 WS_EX_LAYERED 再重新加上"时,GetLayeredStatus 为 0,UpdateLayeredWindow 又能被接受(5.5 节校验条件 1 不再触发)。


6. 分层合成算法

IntUpdateLayeredWindowI 的合成只做了一件事:调用 NtGdiAlphaBlend。真正的像素级混合发生在 GDI 引擎的三级管道中。本节自上而下展开。

6.1 NtGdiAlphaBlend(ntgdi/bitblt.c L12-147)

c 复制代码
BOOL APIENTRY NtGdiAlphaBlend(HDC hDCDest, LONG XOriginDest, LONG YOriginDest,
    LONG WidthDest, LONG HeightDest, HDC hDCSrc, LONG XOriginSrc, LONG YOriginSrc,
    LONG WidthSrc, LONG HeightSrc, BLENDFUNCTION BlendFunc, HANDLE hcmXform)

流程:

  1. 校验 (L38-48):宽高非负;双 DC 非空,否则 ERROR_INVALID_PARAMETER
  2. 锁定双 DC (L53-58):GDIOBJ_bLockMultipleObjects(2, ahDC, apObj, GDIObjType_DC_TYPE) 一次性锁定两个 DC 对象(避免死锁,比逐个锁定安全)。
  3. 信息 DC 短路 (L62-68):任一 DC 是 DCTYPE_INFO → 直接返回 TRUE("Windows 确实这样")。
  4. 坐标换算 (L70-95):目标/源矩形各自 IntLPtoDP(逻辑→设备)再加 ptlDCOrig(DC 原点偏移)。
  5. 边界累计 (L81-84):目标 DC 带 DC_ACCUM_APP|DC_ACCUM_WMGR(窗口管理器累计)标志时 IntUpdateBoundsRect 把目标矩形并入脏区边界(供后续屏幕刷新用)。
  6. 准备 Blit (L109):DC_vPrepareDCsForBlit(解析 DC 表面、可见区域裁剪 → DCDest->co 裁剪对象)。
  7. 取表面 (L112-124):DCDest->dclevel.pSurface/DCSrc->dclevel.pSurface
  8. 颜色转换 (L127):EXLATEOBJ_vInitXlateFromDCs(&exlo, DCSrc, DCDest) 建立源→目标的调色板/格式转换对象。
  9. 核心调用(L131-137):
c 复制代码
bResult = IntEngAlphaBlend(&BitmapDest->SurfObj, &BitmapSrc->SurfObj,
                           (CLIPOBJ *)&DCDest->co, &exlo.xlo,
                           &DestRect, &SourceRect, &BlendObj);
  1. 收尾EXLATEOBJ_vCleanupDC_vFinishBlit、双对象解锁。

6.2 IntEngAlphaBlend 与 EngAlphaBlend(eng/alphablend.c)

IntEngAlphaBlend(L196-244)------"驱动优先"分派器:

c 复制代码
BOOL APIENTRY IntEngAlphaBlend(SURFOBJ *psoDest, SURFOBJ *psoSource, CLIPOBJ *pco,
                               XLATEOBJ *pxlo, RECTL *prclDest, RECTL *prclSrc, BLENDOBJ *pBlendObj)
{
   if (!pco) pco = (CLIPOBJ *)&gxcoTrivial;          // 无裁剪 → 平凡裁剪
   if (pco->rclBounds.left >= pco->rclBounds.right ||
       pco->rclBounds.top  >= pco->rclBounds.bottom) return TRUE;  // 空裁剪,无事可做

   psurfDest = CONTAINING_RECORD(psoDest, SURFACE, SurfObj);
   if (psurfDest->flags & HOOK_ALPHABLEND) {          // 目标表面挂了 DrvAlphaBlend 钩子
      ret = GDIDEVFUNCS(psoDest).AlphaBlend(psoDest, psoSource, pco, pxlo, prclDest, prclSrc, pBlendObj);
   }
   if (!ret)                                          // 驱动不做/失败 → 软件引擎
      ret = EngAlphaBlend(psoDest, psoSource, pco, pxlo, prclDest, prclSrc, pBlendObj);
   return ret;
}
  • gxcoTrivial:GDI 引擎预置的"平凡裁剪"(全包容);
  • HOOK_ALPHABLEND:目标表面的显示驱动若声明支持 DrvAlphaBlend(如 pandisp.c 的 PanAlphaBlend,先混合到影子表面再上屏),优先交给驱动硬件加速;否则回退 EngAlphaBlend 软件实现。

EngAlphaBlend(L18-194)------软件混合核心。校验与分派:

  1. 矩形规范化 (L54-59):目标/源矩形 RECTL_vMakeWellOrdered(防负宽高)。
  2. 源矩形越界校验 (L60-67):源矩形任一边 < 0 或超出源位图尺寸 → ERROR_INVALID_PARAMETER
  3. 自重叠拒绝(L69-75):目标与源是同一表面且矩形相交 → 拒绝(需要临时缓冲才能正确自混)。
  4. 混合参数校验 (L77-91):
    • BlendOp != AC_SRC_OVER → 拒绝(只支持"源覆盖");
    • BlendFlags != 0 → 拒绝;
    • AlphaFormatAC_SRC_ALPHA 之外的位 → 拒绝。
  5. 进入表面 (L103-120):IntEngEnter 分别锁定源/目标表面(处理格式转换、平移 Translate,得到 InputObj/OutputObj 与平移后的矩形)。
  6. 按裁剪复杂度分派(L125-188,见下)。
  7. IntEngLeave 释放,返回结果。

裁剪复杂度分派(L125-188)------混合算法围绕"裁剪区是什么形状"选择策略:

c 复制代码
ClippingType = (ClipRegion == NULL) ? DC_TRIVIAL : ClipRegion->iDComplexity;
switch (ClippingType)
{
case DC_TRIVIAL:  // 无裁剪:整块混合
   Ret = DibFunctionsForBitmapFormat[OutputObj->iBitmapFormat].DIB_AlphaBlend(
             OutputObj, InputObj, &OutputRect, &InputRect, ClipRegion, ColorTranslation, BlendObj);
   break;

case DC_RECT:     // 单矩形裁剪:求交后按比例缩放源矩形,混合一次
   if (RECTL_bIntersectRect(&CombinedRect, &OutputRect, &ClipRect)) {
      Rect.left   = InputRect.left + (CombinedRect.left - OutputRect.left)
                     * (InputRect.right - InputRect.left) / (OutputRect.right - OutputRect.left);
      ... // 按目标裁剪比例反推源矩形
      Ret = DIB_AlphaBlend(OutputObj, InputObj, &CombinedRect, &Rect, ...);
   }
   break;

case DC_COMPLEX:  // 复杂裁剪(多个矩形):枚举每个裁剪矩形分别混合
   CLIPOBJ_cEnumStart(ClipRegion, FALSE, CT_RECTANGLES, CD_ANY, 0);
   do {
      EnumMore = CLIPOBJ_bEnum(ClipRegion, sizeof(RectEnum), &RectEnum);
      for (i = 0; i < RectEnum.c; i++) {
         ClipRect = RectEnum.arcl[i] + Translate;
         if (RECTL_bIntersectRect(&CombinedRect, &OutputRect, &ClipRect)) {
            Rect = 按比例缩放后的源矩形;
            Ret = DIB_AlphaBlend(...) && Ret;
         }
      }
   } while (EnumMore);
   break;
}

核心机制:目标裁剪矩形按比例映射回源矩形(线性插值公式,L142-145/170-173),保证混合只处理"裁剪后真正可见"的像素。

像素级混合 DIB_AlphaBlend(dib/ 目录,按 OutputObj->iBitmapFormat 分派到 8/16/24/32bpp 的专用实现):对每个输出像素计算

复制代码
Dst = Src × α + Dst × (1 − α)

其中 α 由 SourceConstantAlpha 与(AC_SRC_ALPHA 时)源像素 alpha 通道相乘得到:

复制代码
α_effective = (SourceConstantAlpha / 255) × (SrcAlpha / 255)

AC_SRC_ALPHA = 0SrcAlpha 视为 255,α 只由 SourceConstantAlpha 决定------这正是"整窗 alpha"与"每像素 alpha"两种模式的统一公式。

6.3 NtGdiEngAlphaBlend(L249-278)

EngAlphaBlend导出系统调用版 (给驱动/其他内核组件直接调用的入口):SEH2 ProbeForRead 拷贝用户提供的目标/源矩形后转发 EngAlphaBlendIntUpdateLayeredWindowI 不经过它(走 NtGdiAlphaBlend),但显示驱动合成路径可能用到。

6.4 ColorKey 透明:NtGdiTransparentBlt 与 StretchBlt 的 crBackColor

ColorKey 在分层窗口中的两条路径

路径 A:IntUpdateLayeredWindowI 的 StretchBlt 携带 crKey(L156、L181-192)

c 复制代码
COLORREF color_key = (info->dwFlags & ULW_COLORKEY) ? info->crKey : CLR_INVALID;
NtGdiStretchBlt(..., SRCCOPY, color_key);

NtGdiStretchBlt(ntgdi/bitblt.c L838-871)剥掉 NOMIRRORBITMAP|CAPTUREBLT 后转发 GreStretchBltMask(L576 起),最终 IntEngStretchBlt(..., dwBackColor)------源像素颜色等于 dwBackColor 时以 ROP4_SRCCOPY 的"替换目标为背景色"语义处理 ,实现"该像素不呈现原内容"。CLR_INVALID(-1)表示不用颜色键。

路径 B:独立 API NtGdiTransparentBlt (L199-309)------TransparentBlt 系统调用:

c 复制代码
/* Translate Transparent (RGB) Color to the source palette */
EXLATEOBJ_vInitialize(&exlo, &gpalRGB, BitmapSrc->ppal, 0, 0, 0);
TransparentColor = XLATEOBJ_iXlate(&exlo.xlo, (ULONG)TransColor);
EXLATEOBJ_vCleanup(&exlo);

EXLATEOBJ_vInitXlateFromDCs(&exlo, DCSrc, DCDest);
Ret = IntEngTransparentBlt(&BitmapDest->SurfObj, &BitmapSrc->SurfObj,
                           (CLIPOBJ *)&DCDest->co, &exlo.xlo,
                           &rcDest, &rcSrc, TransparentColor, 0);

关键步骤:把 RGB 透明色先用 gpalRGB(全局 RGB 调色板)转换到源表面的调色板索引XLATEOBJ_iXlate),再交给 IntEngTransparentBlt------因为颜色键比较必须在同一色彩空间 进行。分层窗口的 ColorKey(路径 A)走的是 StretchBlt 的 crBackColor 机制,语义等价但通道不同。

6.5 ULW_OPAQUE 与组合语义

ULW_OPAQUE(0x4)在 IntUpdateLayeredWindowI 中没有显式分支------它通过默认 blend 参数 体现:不传 ULW_ALPHAblend = {AC_SRC_OVER, 0, 255, 0}(SourceConstantAlpha=255、无每像素 alpha)即完全不透明;同时不传 ULW_COLORKEYcolor_key = CLR_INVALID 即无颜色键。于是:

标志组合 效果 机制
ULW_OPAQUE 内容原样覆盖(等价普通 BitBlt) SRCCOPY StretchBlt + 255 alpha
`ULW_OPAQUE ULW_COLORKEY` 指定颜色变透明,其余原样
ULW_ALPHA(无 AC_SRC_ALPHA) 整窗按 SourceConstantAlpha 半透明 AlphaBlend 统一 α
`ULW_ALPHA AC_SRC_ALPHA` 每像素 alpha 混合
`ULW_ALPHA ULW_COLORKEY` 颜色键 + alpha 同时生效
ULW_EX_NORESIZE 禁止隐式缩放 5.6 第一步校验

7. 分层窗口与普通窗口的绘制差异

7.1 普通窗口:WM_PAINT 协议(分册 _4/_5 回顾)

复制代码
内容变化 → InvalidateRect 标脏 → hrgnUpdate 累积 → QS_PAINT
→ GetMessage 返回 WM_PAINT → 窗口过程处理
→ BeginPaint 取 DC(可见区 ∩ 更新区为裁剪)→ GDI 绘制到窗口表面 → EndPaint

特征:系统记账、应用绘制、区域驱动。窗口表面由系统管理,应用绘制结果被 vis.c 的可见区域裁剪后最终呈现。

7.2 分层窗口:直接更新表面

复制代码
内容变化 → UpdateLayeredWindow(hdcSrc=应用内容) 
→ 内核 StretchBlt + AlphaBlend 合成 → co_WinPosSetWindowPos(SWP_NOREDRAW)

特征:应用供内容、系统合成、无 WM_PAINT。差异逐项对比:

维度 普通窗口 分层窗口
内容来源 应用在 WM_PAINT 中绘制 应用通过 hdcSrc 直接提供
是否走消息 是(WM_PAINT/WM_ERASEBKGND/WM_NCPAINT) 否(无绘制消息)
透明度 无(不透明矩形) 整窗 alpha / 每像素 alpha / ColorKey
表面 窗口表面(hrgnVis 裁剪) 临时位图合成(无持久表面,见 4.6)
移动/缩放 winpos 常规路径 + 重绘 co_WinPosSetWindowPos(SWP_NOREDRAW) 直接搬
首次分层 --- IntSetLayeredWindowAttributes 触发 `RDW_INVALIDATE
两种 API 互斥 --- SetLayeredWindowAttributes(属性块风格)与 UpdateLayeredWindow(推内容风格)不能混用(5.5 校验 1)

7.3 WS_EX_TRANSPARENT 点击穿透

WS_EX_LAYERED | WS_EX_TRANSPARENT 组合使窗口不再接收鼠标命中(点击穿透到下层)。两处实现:

msgqueue.c L81-84UserFindWindowFromPoint 的候选循环):

c 复制代码
if ((pWnd->style & WS_VISIBLE) &&
    (pWnd->ExStyle & (WS_EX_LAYERED|WS_EX_TRANSPARENT)) != (WS_EX_LAYERED|WS_EX_TRANSPARENT) &&
    IntPtInWindow(pWnd, x, y))
   return pWnd;

winpos.c L2937-2941IntWindowHitTest 家族,WindowFromPoint 路径):

c 复制代码
/* transparent */
if ((ScopeWin->ExStyle & (WS_EX_LAYERED|WS_EX_TRANSPARENT)) == (WS_EX_LAYERED|WS_EX_TRANSPARENT))
   return NULL;

注意两处都是精确等于组合值 的判断:只有两个位同时置位才穿透,单独 WS_EX_TRANSPARENT 或单独 WS_EX_LAYERED 都不穿透。这是 Windows 的经典语义:WS_EX_TRANSPARENT 单独用于"同线程兄弟窗口绘制顺序",与分层组合时才产生点击穿透。

7.4 性能与正确性注意

  1. 全量重合成 :无持久表面意味着每次 UpdateLayeredWindow 都重新 StretchBlt + AlphaBlend 整窗内容。prcDirty 参数虽存在但只是"合成后涂黑"占位(5.6 第三步),实际仍是全量混合。频繁更新的应用(动画)应缩小窗口或降低更新频率。
  2. 源格式决定 alpha 效果AC_SRC_ALPHA 要求源是 32bpp 带 alpha 通道的 DIB 位图(GreGetObject 检查 biBitCount,L172-174);若源是 24bpp,AlphaFormat 的每像素 alpha 无意义,只有 SourceConstantAlpha 生效。
  3. 分层窗口仍是窗口:它依然参与 Z 序、可见区域合成(vis.c)、窗口枚举与命中测试(除非透明组合)。分层只改变"内容怎么呈现",不改变"窗口是什么"。

8. draw.c 绘制辅助实现细节

draw.c(1137 行)的全部函数都是内核态绘制工具。本节逐一展开 14 个函数。

8.1 颜色查找表(L21-92)

draw.c 的核心技巧是表驱动 :8 张 signed char 表把"边缘类型 × 绘制模式"映射为系统颜色索引。表的下标是 uType & (BDR_INNER | BDR_OUTER)(边缘类型只有 4 种组合:无/内/外/内外),值为 COLOR_* 系统色索引或 -1(该位置不画)。

用途 对应模式
LTInnerNormal/LTOuterNormal(L21-33) 左上内/外线,普通凸起 默认(BF_SOFT 之外)
RBInnerNormal/RBOuterNormal(L35-47) 右下内/外线,普通凸起 默认
LTInnerSoft/LTOuterSoft(L49-61) 左上内/外线,柔和(3D 弱化) BF_SOFT(非 BF_BOTTOM)
RBInnerSoft/RBOuterSoft(L63-64 别名) 右下内/外线,柔和 BF_SOFT(BF_BOTTOM)
LTRBOuterMono/LTRBInnerMono(L66-78) 单色边框(WINDOW/WINDOWFRAME) BF_MONO
LTRBOuterFlat/LTRBInnerFlat(L80-92) 平面边框(BTNSHADOW/BTNFACE) BF_FLAT

表值是"系统色索引",绘制时经 IntGetSysColor(表值) 转成 RGB 再 IntSetDCPenColor 设置画笔颜色(见 8.2)。-1 表示该边缘不绘制(选择 NULL_PEN)。由于表值是 signed char-1 不会被误认为颜色索引。

关于任务描述中的函数名澄清 :总览/任务提到的 IntFillRectIntFrameRectIntDrawEdge 在当前 draw.c 中对应导出名 FillRect(L1056)、FrameRect(L1037)、DrawEdge(L1084)------它们不是 Int 前缀,但语义相同(内核内部直接可调用)。IntDrawFocusRect 对应 user32 的 DrawFocusRect(见 9.6),IntDrawCaptionText 对应 painting.c 的 UserDrawCaptionText(见 9.1),IntDrawIconEx 对应 cursoricon.c 的 UserDrawIconEx(见 9.3),IntDrawText 对应 defwnd.c 的 DrawTextW(见 9.5)。本册一律使用真实函数名。

8.2 IntDrawRectEdge(L410-564):矩形边缘绘制

IntDrawRectEdgeDrawEdgeBF_DIAGONAL 时的实现(注释明确:Same as DrawEdge invoked without BF_DIAGONAL,移植自 WINE20020904,23-Nov-1997: Changed by Bertho Stultiens)。分五步:

第一步:按模式选色(L430-464)

c 复制代码
if (uFlags & BF_MONO) {
   LTInnerI = RBInnerI = LTRBInnerMono[uType & (BDR_INNER|BDR_OUTER)];
   LTOuterI = RBOuterI = LTRBOuterMono[uType & (BDR_INNER|BDR_OUTER)];
} else if (uFlags & BF_FLAT) {
   LTInnerI = RBInnerI = LTRBInnerFlat[uType & (BDR_INNER|BDR_OUTER)];
   LTOuterI = RBOuterI = LTRBOuterFlat[uType & (BDR_INNER|BDR_OUTER)];
   /* win98 起 BF_FLAT 的内边与中间同色(COLOR_BTNFACE)------Dennis Björklund, 10 June, 99 */
   if (LTInnerI != -1) LTInnerI = RBInnerI = COLOR_BTNFACE;
} else if (uFlags & BF_SOFT) {
   LTInnerI = LTInnerSoft[...]; LTOuterI = LTOuterSoft[...];
   RBInnerI = RBInnerSoft[...]; RBOuterI = RBOuterSoft[...];
} else {
   LTInnerI = LTInnerNormal[...]; LTOuterI = LTOuterNormal[...];
   RBInnerI = RBInnerNormal[...]; RBOuterI = RBOuterNormal[...];
}

四组颜色:LTInner(左上内线)、LTOuter(左上外线)、RBInner(右下内线)、RBOuter(右下外线)。文件头的注释表(L351-408)用数字 15/16/20/21/22 对应 COLOR_BTNFACE/COLOR_BTNSHADOW/COLOR_BTNHIGHLIGHT/COLOR_3DDKSHADOW/COLOR_3DLIGHT,逐条列出 uType × uFlags 的 256 种组合------这是为了精确复刻 win95 的观感。

第二步:角点修正(L466-473)

c 复制代码
if ((uFlags & BF_BOTTOMLEFT) == BF_BOTTOMLEFT)  LBpenplus = 1;
if ((uFlags & BF_TOPRIGHT) == BF_TOPRIGHT)      RTpenplus = 1;
if ((uFlags & BF_BOTTOMRIGHT) == BF_BOTTOMRIGHT) RBpenplus = 1;
if ((uFlags & BF_TOPLEFT) == BF_TOPLEFT)        LTpenplus = 1;

BF_TOPLEFT(0x4)等单角标志使对应边向内收缩 1 像素 (因为角的斜切会让该侧边框"短一截")。penplus 只在画内线时使用(L521-539)。

第三步:画外圈(L490-514)

c 复制代码
NtGdiSelectPen(hdc, LTOuterPen);
IntSetDCPenColor(hdc, IntGetSysColor(LTOuterI));
if (uFlags & BF_TOP)   { GreMoveTo(hdc, InnerRect.left,  InnerRect.top, NULL);
                         NtGdiLineTo(hdc, InnerRect.right, InnerRect.top); }
if (uFlags & BF_LEFT)  { GreMoveTo(hdc, InnerRect.left,  InnerRect.top, NULL);
                         NtGdiLineTo(hdc, InnerRect.left,  InnerRect.bottom); }
NtGdiSelectPen(hdc, RBOuterPen);
IntSetDCPenColor(hdc, IntGetSysColor(RBOuterI));
if (uFlags & BF_BOTTOM){ GreMoveTo(hdc, InnerRect.left,  InnerRect.bottom-1, NULL);
                         NtGdiLineTo(hdc, InnerRect.right, InnerRect.bottom-1); }
if (uFlags & BF_RIGHT) { GreMoveTo(hdc, InnerRect.right-1, InnerRect.top, NULL);
                         NtGdiLineTo(hdc, InnerRect.right-1, InnerRect.bottom); }
  • 画笔选择:颜色索引为 -1 时保持 NULL_PEN(不画),否则选 NtGdiGetStockObject(DC_PEN)(DC 画笔,颜色由 IntSetDCPenColor 动态设置,避免每次建笔);
  • 边线用 GreMoveTo + NtGdiLineTo(内核 GDI 画线);
  • 画线坐标含 -1 修正 :下边线 y = bottom-1、右边线 x = right-1,因为 GDI 矩形是"左闭右开/上闭下开"。

第四步:画内圈 (L516-540):同理,但每条线向内缩 1 像素(top+1/left+1/bottom-2/right-2)并叠加 penplus 角点修正。

第五步:中间填充与矩形调整(L483-487、L542-558)

c 复制代码
if ((uFlags & BF_MIDDLE) && retval)
   FillRect(hdc, &InnerRect, IntGetSysColorBrush(uFlags & BF_MONO ? COLOR_WINDOW : COLOR_BTNFACE));

if (((uFlags & BF_MIDDLE) && retval) || (uFlags & BF_ADJUST)) {
   int add = (LTRBInnerMono[...] != -1 ? 1 : 0) + (LTRBOuterMono[...] != -1 ? 1 : 0);
   if (uFlags & BF_LEFT)   InnerRect.left   += add;
   if (uFlags & BF_RIGHT)  InnerRect.right  -= add;
   if (uFlags & BF_TOP)    InnerRect.top    += add;
   if (uFlags & BF_BOTTOM) InnerRect.bottom -= add;
   if (uFlags & BF_ADJUST) *rc = InnerRect;   // 收缩后的矩形写回调用者
}
  • BF_MIDDLE:用 FillRect(本文件的 8.5 节函数)填充内部,颜色按 BF_MONOCOLOR_WINDOW 否则 COLOR_BTNFACE
  • BF_ADJUST:把"边框占掉的像素"从矩形中扣掉并写回 *rc------调用者常用它先调整矩形再在其中画内容(nonclient.c L982 等);
  • retval(L423-425)在"既有 BDR_INNER 又有 BDR_OUTER 且非 FLAT/MONO"时为 FALSE(此时是普通 3D 凸起,不需中间填充),其余为 TRUE。

收尾 (L560-563):选回 SavePenGreMoveTo 恢复原点。返回值 retval 供调用者判断是否绘制了中间填充。

8.3 IntDrawDiagEdge(L96-338):对角线边缘绘制

IntDrawDiagEdgeDrawEdgeBF_DIAGONAL 时的实现(注释:Same as DrawEdge invoked with BF_DIAGONAL),用于绘制带斜切角的边缘(如斜角按钮)。与矩形版共享 8 张颜色表,但只画一条对角线 ,并依 BF_DIAGONAL_ENDBOTTOMLEFT(0x8)/BF_DIAGONAL_ENDBOTTOMRIGHT(0x10)/BF_DIAGONAL_ENDTOPRIGHT(0x20)/BF_DIAGONAL_ENDTOPLEFT(0x40)决定斜线方向。

关键步骤:

  1. 计算端点 (L105-107):SmallDiam = min(Width, Height),斜线跨越"小直径"(方形切角)。
  2. 选色 (L120-155):同样按 BF_MONO/BF_FLAT/BF_SOFT/默认选四组表,但这里 InnerI/OuterI 各一个(斜线只有"内/外"两条)。
  3. 初始画笔 (L115-117):InnerPen = OuterPen = NtGdiGetStockObject(NULL_PEN);颜色索引有效时才换 DC_PEN
  4. 按 uFlags 的 BF_RECT 组合选择端点(L170-208):
c 复制代码
switch (uFlags & BF_RECT) {
case 0: case BF_LEFT: case BF_BOTTOM: case BF_BOTTOMLEFT:
   /* Left bottom endpoint */
   epx = rc->left-1;  spx = epx + SmallDiam;
   epy = rc->bottom;  spy = epy - SmallDiam;
   break;
case BF_TOPLEFT: case BF_BOTTOMRIGHT:
   /* Left top endpoint */ ...
case BF_TOP: case BF_RIGHT: ... case BF_RECT:
   /* Right top endpoint */
   spx = rc->left; epx = spx + SmallDiam;
   spy = rc->bottom-1; epy = spy - SmallDiam;
   break;
}
  1. 画外线 (L210-213):GreMoveTo(spx, spy)NtGdiLineTo(epx, epy)OuterPen)。
  2. 画内线 + 记录填充多边形 (L218-304):switch (uFlags & (BF_RECT|BF_DIAGONAL)) 有 8 个分支,每个分支画第二条线并填 Points[4](填充多边形的 4 个顶点)。例如 BF_DIAGONAL_ENDBOTTOMLEFT 分支:
c 复制代码
GreMoveTo(hdc, spx-1, spy, NULL);
NtGdiLineTo(hdc, epx, epy-1);
Points[0] = {spx-add, spy};
Points[1] = {rc->left, rc->top};
Points[2] = {epx+1, epy-1-add};
Points[3] = Points[2];
break;
  1. BF_MIDDLE 填充 (L307-318):选 DC_BRUSH/DC_PENIntSetDCBrushColor+IntSetDCPenColor 设为 COLOR_WINDOW(MONO)或 COLOR_BTNFACEIntPolygon(hdc, Points, 4) 画四边形------用同色画笔描边 + 画刷填充完成斜角内部。
  2. BF_ADJUST (L321-331):按 add 收缩矩形写回 *rc
  3. 收尾(L333-335):还原画笔与原点。

add(L111-112)来自两张 Mono 表:"该边缘类型有几层边框"就收缩几像素,保证 ADJUST 后的矩形正好是内容区。


9. 标题文字、图标与聚焦矩形的绘制(painting.c + cursoricon.c + user32)

draw.c 管"边缘/控件符号",而"标题文字、图标、聚焦矩形"是三个相邻的绘制辅助,分别位于 painting.c、cursoricon.c 与 user32。本节逐一展开。

9.1 UserDrawCaptionText(painting.c L2146-2248):标题栏文字

UserDrawCaption(9.2)调用,负责把窗口标题画进给定矩形。完整流程:

  1. 取非客户区度量 (L2167-2172):UserSystemParametersInfo(SPI_GETNONCLIENTMETRICS, cbSize, &nclm, 0) 获取 NONCLIENTMETRICSW(内含 lfCaptionFont/lfSmCaptionFont)。
  2. 选字体 (L2174-2188):调用者未给 hFont 时用 TextIntCreateFontIndirectDC_SMALLCAP 创建小标题字体或标准标题字体,bDeleteFont = TRUE(用完自删)。
  3. 设置背景/前景 (L2190-2198):IntGdiSetBkMode(hDc, TRANSPARENT);颜色按 DC_INBUTTON(按钮内,COLOR_BTNTEXT)或 DC_ACTIVE(活动标题 COLOR_CAPTIONTEXT / 非活动 COLOR_INACTIVECAPTIONTEXT)区分。
  4. 为系统菜单让位(L2200-2209):
c 复制代码
if (pWnd && pWnd->style & WS_SYSMENU) {
   r.right -= UserGetSystemMetrics(SM_CYCAPTION) - 1;
   if ((pWnd->style & (WS_MAXIMIZEBOX|WS_MINIMIZEBOX)) && !(pWnd->ExStyle & WS_EX_TOOLWINDOW)) {
      r.right -= UserGetSystemMetrics(SM_CXSIZE) + 1;
      r.right -= UserGetSystemMetrics(SM_CXSIZE) + 1;   // 最大化/最小化各占一个 SM_CXSIZE
   }
}

标题文字右侧留出系统菜单按钮(高度约一行的宽度)与最大/最小化按钮的空间。

  1. 测量与截断判断 (L2211-2218):GreGetTextExtentExW(hDc, Buffer, len, maxWidth, &fit, 0, &Size, 0) 求"能放进多宽"的字符数 fitLength = (len == fit ? fit : fit+1)------放不下时多取 1 字符 (为了让省略号路径接管);len > Length 说明被截断,Ret = FALSE

  2. 两条绘制路径(L2220-2237):

  • 放得下:UserExtTextOutW(hDc, left, top + (H - Size.cy)/2, ETO_CLIPPED, lpRc, Buffer, Length)------内核直出(等宽 ASCII 加速),垂直居中、按矩形裁剪;
  • 放不下:DrawTextW(hDc, Buffer, len, &r, DT_END_ELLIPSIS|DT_SINGLELINE|DT_VCENTER|DT_NOPREFIX|DT_LEFT)(defwnd.c,见 9.5)------走完整文本布局,右端省略号。
  1. 收尾 (L2239-2247):恢复文本色/字体,bDeleteFontGreDeleteObject;返回 Ret(是否完整显示,供调用者记 WNDS2_CAPTIONTEXTTRUNCATED)。

9.2 UserDrawCaption(painting.c L2253-2400+):整个标题栏

UserDrawCaption 把"背景 + 图标 + 文字"组装成一次完整标题绘制(被 nonclient.c 的 WM_NCPAINT/WM_NCACTIVATE 路径调用):

  1. 矩形规范化RECTL_vMakeWellOrdered(lpRc)(防反)。
  2. 是否画图标 (L2269-2276):调用者给了 hIcon 直接算;否则 HasIcon = (uFlags & DC_ICON) && !(uFlags & DC_SMALLCAP) && (pWnd->style & WS_SYSMENU) && !(pWnd->ExStyle & WS_EX_TOOLWINDOW)------标准规则:有系统菜单、非工具窗口、非小标题才显示图标。
  3. 背景绘制 (L2278-2336):
    • DC_GRADIENT && !DC_INBUTTON:构造两个 TRIVERTEX(活动/非活动标题色与渐变色的 16 位分量,L2285-2303),GreGradientFill(hDc, Vertices, 2, &gcap, 1, GRADIENT_FILL_RECT_H) 水平渐变;
    • 否则:选 COLOR_3DFACE(按钮)/COLOR_ACTIVECAPTION/COLOR_INACTIVECAPTION 系统刷,NtGdiPatBlt(PATCOPY) 整块填充。
  4. 画图标 (L2338-2368):hIconNC_IconForWindow(pWnd)(取窗口类图标,FIXME 不自动引用故手动 UserReferenceObject);x = Rect.left - cx/2 + 1 + (Rect.bottom-Rect.top)/2(注释:this is really what Window does------图标垂直居中于标题栏、水平贴近左侧),y = (top+bottom-cy)/2UserDrawIconEx(hDc, x, y, pIcon, SM_CXSMICON, SM_CYSMICON, 0, NULL, DI_NORMAL)(见 9.3)。
  5. 画文字 (L2370-2395):有图标时 Rect.left += Rect.bottom - Rect.top(让出图标方块);Rect.left += 2Str 为空且 pWnd != NULL 时用 pWnd->strName(FIXME:LARGE_STRING 截断);调 UserDrawCaptionText;按返回值维护 pWnd->state2WNDS2_CAPTIONTEXTTRUNCATED(截断标记,供系统在需要时提示)。
  6. 返回 TRUE。

9.3 UserDrawIconEx(cursoricon.c L1690-2079):图标/光标绘制核心

DrawIconEx/DrawIcon/DrawState 的内核实现落点(NtUserDrawIconExUserDrawIconEx)。这是图标三要素(颜色位图 + 掩码位图 + alpha 位图)的合成算法。分段展开:

入口检查与动画解引用(L1711-1727):

c 复制代码
if ((diFlags & DI_NORMAL) == 0) { ERR(...); return FALSE; }   // 必须含 DI_NORMAL
if (pIcon->CURSORF_flags & CURSORF_ACON) {                    // 动画光标
   ACON* pAcon = (ACON*)pIcon;
   if (istepIfAniCur >= pAcon->cicur) { ERR(...); return FALSE; }
   pIcon = pAcon->aspcur[pAcon->aicur[istepIfAniCur]];        // 取指定帧
}
hbmMask = pIcon->hbmMask;  hbmColor = pIcon->hbmColor;  hbmAlpha = pIcon->hbmAlpha;

锁定对象 (L1733-1765):SURFACE_ShareLockSurface(hbmMask)/(hbmColor) 共享锁 (只读位图,注释强调"共享锁足够");DC_LockDc(hDc) 锁目标 DC;hbmColor == NULL 时断言掩码位图高度 = 2×图标高度(信息在掩码位图下半部,见后)。

尺寸默认值 (L1767-1785):cxWidth/cyHeight 为 0 时:DI_DEFAULTSIZESM_CXICON/SM_CYICON(图标)或 SM_CXCURSOR/SM_CYCURSOR(光标);否则用 pIcon->cx/pIcon->cy(原始尺寸)。

目标矩形 (L1787-1793):RECTL_vSetRect(&rcDest, xLeft, yTop, xLeft+cxWidth, yTop+cyHeight)IntLPtoDP(逻辑→设备)→ 加 ptlDCOrigDC_vPrepareDCsForBlit(pdc, &rcDest, NULL, NULL) 准备表面与裁剪(pdc->dclevel.pSurface 即目标表面)。

防闪烁背景 (L1811-1902):hbrFlickerFreeDraw 非空且是刷子时 bOffScreen = TRUE,用 EBRUSHOBJ_vInit + IntEngBitBlt(ROP4_PATCOPY) 先把背景刷到目标表面(注释 #if 0 掉的原离屏表面方案被放弃------"我们在整个函数期间锁定 DC 表面,用离屏表面做防闪烁没有意义")。

第一段:alpha 混合(L1905-1933):

c 复制代码
if (hbmAlpha && ((diFlags & DI_NORMAL) == DI_NORMAL)) {
   BLENDOBJ blendobj = { {AC_SRC_OVER, 0, 255, AC_SRC_ALPHA } };
   psurf = SURFACE_ShareLockSurface(hbmAlpha);
   EXLATEOBJ_vInitialize(&exlo, psurf->ppal, psurfDest->ppal, 0xFFFFFFFF, 0xFFFFFFFF, 0);
   Ret = IntEngAlphaBlend(&psurfDest->SurfObj, &psurf->SurfObj,
                          pdcClipObj, &exlo.xlo, &rcDest, &rcSrc, &blendobj);
   EXLATEOBJ_vCleanup(&exlo);
   SURFACE_ShareUnlockSurface(psurf);
   if (Ret) goto done;    // alpha 全权接管,跳过掩码/图像两段
   ERR("NtGdiAlphaBlend failed!\n");
}
NoAlpha:

现代图标(hbmAlpha 存在)直接走 IntEngAlphaBlendAC_SRC_ALPHA 每像素 alpha,6.2 节算法),成功即完成整个绘制。

第二段:掩码(DI_MASK)(L1935-1961):

c 复制代码
DWORD rop4 = (diFlags & DI_IMAGE) ? ROP4_SRCAND : ROP4_SRCCOPY;
EXLATEOBJ_vInitSrcMonoXlate(&exlo, psurfDest->ppal, 0x00FFFFFF, 0);
Ret = IntEngStretchBlt(&psurfDest->SurfObj, &psurfMask->SurfObj, NULL, pdcClipObj,
                       &exlo.xlo, NULL, &rcDest, &rcSrc, NULL, NULL, NULL, rop4);

掩码是 1bpp 单色位图:ROP4_SRCAND(与 DI_IMAGE 配合时先"扣洞")或 ROP4_SRCCOPY(纯掩码绘制)。EXLATEOBJ_vInitSrcMonoXlate 为单色源建转换(0/1 → 前景/背景色)。

第三段:图像(DI_IMAGE)(L1963-2021):

c 复制代码
if (psurfColor) {
   DWORD rop4 = (diFlags & DI_MASK) ? ROP4_SRCINVERT : ROP4_SRCCOPY;
   EXLATEOBJ_vInitialize(&exlo, psurfColor->ppal, psurfDest->ppal, 0x00FFFFFF, 0x00FFFFFF, 0);
   Ret = IntEngStretchBlt(&psurfDest->SurfObj, &psurfColor->SurfObj, NULL, pdcClipObj,
                          &exlo.xlo, NULL, &rcDest, &rcSrc, NULL, NULL, NULL, rop4);
} else {
   /* Mask bitmap holds the information in its bottom half */
   DWORD rop4 = (diFlags & DI_MASK) ? ROP4_SRCINVERT : ROP4_SRCCOPY;
   RECTL_vOffsetRect(&rcSrc, 0, pIcon->cy);   // 源下移一个图标高度 → 下半部
   EXLATEOBJ_vInitSrcMonoXlate(&exlo, psurfDest->ppal, 0x00FFFFFF, 0);
   Ret = IntEngStretchBlt(&psurfDest->SurfObj, &psurfMask->SurfObj, NULL, pdcClipObj,
                          &exlo.xlo, NULL, &rcDest, &rcSrc, NULL, NULL, NULL, rop4);
}

经典的"掩码 + 图像"三步合成(无 alpha 位图的旧式图标):

  1. DI_MASKSRCAND 用掩码把目标"抠洞"(掩码为 1 处目标清 0);
  2. DI_IMAGESRCINVERT 把颜色位图 XOR 上去(洞内像素 = 颜色,洞外 = 颜色 XOR 原目标);
  3. 实际效果:颜色像素落在洞内 → 完全显示;掩码为 0 处 → XOR 两次抵消。

hbmColor 时从掩码位图下半部 取图像(rcSrc 偏移 pIcon->cy),这是 1bpp 图标的经典存储格式(上半掩码、下半图像)。

收尾 (L2061-2078):DC_vFinishBlit + DC_UnlockDcSURFACE_ShareUnlockSurface 两个位图。返回值 Ret

9.4 NtUserDrawIconEx(cursoricon.c L2084-2126):系统调用包装

c 复制代码
BOOL APIENTRY NtUserDrawIconEx(HDC hdc, int xLeft, int yTop, HICON hIcon,
    int cxWidth, int cyHeight, UINT istepIfAniCur, HBRUSH hbrFlickerFreeDraw,
    UINT diFlags, BOOL bMetaHDC, PVOID pDIXData)
  • UserEnterExclusive()UserGetCurIconObject(hIcon) 句柄→PCURICON_OBJECT(失败返回 FALSE);
  • UserDrawIconEx(hdc, xLeft, yTop, pIcon, cxWidth, cyHeight, istepIfAniCur, hbrFlickerFreeDraw, diFlags)
  • UserDereferenceObject(pIcon) 释放引用,UserLeave()

bMetaHDC/pDIXData 两个参数当前未使用(元文件 DC 场景在 user32 侧处理)。

9.5 DrawTextW(defwnd.c L16-28):内核文本绘制入口

c 复制代码
INT WINAPI DrawTextW(HDC hdc, LPCWSTR str, INT count, LPRECT rect, UINT flags)
{
   DRAWTEXTPARAMS dtp;
   memset(&dtp, 0, sizeof(dtp));
   dtp.cbSize = sizeof(dtp);
   if (flags & DT_TABSTOP) {
      dtp.iTabLength = (flags >> 8) & 0xff;   // 制表符长度编码在 flags 高 8 位
      flags &= 0xffff00ff;
   }
   return DrawTextExWorker(hdc, (LPWSTR)str, count, rect, flags, &dtp);
}

DrawTextW 是内核版文本布局入口(DrawTextExWorker 在 defwnd.c 实现完整布局:DT_SINGLELINE/DT_VCENTER/DT_END_ELLIPSIS 等)。被 UserDrawCaptionText(截断路径)、menu.c(菜单项文本)等调用。注意与用户态 gdi32/user32 的 DrawTextW 是两套独立实现------内核版供系统绘制路径使用。

9.6 DrawFocusRect(user32/windows/draw.c L1608-1630):聚焦矩形

聚焦矩形是纯用户态实现(任务描述中的"IntDrawFocusRect"并不存在,grep 无此符号):

c 复制代码
BOOL WINAPI DrawFocusRect(HDC hdc, CONST RECT *rect)
{
   UINT cx, cy;
   NtUserSystemParametersInfo(SPI_GETFOCUSBORDERWIDTH,  0, &cx, 0);
   NtUserSystemParametersInfo(SPI_GETFOCUSBORDERHEIGHT, 0, &cy, 0);
   OldObj = SelectObject(hdc, gpsi->hbrGray);   // gpsi->hbrGray:全局灰度画刷(内核 gdi 共享)
   /* top    */ PatBlt(hdc, rect->left, rect->top, rect->right-rect->left, cy, PATINVERT);
   /* bottom */ PatBlt(hdc, rect->left, rect->bottom-cy, rect->right-rect->left, cy, PATINVERT);
   /* left   */ PatBlt(hdc, rect->left, rect->top+cy, cx, rect->bottom-rect->top-(2*cy), PATINVERT);
   /* right  */ PatBlt(hdc, rect->right-cx, rect->top+cy, cx, rect->bottom-rect->top-(2*cy), PATINVERT);
   SelectObject(hdc, OldObj);
   return TRUE;
}
  • XOR 语义 :四条边用 PATINVERT(R2 反转)绘制------第二次调用同一矩形即擦除,这是聚焦矩形的经典"可逆绘制"(不需要知道原背景);
  • 边框粗细来自系统参数 SPI_GETFOCUSBORDERWIDTH/HEIGHT(无障碍设置,Windows 2000+);
  • 画刷是内核 GDI 的全局灰度刷 gpsi->hbrGray(同一画刷也用于滚动条背景与 UITOOLS_DrawCheckedRect 的灰点阵,draw.c L599);
  • 该函数在 user32(用户态)实现,因为它是应用可直接调用的 API(对话框/控件画焦点框),不涉及系统内部绘制路径。

10. 调用链(mermaid)

10.1 分层窗口更新主链

#mermaid-svg-Td5J4vqu3NF8yCNp{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-Td5J4vqu3NF8yCNp .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Td5J4vqu3NF8yCNp .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Td5J4vqu3NF8yCNp .error-icon{fill:#552222;}#mermaid-svg-Td5J4vqu3NF8yCNp .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Td5J4vqu3NF8yCNp .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Td5J4vqu3NF8yCNp .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Td5J4vqu3NF8yCNp .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Td5J4vqu3NF8yCNp .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Td5J4vqu3NF8yCNp .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Td5J4vqu3NF8yCNp .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Td5J4vqu3NF8yCNp .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Td5J4vqu3NF8yCNp .marker.cross{stroke:#333333;}#mermaid-svg-Td5J4vqu3NF8yCNp svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Td5J4vqu3NF8yCNp p{margin:0;}#mermaid-svg-Td5J4vqu3NF8yCNp .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Td5J4vqu3NF8yCNp .cluster-label text{fill:#333;}#mermaid-svg-Td5J4vqu3NF8yCNp .cluster-label span{color:#333;}#mermaid-svg-Td5J4vqu3NF8yCNp .cluster-label span p{background-color:transparent;}#mermaid-svg-Td5J4vqu3NF8yCNp .label text,#mermaid-svg-Td5J4vqu3NF8yCNp span{fill:#333;color:#333;}#mermaid-svg-Td5J4vqu3NF8yCNp .node rect,#mermaid-svg-Td5J4vqu3NF8yCNp .node circle,#mermaid-svg-Td5J4vqu3NF8yCNp .node ellipse,#mermaid-svg-Td5J4vqu3NF8yCNp .node polygon,#mermaid-svg-Td5J4vqu3NF8yCNp .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Td5J4vqu3NF8yCNp .rough-node .label text,#mermaid-svg-Td5J4vqu3NF8yCNp .node .label text,#mermaid-svg-Td5J4vqu3NF8yCNp .image-shape .label,#mermaid-svg-Td5J4vqu3NF8yCNp .icon-shape .label{text-anchor:middle;}#mermaid-svg-Td5J4vqu3NF8yCNp .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Td5J4vqu3NF8yCNp .rough-node .label,#mermaid-svg-Td5J4vqu3NF8yCNp .node .label,#mermaid-svg-Td5J4vqu3NF8yCNp .image-shape .label,#mermaid-svg-Td5J4vqu3NF8yCNp .icon-shape .label{text-align:center;}#mermaid-svg-Td5J4vqu3NF8yCNp .node.clickable{cursor:pointer;}#mermaid-svg-Td5J4vqu3NF8yCNp .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Td5J4vqu3NF8yCNp .arrowheadPath{fill:#333333;}#mermaid-svg-Td5J4vqu3NF8yCNp .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Td5J4vqu3NF8yCNp .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Td5J4vqu3NF8yCNp .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Td5J4vqu3NF8yCNp .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Td5J4vqu3NF8yCNp .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Td5J4vqu3NF8yCNp .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Td5J4vqu3NF8yCNp .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Td5J4vqu3NF8yCNp .cluster text{fill:#333;}#mermaid-svg-Td5J4vqu3NF8yCNp .cluster span{color:#333;}#mermaid-svg-Td5J4vqu3NF8yCNp 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-Td5J4vqu3NF8yCNp .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Td5J4vqu3NF8yCNp rect.text{fill:none;stroke-width:0;}#mermaid-svg-Td5J4vqu3NF8yCNp .icon-shape,#mermaid-svg-Td5J4vqu3NF8yCNp .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Td5J4vqu3NF8yCNp .icon-shape p,#mermaid-svg-Td5J4vqu3NF8yCNp .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Td5J4vqu3NF8yCNp .icon-shape .label rect,#mermaid-svg-Td5J4vqu3NF8yCNp .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Td5J4vqu3NF8yCNp .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Td5J4vqu3NF8yCNp .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Td5J4vqu3NF8yCNp :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} user32.spec L660 类似映射
SEH2 拷贝参数 + 校验
hdc = UserGetDCEx(DCX_USESTYLE)
hbmSrc/hbmDst = NtGdiCreateCompatibleBitmap
ULW_COLORKEY 时 crKey
prcDirty → NtGdiPatBlt(BLACKNESS)
HOOK_ALPHABLEND
回退
裁剪复杂度分派
co_WinPosSetWindowPos(SWP_NOREDRAW...)
应用:UpdateLayeredWindow(hWnd, hdcDst, pptDst, psize, hdcSrc, ...)
NtUserUpdateLayeredWindow

layered.c L339
应用:UpdateLayeredWindowIndirect(info)
组装 UPDATELAYEREDWINDOWINFO
IntUpdateLayeredWindowI

layered.c L100
DC
Bitmaps
NtGdiStretchBlt(hdcBuffer ← hdcSrc,

SRCCOPY, color_key)

ntgdi/bitblt.c L838
NtGdiAlphaBlend(hdc ← hdcBuffer, blend)

ntgdi/bitblt.c L12
IntEngAlphaBlend

eng/alphablend.c L196
GDIDEVFUNCS().AlphaBlend

(显示驱动)
EngAlphaBlend

eng/alphablend.c L18
DIB_AlphaBlend

dib/(按位深分派)
winpos.c

窗口移动/缩放
vis.c 可见区域重算

下层窗口重绘

10.2 SetLayeredWindowAttributes 属性链

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

user32.spec L660
NtUserSetLayeredWindowAttributes

layered.c L310
IntSetLayeredWindowAttributes

layered.c L45
co_UserRedrawWindow

RDW_INVALIDATE|RDW_ERASE|RDW_FRAME

painting.c
LRD_PROP 挂到 AtomLayer 属性

prop.c
GetLayeredWindowAttributes

user32.spec L322
NtUserGetLayeredWindowAttributes

layered.c L239
Key / Alpha / Flags
SetWindowLong(GWL_EXSTYLE) 去掉 WS_EX_LAYERED

window.c L3934
SetLayeredStatus(0)

layered.c L33

10.3 绘制辅助调用全景

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

DefWindowProc → nonclient.c
DrawFrameControl

draw.c L1093
UITOOLS95_DrawFrameCaption

L788 → Marlett 字符 r/s/0/1/2
UITOOLS95_DrawFrameButton

L758
UITOOLS95_DFC_ButtonPush

L620
UITOOLS95_DFC_ButtonCheckRadio

L664 → Marlett c/d/e/f/g/b/i
UITOOLS95_DrawFrameScroll

L856 → Marlett 3/4/5/6/o/p/x/y
UITOOLS95_DrawFrameMenu

L959 → Marlett 5/6/8/w/h/a
DrawEdge

draw.c L1084
IntDrawDiagEdge

L96
IntDrawRectEdge

L410
FillRect

draw.c L1056
NtGdiPatBlt(PATCOPY)
UserDrawCaption

painting.c L2253
UserDrawCaptionText

painting.c L2146
UserExtTextOutW(ETO_CLIPPED)
DrawTextW → DrawTextExWorker

defwnd.c L16
UserDrawIconEx

cursoricon.c L1690
IntEngAlphaBlend

(AC_SRC_ALPHA)
IntEngStretchBlt

(ROP4_SRCAND/SRCINVERT/SRCCOPY)
menu.c 菜单项

L1773/L2332
应用 DrawFocusRect

user32/windows/draw.c L1608


11. 与其他模块的关系

11.1 与 GDI 引擎(bitblt/eng/dib)的关系

分层合成的核心依赖链:

复制代码
layered.c (USER) ──NtGdiAlphaBlend──→ ntgdi/bitblt.c (GDI 系统调用层)
      └─ IntEngAlphaBlend (eng/alphablend.c, 引擎分派)
            ├─ DrvAlphaBlend (显示驱动钩子, HOOK_ALPHABLEND)
            └─ EngAlphaBlend (软件引擎)
                  └─ DIB_AlphaBlend (dib/ 按位深分派, 像素级 α 混合)
  • USER 不直接算像素IntUpdateLayeredWindowI 只负责"摆 DC、摆位图、摆参数",混合数学全部在 GDI 引擎;这正是总览文档反复强调的"USER 管、GDI 画"边界。
  • ColorKey 通道 :分层窗口的 ColorKey 经 NtGdiStretchBltGreStretchBltMaskIntEngStretchBlt(dwBackColor);独立 TransparentBltIntEngTransparentBlt。两条通道都需要 EXLATEOBJ(调色板/格式转换对象)保证颜色比较在同一色彩空间。
  • 裁剪(CLIPOBJ)EngAlphaBlendiDComplexity 分派 TRIVIAL/RECT/COMPLEX 三种策略,复杂裁剪时枚举矩形并按比例映射回源矩形 (6.2 节)------分层窗口合成到窗口 DC 时,可见区域(vis.c 提供的 DCDest->co)正是这个裁剪对象。

11.2 与 vis.c / windc.c 的关系

  • IntUpdateLayeredWindowI 取窗口 DC 用 UserGetDCEx(pWnd, NULL, DCX_USESTYLE)(windc.c 的 DC 获取),该 DC 的可见区域裁剪hrgnVis,vis.c 合成)在 NtGdiAlphaBlendDC_vPrepareDCsForBlit 中被转成 CLIPOBJ(L109/DCDest->co)------分层窗口合成不会画出自己的可见范围(如被 Z 序上层窗口遮挡的部分不会覆盖屏幕)。
  • 分层窗口移动(co_WinPosSetWindowPos)后,vis.c 需要为它和受影响的下层窗口重算可见区域------这是 winpos 常规路径(分册 _3)的职责,分层窗口本身只是"不重绘"。

11.3 与 winpos.c / painting.c 的关系

交互点 方向 说明
co_WinPosSetWindowPos(layered.c L231) layered → winpos 应用移动/缩放(SWP_NOREDRAW)
IntWindowHitTest(winpos.c L2938) winpos → layered `WS_EX_LAYERED
UserFindWindowFromPoint(msgqueue.c L82) input → layered 同上(鼠标命中穿透)
`co_UserRedrawWindow(RDW_INVALIDATE RDW_ERASE RDW_FRAME)`(layered.c L94)
UserDrawCaption(painting.c L2253) painting → draw/cursoricon 标题栏绘制调用绘制辅助
SetLayeredStatus(0)(window.c L3936) window → layered 清 WS_EX_LAYERED 时复位状态

draw.c 的两个主要"客户":

  • nonclient.c (分册 _6):标题按钮 DrawFrameControl(DFC_CAPTION, DFCS_CAPTIONCLOSE/HELP/MIN/MAX/RESTORE)(L837/856/881)、边框 DrawEdge(EDGE_RAISED, BF_RECT|BF_ADJUST)(L982)与 DrawEdge(BDR_SUNKENINNER, BF_RECT|BF_ADJUST|BF_FLAT)(L987)------BF_ADJUST 让"先画边框、再在缩进后的矩形里画内容"成为可能。
  • menu.c (分册 _11/_12):菜单项标题按钮 DrawFrameControl(hdc, &r, DFC_CAPTION, flags)(L1773)、菜单项分隔线/边框 DrawEdge(hdc, &rect, BDR_SUNKENOUTER, BF_RECT)(L2332)。

这些调用点说明 draw.c 是**"系统外观"的公共实现**:无论按钮在标题栏还是菜单里,外观都由同一套 Marlett 字符 + 颜色表代码产生。


12. 源码索引

12.1 layered.c(win32ss/user/ntuser/layered.c,427 行)

符号 行号 说明
LRD_PROP L13-20 分层窗口属性块(Key/Alpha/is_Layered/Flags)
GetLayeredStatus L22-31 读 is_Layered
SetLayeredStatus L33-43 写 is_Layered
IntSetLayeredWindowAttributes L45-98 属性设置(分配 LRD_PROP、首设重绘)
IntUpdateLayeredWindowI L100-233 合成管线核心(移动/缩放 + StretchBlt + AlphaBlend)
NtUserGetLayeredWindowAttributes L239-305 读属性(ProbeForWrite 回写)
NtUserSetLayeredWindowAttributes L310-334 系统调用包装
NtUserUpdateLayeredWindow L339-425 系统调用(SEH2 拷贝 + 校验 + 组装 info)

12.2 draw.c(win32ss/user/ntuser/draw.c,1137 行)

符号 行号 说明
8 张颜色表 L21-92 边缘颜色查找表(Normal/Soft/Mono/Flat × Inner/Outer)
IntDrawDiagEdge L96-338 对角线边缘(BF_DIAGONAL)
IntDrawRectEdge L410-564 矩形边缘(默认)
UITOOLS_MakeSquareRect L566-587 矩形转正方形(Marlett 符号用)
UITOOLS_DrawCheckedRect L589-608 检查态背景(灰点阵 hbrGray)
UITOOLS95_DFC_ButtonPush L620-662 按压按钮
UITOOLS95_DFC_ButtonCheckRadio L664-756 复选框/单选钮(Marlett c~g/b/i)
UITOOLS95_DrawFrameButton L758-786 按钮分发(PUSH/CHECK/3STATE/RADIO)
UITOOLS95_DrawFrameCaption L788-854 标题按钮(Marlett r/s/0/1/2)
UITOOLS95_DrawFrameScroll L856-957 滚动按钮(3/4/5/6)与尺寸柄(o/p/x/y)
UITOOLS95_DrawFrameMenu L959-1028 菜单符号(5/6/8/w/h/a)
FrameRect L1037-1053 边框(4 条 1px PatBlt)
FillRect L1056-1082 填充(系统色刷转换 + PatBlt PATCOPY)
DrawEdge L1084-1091 边缘分发(Diag/Rect)
DrawFrameControl L1093-1137 控件符号分发(BUTTON/CAPTION/MENU/SCROLL)

12.3 支撑源码

文件 符号 行号 说明
ntuser/ntuser.c AtomLayer 定义/注册 L21、L68 全局原子 "SysLayer"
ntuser/window.c SetLayeredStatus(0) L3934-3937 清 WS_EX_LAYERED 复位
ntuser/msgqueue.c 点击穿透判断 L81-84 命中候选跳过透明分层窗
ntuser/winpos.c 点击穿透判断 L2937-2941 WindowFromPoint 跳过
ntuser/painting.c UserDrawCaptionText L2146-2248 标题文字(测量/截断/ETO/DrawTextW)
ntuser/painting.c UserDrawCaption L2253-2400+ 标题栏整体(渐变背景/图标/文字)
ntuser/cursoricon.c UserDrawIconEx L1690-2079 图标合成(alpha/掩码/图像三段)
ntuser/cursoricon.c NtUserDrawIconEx L2084-2126 系统调用包装
ntuser/defwnd.c DrawTextW L16-28 内核文本入口(DT_TABSTOP 解包)
gdi/ntgdi/bitblt.c NtGdiAlphaBlend L12-147 合成系统调用
gdi/ntgdi/bitblt.c NtGdiTransparentBlt L199-309 颜色键系统调用
gdi/ntgdi/bitblt.c NtGdiStretchBlt/GreStretchBltMask L838-871/L576+ crBackColor 通道
gdi/eng/alphablend.c EngAlphaBlend L18-194 软件混合(裁剪分派)
gdi/eng/alphablend.c IntEngAlphaBlend L196-244 驱动优先分派
gdi/eng/alphablend.c NtGdiEngAlphaBlend L249-278 导出系统调用版
user32/windows/window.c UpdateLayeredWindow L1751-1777 用户态包装(拒 ULW_EX_NORESIZE)
user32/windows/window.c UpdateLayeredWindowIndirect L1782-1801 用户态包装(cbSize 校验)
user32/windows/draw.c DrawFocusRect L1608-1630 XOR 聚焦矩形(PATINVERT + hbrGray)
user32/user32.spec 映射表 L322、L660 Get/SetLayeredWindowAttributes → NtUser*
gdi/eng/pandisp.c PanAlphaBlend L428-440 显示驱动混合示例(影子表面)

12.4 本册覆盖统计

  • layered.c :全部 8 个函数 + LRD_PROP 结构;
  • draw.c:全部 14 个函数 + 8 张颜色表;
  • 支撑函数UserDrawIconEx/NtUserDrawIconEx(cursoricon.c)、UserDrawCaptionText/UserDrawCaption(painting.c)、DrawTextW(defwnd.c)、DrawFocusRect(user32)、UpdateLayeredWindow/UpdateLayeredWindowIndirect(user32)、NtGdiAlphaBlend/NtGdiTransparentBlt/NtGdiStretchBlt/GreStretchBltMask(ntgdi/bitblt.c)、EngAlphaBlend/IntEngAlphaBlend/NtGdiEngAlphaBlend(eng/alphablend.c);
  • 共覆盖约 33 个函数 + 1 个结构体(含关联点窗口:window.c、msgqueue.c、winpos.c、nonclient.c、menu.c 的分层/绘制交互)。

13. 总结

本册是"窗口显示"主线的收官之册,回答了两个问题:非常规内容怎么显示 (分层窗口)与系统自身怎么绘制(绘制辅助)。

分层窗口(layered.c)

  • 状态即属性:LRD_PROP 挂在 AtomLayer 全局原子属性上,is_LayeredWS_EX_LAYERED 两个开关各有分工(运行时状态 vs 扩展样式位);
  • 两条 API 风格互斥:SetLayeredWindowAttributes(写属性块,首设整窗重绘)与 UpdateLayeredWindow(推内容,SEH2 校验 + 组装 info);
  • 合成管线:IntUpdateLayeredWindowI 用"两张兼容位图 + 兼容 DC"承接源内容(StretchBlt 处理 ColorKey),NtGdiAlphaBlend 完成混合,co_WinPosSetWindowPos(SWP_NOREDRAW) 落地位置/尺寸;
  • 合成算法三级管道:NtGdiAlphaBlend(锁双 DC、坐标换算、边界累计)→ IntEngAlphaBlend(驱动优先)→ EngAlphaBlend(按裁剪复杂度分派 TRIVIAL/RECT/COMPLEX)→ DIB_AlphaBlend(像素级 Src×α + Dst×(1−α)AC_SRC_ALPHA 时 α 含每像素通道);
  • 点击穿透:WS_EX_LAYERED | WS_EX_TRANSPARENT 精确组合在命中测试两处(msgqueue.c/winpos.c)被跳过;
  • 重要澄清 :当前 ReactOS 实现是"内存位图即时合成"模型,没有 Windows 式的独立分层表面(hSection/映射),prcDirty 优化仍是占位级------这是与总览文档 4.5 节描述的主要出入点。

绘制辅助(draw.c)

  • 表驱动:8 张颜色表把"边缘类型 × 模式"映射为系统色,IntDrawRectEdge/IntDrawDiagEdgeDC_PEN + IntSetDCPenColor + MoveTo/LineTo 画出 3D 边缘,BF_ADJUST 收缩矩形、BF_MIDDLE 填充内部;
  • Marlett 字体符号:UITOOLS95_* 系列用 GreCreateFontIndirectW(L"Marlett") + GreTextOutW 画按钮/标题/滚动/菜单符号------系统外观统一来自这一处;
  • 配套工具:FillRect/FrameRect(PatBlt 系)、DrawEdge/DrawFrameControl(分发器);
  • 标题与图标:UserDrawCaptionText(测量→ETO 直出或 DrawTextW 省略号)、UserDrawIconEx(alpha 位图优先,否则经典的"掩码 SRCAND + 图像 SRCINVERT"三步合成);
  • 聚焦矩形:user32 的 DrawFocusRectPATINVERT + 灰度刷实现可逆 XOR 绘制。

至此,从窗口对象(_1)、窗口类(_2)、位置与可见性(_3)、绘制协议(_4)、可见区域与窗口 DC(_5)、非客户区(_6)到分层窗口与绘制辅助(_7),"窗口显示"主线的全部代码路径已完整覆盖。


(本册完。下一主线见《ReactOS 窗口系统分析(8):消息队列 --- msgqueue.c》)

相关推荐
丰锋ff1 小时前
基于 Qt 的智慧社区物业管理系统
开发语言·qt
夜不会漫长2 小时前
C++入门(1)
开发语言·c++·算法
大鹏说大话2 小时前
从爬虫到决策引擎:大数据下自媒体如何用Python挖掘用户痛点
开发语言·爬虫·python
Niuguangshuo2 小时前
silero-vad:超轻量级开源 VAD 实践指南
开发语言·python
哭哭啼2 小时前
JAVA服务问题诊断
java·开发语言·jvm
..Dauntless..4 小时前
【C++】继承——基类和派生类的配合
开发语言·c++·算法
CRMEB定制开发4 小时前
2026年最值得推荐的10大开源商城系统盘点
开发语言·人工智能·开源·商城系统·小程序商城
不灭的黄金瞳1234 小时前
预处理详解
c语言
buhuizhiyuci5 小时前
【QT-百日筑基篇】修真世界摸爬滚打多年,终于黄天不负有心人,突破炼器中期——常用控件QWight的属性
开发语言·c++·qt·gui·图形化