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.c(UserDrawCaptionText/UserDrawCaption,标题绘制)win32ss/user/ntuser/cursoricon.c(UserDrawIconEx/NtUserDrawIconEx,图标绘制)win32ss/user/ntuser/defwnd.c(DrawTextW,文本绘制入口)win32ss/gdi/ntgdi/bitblt.c(NtGdiAlphaBlend/NtGdiTransparentBlt/NtGdiStretchBlt)win32ss/gdi/eng/alphablend.c(IntEngAlphaBlend/EngAlphaBlend,混合引擎)win32ss/gdi/dib/(DIB_AlphaBlend像素级混合)win32ss/user/user32/windows/window.c(用户态UpdateLayeredWindow/UpdateLayeredWindowIndirect)win32ss/user/user32/windows/draw.c(用户态DrawFocusRect等)- 关联:
winpos.c(co_WinPosSetWindowPos移动分层窗口)、msgqueue.c(透明命中测试)、nonclient.c/menu.c(DrawEdge/DrawFrameControl调用者)、prop.c(UserGetProp/UserSetProp承载分层属性)、ntuser.c(AtomLayer全局原子)
目录
- 前言与本册定位
- 概述:分层合成与绘制辅助两条线
- 设计动机
- 核心数据结构
- [layered.c 核心函数逐一展开](#layered.c 核心函数逐一展开)
- 分层合成算法
- 分层窗口与普通窗口的绘制差异
- [draw.c 绘制辅助实现细节](#draw.c 绘制辅助实现细节)
- [标题文字、图标与聚焦矩形的绘制(painting.c + cursoricon.c + user32)](#标题文字、图标与聚焦矩形的绘制(painting.c + cursoricon.c + user32))
- 调用链(mermaid)
- 与其他模块的关系
- 源码索引
- 总结
1. 前言与本册定位
窗口的"显示"由五件事共同完成:创建窗口对象 (window.c,分册 _1)、注册窗口类 (class.c,分册 _2)、摆放窗口并决定可见性 (winpos.c,分册 _3)、按 WM_PAINT 协议把内容画到屏幕 (painting.c + windc.c + vis.c,分册 _4/_5)、绘制非客户区(nonclient.c,分册 _6)。本册(_7)是该主线的收官之册,讲述最后两块拼图:
-
分层窗口(layered.c) :一种不走 WM_PAINT 协议的"特殊显示"路径。窗口内容不是由应用在 WM_PAINT 里画到窗口表面,而是应用直接提供一个源 DC 的内容,由内核用 AlphaBlend(逐像素 alpha 混合)把内容合成到窗口上。典型应用:任务栏缩略图、半透明 OSD、动画光标形状的异形窗口、工具提示阴影等。
-
绘制辅助(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 个函数 (含
NtUserUpdateLayeredWindow→IntUpdateLayeredWindowI的完整合成管线); - 接着剖析分层合成算法 :从
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 月)。所有描述均基于真实源码,未做任何虚构。特别提醒:任务描述/总览中提到的
IntLayeredAlphaBlend、IntUpdateWindowPos、IntDrawFocusRect、IntDrawCaptionText、IntFillRect、IntFrameRect、IntDrawIconEx、IntDrawText等函数名在当前源码中并不存在,本册按真实函数名逐一对应介绍(详见各节)。
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 → 应用绘制。这个模型有两个固有弱点:
- 不透明:普通窗口的内容是"画在"窗口表面上的,覆盖即遮挡,无法做到半透明、无法让下层内容透上来。
- 异步延迟:内容变化要经过"标脏 → 消息 → 绘制"的完整循环,不适合需要"每帧直接推内容"的场景(视频、动画、游戏)。
分层窗口(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),原因有三:
- 非客户区绘制发生在内核 :标题栏按钮、边框由
DefWindowProc的 WM_NCPAINT 路径绘制(nonclient.c),该路径在内核态执行,无法调用用户态 gdi32.dll 的DrawEdge。必须由内核直接发 GDI 调用(NtGdiSelectPen/NtGdiLineTo/NtGdiPatBlt/GreTextOutW等)。 - 绘制的是系统语义 :边缘的颜色来自系统颜色表(
COLOR_BTNHIGHLIGHT等),按钮符号来自 Marlett 字体------这些是"系统外观"的组成部分,理应集中在内核一处实现,保证系统所有 UI(标题按钮、菜单项、滚动条按钮)外观一致。 - 性能与减少切换 :
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)上,属性名是全局原子AtomLayer(L"SysLayer")。这一设计让"分层状态"成为窗口的一个可选附加属性------绝大多数窗口没有分层属性,不占 WND 的空间;需要时才分配。 - 分配方式 :
ExAllocatePoolWithTag(PagedPool, sizeof(LRD_PROP), USERTAG_REDIRECT)(L64),从分页池 分配(窗口属性生命周期内可被换出,访问时由内核保证驻留)。标签USERTAG_REDIRECT用于池泄漏追踪。 - 两个布尔语义 :
is_Layered与WS_EX_LAYERED是两个不同的开关 。WS_EX_LAYERED是扩展样式位(SetWindowLong/CreateWindowEx设置);is_Layered是"是否真正被初始化过"的运行时标志(由IntSetLayeredWindowAttributes置 1)。NtUserUpdateLayeredWindow用GetLayeredStatus()判断"已分层则拒绝再走属性路径"(见 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 存放在全局变量 AtomLayer(window.h 中 extern 声明)。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) |
实现细节:IntSetLayeredWindowAttributes 中 if (dwFlags & LWA_ALPHA) pLrdProp->Alpha = bAlpha; else { if (!pLrdProp->is_Layered) pLrdProp->Alpha = 0; }------即只认 LWA_ALPHA ,LWA_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)是:
- 每次调用时用
NtGdiCreateCompatibleBitmap(info->hdcSrc, w, h)创建两张临时内存位图(hbmSrc/hbmDst); - 源内容经
NtGdiStretchBlt拷入 hbmSrc(处理 ColorKey); - 用
NtGdiAlphaBlend把 hbmSrc 混合到目标 DC 当前表面 (hdcDst或窗口 DC 的可见表面); - 临时位图/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 非空 ,UpdateLayeredWindow 的 psize 参数不可为 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.cL3936 用来在清除WS_EX_LAYERED时复位分层状态(见 5.8 节)。 - 两个函数都只做"读/写一个字节",不分配内存------分配只在
IntSetLayeredWindowAttributes中发生。声明在userfuncs.hL167-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检查在此再做一次(内核入口自校验,不信任用户态)。- 用户态入口
SetLayeredWindowAttributes经user32.specL660 直接映射到本系统调用,无额外包装。
5.4 NtUserGetLayeredWindowAttributes(L239-305)
c
BOOL APIENTRY NtUserGetLayeredWindowAttributes(HWND hwnd, COLORREF *pcrKey, BYTE *pbAlpha, DWORD *pdwFlags)
流程:
UserEnterExclusive();UserGetWindowObject+WS_EX_LAYERED检查(L254-255),失败goto Exit。UserGetProp(pWnd, AtomLayer, TRUE)取属性块;无属性块或is_Layered == 0(L269)都视为"未分层",退出返回 FALSE。_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; }
_SEH2_EXCEPT捕获访问违规:SetLastNtError(exception code)并返回 FALSE(写用户地址可能因页面无效而异常,SEH 是内核访问用户缓冲的标准保护手段)。- 全成功
Ret = TRUE;UserLeave()返回。
返回值语义: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;
}
三个拒绝条件:
GetLayeredStatus(pWnd)为真------窗口已经被SetLayeredWindowAttributes初始化过 (is_Layered == 1),此时不允许再用UpdateLayeredWindow(两种分层风格互斥,Windows 语义同)。dwFlags含未知位(超出四个 ULW_* 的并集)。- 窗口没有
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 拆开转发,prcDirty 传 info->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)------因为UpdateLayeredWindow的pptDst对子窗口而言是父客户区坐标。平移后再算偏移(见下)。
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给出新尺寸 → 尺寸差 = 新宽高 − 当前宽高;- 尺寸必须为正(
<= 0报ERROR_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)取窗口 DC (DCX_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);
}
NtGdiStretchBlt带color_key参数 (第 12 参crBackColor,SRCCOPY三元光栅操作)把源内容(含pptSrc偏移)拷入hdcBuffer/hbmSrc。ColorKey 透明就是在这条 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用调用者pblend(ULW_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)
流程:
- 校验 (L38-48):宽高非负;双 DC 非空,否则
ERROR_INVALID_PARAMETER。 - 锁定双 DC (L53-58):
GDIOBJ_bLockMultipleObjects(2, ahDC, apObj, GDIObjType_DC_TYPE)一次性锁定两个 DC 对象(避免死锁,比逐个锁定安全)。 - 信息 DC 短路 (L62-68):任一 DC 是
DCTYPE_INFO→ 直接返回 TRUE("Windows 确实这样")。 - 坐标换算 (L70-95):目标/源矩形各自
IntLPtoDP(逻辑→设备)再加ptlDCOrig(DC 原点偏移)。 - 边界累计 (L81-84):目标 DC 带
DC_ACCUM_APP|DC_ACCUM_WMGR(窗口管理器累计)标志时IntUpdateBoundsRect把目标矩形并入脏区边界(供后续屏幕刷新用)。 - 准备 Blit (L109):
DC_vPrepareDCsForBlit(解析 DC 表面、可见区域裁剪 →DCDest->co裁剪对象)。 - 取表面 (L112-124):
DCDest->dclevel.pSurface/DCSrc->dclevel.pSurface。 - 颜色转换 (L127):
EXLATEOBJ_vInitXlateFromDCs(&exlo, DCSrc, DCDest)建立源→目标的调色板/格式转换对象。 - 核心调用(L131-137):
c
bResult = IntEngAlphaBlend(&BitmapDest->SurfObj, &BitmapSrc->SurfObj,
(CLIPOBJ *)&DCDest->co, &exlo.xlo,
&DestRect, &SourceRect, &BlendObj);
- 收尾 :
EXLATEOBJ_vCleanup、DC_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)------软件混合核心。校验与分派:
- 矩形规范化 (L54-59):目标/源矩形
RECTL_vMakeWellOrdered(防负宽高)。 - 源矩形越界校验 (L60-67):源矩形任一边 < 0 或超出源位图尺寸 →
ERROR_INVALID_PARAMETER。 - 自重叠拒绝(L69-75):目标与源是同一表面且矩形相交 → 拒绝(需要临时缓冲才能正确自混)。
- 混合参数校验 (L77-91):
BlendOp != AC_SRC_OVER→ 拒绝(只支持"源覆盖");BlendFlags != 0→ 拒绝;AlphaFormat含AC_SRC_ALPHA之外的位 → 拒绝。
- 进入表面 (L103-120):
IntEngEnter分别锁定源/目标表面(处理格式转换、平移Translate,得到InputObj/OutputObj与平移后的矩形)。 - 按裁剪复杂度分派(L125-188,见下)。
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 = 0 时 SrcAlpha 视为 255,α 只由 SourceConstantAlpha 决定------这正是"整窗 alpha"与"每像素 alpha"两种模式的统一公式。
6.3 NtGdiEngAlphaBlend(L249-278)
EngAlphaBlend 的导出系统调用版 (给驱动/其他内核组件直接调用的入口):SEH2 ProbeForRead 拷贝用户提供的目标/源矩形后转发 EngAlphaBlend。IntUpdateLayeredWindowI 不经过它(走 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_ALPHA 时 blend = {AC_SRC_OVER, 0, 255, 0}(SourceConstantAlpha=255、无每像素 alpha)即完全不透明;同时不传 ULW_COLORKEY 时 color_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-84 (UserFindWindowFromPoint 的候选循环):
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-2941 (IntWindowHitTest 家族,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 性能与正确性注意
- 全量重合成 :无持久表面意味着每次
UpdateLayeredWindow都重新 StretchBlt + AlphaBlend 整窗内容。prcDirty参数虽存在但只是"合成后涂黑"占位(5.6 第三步),实际仍是全量混合。频繁更新的应用(动画)应缩小窗口或降低更新频率。 - 源格式决定 alpha 效果 :
AC_SRC_ALPHA要求源是 32bpp 带 alpha 通道的 DIB 位图(GreGetObject检查biBitCount,L172-174);若源是 24bpp,AlphaFormat的每像素 alpha 无意义,只有SourceConstantAlpha生效。 - 分层窗口仍是窗口:它依然参与 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 不会被误认为颜色索引。
关于任务描述中的函数名澄清 :总览/任务提到的
IntFillRect、IntFrameRect、IntDrawEdge在当前 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):矩形边缘绘制
IntDrawRectEdge 是 DrawEdge 无 BF_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_MONO取COLOR_WINDOW否则COLOR_BTNFACE;BF_ADJUST:把"边框占掉的像素"从矩形中扣掉并写回*rc------调用者常用它先调整矩形再在其中画内容(nonclient.c L982 等);retval(L423-425)在"既有 BDR_INNER 又有 BDR_OUTER 且非 FLAT/MONO"时为 FALSE(此时是普通 3D 凸起,不需中间填充),其余为 TRUE。
收尾 (L560-563):选回 SavePen、GreMoveTo 恢复原点。返回值 retval 供调用者判断是否绘制了中间填充。
8.3 IntDrawDiagEdge(L96-338):对角线边缘绘制
IntDrawDiagEdge 是 DrawEdge 带 BF_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)决定斜线方向。
关键步骤:
- 计算端点 (L105-107):
SmallDiam = min(Width, Height),斜线跨越"小直径"(方形切角)。 - 选色 (L120-155):同样按
BF_MONO/BF_FLAT/BF_SOFT/默认选四组表,但这里InnerI/OuterI各一个(斜线只有"内/外"两条)。 - 初始画笔 (L115-117):
InnerPen = OuterPen = NtGdiGetStockObject(NULL_PEN);颜色索引有效时才换DC_PEN。 - 按 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;
}
- 画外线 (L210-213):
GreMoveTo(spx, spy)→NtGdiLineTo(epx, epy)(OuterPen)。 - 画内线 + 记录填充多边形 (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;
- BF_MIDDLE 填充 (L307-318):选
DC_BRUSH/DC_PEN,IntSetDCBrushColor+IntSetDCPenColor设为COLOR_WINDOW(MONO)或COLOR_BTNFACE,IntPolygon(hdc, Points, 4)画四边形------用同色画笔描边 + 画刷填充完成斜角内部。 - BF_ADJUST (L321-331):按
add收缩矩形写回*rc。 - 收尾(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)调用,负责把窗口标题画进给定矩形。完整流程:
- 取非客户区度量 (L2167-2172):
UserSystemParametersInfo(SPI_GETNONCLIENTMETRICS, cbSize, &nclm, 0)获取NONCLIENTMETRICSW(内含lfCaptionFont/lfSmCaptionFont)。 - 选字体 (L2174-2188):调用者未给
hFont时用TextIntCreateFontIndirect按DC_SMALLCAP创建小标题字体或标准标题字体,bDeleteFont = TRUE(用完自删)。 - 设置背景/前景 (L2190-2198):
IntGdiSetBkMode(hDc, TRANSPARENT);颜色按DC_INBUTTON(按钮内,COLOR_BTNTEXT)或DC_ACTIVE(活动标题COLOR_CAPTIONTEXT/ 非活动COLOR_INACTIVECAPTIONTEXT)区分。 - 为系统菜单让位(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
}
}
标题文字右侧留出系统菜单按钮(高度约一行的宽度)与最大/最小化按钮的空间。
-
测量与截断判断 (L2211-2218):
GreGetTextExtentExW(hDc, Buffer, len, maxWidth, &fit, 0, &Size, 0)求"能放进多宽"的字符数fit;Length = (len == fit ? fit : fit+1)------放不下时多取 1 字符 (为了让省略号路径接管);len > Length说明被截断,Ret = FALSE。 -
两条绘制路径(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)------走完整文本布局,右端省略号。
- 收尾 (L2239-2247):恢复文本色/字体,
bDeleteFont时GreDeleteObject;返回Ret(是否完整显示,供调用者记WNDS2_CAPTIONTEXTTRUNCATED)。
9.2 UserDrawCaption(painting.c L2253-2400+):整个标题栏
UserDrawCaption 把"背景 + 图标 + 文字"组装成一次完整标题绘制(被 nonclient.c 的 WM_NCPAINT/WM_NCACTIVATE 路径调用):
- 矩形规范化 :
RECTL_vMakeWellOrdered(lpRc)(防反)。 - 是否画图标 (L2269-2276):调用者给了
hIcon直接算;否则HasIcon = (uFlags & DC_ICON) && !(uFlags & DC_SMALLCAP) && (pWnd->style & WS_SYSMENU) && !(pWnd->ExStyle & WS_EX_TOOLWINDOW)------标准规则:有系统菜单、非工具窗口、非小标题才显示图标。 - 背景绘制 (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)整块填充。
- 画图标 (L2338-2368):
hIcon或NC_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)/2;UserDrawIconEx(hDc, x, y, pIcon, SM_CXSMICON, SM_CYSMICON, 0, NULL, DI_NORMAL)(见 9.3)。 - 画文字 (L2370-2395):有图标时
Rect.left += Rect.bottom - Rect.top(让出图标方块);Rect.left += 2;Str为空且pWnd != NULL时用pWnd->strName(FIXME:LARGE_STRING 截断);调UserDrawCaptionText;按返回值维护pWnd->state2的WNDS2_CAPTIONTEXTTRUNCATED(截断标记,供系统在需要时提示)。 - 返回 TRUE。
9.3 UserDrawIconEx(cursoricon.c L1690-2079):图标/光标绘制核心
DrawIconEx/DrawIcon/DrawState 的内核实现落点(NtUserDrawIconEx → UserDrawIconEx)。这是图标三要素(颜色位图 + 掩码位图 + 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_DEFAULTSIZE → SM_CXICON/SM_CYICON(图标)或 SM_CXCURSOR/SM_CYCURSOR(光标);否则用 pIcon->cx/pIcon->cy(原始尺寸)。
目标矩形 (L1787-1793):RECTL_vSetRect(&rcDest, xLeft, yTop, xLeft+cxWidth, yTop+cyHeight) → IntLPtoDP(逻辑→设备)→ 加 ptlDCOrig;DC_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 存在)直接走 IntEngAlphaBlend(AC_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 位图的旧式图标):
DI_MASK:SRCAND用掩码把目标"抠洞"(掩码为 1 处目标清 0);DI_IMAGE:SRCINVERT把颜色位图 XOR 上去(洞内像素 = 颜色,洞外 = 颜色 XOR 原目标);- 实际效果:颜色像素落在洞内 → 完全显示;掩码为 0 处 → XOR 两次抵消。
无 hbmColor 时从掩码位图下半部 取图像(rcSrc 偏移 pIcon->cy),这是 1bpp 图标的经典存储格式(上半掩码、下半图像)。
收尾 (L2061-2078):DC_vFinishBlit + DC_UnlockDc,SURFACE_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 经
NtGdiStretchBlt→GreStretchBltMask→IntEngStretchBlt(dwBackColor);独立TransparentBlt走IntEngTransparentBlt。两条通道都需要EXLATEOBJ(调色板/格式转换对象)保证颜色比较在同一色彩空间。 - 裁剪(CLIPOBJ) :
EngAlphaBlend按iDComplexity分派 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 合成)在NtGdiAlphaBlend的DC_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 时复位状态 |
11.4 与 nonclient.c / menu.c 的关系
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_Layered与WS_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/IntDrawDiagEdge以DC_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 的
DrawFocusRect用PATINVERT+ 灰度刷实现可逆 XOR 绘制。
至此,从窗口对象(_1)、窗口类(_2)、位置与可见性(_3)、绘制协议(_4)、可见区域与窗口 DC(_5)、非客户区(_6)到分层窗口与绘制辅助(_7),"窗口显示"主线的全部代码路径已完整覆盖。
(本册完。下一主线见《ReactOS 窗口系统分析(8):消息队列 --- msgqueue.c》)