ReactOS 窗口系统分析(25):标题栏显示与系统按钮 --- nonclient.c 标题栏专题
本文属于"窗口系统三大主线之窗口显示 "分册系列(总览见
doc/窗口管理/ReactOS窗口系统架构分析.md第 4.4 节)。本册主题:标题栏(Caption)与三个系统按钮 ------最小化、最大化/还原、关闭按钮从绘制、命中到行为落地的完整实现链路。本册是《ReactOS 窗口系统分析(6):非客户区绘制 --- nonclient.c》的专题深化篇 :第 6 册从全局俯瞰整个非客户区(标题栏、边框、菜单栏、滚动条、尺寸调整框),本册则把镜头对准标题栏这一条竖切面,逐函数拆解"标题文字+图标+三按钮"的像素级布局、Marlett 字体符号绘制原理、按钮按下状态机与 SC_* 系统命令的最终落地。
核心源码:
win32ss/user/ntuser/nonclient.c(标题栏绘制与交互主战场)、win32ss/user/ntuser/draw.c(Marlett 字体符号绘制 / DrawFrameControl 内核实现)、win32ss/user/ntuser/painting.c(标题背景与文字绘制)、win32ss/user/ntuser/defwnd.c(消息分发与 SC_* 落地)、win32ss/user/ntuser/winpos.c(边框/标题高度计算)、win32ss/user/ntuser/menu.c(系统菜单构建与弹出)、win32ss/user/ntuser/metric.c(系统度量初始化)。本文所有内容均基于 ReactOS 真实源码分析,不包含虚构信息。行号以 2026 年 8 月源码为准,与第 6 册引用一致(
nonclient.c约 2185 行)。
目录
- 概述
- 设计动机
- 核心数据结构
- 标题栏几何布局算法
- 绘制侧核心函数展开
- 激活/非激活双外观
- 三按钮交互状态机
- 最小化/最大化/关闭行为落地
- 调用链总览(mermaid)
- 与相关模块的关系
- 源码索引
- 附:关键结论
1. 概述
1.1 标题栏是什么
标题栏是带 WS_CAPTION(WS_BORDER | WS_DLGFRAME,值 0xc00000)样式的窗口顶部一条横向带状区域,高度由系统度量 SM_CYCAPTION 决定(经典主题默认 19 像素)。它同时承担四种职责:
-
标识:显示窗口标题文字与窗口图标,用户据此识别窗口;
-
操作入口:承载系统菜单按钮(图标)、最小化/最大化/关闭三个系统按钮;
-
移动载体 :按住标题栏空白区拖动可移动窗口(
HTCAPTION→SC_MOVE); -
快捷操作 :双击标题栏空白区切换最大化/还原(
NC_HandleNCLButtonDblClk)。┌──────────────────────────────────────────────────────────────────┐
│ WS_THICKFRAME 粗边框(NC_DrawFrame 绘制) │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ 标题栏 (SM_CYCAPTION = 19px) │ │
│ │ ┌─────┐ ┌──────────────────────────────┐ ┌──┐┌──┐┌──┐ │ │
│ │ │ 图标 │ │ 窗口标题文字 │ │━ ││□ ││✕ │ │ │
│ │ │SMICON│ │ (标题栏中央垂直居中) │ │MIN││MAX││CLOSE│ │ │
│ │ └─────┘ └──────────────────────────────┘ └──┘└──┘└──┘ │ │
│ │ HTSYSMENU HTCAPTION(可拖动) HTMIN HTMAX HTCLOSE │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ 标题栏底线(Caption Line,COLOR_3DFACE 1 像素横线) │
│ 菜单栏(WNDS_HASMENU 时,MENU_DrawMenuBar 绘制) │
│ 客户区(WM_NCPAINT 之上的应用自由绘制区域) │
└──────────────────────────────────────────────────────────────────┘
图中每个区域的边界并非由硬编码坐标决定,而是由"绘制函数(UserDrawCaptionBar / UserDrawCaptionButton)"与"命中函数(GetNCHitEx 的标题栏分支)"各自按同一套系统度量推导,二者必须严格一致------否则会出现"画出来的按钮点不中、点中的地方没有按钮"的错位。
1.2 两条主线架构图
标题栏的工作可以归纳为两条主线:
【绘制主线】重绘标题栏
WM_NCPAINT / WM_NCACTIVATE / WM_SETTEXT / WM_SETICON / WM_SYSCOLORCHANGE
│
▼
IntDefWindowProc(defwnd.c 消息分发)
│
▼
NC_DoNCPaint(nonclient.c L1097,整窗 NC 重绘)
│ └─ UserDrawCaption(painting.c L2254:背景/图标/文字)
▼
UserDrawCaptionBar(nonclient.c L955,WM_NCUAHDRAWCAPTION 直接入口)
│ ├─ DrawEdge / NC_DrawFrame(外缘/边框)
│ ├─ UserDrawCaption(标题栏背景 + 图标 + 文字)
│ └─ UserDrawCaptionButton ×3(DFCS_CAPTIONMIN / MAX / CLOSE)
│ └─ DrawFrameControl(DFC_CAPTION) → UITOOLS95_DrawFrameCaption(draw.c L788)
│ └─ GreTextOutW 输出 Marlett 字体单字符符号
▼
【交互主线】点击按钮
WM_NCLBUTTONDOWN(HTMINBUTTON/HTMAXBUTTON/HTCLOSE)
│
▼
IntDefWindowProc → NC_HandleNCLButtonDown(nonclient.c L1582)
│
▼
NC_DoButton(nonclient.c L1502:按下状态机)
│ ├─ UserDrawCaptionButtonWnd(TRUE) (压入外观)
│ ├─ 捕获鼠标,循环跟踪 WM_MOUSEMOVE/WM_LBUTTONUP
│ │ └─ GetNCHitEx 判定是否仍在按钮内(移出弹起)
│ └─ UserDrawCaptionButtonWnd(FALSE) (弹起外观)
▼
co_IntSendMessage(WM_SYSCOMMAND, SC_MINIMIZE/SC_MAXIMIZE/SC_RESTORE/SC_CLOSE)
│
▼
DefWndHandleSysCommand(defwnd.c L123)
│ ├─ SC_MINIMIZE/MAXIMIZE/RESTORE → co_WinPosShowWindow(SW_*)
│ └─ SC_CLOSE → WM_CLOSE → co_UserDestroyWindow
1.3 与第 6 册的分工
第 6 册已覆盖:HT_* 命中码全表、WNDS_* 状态位、NCCALCSIZE_PARAMS、系统度量总表、框架判定宏、DefWindowProc NC 分发框架、NC_HandleNCCalcSize 完整展开、GetNCHitEx 完整展开、NC_DoNCPaint 整体流程、移动缩放(SC_MOVE/SC_SIZE)等。本册不重复这些内容,只在其基础上向"标题栏"这个专题纵深展开:
| 主题 | 第 6 册(广度) | 本册(深度) |
|---|---|---|
| 标题栏绘制 | 简述 UserDrawCaptionBar 在 NC 绘制中的位置 |
逐函数拆解:背景/图标/文字/按钮/底线/菜单栏六段绘制 |
| 系统按钮 | 简述 UserDrawCaptionButton 调用 DrawFrameControl |
像素级布局公式、Marlett 符号映射表、按下状态机、灰化逻辑 |
| 激活外观 | WM_NCACTIVATE 流程概述 |
双外观颜色差异、DC_ACTIVE 标志传播、渐变标题 |
| 按钮行为 | 简述 NC_DoButton 循环 |
完整状态机 + SC_* 落地到 co_WinPosShowWindow/DestroyWindow |
| 系统菜单 | 提到 SC_MOUSEMENU |
系统菜单构建(SYSMENU 模板)、项状态刷新、弹出位置计算 |
2. 设计动机
2.1 标题栏由系统统一绘制
标题栏是"窗口性"最强的区域:窗口的打开/关闭/最小化/最大化/移动这些操作全部发生在标题栏上,而它们与应用程序的业务逻辑无关。因此 Windows 把这些装饰与交互统一收归系统(DefWindowProc → NC_DoNCPaint/NC_HandleNCLButtonDown),应用只要声明 WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX 样式组合,系统就自动完成绘制与交互。
这种设计带来三个直接后果:
- 外观一致:所有标准窗口的标题栏长得一样,用户不必在每个应用里重新学习"哪个是关闭按钮";
- 行为一致 :
GetNCHitEx输出的 HT 码与NC_DoButton执行的 SC_* 命令对所有窗口统一生效,应用无需感知; - 可定制入口仍在 :应用可以通过处理
WM_NCPAINT/WM_NCCALCSIZE/WM_NCHITTEST完全接管标题栏(此时系统置WNDS_NONCPAINT跳过系统绘制),或通过CS_NOCLOSE类样式单独让关闭按钮灰化。
2.2 激活/非激活双外观
标题栏必须回答"当前哪个窗口是活动窗口"这一问题。Windows 用颜色差异表达激活状态:
| 部件 | 激活(Active) | 非激活(Inactive) |
|---|---|---|
| 标题栏背景 | COLOR_ACTIVECAPTION(默认深蓝) |
COLOR_INACTIVECAPTION(默认灰) |
| 标题文字 | COLOR_CAPTIONTEXT(白) |
COLOR_INACTIVECAPTIONTEXT(浅灰) |
| 按钮符号 | COLOR_BTNTEXT(黑) |
COLOR_BTNSHADOW(灰) + 右下偏移阴影 |
| 边框 | COLOR_ACTIVEBORDER |
COLOR_INACTIVEBORDER |
激活状态由 WND.state & WNDS_ACTIVEFRAME 位记录,focus.c 在激活窗口切换时发送 WM_NCACTIVATE(TRUE/FALSE),NC_HandleNCActivate 改写状态位并触发局部重绘(见第 6 章)。DC_ACTIVE 是这条信息在绘制链路上传递的载体标志。
2.3 按钮交互状态机
三个系统按钮不是"按下立即执行",而是有一个完整的跟踪状态机(NC_DoButton):
鼠标在按钮上按下(WM_NCLBUTTONDOWN, HTMIN/MAX/CLOSE)
│
▼
绘制压入外观(DFCS_PUSHED)→ 捕获鼠标
│
├─ 按住并移出按钮区 → 弹起外观(等待移回)
│ │
│ └─ 移回按钮区 → 再次压入外观
│
├─ 在按钮区抬起(WM_LBUTTONUP)→ 弹起外观 → 发送 WM_SYSCOMMAND(SC_*)
└─ 在按钮区外抬起 → 弹起外观 → 不发送任何命令(取消)
这个状态机的意义:只有"按下→在按钮内抬起"才触发命令 ,与 Windows 标准按钮(BS_PUSHBUTTON)的交互语义一致,防止用户在按钮上按下后反悔(移出再抬起)却仍然执行了操作。
2.4 标题栏是移动与双击的载体
标题栏空白区(HTCAPTION)同时是移动窗口的把手:NC_HandleNCLButtonDown 的 HTCAPTION 分支把点击转换成 WM_SYSCOMMAND(SC_MOVE + HTCAPTION),进入 DefWndDoSizeMove 的模态移动循环;双击标题栏(NC_HandleNCLButtonDblClk)则在"最大化↔还原"之间切换。这些行为全部以"标题栏存在"(WNDS_HASCAPTION)为前提。
3. 核心数据结构
3.1 系统度量(SM_*)------标题栏的"计量单位"
标题栏的一切尺寸都来自 UserGetSystemMetrics(内核版 GetSystemMetrics,metric.c)。标题栏专题相关的度量及其默认值(经典主题,metric.c L78-93、L112-113、L130-131):
| 度量 | 默认值 | 在标题栏中的用途 |
|---|---|---|
SM_CYCAPTION |
iCaptionHeight+1 = 19 |
标准标题栏高度;也用作按钮区域宽度估算(经典主题下按钮为正方形) |
SM_CYSMCAPTION |
iSmCaptionHeight+1 = 15 |
工具窗口(WS_EX_TOOLWINDOW)小标题栏高度 |
SM_CXSIZE / SM_CYSIZE |
iCaptionHeight = 18 |
标准系统按钮(最小化/最大化/关闭)的宽/高 |
SM_CXSMSIZE / SM_CYSMSIZE |
12 / 14 | 工具窗口小按钮尺寸(关闭按钮专用) |
SM_CXSMICON / SM_CYSMICON |
16 / 16 | 标题栏小图标(系统菜单按钮图标)尺寸 |
SM_CXFRAME / SM_CYFRAME |
SM_CXDLGFRAME+iBorderWidth = 4 |
粗边框宽/高(NC_DrawFrame 与 NC_GetInsideRect 用) |
SM_CXDLGFRAME / SM_CYDLGFRAME |
3 | 对话框边框宽/高 |
SM_CXBORDER / SM_CYBORDER |
1 | 细边框宽/高(硬编码 1) |
SM_CXEDGE / SM_CYEDGE |
2 | 3D 边缘(WS_EX_CLIENTEDGE) |
SM_CXMINIMIZED / SM_CYMINIMIZED |
mm.iWidth+6 / SM_CYCAPTION+5=24 |
最小化窗口(任务栏按钮)尺寸 |
SM_CYMIN |
SM_CYCAPTION + 2*SM_CYFRAME = 27 |
最小跟踪尺寸(拖拽下边框下限) |
关键关系:SM_CXSIZE(18)与 SM_CYCAPTION(19)相差 1。这个"1 像素"在按钮布局中反复出现(见 4.2 节),是按钮矩形微调(- 2、- 1、+ 1)的根源。
3.2 DFCS_* / DFC_* 绘制标志(winuser.h L473-511)
DrawFrameControl 以 uType(DFC_,控制类型)+ uState(DFCS_ ,状态/子类型)为参数。标题栏按钮使用 DFC_CAPTION 类型:
c
#define DFC_CAPTION 1 // 标题栏按钮
// uState 的低 4 位是"子类型"(按钮种类):
#define DFCS_CAPTIONCLOSE 0 // 关闭
#define DFCS_CAPTIONMIN 1 // 最小化
#define DFCS_CAPTIONMAX 2 // 最大化
#define DFCS_CAPTIONRESTORE 3 // 还原
#define DFCS_CAPTIONHELP 4 // 帮助
// 高位的"状态修饰"位:
#define DFCS_INACTIVE 256 // 禁用/非激活外观(灰化)
#define DFCS_PUSHED 512 // 被按下(凹陷外观)
#define DFCS_CHECKED 1024 // 选中
#define DFCS_TRANSPARENT 0x800
#define DFCS_HOT 0x1000
#define DFCS_ADJUSTRECT 0x2000 // 绘制后收缩矩形
#define DFCS_FLAT 0x4000 // 平面外观
#define DFCS_MONO 0x8000 // 单色外观
注意"子类型"编码方式:DFCS_CAPTIONMIN/MAX/CLOSE 与 DFCS_CAPTIONRESTORE/HELP 同属一个低 4 位字段,因此 UITOOLS95_DrawFrameCaption 用 switch(uFlags & 0xf) 分派(draw.c L796)。DFCS_PUSHED | DFCS_INACTIVE 这类修饰位可以叠加在子类型之上。
3.3 HT* 命中码------标题栏相关的六个
GetNCHitEx 在标题栏分支产出的命中码(winuser.h L2512-2536):
| 命中码 | 值 | 含义 | 本册涉及的消费方 |
|---|---|---|---|
HTCAPTION |
2 | 标题栏空白区 | NC_HandleNCLButtonDown→SC_MOVE;双击→SC_MAXIMIZE/RESTORE |
HTSYSMENU |
3 | 系统菜单按钮(图标区) | 按下→SC_MOUSEMENU+HTSYSMENU;双击→SC_CLOSE |
HTMINBUTTON(HTREDUCE) |
8 | 最小化按钮 | NC_DoButton→SC_MINIMIZE/SC_RESTORE |
HTMAXBUTTON(HTZOOM) |
9 | 最大化按钮 | NC_DoButton→SC_MAXIMIZE/SC_RESTORE |
HTCLOSE |
20 | 关闭按钮 | NC_DoButton→SC_CLOSE |
HTHELP |
21 | 帮助按钮 | 当前 GetNCHitEx 不产生(无 DFCS_CAPTIONHELP 绘制路径) |
HTMINBUTTON/HTMAXBUTTON 是 NC_DoButton 的 switch(wParam) 唯一接受的三个值(default: ASSERT(FALSE)),说明关闭/最小化/最大化三按钮的交互完全走同一条状态机。
3.4 DC_* 绘制标志(DrawCaption 系列)
UserDrawCaption/UserDrawCaptionBar/UserPaintCaption 之间通过 Flags(DC_* 位)传递绘制要求:
c
// winuser.h L427-436(公开的 DrawCaption 标志)
#define DC_ACTIVE 0x00000001 // 按激活外观绘制
#define DC_SMALLCAP 0x00000002 // 小标题栏(工具窗口)
#define DC_ICON 0x00000004 // 绘制图标
#define DC_TEXT 0x00000008 // 绘制标题文字
#define DC_INBUTTON 0x00000010 // 标题栏作为按钮背景(COLOR_3DFACE)
#define DC_GRADIENT 0x00000020 // 渐变标题
#define DC_BUTTONS 0x00001000 // 绘制按钮(UserDrawCaptionBar 内部使用)
// undocuser.h L148-154(未公开标志)
#define DC_NOVISIBLE 0x0800 // 不检查窗口可见性
#define DC_NOSENDMSG 0x2000 // 不发送消息(菜单栏绘制跳过)
#define DC_FRAME 0x8000 // 同时绘制边框(WinUser.H 缺失)
#define DC_DRAWCAPTIONMD 0x10000000 // 绘制整个标题栏(NtUserDrawCaptionTemp 专用)
#define DC_REDRAWHUNGWND 0x20000000 // 挂起窗口重绘(转 DC_NOSENDMSG)
#define DC_DRAWFRAMEMD 0x80000000 // 绘制整个 NC(NtUserDrawCaptionTemp 专用)
关键语义:DC_TEXT|DC_BUTTONS 是标题栏的"全要素"标志组合(UserDrawCaptionBar 中强制置位);DC_ACTIVE 决定颜色分支;DC_ICON 决定是否画图标;DC_SMALLCAP 决定使用 SM_CYSMCAPTION/lfSmCaptionFont。
3.5 窗口状态位(WNDS_* / WNDS2_*)------标题栏相关
ntuser.h L605-636、L666:
c
#define WNDS_HASCAPTION 0X00000008 // 有标题栏(NC_HandleNCCalcSize 置位)
#define WNDS_ACTIVEFRAME 0X00000040 // 激活外观(活动标题栏)
#define WNDS_NONCPAINT 0X00000100 // 应用接管 NC 绘制(系统跳过)
#define WNDS_FORCEMENUDRAW 0X00008000 // WM_NCPAINT 期间强制重绘菜单栏
#define WNDS2_CAPTIONTEXTTRUNCATED 0X08000000 // 标题文字被截断(UserDrawCaption 写入)
WNDS_HASCAPTION 在 NC_HandleNCCalcSize 检测到 WS_CAPTION 时置位,是"有没有标题栏"的权威判据;WNDS_ACTIVEFRAME 由 NC_HandleNCActivate 维护,绘制时经 DC_ACTIVE 传导到颜色选择;WNDS2_CAPTIONTEXTTRUNCATED 在 UserDrawCaption 调用 UserDrawCaptionText 后按"文字是否放得下"置位/清除(UI 自动化与提示用)。
3.6 Marlett 字体符号映射表(draw.c 全表)
标题栏按钮符号的本质:Marlett 是一种符号字体,每个可见字符对应一个矢量图标 。UITOOLS95_DrawFrameCaption 把 DFCS_* 子类型映射为单个 WCHAR,再用 GreTextOutW 输出。draw.c 中的完整映射(含滚动条/菜单/单选/复选符号,本册一并列出以展示 Marlett 的"符号字典"性质):
| 字符 | 符号含义 | 对应 DFCS_* | 出处 |
|---|---|---|---|
'0' |
最小化(下划线) | DFCS_CAPTIONMIN |
draw.c L804-806 |
'1' |
最大化(方框) | DFCS_CAPTIONMAX |
draw.c L807-809 |
'2' |
还原(重叠双框) | DFCS_CAPTIONRESTORE |
draw.c L810-812 |
'r' |
关闭(叉) | DFCS_CAPTIONCLOSE |
draw.c L798-800 |
's' |
帮助(问号) | DFCS_CAPTIONHELP |
draw.c L801-803 |
'3'/'4' |
左/右箭头 | DFCS_SCROLLLEFT/DFCS_SCROLLRIGHT |
draw.c L875-881 |
'5'/'6' |
上/下箭头 | DFCS_SCROLLUP/DFCS_SCROLLDOWN(及 COMBOBOX) |
draw.c L866-873 |
'o'/'p' |
尺寸调整框高光/阴影 | DFCS_SCROLLSIZEGRIP |
draw.c L903-910 |
'x'/'y' |
尺寸调整框(右对齐)高光/阴影 | DFCS_SCROLLSIZEGRIPRIGHT |
draw.c L903-910 |
'w' |
菜单右箭头 | DFCS_MENUARROWRIGHT |
draw.c L981-983 |
'h' |
菜单圆点 | DFCS_MENUBULLET |
draw.c L985-987 |
'a' |
菜单复选 | DFCS_MENUCHECK |
draw.c L989-992 |
'b'/'i' |
复选/单选选中标记 | DFCS_CHECKED(按钮) |
draw.c L744 |
'c'~'g' |
复选钮外右/外左/内左/内右/中心 | DFCS_BUTTONCHECK 组件 |
draw.c L683-688 |
'j'~'n' |
单选钮外右/外左/内左/内右/中心 | DFCS_BUTTONRADIO 组件 |
draw.c L676-680 |
'8' |
菜单上箭头 | DFCS_MENUARROW |
draw.c L977-979 |
menu.c 的 MENU_DrawPopupGlyph(L1398-1454)在系统菜单项(还原/最小化/最大化/关闭)上也复用同一套字符('2'/'0'/'1'/'r')------系统菜单里的四个命令图标与标题栏三按钮是同一份 Marlett 字形。
3.7 按钮 Type 常量与 Switch 映射
UserDrawCaptionButton 的第四个参数 Type 直接使用 DFCS_CAPTIONMIN/MAX/CLOSE 三个常量;NC_DoButton 内部维护 ULONG ButtonType 并赋值 DFCS_CAPTIONCLOSE/MIN/MAX。两处 switch 的键完全相同,形成"命中码 → Type → 符号字符"的完整链条:
HTCLOSE → DFCS_CAPTIONCLOSE → 'r'
HTMINBUTTON → DFCS_CAPTIONMIN → '0'(最小化时变 DFCS_CAPTIONRESTORE → '2')
HTMAXBUTTON → DFCS_CAPTIONMAX → '1'(最大化时变 DFCS_CAPTIONRESTORE → '2')
4. 标题栏几何布局算法
4.1 分区总览
标题栏矩形 TempRect(窗口坐标系,left/top=0,right=窗口宽,bottom=SM_CYCAPTION-1)在逻辑上被划分为从左到右的五个区域(LTR 布局):
┌──系统菜单──┬───────────标题文字──────────┬──关闭──┬──最大化──┬──最小化──┐
│ 图标区 │ │ 按钮 │ 按钮 │ 按钮 │
│ SM_CYCAPTION│ 右边界预留:SM_CYCAPTION-1 │ SM_CXSIZE│ SM_CXSIZE │ SM_CXSIZE │
│ 宽的正方形 │ + 2×(SM_CXSIZE+1) (若有极值框)│ +2px余量│ +1px微调 │ +1px微调 │
└────────────┴────────────────────────────┴────────┴──────────┴──────────┘
三按钮的宽度:关闭按钮占 SM_CXSIZE+2(右边界 -2 微调),最大化/最小化各占 SM_CXSIZE(右边界 -1)。GetNCHitEx 的标题栏分支(L2071-2143)按从右往左的顺序检查:关闭 → 最大化 → 最小化 → 系统菜单,与绘制时的分区完全对应(见 4.5 节)。
4.2 按钮矩形计算(UserDrawCaptionButton 的像素级推导)
UserDrawCaptionButton(nonclient.c L808-887)接收的 Rect 是整个标题栏矩形(TempRect,即"窗口内边矩形去掉边框后的顶部条带"),函数内部把 TempRect 收缩出每个按钮自己的矩形。三个分支的推导:
DFCS_CAPTIONMIN(最小化/还原按钮,L821-842):
c
TempRect = *Rect;
if (ExStyle & WS_EX_TOOLWINDOW) return; // 工具窗口无最小化/最大化按钮
if (Style & WS_SYSMENU) TempRect.right -= SM_CXSIZE + 1; // 给关闭按钮让位
if (Style & (WS_MAXIMIZEBOX | WS_MINIMIZEBOX)) TempRect.right -= SM_CXSIZE - 2; // 给最大化按钮让位
TempRect.left = TempRect.right - SM_CXSIZE + 1; // 按钮左边界
TempRect.bottom= TempRect.top + SM_CYSIZE - 2; // 底部基线 = 标题栏顶 + SM_CYSIZE - 2
TempRect.top += 2; // 顶部内缩 2(最终高 = SM_CYSIZE-4)
TempRect.right-= 1; // 右边界再缩 1(最终宽 = SM_CXSIZE-2)
DrawFrameControl(hDC, &TempRect, DFC_CAPTION,
((Style & WS_MINIMIZE) ? DFCS_CAPTIONRESTORE : DFCS_CAPTIONMIN) | // 最小化态画"还原"符号
(bDown ? DFCS_PUSHED : 0) |
((Style & WS_MINIMIZEBOX) ? 0 : DFCS_INACTIVE)); // 无极值框样式 → 灰化
DFCS_CAPTIONMAX(最大化/还原按钮,L843-861):与 MIN 几乎相同,只是不给最小化让位(最小化在最左):
c
if (Style & WS_SYSMENU) TempRect.right -= SM_CXSIZE + 1; // 仅给关闭让位
TempRect.left = TempRect.right - SM_CXSIZE + 1;
...
DrawFrameControl(hDC, &TempRect, DFC_CAPTION,
((Style & WS_MAXIMIZE) ? DFCS_CAPTIONRESTORE : DFCS_CAPTIONMAX) | // 最大化态画"还原"
(bDown ? DFCS_PUSHED : 0) |
((Style & WS_MAXIMIZEBOX) ? 0 : DFCS_INACTIVE));
DFCS_CAPTIONCLOSE(关闭按钮,L862-885)------最复杂,因为它还检查系统菜单状态与类样式:
c
PMENU pSysMenu = IntGetSystemMenu(pWnd, FALSE);
UINT MenuState = IntGetMenuState(pSysMenu ? UserHMGetHandle(pSysMenu) : NULL,
SC_CLOSE, MF_BYCOMMAND);
if (ExStyle & WS_EX_TOOLWINDOW) // 工具窗口用小的关闭按钮
{
TempRect.left = TempRect.right - SM_CXSMSIZE; // 宽 SM_CXSMSIZE(12)
TempRect.bottom = TempRect.top + SM_CYSMSIZE - 2;
}
else
{
TempRect.left = TempRect.right - SM_CXSIZE; // 宽 SM_CXSIZE(18)
TempRect.bottom = TempRect.top + SM_CYSIZE - 2;
}
TempRect.top += 2;
TempRect.right -= 2; // 比 MIN/MAX 多缩 1px(按钮略宽)
DrawFrameControl(hDC, &TempRect, DFC_CAPTION,
(DFCS_CAPTIONCLOSE | (bDown ? DFCS_PUSHED : 0) |
((!(MenuState & (MF_GRAYED|MF_DISABLED)) && !(pWnd->pcls->style & CS_NOCLOSE))
? 0 : DFCS_INACTIVE))); // 关闭项被禁 or CS_NOCLOSE → 灰化
灰化判定要点:关闭按钮的灰化不取决于样式位,而取决于系统菜单里 SC_CLOSE 项的实时状态 (MF_GRAYED|MF_DISABLED)或 CS_NOCLOSE 类样式。这意味着应用调用 EnableMenuItem(GetSystemMenu(hWnd,FALSE), SC_CLOSE, MF_GRAYED) 会实时反映到标题栏按钮上------menu.c L1249 在菜单状态变化时调用 UserPaintCaption(MenuObject->spwndNotify, DC_BUTTONS) 触发局部重绘。
最终矩形尺寸的推导结论 :把上述调整累加,经典主题(SM_CXSIZE=SM_CYSIZE=18)下三个按钮的实际绘制矩形为:
| 按钮 | 左边界相对标题栏右缘 | 最终宽 | 最终高 | 说明 |
|---|---|---|---|---|
| 关闭(标准) | right - SM_CXSIZE,再 right -= 2 |
SM_CXSIZE-2 = 16 |
SM_CYSIZE-4 = 14 |
顶部内缩 2 |
| 关闭(工具窗口) | right - SM_CXSMSIZE,再 right -= 2 |
SM_CXSMSIZE-2 = 10 |
SM_CYSMSIZE-4 = 10 |
小尺寸 |
| 最大化 | 右缘让出 SM_CXSIZE+1 |
SM_CXSIZE-2 = 16 |
SM_CYSIZE-4 = 14 |
--- |
| 最小化 | 右缘让出 SM_CXSIZE+1 + SM_CXSIZE-2 |
SM_CXSIZE-2 = 16 |
SM_CYSIZE-4 = 14 |
最靠左 |
即每个按钮的分区宽度约 SM_CXSIZE、实际绘制矩形再向内缩 2~4 像素 。GetNCHitEx 的命中区以整 SM_CXSIZE/SM_CYCAPTION 为单位切分(4.5 节),命中区比绘制矩形略宽------这是刻意为之:按钮"可点区域"稍大于"可视区域",符合人机工程习惯,也避免按钮间 1~2px 空隙成为死区。
**UserDrawCaptionButtonWnd(L890-906)**是"直接重绘单个按钮"的便捷包装:取窗口矩形(窗口坐标系)→ UserGetWindowBorders 扣除边框 → 把结果交给 UserDrawCaptionButton。NC_DoButton 在按下/弹起瞬间用它做按钮级局部重绘,避免整窗重绘。
4.3 图标与文字区域的划分(UserDrawCaption)
UserDrawCaption(painting.c L2254)画完背景后按 HasIcon 收缩文字区:
c
if (HasIcon) // 有图标:SM_CYSMICON 且非工具窗口且 WS_SYSMENU
Rect.left += Rect.bottom - Rect.top; // 文字区左边界右移"标题栏高度"个像素
if (uFlags & DC_TEXT)
{
Rect.left += 2; // 再留 2px 左内边距
... UserDrawCaptionText(pWnd, hDc, Str, &Rect, uFlags, hFont);
}
图标绘制坐标(L2358-2362):
c
LONG cx = UserGetSystemMetrics(SM_CXSMICON); // 16
LONG cy = UserGetSystemMetrics(SM_CYSMICON); // 16
LONG x = Rect.left - cx/2 + 1 + (Rect.bottom - Rect.top)/2; // 图标水平居中于标题栏高度
LONG y = (Rect.top + Rect.bottom - cy)/2; // 垂直居中
UserDrawIconEx(hDc, x, y, pIcon, cx, cy, 0, NULL, DI_NORMAL);
注意 x 的算法:Rect.left - cx/2 + 1 + 高度/2 ------ 以标题栏左缘为基准,先左移半个图标宽、再右移标题栏高度的一半,等价于"图标中心对准标题栏左缘向右 高度/2 处"。当 HasIcon 时文字区左边界为 Rect.left += 标题栏高度,与图标区宽度(SM_CYCAPTION)一致------这与 GetNCHitEx 中系统菜单区宽度 SM_CYCAPTION - 1(L2116)及 UserDrawCaptionText 中文字右边界预留 SM_CYCAPTION - 1(L2204)相互咬合。
4.4 标题文字右边界(UserDrawCaptionText 的按钮预留)
UserDrawCaptionText(painting.c L2148)在测量文字前先为右侧按钮区预留空间(L2202-2210):
c
// Adjust for system menu.
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_CYCAPTION-1 + 极值框区 2×(SM_CXSIZE+1))。这与绘制侧按钮占用空间一致,保证文字不会画到按钮底下。随后用 GreGetTextExtentExW 计算能放下的字符数:
c
GreGetTextExtentExW(hDc, Text->Buffer, Text->Length/sizeof(WCHAR),
r.right - r.left, &fit, 0, &Size, 0);
Length = (Text->Length/sizeof(WCHAR) == fit ? fit : fit+1); // fit+1:为省略号留字符
if (Text->Length/sizeof(WCHAR) > Length) Ret = FALSE; // 被截断
- 放得下 (快速路径):
UserExtTextOutW(hDc, left, top + (h - Size.cy)/2, ETO_CLIPPED, lpRc, Buffer, Length)------ 单行垂直居中,直接输出,不做省略号; - 放不下 (慢速路径):
DrawTextW(hDc, ..., DT_END_ELLIPSIS|DT_SINGLELINE|DT_VCENTER|DT_NOPREFIX|DT_LEFT)------ 交给 DrawText 引擎做尾部省略号。
返回值 Ret 被 UserDrawCaption 用来维护 WNDS2_CAPTIONTEXTTRUNCATED 状态位。
4.5 GetNCHitEx 的按钮分区镜像
GetNCHitEx(nonclient.c L2071-2143)的标题栏分支与绘制侧保持同一套度量推导 ,顺序为从右到左(LTR 情况):
c
if ((Style & WS_CAPTION) == WS_CAPTION)
{
if (ExStyle & WS_EX_TOOLWINDOW) rcWindow.top += SM_CYSMCAPTION - 1;
else rcWindow.top += SM_CYCAPTION - 1; // 标题栏下界
if (!RECTL_bPointInRect(&rcWindow, pt.x, pt.y)) // 点在标题栏内
{
BOOL min_or_max_box = (Style & WS_SYSMENU) && (Style & (WS_MINIMIZEBOX|WS_MAXIMIZEBOX));
// LTR:
/* Check system menu */
if ((Style & WS_SYSMENU) && !(ExStyle & WS_EX_TOOLWINDOW) && NC_IconForWindow(pWnd))
{
rcWindow.left += SM_CYCAPTION - 1;
if (pt.x < rcWindow.left) return HTSYSMENU; // 左端图标区
}
/* Check close button */
if (Style & WS_SYSMENU)
{
rcWindow.right -= SM_CYCAPTION;
if (pt.x > rcWindow.right) return HTCLOSE; // 右端
}
/* Check maximize box */
if (min_or_max_box && !(ExStyle & WS_EX_TOOLWINDOW))
{
rcWindow.right -= SM_CXSIZE;
if (pt.x > rcWindow.right) return HTMAXBUTTON;
}
/* Check minimize box */
if (min_or_max_box && !(ExStyle & WS_EX_TOOLWINDOW))
{
rcWindow.right -= SM_CXSIZE;
if (pt.x > rcWindow.right) return HTMINBUTTON;
}
return HTCAPTION; // 其余都是可拖动区
}
}
三个细节值得注意:
min_or_max_box的"Win95 兼容" :只要WS_SYSMENU且带任一极值框样式,命中区就同时给最大化和最小化各留一个SM_CXSIZE(注释:"In Win95 there is automatically a Maximize button when there is a minimize one")。绘制侧UserDrawCaptionBar的按钮循环条件(Style & (WS_MAXIMIZEBOX|WS_MINIMIZEBOX))(L1040)与之完全对应;HTSYSMENU需要图标 :NC_IconForWindow(pWnd)返回非空才划分系统菜单区------没有图标的窗口(如WS_EX_DLGMODALFRAME)标题栏左端不响应系统菜单点击;- RTL 镜像 :
WS_EX_LAYOUTRTL时把"系统菜单在左、关闭在右"整体镜像为"系统菜单在右、关闭在左"(L2080-2110),检查顺序与符号方向同步反转。
5. 绘制侧核心函数展开
5.1 UserDrawCaptionBar(nonclient.c L955-1082)------标题栏主绘制
UserDrawCaptionBar 是标题栏绘制的总入口 :WM_NCUAHDRAWCAPTION 分支(defwnd.c L1316)与 UserPaintCaption(defwnd.c L421)都直接调用它。它接受 Flags(DC_* 位,含 DC_FRAME)决定画哪些要素。按源码顺序的完整流程:
① 可见性检查:
c
if (!(Flags & DC_NOVISIBLE) && !IntIsWindowVisible(pWnd)) return;
② 取窗口矩形(窗口坐标系):
c
IntGetWindowRect(pWnd, &WindowRect);
CurrentRect.top = CurrentRect.left = 0;
CurrentRect.right = WindowRect.right - WindowRect.left;
CurrentRect.bottom = WindowRect.bottom - WindowRect.top;
③ 外缘(Outer Edge) ------UserHasWindowEdge(有 WS_EX_WINDOWEDGE)时画凸起外缘:
c
if (UserHasWindowEdge(Style, ExStyle))
DrawEdge(hDC, &CurrentRect, EDGE_RAISED, BF_RECT | BF_ADJUST); // 3D 凸边,收缩
else if (ExStyle & WS_EX_STATICEDGE) // 静态边缘
{
// 手工画 1px BTNSHADOW 上/左边 + 1px BTNHIGHLIGHT 下/右边
NtGdiSelectBrush(hDC, IntGetSysColorBrush(COLOR_BTNSHADOW));
NtGdiPatBlt(... 1px ...);
NtGdiSelectBrush(hDC, IntGetSysColorBrush(COLOR_BTNHIGHLIGHT));
NtGdiPatBlt(... 1px ...);
RECTL_vInflateRect(&CurrentRect, -1, -1);
}
④ 边框 (仅在带 DC_FRAME 时):NC_DrawFrame(hDC, &CurrentRect, (Flags & DC_ACTIVE), Style, ExStyle)(见 5.6)。
⑤ 标题栏------核心段(L1004-1061):
c
if ((Style & WS_CAPTION) == WS_CAPTION)
{
TempRect = CurrentRect;
Flags |= DC_TEXT|DC_BUTTONS; // 强制文字+按钮要素
if (UserSystemParametersInfo(SPI_GETGRADIENTCAPTIONS, 0, &Gradient, 0) && Gradient)
Flags |= DC_GRADIENT; // 系统开启渐变 → 渐变标题
if (ExStyle & WS_EX_TOOLWINDOW)
{
Flags |= DC_SMALLCAP;
TempRect.bottom = TempRect.top + SM_CYSMCAPTION - 1; // 小标题栏下界
CurrentRect.top += SM_CYSMCAPTION; // 后续菜单栏从这开始
}
else
{
TempRect.bottom = TempRect.top + SM_CYCAPTION - 1; // 标准下界
CurrentRect.top += SM_CYCAPTION;
}
// 无 DC_ICON 且非小标题栏且带系统菜单 → 主动取图标(DC_ICON 由 UserDrawCaption 内部处理)
if (!(Flags & DC_ICON) && !(Flags & DC_SMALLCAP) &&
(Style & WS_SYSMENU) && !(ExStyle & WS_EX_TOOLWINDOW))
pIcon = NC_IconForWindow(pWnd);
// 背景+图标+文字(本册 5.2)
UserDrawCaption(pWnd, hDC, &TempRect, NULL,
pIcon ? UserHMGetHandle(pIcon) : NULL, NULL, Flags);
/* Draw buttons ------ 标题栏按钮绘制,顺序:关闭→最小化→最大化 */
if (Style & WS_SYSMENU)
{
UserDrawCaptionButton(pWnd, &TempRect, Style, ExStyle, hDC, FALSE, DFCS_CAPTIONCLOSE);
if ((Style & (WS_MAXIMIZEBOX | WS_MINIMIZEBOX)) && !(ExStyle & WS_EX_TOOLWINDOW))
{
UserDrawCaptionButton(pWnd, &TempRect, Style, ExStyle, hDC, FALSE, DFCS_CAPTIONMIN);
UserDrawCaptionButton(pWnd, &TempRect, Style, ExStyle, hDC, FALSE, DFCS_CAPTIONMAX);
}
}
/* Line under caption ------ 标题栏底线 */
if (!(Style & WS_MINIMIZE))
{
PreviousPen = NtGdiSelectPen(hDC, NtGdiGetStockObject(DC_PEN));
IntSetDCPenColor(hDC, IntGetSysColor(
((ExStyle & (WS_EX_STATICEDGE|WS_EX_CLIENTEDGE|WS_EX_DLGMODALFRAME)) == WS_EX_STATICEDGE)
? COLOR_WINDOWFRAME : COLOR_3DFACE));
GreMoveTo(hDC, TempRect.left, TempRect.bottom, NULL);
NtGdiLineTo(hDC, TempRect.right, TempRect.bottom);
NtGdiSelectPen(hDC, PreviousPen);
}
}
要点:
- 三按钮共用一个
TempRect(整个标题栏矩形),每个UserDrawCaptionButton调用内部自行收缩出自己的按钮矩形(见 4.2)------这就是为什么绘制代码看起来"只是三次调用",而实际布局由 4.2 的公式完成; - 按钮绘制全部传
bDown=FALSE------静态绘制时按钮不凹陷;NC_DoButton交互时用UserDrawCaptionButtonWnd(..., TRUE/FALSE)单独刷新凹陷状态; - 底线 是标题栏与菜单栏/客户区之间 1px 横线,颜色
COLOR_3DFACE(仅WS_EX_STATICEDGE时为COLOR_WINDOWFRAME)。最小化窗口(任务栏按钮形态)不画底线。
⑥ 菜单栏与客户边缘(L1063-1081,非最小化时):
c
if (pWnd->state & WNDS_HASMENU && pWnd->IDMenu)
{
menu = UserGetMenuObject(UlongToHandle(pWnd->IDMenu));
TempRect = CurrentRect;
TempRect.bottom = TempRect.top + menu->cyMenu; // 菜单栏高度
CurrentRect.top += MENU_DrawMenuBar(hDC, &TempRect, pWnd, FALSE);
}
if (ExStyle & WS_EX_CLIENTEDGE)
DrawEdge(hDC, &CurrentRect, EDGE_SUNKEN, BF_RECT | BF_ADJUST); // 客户区凹陷边缘
WNDS_HASMENU 是 NC_HandleNCCalcSize 在 NCCALCSIZE 阶段写入的(见第 6 册 6.3 节),绘制阶段直接消费;MENU_DrawMenuBar 第四个参数 suppress_draw=FALSE 表示这次是真正绘制(NCCALCSIZE 里用 TRUE 只测高度)。
5.2 UserDrawCaption / UserDrawCaptionText(painting.c L2254 / L2148)------背景与文字
UserDrawCaption(L2254-2404)负责"背景 + 图标 + 文字"三段:
背景(渐变 or 平涂):
c
if ((uFlags & DC_GRADIENT) && !(uFlags & DC_INBUTTON))
{
static GRADIENT_RECT gcap = {0, 1};
TRIVERTEX Vertices[2];
Colors[0] = IntGetSysColor((uFlags & DC_ACTIVE) ? COLOR_ACTIVECAPTION : COLOR_INACTIVECAPTION);
Colors[1] = IntGetSysColor((uFlags & DC_ACTIVE) ? COLOR_GRADIENTACTIVECAPTION : COLOR_GRADIENTINACTIVECAPTION);
// 把 COLORREF 拆成 16 位 TRIVERTEX 通道(R<<8 / G&B / B>>8 & 0xFF00)
if (!GreGradientFill(hDc, Vertices, 2, &gcap, 1, GRADIENT_FILL_RECT_H)) goto cleanup;
}
else
{
hBgBrush = (uFlags & DC_INBUTTON) ? IntGetSysColorBrush(COLOR_3DFACE) :
(uFlags & DC_ACTIVE) ? IntGetSysColorBrush(COLOR_ACTIVECAPTION) :
IntGetSysColorBrush(COLOR_INACTIVECAPTION);
NtGdiSelectBrush(hDc, hBgBrush);
NtGdiPatBlt(hDc, Rect.left, Rect.top, Rect.right-Rect.left, Rect.bottom-Rect.top, PATCOPY);
}
渐变标题的数据来源是 SPI_GETGRADIENTCAPTIONS(显示属性开关)与 COLOR_GRADIENTACTIVECAPTION/INACTIVECAPTION 系统色;渐变方向水平(GRADIENT_FILL_RECT_H),从主色到渐变色的 16 位通道插值由 GDI 引擎完成。
图标与文字 :见 4.3 节(坐标计算)与 4.4 节(文字测量)。UserDrawCaption 末尾维护截断位:
c
if (pWnd)
{
if (Set) pWnd->state2 &= ~WNDS2_CAPTIONTEXTTRUNCATED;
else pWnd->state2 |= WNDS2_CAPTIONTEXTTRUNCATED;
}
UserDrawCaptionText(L2148-2249)的字体选择逻辑:
c
if (!hFont)
{
if (uFlags & DC_SMALLCAP)
Status = TextIntCreateFontIndirect(&nclm.lfSmCaptionFont, &hFont); // 小标题字体
else
Status = TextIntCreateFontIndirect(&nclm.lfCaptionFont, &hFont); // 标题字体
bDeleteFont = TRUE;
}
字体来自 SPI_GETNONCLIENTMETRICS 返回的 NONCLIENTMETRICSW(lfCaptionFont/lfSmCaptionFont,由系统维护);颜色选择:
c
if (uFlags & DC_INBUTTON)
OldTextColor = IntGdiSetTextColor(hDc, IntGetSysColor(COLOR_BTNTEXT));
else
OldTextColor = IntGdiSetTextColor(hDc,
IntGetSysColor(uFlags & DC_ACTIVE ? COLOR_CAPTIONTEXT : COLOR_INACTIVECAPTIONTEXT));
即"标题栏当按钮背景"(DC_INBUTTON,任务栏按钮场景)时用按钮文字色,否则用激活/非激活标题文字色。
5.3 NC_IconForWindow 与 UserDrawSysMenuButton------图标提取与系统菜单按钮
NC_IconForWindow(nonclient.c L736-764)按优先级链提取标题栏图标:
c
hIcon = UserGetProp(pWnd, gpsi->atomIconSmProp, TRUE); // 属性 "SysICS"(小图标,WM_SETICON(ICON_SMALL))
if (!hIcon) hIcon = UserGetProp(pWnd, gpsi->atomIconProp, TRUE); // 属性 "SysIC"(大图标)
if (!hIcon && pWnd->pcls->spicnSm) return pWnd->pcls->spicnSm; // 类小图标
if (!hIcon && pWnd->pcls->spicn) return pWnd->pcls->spicn; // 类图标
// 注意:DLGMODALFRAME 窗口不用默认图标(Wine 的实现是错的,这里按 Windows 行为修正)
if (!hIcon && !(pWnd->ExStyle & WS_EX_DLGMODALFRAME))
{
hIcon = gpsi->hIconSmWindows; // 默认 IDI_WINLOGO 小图标
if (!hIcon) hIcon = gpsi->hIconWindows;
}
if (hIcon) pIcon = (PCURICON_OBJECT)UserGetObjectNoErr(gHandleTable, hIcon, TYPE_CURSOR);
return pIcon;
两个全局原子 SysICS/SysIC 在 ntuser.c L52-53 注册(IntAddGlobalAtom),DefWndSetIcon(defwnd.c L432)通过 UserSetProp 写入。整个链:窗口属性小图标 → 窗口属性大图标 → 类小图标 → 类图标 → 系统默认 WinLogo 图标 。HTSYSMENU 命中测试也调用它(有图标才划分系统菜单区,见 4.5)。
UserDrawSysMenuButton(nonclient.c L767-787)------系统菜单按钮(标题栏左上角图标)的按下外观:
c
if ((WindowIcon = NC_IconForWindow(pWnd)))
{
UserReferenceObject(WindowIcon);
Ret = UserDrawIconEx(hDC,
Rect->left + 2, Rect->top + 2, // 左/上 2px 内边距
WindowIcon,
SM_CXSMICON, SM_CYSMICON, // 16×16
0, NULL, DI_NORMAL);
UserDereferenceObject(WindowIcon);
}
它只负责"按下时的图标渲染"(NC_HandleNCLButtonDown 的 HTSYSMENU 分支在发送 SC_MOUSEMENU 前调用它画凹陷效果),常规图标绘制由 UserDrawCaption 完成;按下效果仅仅是把图标整体向右下偏移 2px (+2,+2),模拟按钮凹陷。
5.4 UserDrawCaptionButton / UserDrawCaptionButtonWnd------三按钮绘制
见 4.2 节完整展开。补充两点:
UserDrawCaptionButton入口即检查if (!(Style & WS_SYSMENU)) return;------没有WS_SYSMENU就没有任何系统按钮 (三按钮与系统菜单按钮同源,由WS_SYSMENU门控);- 最小化/最大化分支对工具窗口直接
return("ToolWindows don't have min/max buttons"),关闭按钮则用小的SM_CXSMSIZE/SM_CYSMSIZE尺寸继续绘制------工具窗口只有一个缩小版关闭按钮。
5.5 UITOOLS95_DrawFrameCaption(draw.c L788-854)------Marlett 符号绘制
这是 DrawFrameControl(hDC, rc, DFC_CAPTION, uState) 在内核的落地实现。完整流程:
c
switch (uFlags & 0xf) // 低 4 位 = 按钮子类型 → 符号字符
{
case DFCS_CAPTIONCLOSE: Symbol = 'r'; break;
case DFCS_CAPTIONHELP: Symbol = 's'; break;
case DFCS_CAPTIONMIN: Symbol = '0'; break;
case DFCS_CAPTIONMAX: Symbol = '1'; break;
case DFCS_CAPTIONRESTORE:Symbol = '2'; break;
default: ERR("Invalid caption; flags=0x%04x\n", uFlags); return FALSE;
}
/* 1. 先画按钮的 3D 边框:按下 → 凹陷(EDGE_SUNKEN),否则凸起(EDGE_RAISED) */
IntDrawRectEdge(dc, r, (uFlags & DFCS_PUSHED) ? EDGE_SUNKEN : EDGE_RAISED,
BF_RECT | BF_MIDDLE | BF_SOFT);
/* 2. 把按钮矩形改造成正方形并内缩 1px 作为符号区 */
UITOOLS_MakeSquareRect(r, &myr); // 取宽高较小者为边长、居中
myr.left += 1; myr.top += 1; myr.right -= 1; myr.bottom -= 1;
if (uFlags & DFCS_PUSHED) RECTL_vOffsetRect(&myr, 1, 1); // 按下时符号整体右下移 1px
/* 3. 创建 Marlett 字体,字号 = 符号区高度 */
lf.lfHeight = myr.bottom - myr.top;
lf.lfWeight = FW_NORMAL;
lf.lfCharSet = DEFAULT_CHARSET;
lf.lfQuality = NONANTIALIASED_QUALITY; // 关闭抗锯齿(保持像素锐利)
RtlCopyMemory(lf.lfFaceName, L"Marlett", sizeof(L"Marlett"));
hFont = GreCreateFontIndirectW(&lf);
hOldFont = NtGdiSelectFont(dc, hFont);
/* 4. 输出符号:非激活先画右下 1px 高光阴影,再画主符号 */
IntGdiSetBkMode(dc, TRANSPARENT);
if (uFlags & DFCS_INACTIVE)
{
IntGdiSetTextColor(dc, IntGetSysColor(COLOR_BTNHIGHLIGHT)); // 阴影用高光色
GreTextOutW(dc, myr.left + 1, myr.top + 1, &Symbol, 1);
}
IntGdiSetTextColor(dc, IntGetSysColor((uFlags & DFCS_INACTIVE) ? COLOR_BTNSHADOW : COLOR_BTNTEXT));
GreTextOutW(dc, myr.left, myr.top, &Symbol, 1);
/* 5. 恢复字体/颜色/背景模式并删除临时字体 */
IntGdiSetTextColor(dc, clrsave);
NtGdiSelectFont(dc, hOldFont);
IntGdiSetBkMode(dc, bkmode);
GreDeleteObject(hFont);
要点:
- "按下"的视觉 = 3D 边从凸起变凹陷 + 符号向右下移 1px (
DFCS_PUSHED同时作用于IntDrawRectEdge的 edge 参数与myr偏移); - "灰化"的视觉 = 底色
BTNSHADOW(灰)符号 + 右下BTNHIGHLIGHT(亮)1px 偏移阴影,制造"浮雕但不可用"的观感; UITOOLS_MakeSquareRect(L566-587)把非正方形按钮矩形(经典主题下标题栏按钮约为 16×14,见 4.2 推导)改造成居中的正方形,保证 Marlett 符号不变形;- 整个按钮的"背景"由
IntDrawRectEdge(..., BF_RECT | BF_MIDDLE | BF_SOFT)一并画出:BF_MIDDLE让边框引擎用系统 3D 配色(EDGE_RAISED/EDGE_SUNKEN对应的亮/暗/中间色)填充按钮中间区域,从而得到经典的"凸起按钮面"------标题栏按钮没有单独的 FillRect 背景调用。
5.6 NC_DrawFrame(nonclient.c L909-953)------边框绘制
NC_DrawFrame 按"先粗边框、后其余边框"的顺序绘制窗口四周,每画完一层就收缩矩形,使后续内容(标题栏)落在正确位置:
c
/* ① 粗边框(可缩放边框):仅在 WS_THICKFRAME 且未最小化时 */
if ((Style & WS_THICKFRAME) && !(Style & WS_MINIMIZE))
{
LONG Width = (SM_CXFRAME - SM_CXDLGFRAME) * SM_CXBORDER; // (4-3)*1 = 1
LONG Height = (SM_CYFRAME - SM_CYDLGFRAME) * SM_CYBORDER;
NtGdiSelectBrush(hDC, IntGetSysColorBrush(Active ? COLOR_ACTIVEBORDER : COLOR_INACTIVEBORDER));
// 四条 PatBlt(PATCOPY):上、左、下(高度取负=向上)、右(宽度取负=向左)
NtGdiPatBlt(hDC, CurrentRect->left, CurrentRect->top, 右-左, Height, PATCOPY);
NtGdiPatBlt(hDC, CurrentRect->left, CurrentRect->top, Width, 下-上, PATCOPY);
NtGdiPatBlt(hDC, CurrentRect->left, CurrentRect->bottom, 右-左, -Height, PATCOPY);
NtGdiPatBlt(hDC, CurrentRect->right, CurrentRect->top, -Width, 下-上, PATCOPY);
RECTL_vInflateRect(CurrentRect, -Width, -Height);
}
/* ② 其余边框(对话框/普通/模态边框) */
if (Style & (WS_DLGFRAME | WS_BORDER) || ExStyle & WS_EX_DLGMODALFRAME)
{
LONG Width = SM_CXBORDER, Height = SM_CYBORDER; // 1
NtGdiSelectBrush(hDC, IntGetSysColorBrush(
(ExStyle & (WS_EX_DLGMODALFRAME | WS_EX_CLIENTEDGE)) ? COLOR_3DFACE :
(ExStyle & WS_EX_STATICEDGE) ? COLOR_WINDOWFRAME :
(Style & (WS_DLGFRAME | WS_THICKFRAME)) ? COLOR_3DFACE :
COLOR_WINDOWFRAME));
... 同样的四条 PatBlt ...
RECTL_vInflateRect(CurrentRect, -Width, -Height);
}
边框颜色随激活状态(Active 参数,来自 DC_ACTIVE)变化------COLOR_ACTIVEBORDER/COLOR_INACTIVEBORDER。NC_DoNCPaint 与 UserDrawCaptionBar(带 DC_FRAME 时)都调用它,并各自维护自己的 CurrentRect 收缩链。
5.7 NC_DoNCPaint(nonclient.c L1097-1305)------WM_NCPAINT 处理
NC_DoNCPaint 是"整窗 NC 重绘"的容器(第 6 册 8 节已展开整体,本册只强调标题栏相关的骨架与细节):
c
LRESULT NC_DoNCPaint(PWND pWnd, HDC hDC, INT Flags)
{
...
/* 入口检查:不可见 / 应用接管 NC(WNDS_NONCPAINT)且非强制菜单绘制 / 无边框(rcWindow==rcClient)→ 跳过 */
if (!IntIsWindowVisible(pWnd) ||
(pWnd->state & WNDS_NONCPAINT && !(pWnd->state & WNDS_FORCEMENUDRAW)) ||
IntEqualRect(&pWnd->rcWindow, &pWnd->rcClient))
return 0;
/* 激活判定 */
if (Flags == -1) // WM_NCPAINT 模式
{
if (ExStyle & WS_EX_MDICHILD)
{
Active = IntIsChildWindow(gpqForeground->spwndActive, pWnd);
if (Active) Active = (UserHMGetHandle(pWnd) == (HWND)co_IntSendMessage(
UserHMGetHandle(Parent), WM_MDIGETACTIVE, 0, 0));
}
else
Active = (gpqForeground == pWnd->head.pti->MessageQueue); // 前台队列 == 窗口线程队列
Flags = DC_NC;
}
else
Flags |= DC_NC;
...
/* 激活标志同步(激活切换的竞态窗口期) */
if (Active)
{
if (pWnd->state & WNDS_ACTIVEFRAME) Flags |= DC_ACTIVE;
else { pWnd->state |= WNDS_ACTIVEFRAME; Flags |= DC_ACTIVE; } // 补齐状态位
}
...
/* 标题栏段:TempRect 取 SM_CYCAPTION/-1(或 SM_CYSMCAPTION)下界 */
UserDrawCaption(pWnd, hDC, &TempRect, NULL, NULL, NULL, Flags);
/* 按钮段:与 UserDrawCaptionBar 完全相同的三连调用 */
if (Style & WS_SYSMENU)
{
UserDrawCaptionButton(pWnd, &TempRect, Style, ExStyle, hDC, FALSE, DFCS_CAPTIONCLOSE);
if ((Style & (WS_MAXIMIZEBOX | WS_MINIMIZEBOX)) && !(ExStyle & WS_EX_TOOLWINDOW))
{
UserDrawCaptionButton(pWnd, &TempRect, Style, ExStyle, hDC, FALSE, DFCS_CAPTIONMIN);
UserDrawCaptionButton(pWnd, &TempRect, Style, ExStyle, hDC, FALSE, DFCS_CAPTIONMAX);
}
}
/* 底线、菜单栏(DC_NOSENDMSG 时跳过)、CLIENTEDGE、滚动条 + 尺寸调整框 */
...
return 0;
}
与 UserDrawCaptionBar 的差异点:
NC_DoNCPaint的激活判定基于gpqForeground(前台线程队列)与WM_MDIGETACTIVE(MDI 子窗口),而UserDrawCaptionBar的激活由调用方传入的Flags & DC_ACTIVE决定(UserPaintCaption用Wnd->head.pti->MessageQueue == gpqForeground判定,见 6.4);NC_DoNCPaint的Flags == -1是WM_NCPAINT模式的哨兵值(defwnd.c L1306 传入-1),表示"重绘一切";DC_REDRAWHUNGWND→DC_NOSENDMSG的转换(L1171-1175)用于挂起窗口重绘时避免发送消息;- 滚动条与尺寸调整框绘制在本册不展开(第 6 册 8.3 节已覆盖)。
WM_NCPAINT 的 defwnd.c 分支(L1302-1310)是它的唯一"官方"入口:
c
case WM_NCPAINT:
{
HDC hDC = UserGetDCEx(Wnd, (HRGN)wParam, DCX_WINDOW | DCX_INTERSECTRGN | DCX_USESTYLE | DCX_KEEPCLIPRGN);
Wnd->state |= WNDS_FORCEMENUDRAW; // 强制菜单栏重绘(绕过菜单自身的绘制完成标志)
NC_DoNCPaint(Wnd, hDC, -1);
Wnd->state &= ~WNDS_FORCEMENUDRAW;
UserReleaseDC(Wnd, hDC, FALSE);
return 0;
}
wParam 携带更新区域 HRGN,经 DCX_INTERSECTRGN | DCX_KEEPCLIPRGN 并入 DC 裁剪------NC 只重绘被更新部分 ;DCX_WINDOW 让 DC 以窗口矩形(而非客户区)为表面基准,是绘制 NC 的前提(与第 6 册 5.4 节一致)。
5.8 DrawFrameControl 分发(draw.c L1094-1137)
DrawFrameControl 是内核版绘制控制 API 的入口(user32 的 DrawFrameControl 经 NtGdi 或直接调用到此处):
c
BOOL WINAPI DrawFrameControl(HDC hDC, LPRECT rc, UINT uType, UINT uState)
{
if (GreGetMapMode(hDC) != MM_TEXT) return FALSE; // 非 MM_TEXT 映射模式不支持
switch (uType)
{
case DFC_BUTTON: return UITOOLS95_DrawFrameButton(hDC, rc, uState);
case DFC_CAPTION: return UITOOLS95_DrawFrameCaption(hDC, rc, uState); // 标题栏按钮走这里
case DFC_MENU: /* 填充 + UITOOLS95_DrawFrameMenu */ ...
case DFC_SCROLL: return UITOOLS95_DrawFrameScroll(hDC, rc, uState);
}
return FALSE;
}
UITOOLS95_DrawFrameButton(L759-786)按 uState & 0x1f 分派到 UITOOLS95_DFC_ButtonPush(L620-662,普通按钮)与 UITOOLS95_DFC_ButtonCheckRadio(L664-756,复选/单选)。DFC_CAPTION 是最简单的分支------不涉及背景填充,直接画 3D 边 + Marlett 符号。
6. 激活/非激活双外观
6.1 NC_HandleNCActivate / NC_DoNCActive(nonclient.c L1445-1499 / L1427-1443)
WM_NCACTIVATE 的处理函数:
c
LRESULT NC_HandleNCActivate(PWND Wnd, WPARAM wParam, LPARAM lParam)
{
INT Flags;
/* Lotus Notes 在标题栏绘制菜单描述文字;它向自己发送 WM_NCACTIVATE
以恢复"系统视图",因此这里不能做任何减少重绘的优化 */
if (wParam & DC_ACTIVE) // 激活:wParam 非零(TRUE 或 DC_ACTIVE)
{
Wnd->state |= WNDS_ACTIVEFRAME | WNDS_HASCAPTION;
wParam = DC_CAPTION | DC_ACTIVE; // 重绘范围:标题栏 + 激活外观
}
else // 失活
{
Wnd->state &= ~WNDS_ACTIVEFRAME;
wParam = DC_CAPTION; // 仅标题栏(非激活外观)
}
if ((Wnd->state & WNDS_NONCPAINT) || !(Wnd->style & WS_VISIBLE))
return TRUE; // 应用接管 NC 或不可见 → 不重绘
/* lParam == -1 时不重绘(MSDN 未记载,Outlook 2007 依赖此行为) */
if (lParam != -1 && (Flags = NC_DoNCActive(Wnd)) != 0)
{
HDC hDC;
HRGN hRgnTemp = NULL, hRgn = (HRGN)lParam;
if (GreIsHandleValid(hRgn)) // lParam 可能是区域句柄
{
hRgnTemp = NtGdiCreateRectRgn(0, 0, 0, 0);
if (NtGdiCombineRgn(hRgnTemp, hRgn, 0, RGN_COPY) == ERROR) { ... } // 复制后交由 DC 接管
}
if ((hDC = UserGetDCEx(Wnd, hRgnTemp, DCX_WINDOW | DCX_USESTYLE)))
{
NC_DoNCPaint(Wnd, hDC, wParam | Flags); // 重绘标题栏(含菜单栏)
UserReleaseDC(Wnd, hDC, FALSE);
}
else
GreDeleteObject(hRgnTemp);
}
return TRUE;
}
要点:
wParam & DC_ACTIVE而不是wParam != 0------NT 内核在激活时可能直接传DC_ACTIVE(0x1),二者等价(TRUE == 1 == DC_ACTIVE),但以DC_ACTIVE判断语义更明确;- 激活时同时置
WNDS_ACTIVEFRAME|WNDS_HASCAPTION:前者驱动外观,后者是"有标题栏"的冗余保险; - 重绘范围由
NC_DoNCActive决定,wParam | Flags传入NC_DoNCPaint后变成DC_ACTIVE|DC_CAPTION|(DC_FRAME)组合。
NC_DoNCActive(L1427-1443)------决定"激活变化需要重绘哪些部分":
c
static INT NC_DoNCActive(PWND Wnd)
{
INT Ret = 0;
if ( IntGetSysColor(COLOR_CAPTIONTEXT) != IntGetSysColor(COLOR_INACTIVECAPTIONTEXT) ||
IntGetSysColor(COLOR_ACTIVECAPTION) != IntGetSysColor(COLOR_INACTIVECAPTION) )
Ret = DC_CAPTION; // 文字或背景色不同 → 重绘标题栏
if (!(Wnd->style & WS_MINIMIZED) && UserHasThickFrameStyle(Wnd->style, Wnd->ExStyle))
{
Ret = DC_FRAME; // 粗边框窗口 → 重绘边框(活动边框色不同)
}
return Ret;
}
它是"最小重绘优化":只有激活/非激活颜色确实不同 才要求重绘对应区域(如果系统把激活与非激活色配成一样,切换激活就不必重画)。DC_CAPTION 与 DC_FRAME 这两个返回值没有位与关系(第二段直接赋值覆盖),最终 NC_DoNCPaint 收到的标志是 DC_CAPTION|DC_ACTIVE|DC_FRAME(粗边框窗口)或 DC_CAPTION|DC_ACTIVE(无粗边框窗口)。
6.2 激活/非激活颜色差异表
标题栏双外观的全部颜色都来自系统颜色(IntGetSysColor,syscolor.c 维护),本册汇总其在绘制链路中的消费点:
| 系统颜色 | 激活外观值(经典默认) | 消费点 |
|---|---|---|
COLOR_ACTIVECAPTION / COLOR_INACTIVECAPTION |
深蓝 / 灰 | UserDrawCaption 平涂背景(L2317-2319) |
COLOR_GRADIENTACTIVECAPTION / COLOR_GRADIENTINACTIVECAPTION |
渐变终点色 | UserDrawCaption 渐变背景(L2286-2290) |
COLOR_CAPTIONTEXT / COLOR_INACTIVECAPTIONTEXT |
白 / 浅灰 | UserDrawCaptionText 文字色(L2198-2199) |
COLOR_ACTIVEBORDER / COLOR_INACTIVEBORDER |
灰蓝 / 灰 | NC_DrawFrame 粗边框(L922) |
COLOR_BTNTEXT / COLOR_BTNSHADOW + COLOR_BTNHIGHLIGHT |
黑 / 灰 + 亮 | UITOOLS95_DrawFrameCaption 按钮符号(L842-845) |
COLOR_3DFACE |
按钮面灰 | 标题栏底线(L1053)、按钮 3D 面 |
COLOR_WINDOWFRAME |
黑 | WS_EX_STATICEDGE 场景底线(L1053) |
DC_ACTIVE 标志在整条链上传递:NC_HandleNCActivate/NC_DoNCPaint 置位 → UserDrawCaption 读 uFlags & DC_ACTIVE 选色 → UserDrawCaptionText 读 uFlags & DC_ACTIVE 选文字色。UserDrawCaptionButton 本身不直接读 DC_ACTIVE------按钮的"激活"体现在符号颜色上(DFCS_INACTIVE 未置位时用 COLOR_BTNTEXT 黑色),与标题栏背景色无关,因此三按钮在激活/非激活窗口上符号颜色相同,仅背景(标题栏底色)不同。
6.3 渐变标题(SPI_GETGRADIENTCAPTIONS)
显示属性"渐变标题栏"开关经 UserSystemParametersInfo(SPI_GETGRADIENTCAPTIONS, ...) 读取(UserDrawCaptionBar L1010 与 NC_DoNCPaint L1177 各读一次)。开启后 Flags |= DC_GRADIENT,UserDrawCaption 走 GreGradientFill(GRADIENT_FILL_RECT_H) 从 COLOR_ACTIVECAPTION 渐变到 COLOR_GRADIENTACTIVECAPTION(非激活同理)。这是"标题栏背景"唯一的第二形态;渐变只作用于背景,图标/文字/按钮不受影响。
6.4 与 focus.c 的关系------WM_NCACTIVATE 从哪来
激活状态变化的源头是 focus.c(co_IntSetForegroundWindow/IntSetActiveWindow 等)。激活切换时系统向新旧活动窗口发送 WM_NCACTIVATE:
- 激活窗口 :
WM_NCACTIVATE(TRUE)→NC_HandleNCActivate置WNDS_ACTIVEFRAME,重绘为激活外观; - 失活窗口 :
WM_NCACTIVATE(FALSE)→ 清WNDS_ACTIVEFRAME,重绘为非激活外观。
UserPaintCaption(defwnd.c L394-428)在非 WM_NCACTIVATE 路径(如 WM_SETTEXT、WM_SETICON、WM_SYSCOLORCHANGE)重绘标题栏时自行判定激活:
c
if (pWnd->state & WNDS_HASCAPTION && pWnd->head.pti->MessageQueue == gpqForeground)
Flags |= DC_ACTIVE; // 窗口线程队列 == 前台队列 → 视为激活
而 NC_DoNCPaint 在 WM_NCPAINT 路径用同一个判据(gpqForeground == pWnd->head.pti->MessageQueue),并处理"队列已切前台但 WNDS_ACTIVEFRAME 尚未同步"的竞态(L1184-1193:补齐状态位)。两条判定路径最终都汇入 DC_ACTIVE,保证外观与队列激活状态一致。
7. 三按钮交互状态机
7.1 NC_HandleNCLButtonDown(nonclient.c L1582-1670)------按钮命中分发
WM_NCLBUTTONDOWN 由 msgqueue.c 的鼠标转换逻辑产生(命中码在 lParam 中),IntDefWindowProc L807 转交本函数。标题栏相关的分支:
c
case HTMINBUTTON:
case HTMAXBUTTON:
case HTCLOSE:
{
NC_DoButton(pWnd, wParam, lParam); // 三个系统按钮统一进状态机
break;
}
case HTCAPTION:
{
/* 沿 owner/父链上溯到最顶层窗口 */
PWND TopWnd = pWnd, parent;
while (1)
{
if ((TopWnd->style & (WS_POPUP|WS_CHILD)) != WS_CHILD) break;
parent = UserGetAncestor(TopWnd, GA_PARENT);
if (!parent || UserIsDesktopWindow(parent)) break;
TopWnd = parent;
}
if ((pWnd && (pWnd->ExStyle & WS_EX_NOACTIVATE)) ||
co_IntSetForegroundWindowMouse(TopWnd) || // 激活由"鼠标按下激活"路径处理
UserGetActiveWindow() == UserHMGetHandle(TopWnd))
{
co_IntSendMessage(UserHMGetHandle(pWnd), WM_SYSCOMMAND, SC_MOVE + HTCAPTION, lParam);
}
break;
}
case HTSYSMENU:
{
LONG style = pWnd->style;
if (style & WS_SYSMENU)
{
if (!(style & WS_MINIMIZE)) // 非最小化:先画按下图标
{
RECT rect;
HDC hDC = UserGetWindowDC(pWnd);
NC_GetInsideRect(pWnd, &rect);
UserDrawSysMenuButton(pWnd, hDC, &rect, TRUE); // 图标向右下偏移 2px
UserReleaseDC(pWnd, hDC, FALSE);
}
co_IntSendMessage(UserHMGetHandle(pWnd), WM_SYSCOMMAND, SC_MOUSEMENU + HTSYSMENU, lParam);
}
break;
}
case HTMENU: /* 菜单栏 → SC_MOUSEMENU + HTMENU */
case HTHSCROLL: /* → SC_HSCROLL + HTHSCROLL */
case HTVSCROLL: /* → SC_VSCROLL + HTVSCROLL */
case HTLEFT ... HTBOTTOMRIGHT: /* → SC_SIZE + wParam - (HTLEFT - WMSZ_LEFT) */
case HTBORDER: break; /* 无可缩放边框,忽略 */
要点:
- 三按钮共用
NC_DoButton:命中码只是选择ButtonType与SCMsg的钥匙; - HTCAPTION 的激活竞争 :点击非活动窗口标题栏时
WM_MOUSEACTIVATE返回MA_NOACTIVATE(defwnd.c L1111-1124),激活由co_IntSetForegroundWindowMouse(TopWnd)完成后再进SC_MOVE------避免"先激活再拖动"的两次鼠标语义冲突(第 6 册 5.6 节); - HTSYSMENU 先画后弹 :
UserDrawSysMenuButton(TRUE)给出按下反馈,然后立即发送SC_MOUSEMENU进入系统菜单跟踪(弹出后由菜单循环接管),菜单关闭后不再有弹起绘制------因为系统菜单本身就是"按下→弹出"语义。
7.2 NC_DoButton(nonclient.c L1502-1578)------按下状态机
完整源码(核心):
c
VOID NC_DoButton(PWND pWnd, WPARAM wParam, LPARAM lParam)
{
MSG Msg;
HDC WindowDC;
BOOL Pressed = TRUE, OldState;
WPARAM SCMsg;
PMENU SysMenu;
ULONG ButtonType;
DWORD Style;
UINT MenuState;
Style = pWnd->style;
switch (wParam)
{
case HTCLOSE:
SysMenu = IntGetSystemMenu(pWnd, FALSE);
MenuState = IntGetMenuState(SysMenu ? UserHMGetHandle(SysMenu) : NULL, SC_CLOSE, MF_BYCOMMAND);
if (!(Style & WS_SYSMENU) || (MenuState & (MF_GRAYED|MF_DISABLED)) || (pWnd->pcls->style & CS_NOCLOSE))
return; // 关闭被禁:直接忽略按下
ButtonType = DFCS_CAPTIONCLOSE;
SCMsg = SC_CLOSE;
break;
case HTMINBUTTON:
if (!(Style & WS_MINIMIZEBOX)) return;
ButtonType = DFCS_CAPTIONMIN;
SCMsg = ((Style & WS_MINIMIZE) ? SC_RESTORE : SC_MINIMIZE); // 已最小化 → 还原
break;
case HTMAXBUTTON:
if (!(Style & WS_MAXIMIZEBOX)) return;
ButtonType = DFCS_CAPTIONMAX;
SCMsg = ((Style & WS_MAXIMIZE) ? SC_RESTORE : SC_MAXIMIZE); // 已最大化 → 还原
break;
default:
ASSERT(FALSE); // 只接受三个按钮命中码
return;
}
co_IntUpdateWindows(pWnd, RDW_ALLCHILDREN, FALSE); // 冲刷挂起更新(关闭被遮挡窗口时的显示问题)
WindowDC = UserGetWindowDC(pWnd);
UserDrawCaptionButtonWnd(pWnd, WindowDC, TRUE, ButtonType); // ① 压入外观
co_UserSetCapture(UserHMGetHandle(pWnd)); // ② 捕获鼠标
for (;;)
{
if (!co_IntGetPeekMessage(&Msg, 0, WM_MOUSEFIRST, WM_MOUSELAST, PM_REMOVE, TRUE)) break;
if (IntCallMsgFilter(&Msg, MSGF_MAX)) continue;
if (Msg.message == WM_LBUTTONUP) break; // ③ 抬起退出循环
if (Msg.message != WM_MOUSEMOVE) continue; // 只关心移动
OldState = Pressed;
Pressed = (GetNCHitEx(pWnd, Msg.pt) == wParam); // ④ 重新命中判定
if (Pressed != OldState)
UserDrawCaptionButtonWnd(pWnd, WindowDC, Pressed, ButtonType); // ⑤ 状态翻转重绘
}
if (Pressed)
UserDrawCaptionButtonWnd(pWnd, WindowDC, FALSE, ButtonType); // ⑥ 弹起外观(若仍压着)
IntReleaseCapture();
UserReleaseDC(pWnd, WindowDC, FALSE);
if (Pressed)
co_IntSendMessage(UserHMGetHandle(pWnd), WM_SYSCOMMAND, SCMsg,
SCMsg == SC_CLOSE ? lParam : MAKELONG(Msg.pt.x, Msg.pt.y)); // ⑦ 执行
}
状态机时序(结合 2.3 的图):
| 步骤 | 事件 | 动作 |
|---|---|---|
| ① | 进入函数 | 绘制压入外观(UserDrawCaptionButtonWnd(TRUE)) |
| ② | 捕获鼠标 | co_UserSetCapture------按钮跟踪期间所有鼠标消息归本窗口 |
| ③ | WM_MOUSEMOVE |
GetNCHitEx(Msg.pt) 重新命中:在按钮内保持压入,移出则弹起(视觉回弹) |
| ④ | WM_LBUTTONUP |
退出循环 |
| ⑥ | 退出循环后 | 若仍处于压入态,绘制弹起外观 |
| ⑦ | Pressed == TRUE |
发送 WM_SYSCOMMAND(SCMsg)------只有"在按钮内完成按下-抬起"才触发 |
三个细节:
SC_CLOSE的 lParam 原样传递 ,其余命令传MAKELONG(Msg.pt.x, Msg.pt.y)(抬起位置)------SC_CLOSE的 lParam 是按下时的坐标(供 WM_CLOSE 使用),其余命令的坐标仅供系统菜单等场景参考;- 移出移回 :
Pressed在移动中反复翻转,GetNCHitEx每次都实时判定,实现"按住不放、移出取消、移回恢复"的完整语义; - 没有悬停态(HOT) :经典主题没有
DFCS_HOT悬停高亮,NC_DoButton只维护 按下/弹起 两态;DFCS_HOT供主题引擎使用(本路径不置位)。
7.3 HTSYSMENU 的按下与弹出
系统菜单按钮(图标区)不走 NC_DoButton------它直接进系统菜单跟踪(见 7.1 的 HTSYSMENU 分支):按下画图标偏移 → 发送 SC_MOUSEMENU + HTSYSMENU → DefWndHandleSysCommand 的 SC_MOUSEMENU 分支调 MENU_TrackMouseMenuBar(见 8.6)→ 菜单跟踪循环接管。用户按下图标瞬间看到"图标凹陷",随后系统菜单在图标下方弹出,二者在视觉上连贯。
7.4 NC_HandleNCLButtonDblClk(nonclient.c L1674-1725)------双击
WM_NCLBUTTONDBLCLK(defwnd.c L816)与 WM_LBUTTONDBLCLK(L813,以 HTCLIENT 转入)都汇聚到本函数。标题栏相关的三个分支:
c
case HTCAPTION:
{
/* Maximize/Restore the window ------ 双击标题栏切换最大化/还原 */
if ((Style & WS_CAPTION) == WS_CAPTION && (Style & WS_MAXIMIZEBOX))
{
co_IntSendMessage(UserHMGetHandle(pWnd), WM_SYSCOMMAND,
((Style & (WS_MINIMIZE | WS_MAXIMIZE)) ? SC_RESTORE : SC_MAXIMIZE), 0);
}
break;
}
case HTSYSMENU:
{
/* 双击系统菜单图标 = 关闭(若关闭项可用) */
PMENU SysMenu = IntGetSystemMenu(pWnd, FALSE);
UINT state = IntGetMenuState(SysMenu ? UserHMGetHandle(SysMenu) : NULL, SC_CLOSE, MF_BYCOMMAND);
if ((state & (MF_DISABLED | MF_GRAYED)) || (state == 0xFFFFFFFF)) break; // 被禁则忽略
co_IntSendMessage(UserHMGetHandle(pWnd), WM_SYSCOMMAND, SC_CLOSE, lParam);
break;
}
case HTTOP: case HTBOTTOM:
{
/* 双击上/下边框 → 纵向扩展到工作区(经典 Windows 行为) */
... co_WinPosSetWindowPos(pWnd, NULL, left, workarea.top, w, workarea.bottom - workarea.top, 0);
break;
}
default:
return NC_HandleNCLButtonDown(pWnd, wParam, lParam); // 其余命中码退化为单击行为
注意 HTCAPTION 双击的触发条件是 WS_MAXIMIZEBOX------没有最大化框样式的窗口双击标题栏不做任何事 ;HTSYSMENU 双击与关闭按钮按下共享同一套 IntGetMenuState(SC_CLOSE) 可用性判定。
7.5 键盘入口(SC_KEYMENU / ALT+Space)
标题栏按钮不只有鼠标路径。DefWndHandleSysCommand 的 SC_KEYMENU 分支(defwnd.c L201-203)调 MENU_TrackKbdMenuBar(menu.c L4516):用户按 ALT+Space 时系统发送 WM_SYSCOMMAND(SC_KEYMENU, ' '),MENU_TrackKbdMenuBar 判定"无窗口菜单或窗口最小化或空格键"后自动切换到系统菜单跟踪(TrackMenu = get_win_sys_menu(...),L4532-4538)。系统菜单弹出后,用户可以用方向键选择"还原/移动/大小/最小化/最大化/关闭"------这等价于用键盘完成三按钮(+移动/缩放)的全部操作。系统菜单项的图标符号('2'/'0'/'1'/'r')与标题栏按钮符号同源(Marlett,见 3.6)。
8. 最小化/最大化/关闭行为落地
8.1 DefWndHandleSysCommand(defwnd.c L123-211)------SC_* 分发
按钮状态机发出的 WM_SYSCOMMAND 最终由 DefWndHandleSysCommand 落地(IntDefWindowProc L723-728 → WM_SYSCOMMAND case):
c
LRESULT FASTCALL DefWndHandleSysCommand(PWND pWnd, WPARAM wParam, LPARAM lParam)
{
LRESULT lResult = 0;
BOOL Hook = FALSE;
if (ISITHOOKED(WH_CBT) || ...) // CBT 钩子可拦截系统命令
{
Hook = TRUE;
lResult = co_HOOK_CallHooks(WH_CBT, HCBT_SYSCOMMAND, wParam, lParam);
if (lResult) return lResult; // 钩子返回非零 → 取消命令
}
switch (wParam & 0xfff0) // 取 SC_* 高 12 位(去掉 HT 低 4 位)
{
case SC_MOVE: case SC_SIZE:
DefWndDoSizeMove(pWnd, wParam); // 移动/缩放模态循环(第 6 册 12 节)
break;
case SC_MINIMIZE:
if (UserHMGetHandle(pWnd) == UserGetActiveWindow())
IntShowOwnedPopups(pWnd, FALSE); // 隐藏 owned popup(随窗口最小化)
co_WinPosShowWindow(pWnd, SW_MINIMIZE);
break;
case SC_MAXIMIZE:
if (((pWnd->style & WS_MINIMIZE) != 0) && UserHMGetHandle(pWnd) == UserGetActiveWindow())
IntShowOwnedPopups(pWnd, TRUE);
co_WinPosShowWindow(pWnd, SW_MAXIMIZE);
break;
case SC_RESTORE:
if (((pWnd->style & WS_MINIMIZE) != 0) && UserHMGetHandle(pWnd) == UserGetActiveWindow())
IntShowOwnedPopups(pWnd, TRUE);
co_WinPosShowWindow(pWnd, SW_RESTORE);
break;
case SC_CLOSE:
return co_IntSendMessage(UserHMGetHandle(pWnd), WM_CLOSE, 0, 0);
case SC_MOUSEMENU:
{
POINT Pt; Pt.x = (short)LOWORD(lParam); Pt.y = (short)HIWORD(lParam);
MENU_TrackMouseMenuBar(pWnd, wParam & 0x000f, Pt); // 低 4 位 = HT 码
}
break;
case SC_KEYMENU:
MENU_TrackKbdMenuBar(pWnd, wParam, (WCHAR)lParam);
break;
default:
return 0;
}
return (Hook ? 1 : 0);
}
8.2 SC_MINIMIZE → co_WinPosShowWindow(SW_MINIMIZE)
SC_MINIMIZE 的落地分两步:
- 隐藏 owned popup :窗口是活动窗口时
IntShowOwnedPopups(pWnd, FALSE)把它的属主弹窗一并隐藏------最小化一个窗口时其附属对话框不应残留在屏幕上; - ShowWindow 路径 :
co_WinPosShowWindow(pWnd, SW_MINIMIZE)(winpos.c L2630)进入winpos.c的窗口位置/尺寸状态机:窗口尺寸变为SM_CXMINIMIZED × SM_CYMINIMIZED(任务栏按钮形态,NC_HandleNCCalcSize最小化分支据此固定客户区),WS_MINIMIZE(WS_ICONIC)样式被置位,随后产生WM_SIZE(SIZE_MINIMIZED)、WM_MOVE等消息(defwnd.cDefWndHandleWindowPosChangedL100-116)。
最小化后的窗口标题栏不再是"标题栏"------GetNCHitEx L2018 直接 if (Style & WS_MINIMIZE) return HTCAPTION;,整窗(任务栏按钮)都视为可拖动标题栏;DefWndDoSizeMove L731 在最小化且未移动时发送 SC_MOUSEMENU + HTSYSMENU(单击任务栏按钮弹系统菜单)。
8.3 SC_MAXIMIZE / SC_RESTORE
- SC_MAXIMIZE :若窗口正处于最小化且为活动窗口,先
IntShowOwnedPopups(pWnd, TRUE)恢复属主弹窗,再co_WinPosShowWindow(SW_MAXIMIZE)------窗口扩展到工作区(SM_CXMAXIMIZED × SM_CYMAXIMIZED,最大化矩形由co_WinPosGetMinMaxInfo依据工作区计算),WS_MAXIMIZE置位,产生WM_SIZE(SIZE_MAXIMIZED); - SC_RESTORE :从最小化或最大化恢复到"普通矩形"。恢复后的位置/尺寸保存在
WND.InternalPos.NormalRect(普通矩形),最小化/最大化时由winpos.c保存、恢复时读回。
三按钮与这两个命令的对应(NC_DoButton 的 SCMsg 选择):最小化按钮在窗口已最小化 时发 SC_RESTORE(符号也从 '0' 变 '2',UserDrawCaptionButton L838 的 DFCS_CAPTIONRESTORE);最大化按钮在窗口已最大化 时同样发 SC_RESTORE。这就是"最小化按钮在最小化窗口上变成还原按钮"的完整机制。
8.4 SC_CLOSE → WM_CLOSE → DestroyWindow
SC_CLOSE 是唯一不直接调用窗口函数、而是发消息的命令:
SC_CLOSE
→ co_IntSendMessage(WM_CLOSE) (defwnd.c L162)
→ 应用 WM_CLOSE 处理(可拦截:询问保存、拒绝关闭等)
→ 未处理则 DefWindowProc:IntDefWindowProc 的 WM_CLOSE case(defwnd.c L1078-1080)
→ co_UserDestroyWindow(Wnd) (window.c,销毁窗口对象树)
这条"命令 → 消息 → 应用可拦截"的链路是 Windows 关闭语义的核心:WM_CLOSE 给了应用最后一个反悔机会(例如记事本弹"是否保存"),只有应用不处理(或返回 DefWindowProc)才真正销毁窗口。按钮灰化(MF_GRAYED 系统菜单项或 CS_NOCLOSE)正是阻止这条链路被触发的手段。
8.5 系统菜单构建(MENU_GetSystemMenu + SYSMENU 模板)
三按钮的行为之所以能"落地",系统菜单是其权威状态源(关闭按钮的灰化读 IntGetMenuState(SC_CLOSE))。系统菜单的构建在 menu.c:
MENU_GetSystemMenu(L5304-5406)------首次获取时创建:
c
PMENU FASTCALL MENU_GetSystemMenu(PWND Window, PMENU Popup)
{
...
hSysMenu = UserCreateMenu(Window->head.rpdesk, FALSE); // 顶层容器菜单
SysMenu = UserGetMenuObject(hSysMenu);
SysMenu->fFlags |= MNF_SYSMENU;
SysMenu->hWnd = UserHMGetHandle(Window);
if (!Popup)
{
// 加载系统菜单模板(user32 客户端资源):
// MDI 子窗口用 "SYSMENUMDI",普通窗口用 "SYSMENU"
RtlInitUnicodeString(&MenuName, Window->ExStyle & WS_EX_MDICHILD ? L"SYSMENUMDI" : L"SYSMENU");
hNewMenu = co_IntCallLoadMenu(hModClient, &MenuName);
Menu = UserGetMenuObject(hNewMenu);
Menu->fFlags |= MNS_CHECKORBMP | MNF_SYSMENU | MNF_POPUP;
// 给四个命令项挂 Marlett 图标位图(菜单项绘制时经 MENU_DrawPopupGlyph 输出符号)
ItemInfoSet.hbmpItem = HBMMENU_POPUP_CLOSE; IntMenuItemInfo(Menu, SC_CLOSE, ...);
ItemInfoSet.hbmpItem = HBMMENU_POPUP_RESTORE; IntMenuItemInfo(Menu, SC_RESTORE, ...);
ItemInfoSet.hbmpItem = HBMMENU_POPUP_MAXIMIZE; IntMenuItemInfo(Menu, SC_MAXIMIZE, ...);
ItemInfoSet.hbmpItem = HBMMENU_POPUP_MINIMIZE; IntMenuItemInfo(Menu, SC_MINIMIZE, ...);
NewMenu = IntCloneMenu(Menu); // 克隆(每个窗口独立)
UserSetMenuDefaultItem(NewMenu, SC_CLOSE, FALSE);
}
...
if (Window->pcls->style & CS_NOCLOSE)
IntRemoveMenuItem(NewMenu, SC_CLOSE, MF_BYCOMMAND, TRUE); // CS_NOCLOSE → 移除关闭项
// 把 NewMenu 作为子菜单插入 SysMenu
IntInsertMenuItem(SysMenu, (UINT)-1, TRUE, &ItemInfo, NULL);
return SysMenu;
}
IntGetSystemMenu(L5409-5449)------每窗口的缓存与子菜单返回:
c
Menu = Window->SystemMenu ? UserGetMenuObject(Window->SystemMenu) : NULL;
if ((!Menu || Menu->fFlags & MNF_SYSDESKMN) && Window->style & WS_SYSMENU)
{
Menu = MENU_GetSystemMenu(Window, NULL);
Window->SystemMenu = Menu ? UserHMGetHandle(Menu) : NULL; // 缓存到窗口对象
}
if (Window->SystemMenu)
{
HMENU hMenu = IntGetSubMenu(Window->SystemMenu, 0); // 取子菜单(真正的命令菜单)
Menu = UserGetMenuObject(hMenu);
Menu->spwndNotify = Window; // 用于菜单状态变化时通知重绘
Menu->fFlags |= MNF_SYSSUBMENU;
return Menu; // 返回的是子菜单
}
MENU_InitSysMenuPopup(L1362-1389)------弹出前按窗口样式刷新各项使能状态:
c
void FASTCALL MENU_InitSysMenuPopup(PMENU menu, DWORD style, DWORD clsStyle, LONG HitTest)
{
BOOL gray;
gray = !(style & WS_THICKFRAME) || (style & (WS_MAXIMIZE | WS_MINIMIZE));
IntEnableMenuItem(menu, SC_SIZE, (gray ? MF_GRAYED : MF_ENABLED)); // 无粗边框 → 大小灰化
gray = ((style & (WS_MAXIMIZE | WS_MINIMIZE)) != 0);
IntEnableMenuItem(menu, SC_MOVE, (gray ? MF_GRAYED : MF_ENABLED)); // 最大/最小化时移动灰化
gray = !(style & WS_MINIMIZEBOX) || (style & WS_MINIMIZE);
IntEnableMenuItem(menu, SC_MINIMIZE, (gray ? MF_GRAYED : MF_ENABLED)); // 已最小化 → 最小化灰化
gray = !(style & WS_MAXIMIZEBOX) || (style & WS_MAXIMIZE);
IntEnableMenuItem(menu, SC_MAXIMIZE, (gray ? MF_GRAYED : MF_ENABLED));
gray = !(style & (WS_MAXIMIZE | WS_MINIMIZE));
IntEnableMenuItem(menu, SC_RESTORE, (gray ? MF_GRAYED : MF_ENABLED)); // 普通态 → 还原灰化
gray = (clsStyle & CS_NOCLOSE) != 0;
if (gray) IntEnableMenuItem(menu, SC_CLOSE, MF_GRAYED); // 保持禁用态
/* 默认项:最小化→还原;标题栏点击→最大化/还原;其余→关闭 */
if (style & WS_MINIMIZE) DefItem = SC_RESTORE;
else if (HitTest == HTCAPTION) DefItem = ((style & (WS_MAXIMIZE | WS_MINIMIZE)) ? SC_RESTORE : SC_MAXIMIZE);
else DefItem = SC_CLOSE;
UserSetMenuDefaultItem(menu, DefItem, MF_BYCOMMAND);
}
MENU_InitSysMenuPopup 在 MENU_InitPopup(L3404)与 co_UserTrackSystemMenu(L4620)路径上被调用------每次弹出系统菜单前刷新状态。这也是关闭按钮灰化的"数据源":NC_DoButton/UserDrawCaptionButton 通过 IntGetMenuState(SC_CLOSE) 读取的就是这里设置的 MF_GRAYED。
8.6 SC_MOUSEMENU → MENU_TrackMouseMenuBar → NC_GetSysPopupPos
SC_MOUSEMENU 分支把 wParam 低 4 位(HT 码)与 lParam 坐标交给 MENU_TrackMouseMenuBar(menu.c L4486-4509):
c
VOID MENU_TrackMouseMenuBar(PWND pWnd, ULONG ht, POINT pt)
{
PMENU pMenu = (ht == HTSYSMENU) ? IntGetSystemMenu(pWnd, FALSE) : IntGetMenu(UserHMGetHandle(pWnd));
UINT wFlags = TPM_BUTTONDOWN | TPM_LEFTALIGN | TPM_LEFTBUTTON | TPM_VERTICAL;
if (pWnd->ExStyle & WS_EX_LAYOUTRTL) wFlags |= TPM_LAYOUTRTL;
if (VerifyMenu(pMenu))
{
PWND Parent = UserGetAncestor(pWnd, GA_PARENT);
if (Parent != UserGetDesktopWindow()) IntScreenToClient(Parent, &pt); // 转父客户区坐标
MENU_InitTracking(pWnd, pMenu, FALSE, wFlags);
pMenu = (ht == HTSYSMENU) ? get_win_sys_menu(UserHMGetHandle(pWnd)) : IntGetMenu(UserHMGetHandle(pWnd));
MENU_TrackMenu(pMenu, wFlags, pt.x, pt.y, pWnd); // 模态菜单跟踪循环(menu.c 主体)
MENU_ExitTracking(pWnd, FALSE, wFlags);
}
}
MENU_InitTracking 内部(L3402-3413)在 IS_SYSTEM_MENU 且非 MNF_POPUP 时调用 MENU_InitSysMenuPopup 刷新项状态,并用 NC_GetSysPopupPos(nonclient.c L117-137)计算弹出位置:
c
void FASTCALL NC_GetSysPopupPos(PWND Wnd, RECT *Rect)
{
RECT WindowRect;
if ((Wnd->style & WS_MINIMIZE) != 0)
IntGetWindowRect(Wnd, Rect); // 最小化:以任务栏按钮矩形为基准
else
{
NC_GetInsideRect(Wnd, Rect); // 内边矩形(窗口坐标)
IntGetWindowRect(Wnd, &WindowRect);
RECTL_vOffsetRect(Rect, WindowRect.left, WindowRect.top); // 平移到屏幕坐标
if (Wnd->style & WS_CHILD)
IntClientToScreen(IntGetParent(Wnd), (POINT *)Rect); // 子窗口再映射到屏幕
Rect->right = Rect->left + SM_CYCAPTION - 1; // 宽 = 标题栏高(正方形,19px)
Rect->bottom = Rect->top + SM_CYCAPTION - 1;
}
}
系统菜单弹出的默认位置即"标题栏左端图标正下方"(宽高各 SM_CYCAPTION-1 的正方形区域)------与图标所在位置视觉对齐。NC_GetInsideRect(L71-111)本身是"窗口矩形扣除边框"的通用函数(WS_ICONIC 时直接返回整窗矩形,供最小化窗口使用),此处被复用于定位。
9. 调用链总览(mermaid)
9.1 标题栏绘制链(WM_NCPAINT)
#mermaid-svg-GB0nLfzohHwT86NF{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-GB0nLfzohHwT86NF .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-GB0nLfzohHwT86NF .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-GB0nLfzohHwT86NF .error-icon{fill:#552222;}#mermaid-svg-GB0nLfzohHwT86NF .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-GB0nLfzohHwT86NF .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-GB0nLfzohHwT86NF .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-GB0nLfzohHwT86NF .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-GB0nLfzohHwT86NF .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-GB0nLfzohHwT86NF .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-GB0nLfzohHwT86NF .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-GB0nLfzohHwT86NF .marker{fill:#333333;stroke:#333333;}#mermaid-svg-GB0nLfzohHwT86NF .marker.cross{stroke:#333333;}#mermaid-svg-GB0nLfzohHwT86NF svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-GB0nLfzohHwT86NF p{margin:0;}#mermaid-svg-GB0nLfzohHwT86NF .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-GB0nLfzohHwT86NF .cluster-label text{fill:#333;}#mermaid-svg-GB0nLfzohHwT86NF .cluster-label span{color:#333;}#mermaid-svg-GB0nLfzohHwT86NF .cluster-label span p{background-color:transparent;}#mermaid-svg-GB0nLfzohHwT86NF .label text,#mermaid-svg-GB0nLfzohHwT86NF span{fill:#333;color:#333;}#mermaid-svg-GB0nLfzohHwT86NF .node rect,#mermaid-svg-GB0nLfzohHwT86NF .node circle,#mermaid-svg-GB0nLfzohHwT86NF .node ellipse,#mermaid-svg-GB0nLfzohHwT86NF .node polygon,#mermaid-svg-GB0nLfzohHwT86NF .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-GB0nLfzohHwT86NF .rough-node .label text,#mermaid-svg-GB0nLfzohHwT86NF .node .label text,#mermaid-svg-GB0nLfzohHwT86NF .image-shape .label,#mermaid-svg-GB0nLfzohHwT86NF .icon-shape .label{text-anchor:middle;}#mermaid-svg-GB0nLfzohHwT86NF .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-GB0nLfzohHwT86NF .rough-node .label,#mermaid-svg-GB0nLfzohHwT86NF .node .label,#mermaid-svg-GB0nLfzohHwT86NF .image-shape .label,#mermaid-svg-GB0nLfzohHwT86NF .icon-shape .label{text-align:center;}#mermaid-svg-GB0nLfzohHwT86NF .node.clickable{cursor:pointer;}#mermaid-svg-GB0nLfzohHwT86NF .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-GB0nLfzohHwT86NF .arrowheadPath{fill:#333333;}#mermaid-svg-GB0nLfzohHwT86NF .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-GB0nLfzohHwT86NF .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-GB0nLfzohHwT86NF .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GB0nLfzohHwT86NF .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-GB0nLfzohHwT86NF .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GB0nLfzohHwT86NF .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-GB0nLfzohHwT86NF .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-GB0nLfzohHwT86NF .cluster text{fill:#333;}#mermaid-svg-GB0nLfzohHwT86NF .cluster span{color:#333;}#mermaid-svg-GB0nLfzohHwT86NF 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-GB0nLfzohHwT86NF .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-GB0nLfzohHwT86NF rect.text{fill:none;stroke-width:0;}#mermaid-svg-GB0nLfzohHwT86NF .icon-shape,#mermaid-svg-GB0nLfzohHwT86NF .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GB0nLfzohHwT86NF .icon-shape p,#mermaid-svg-GB0nLfzohHwT86NF .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-GB0nLfzohHwT86NF .icon-shape .label rect,#mermaid-svg-GB0nLfzohHwT86NF .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GB0nLfzohHwT86NF .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-GB0nLfzohHwT86NF .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-GB0nLfzohHwT86NF :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} WM_NCPAINT (defwnd.c L1302)
UserGetDCEx(DCX_WINDOW|DCX_INTERSECTRGN|DCX_KEEPCLIPRGN)
Wnd->state |= WNDS_FORCEMENUDRAW
NC_DoNCPaint(Wnd, hDC, -1) (nonclient.c L1097)
激活判定: gpqForeground == 窗口线程队列 / WM_MDIGETACTIVE
DC_ACTIVE 同步 WNDS_ACTIVEFRAME
DrawEdge(EDGE_RAISED) 外缘
NC_DrawFrame 粗边框+细边框 (L909)
UserDrawCaption 背景+图标+文字 (painting.c L2254)
GreGradientFill / PatBlt 背景
UserDrawIconEx 16×16 图标
UserDrawCaptionText 文字+截断 (painting.c L2148)
UserDrawCaptionButton DFCS_CAPTIONCLOSE (L808)
UserDrawCaptionButton DFCS_CAPTIONMIN
UserDrawCaptionButton DFCS_CAPTIONMAX
DrawFrameControl(DFC_CAPTION) (draw.c L1094)
UITOOLS95_DrawFrameCaption (draw.c L788)
GreCreateFontIndirectW(Marlett) + GreTextOutW 单字符
标题栏底线 1px (COLOR_3DFACE)
MENU_DrawMenuBar 菜单栏 (WNDS_HASMENU)
IntDrawScrollBar 滚动条+SizeGrip
9.2 点击按钮链(WM_NCLBUTTONDOWN → SC_*)
#mermaid-svg-HeTwxpBcyIhQXVhY{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-HeTwxpBcyIhQXVhY .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-HeTwxpBcyIhQXVhY .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-HeTwxpBcyIhQXVhY .error-icon{fill:#552222;}#mermaid-svg-HeTwxpBcyIhQXVhY .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-HeTwxpBcyIhQXVhY .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-HeTwxpBcyIhQXVhY .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-HeTwxpBcyIhQXVhY .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-HeTwxpBcyIhQXVhY .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-HeTwxpBcyIhQXVhY .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-HeTwxpBcyIhQXVhY .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-HeTwxpBcyIhQXVhY .marker{fill:#333333;stroke:#333333;}#mermaid-svg-HeTwxpBcyIhQXVhY .marker.cross{stroke:#333333;}#mermaid-svg-HeTwxpBcyIhQXVhY svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-HeTwxpBcyIhQXVhY p{margin:0;}#mermaid-svg-HeTwxpBcyIhQXVhY .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-HeTwxpBcyIhQXVhY .cluster-label text{fill:#333;}#mermaid-svg-HeTwxpBcyIhQXVhY .cluster-label span{color:#333;}#mermaid-svg-HeTwxpBcyIhQXVhY .cluster-label span p{background-color:transparent;}#mermaid-svg-HeTwxpBcyIhQXVhY .label text,#mermaid-svg-HeTwxpBcyIhQXVhY span{fill:#333;color:#333;}#mermaid-svg-HeTwxpBcyIhQXVhY .node rect,#mermaid-svg-HeTwxpBcyIhQXVhY .node circle,#mermaid-svg-HeTwxpBcyIhQXVhY .node ellipse,#mermaid-svg-HeTwxpBcyIhQXVhY .node polygon,#mermaid-svg-HeTwxpBcyIhQXVhY .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-HeTwxpBcyIhQXVhY .rough-node .label text,#mermaid-svg-HeTwxpBcyIhQXVhY .node .label text,#mermaid-svg-HeTwxpBcyIhQXVhY .image-shape .label,#mermaid-svg-HeTwxpBcyIhQXVhY .icon-shape .label{text-anchor:middle;}#mermaid-svg-HeTwxpBcyIhQXVhY .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-HeTwxpBcyIhQXVhY .rough-node .label,#mermaid-svg-HeTwxpBcyIhQXVhY .node .label,#mermaid-svg-HeTwxpBcyIhQXVhY .image-shape .label,#mermaid-svg-HeTwxpBcyIhQXVhY .icon-shape .label{text-align:center;}#mermaid-svg-HeTwxpBcyIhQXVhY .node.clickable{cursor:pointer;}#mermaid-svg-HeTwxpBcyIhQXVhY .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-HeTwxpBcyIhQXVhY .arrowheadPath{fill:#333333;}#mermaid-svg-HeTwxpBcyIhQXVhY .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-HeTwxpBcyIhQXVhY .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-HeTwxpBcyIhQXVhY .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HeTwxpBcyIhQXVhY .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-HeTwxpBcyIhQXVhY .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HeTwxpBcyIhQXVhY .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-HeTwxpBcyIhQXVhY .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-HeTwxpBcyIhQXVhY .cluster text{fill:#333;}#mermaid-svg-HeTwxpBcyIhQXVhY .cluster span{color:#333;}#mermaid-svg-HeTwxpBcyIhQXVhY 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-HeTwxpBcyIhQXVhY .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-HeTwxpBcyIhQXVhY rect.text{fill:none;stroke-width:0;}#mermaid-svg-HeTwxpBcyIhQXVhY .icon-shape,#mermaid-svg-HeTwxpBcyIhQXVhY .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HeTwxpBcyIhQXVhY .icon-shape p,#mermaid-svg-HeTwxpBcyIhQXVhY .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-HeTwxpBcyIhQXVhY .icon-shape .label rect,#mermaid-svg-HeTwxpBcyIhQXVhY .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HeTwxpBcyIhQXVhY .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-HeTwxpBcyIhQXVhY .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-HeTwxpBcyIhQXVhY :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} SC_MINIMIZE
SC_MAXIMIZE
SC_RESTORE
SC_CLOSE
WM_NCLBUTTONDOWN(HTMINBUTTON/HTMAXBUTTON/HTCLOSE)
IntDefWindowProc (defwnd.c L807)
NC_HandleNCLButtonDown (nonclient.c L1582)
NC_DoButton (L1502)
UserDrawCaptionButtonWnd(TRUE) 压入外观
co_UserSetCapture 捕获鼠标
鼠标消息循环
WM_MOUSEMOVE → GetNCHitEx 重判
WM_LBUTTONUP → 退出
UserDrawCaptionButtonWnd(FALSE) 弹起外观
co_IntSendMessage(WM_SYSCOMMAND, SC_MINIMIZE/MAXIMIZE/RESTORE/CLOSE)
DefWndHandleSysCommand (defwnd.c L123)
switch(wParam & 0xfff0)
co_WinPosShowWindow(SW_MINIMIZE)
co_WinPosShowWindow(SW_MAXIMIZE)
co_WinPosShowWindow(SW_RESTORE)
WM_CLOSE → co_UserDestroyWindow (defwnd.c L1078)
9.3 双击链(标题栏双击 / 系统菜单图标双击)
#mermaid-svg-n7XrczWF4gh6TM68{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-n7XrczWF4gh6TM68 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-n7XrczWF4gh6TM68 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-n7XrczWF4gh6TM68 .error-icon{fill:#552222;}#mermaid-svg-n7XrczWF4gh6TM68 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-n7XrczWF4gh6TM68 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-n7XrczWF4gh6TM68 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-n7XrczWF4gh6TM68 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-n7XrczWF4gh6TM68 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-n7XrczWF4gh6TM68 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-n7XrczWF4gh6TM68 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-n7XrczWF4gh6TM68 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-n7XrczWF4gh6TM68 .marker.cross{stroke:#333333;}#mermaid-svg-n7XrczWF4gh6TM68 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-n7XrczWF4gh6TM68 p{margin:0;}#mermaid-svg-n7XrczWF4gh6TM68 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-n7XrczWF4gh6TM68 .cluster-label text{fill:#333;}#mermaid-svg-n7XrczWF4gh6TM68 .cluster-label span{color:#333;}#mermaid-svg-n7XrczWF4gh6TM68 .cluster-label span p{background-color:transparent;}#mermaid-svg-n7XrczWF4gh6TM68 .label text,#mermaid-svg-n7XrczWF4gh6TM68 span{fill:#333;color:#333;}#mermaid-svg-n7XrczWF4gh6TM68 .node rect,#mermaid-svg-n7XrczWF4gh6TM68 .node circle,#mermaid-svg-n7XrczWF4gh6TM68 .node ellipse,#mermaid-svg-n7XrczWF4gh6TM68 .node polygon,#mermaid-svg-n7XrczWF4gh6TM68 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-n7XrczWF4gh6TM68 .rough-node .label text,#mermaid-svg-n7XrczWF4gh6TM68 .node .label text,#mermaid-svg-n7XrczWF4gh6TM68 .image-shape .label,#mermaid-svg-n7XrczWF4gh6TM68 .icon-shape .label{text-anchor:middle;}#mermaid-svg-n7XrczWF4gh6TM68 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-n7XrczWF4gh6TM68 .rough-node .label,#mermaid-svg-n7XrczWF4gh6TM68 .node .label,#mermaid-svg-n7XrczWF4gh6TM68 .image-shape .label,#mermaid-svg-n7XrczWF4gh6TM68 .icon-shape .label{text-align:center;}#mermaid-svg-n7XrczWF4gh6TM68 .node.clickable{cursor:pointer;}#mermaid-svg-n7XrczWF4gh6TM68 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-n7XrczWF4gh6TM68 .arrowheadPath{fill:#333333;}#mermaid-svg-n7XrczWF4gh6TM68 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-n7XrczWF4gh6TM68 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-n7XrczWF4gh6TM68 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-n7XrczWF4gh6TM68 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-n7XrczWF4gh6TM68 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-n7XrczWF4gh6TM68 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-n7XrczWF4gh6TM68 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-n7XrczWF4gh6TM68 .cluster text{fill:#333;}#mermaid-svg-n7XrczWF4gh6TM68 .cluster span{color:#333;}#mermaid-svg-n7XrczWF4gh6TM68 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-n7XrczWF4gh6TM68 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-n7XrczWF4gh6TM68 rect.text{fill:none;stroke-width:0;}#mermaid-svg-n7XrczWF4gh6TM68 .icon-shape,#mermaid-svg-n7XrczWF4gh6TM68 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-n7XrczWF4gh6TM68 .icon-shape p,#mermaid-svg-n7XrczWF4gh6TM68 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-n7XrczWF4gh6TM68 .icon-shape .label rect,#mermaid-svg-n7XrczWF4gh6TM68 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-n7XrczWF4gh6TM68 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-n7XrczWF4gh6TM68 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-n7XrczWF4gh6TM68 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} HTCAPTION 且 WS_MAXIMIZEBOX
HTSYSMENU 且 SC_CLOSE 可用
HTTOP/HTBOTTOM
其他
WM_NCLBUTTONDBLCLK (defwnd.c L816)
NC_HandleNCLButtonDblClk (nonclient.c L1674)
命中码
WM_SYSCOMMAND: 最大/最小 → SC_RESTORE,否则 SC_MAXIMIZE
WM_SYSCOMMAND: SC_CLOSE
co_WinPosSetWindowPos 纵向扩展
退化为 NC_HandleNCLButtonDown(单击语义)
DefWndHandleSysCommand → co_WinPosShowWindow
DefWndHandleSysCommand → WM_CLOSE → DestroyWindow
9.4 激活变化链(WM_NCACTIVATE)
#mermaid-svg-HCeQfmxHEt6oBvIE{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-HCeQfmxHEt6oBvIE .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-HCeQfmxHEt6oBvIE .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-HCeQfmxHEt6oBvIE .error-icon{fill:#552222;}#mermaid-svg-HCeQfmxHEt6oBvIE .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-HCeQfmxHEt6oBvIE .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-HCeQfmxHEt6oBvIE .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-HCeQfmxHEt6oBvIE .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-HCeQfmxHEt6oBvIE .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-HCeQfmxHEt6oBvIE .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-HCeQfmxHEt6oBvIE .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-HCeQfmxHEt6oBvIE .marker{fill:#333333;stroke:#333333;}#mermaid-svg-HCeQfmxHEt6oBvIE .marker.cross{stroke:#333333;}#mermaid-svg-HCeQfmxHEt6oBvIE svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-HCeQfmxHEt6oBvIE p{margin:0;}#mermaid-svg-HCeQfmxHEt6oBvIE .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-HCeQfmxHEt6oBvIE .cluster-label text{fill:#333;}#mermaid-svg-HCeQfmxHEt6oBvIE .cluster-label span{color:#333;}#mermaid-svg-HCeQfmxHEt6oBvIE .cluster-label span p{background-color:transparent;}#mermaid-svg-HCeQfmxHEt6oBvIE .label text,#mermaid-svg-HCeQfmxHEt6oBvIE span{fill:#333;color:#333;}#mermaid-svg-HCeQfmxHEt6oBvIE .node rect,#mermaid-svg-HCeQfmxHEt6oBvIE .node circle,#mermaid-svg-HCeQfmxHEt6oBvIE .node ellipse,#mermaid-svg-HCeQfmxHEt6oBvIE .node polygon,#mermaid-svg-HCeQfmxHEt6oBvIE .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-HCeQfmxHEt6oBvIE .rough-node .label text,#mermaid-svg-HCeQfmxHEt6oBvIE .node .label text,#mermaid-svg-HCeQfmxHEt6oBvIE .image-shape .label,#mermaid-svg-HCeQfmxHEt6oBvIE .icon-shape .label{text-anchor:middle;}#mermaid-svg-HCeQfmxHEt6oBvIE .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-HCeQfmxHEt6oBvIE .rough-node .label,#mermaid-svg-HCeQfmxHEt6oBvIE .node .label,#mermaid-svg-HCeQfmxHEt6oBvIE .image-shape .label,#mermaid-svg-HCeQfmxHEt6oBvIE .icon-shape .label{text-align:center;}#mermaid-svg-HCeQfmxHEt6oBvIE .node.clickable{cursor:pointer;}#mermaid-svg-HCeQfmxHEt6oBvIE .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-HCeQfmxHEt6oBvIE .arrowheadPath{fill:#333333;}#mermaid-svg-HCeQfmxHEt6oBvIE .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-HCeQfmxHEt6oBvIE .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-HCeQfmxHEt6oBvIE .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HCeQfmxHEt6oBvIE .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-HCeQfmxHEt6oBvIE .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HCeQfmxHEt6oBvIE .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-HCeQfmxHEt6oBvIE .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-HCeQfmxHEt6oBvIE .cluster text{fill:#333;}#mermaid-svg-HCeQfmxHEt6oBvIE .cluster span{color:#333;}#mermaid-svg-HCeQfmxHEt6oBvIE 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-HCeQfmxHEt6oBvIE .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-HCeQfmxHEt6oBvIE rect.text{fill:none;stroke-width:0;}#mermaid-svg-HCeQfmxHEt6oBvIE .icon-shape,#mermaid-svg-HCeQfmxHEt6oBvIE .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HCeQfmxHEt6oBvIE .icon-shape p,#mermaid-svg-HCeQfmxHEt6oBvIE .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-HCeQfmxHEt6oBvIE .icon-shape .label rect,#mermaid-svg-HCeQfmxHEt6oBvIE .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HCeQfmxHEt6oBvIE .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-HCeQfmxHEt6oBvIE .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-HCeQfmxHEt6oBvIE :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} focus.c 激活窗口切换
WM_NCACTIVATE(TRUE/FALSE)
NC_HandleNCActivate (nonclient.c L1445)
WNDS_ACTIVEFRAME 置位/清除
NC_DoNCActive (L1427) 决定重绘范围
DC_CAPTION (±DC_FRAME)
NC_DoNCPaint(Wnd, hDC, wParam|Flags)
UserDrawCaption 用 DC_ACTIVE 选激活/非激活色
UserDrawCaptionText 用 DC_ACTIVE 选文字色
9.5 系统菜单链(图标点击 / ALT+Space)
#mermaid-svg-O2FqLDR7AD5C2BLK{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-O2FqLDR7AD5C2BLK .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-O2FqLDR7AD5C2BLK .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-O2FqLDR7AD5C2BLK .error-icon{fill:#552222;}#mermaid-svg-O2FqLDR7AD5C2BLK .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-O2FqLDR7AD5C2BLK .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-O2FqLDR7AD5C2BLK .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-O2FqLDR7AD5C2BLK .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-O2FqLDR7AD5C2BLK .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-O2FqLDR7AD5C2BLK .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-O2FqLDR7AD5C2BLK .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-O2FqLDR7AD5C2BLK .marker{fill:#333333;stroke:#333333;}#mermaid-svg-O2FqLDR7AD5C2BLK .marker.cross{stroke:#333333;}#mermaid-svg-O2FqLDR7AD5C2BLK svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-O2FqLDR7AD5C2BLK p{margin:0;}#mermaid-svg-O2FqLDR7AD5C2BLK .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-O2FqLDR7AD5C2BLK .cluster-label text{fill:#333;}#mermaid-svg-O2FqLDR7AD5C2BLK .cluster-label span{color:#333;}#mermaid-svg-O2FqLDR7AD5C2BLK .cluster-label span p{background-color:transparent;}#mermaid-svg-O2FqLDR7AD5C2BLK .label text,#mermaid-svg-O2FqLDR7AD5C2BLK span{fill:#333;color:#333;}#mermaid-svg-O2FqLDR7AD5C2BLK .node rect,#mermaid-svg-O2FqLDR7AD5C2BLK .node circle,#mermaid-svg-O2FqLDR7AD5C2BLK .node ellipse,#mermaid-svg-O2FqLDR7AD5C2BLK .node polygon,#mermaid-svg-O2FqLDR7AD5C2BLK .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-O2FqLDR7AD5C2BLK .rough-node .label text,#mermaid-svg-O2FqLDR7AD5C2BLK .node .label text,#mermaid-svg-O2FqLDR7AD5C2BLK .image-shape .label,#mermaid-svg-O2FqLDR7AD5C2BLK .icon-shape .label{text-anchor:middle;}#mermaid-svg-O2FqLDR7AD5C2BLK .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-O2FqLDR7AD5C2BLK .rough-node .label,#mermaid-svg-O2FqLDR7AD5C2BLK .node .label,#mermaid-svg-O2FqLDR7AD5C2BLK .image-shape .label,#mermaid-svg-O2FqLDR7AD5C2BLK .icon-shape .label{text-align:center;}#mermaid-svg-O2FqLDR7AD5C2BLK .node.clickable{cursor:pointer;}#mermaid-svg-O2FqLDR7AD5C2BLK .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-O2FqLDR7AD5C2BLK .arrowheadPath{fill:#333333;}#mermaid-svg-O2FqLDR7AD5C2BLK .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-O2FqLDR7AD5C2BLK .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-O2FqLDR7AD5C2BLK .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-O2FqLDR7AD5C2BLK .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-O2FqLDR7AD5C2BLK .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-O2FqLDR7AD5C2BLK .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-O2FqLDR7AD5C2BLK .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-O2FqLDR7AD5C2BLK .cluster text{fill:#333;}#mermaid-svg-O2FqLDR7AD5C2BLK .cluster span{color:#333;}#mermaid-svg-O2FqLDR7AD5C2BLK 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-O2FqLDR7AD5C2BLK .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-O2FqLDR7AD5C2BLK rect.text{fill:none;stroke-width:0;}#mermaid-svg-O2FqLDR7AD5C2BLK .icon-shape,#mermaid-svg-O2FqLDR7AD5C2BLK .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-O2FqLDR7AD5C2BLK .icon-shape p,#mermaid-svg-O2FqLDR7AD5C2BLK .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-O2FqLDR7AD5C2BLK .icon-shape .label rect,#mermaid-svg-O2FqLDR7AD5C2BLK .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-O2FqLDR7AD5C2BLK .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-O2FqLDR7AD5C2BLK .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-O2FqLDR7AD5C2BLK :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} HTSYSMENU 按下 / ALT+Space
WM_SYSCOMMAND: SC_MOUSEMENU+HTSYSMENU / SC_KEYMENU
DefWndHandleSysCommand (defwnd.c L192-203)
MENU_TrackMouseMenuBar / MENU_TrackKbdMenuBar (menu.c L4486/L4516)
IntGetSystemMenu → MENU_GetSystemMenu 构建(SYSMENU 模板+Marlett 图标)
MENU_InitTracking → MENU_InitSysMenuPopup 刷新项使能 (menu.c L1362)
NC_GetSysPopupPos 计算弹出位置 (nonclient.c L117)
MENU_TrackMenu 模态跟踪循环
用户选 SC_* → 同 9.2 落地链
10. 与相关模块的关系
10.1 与 winpos.c(尺寸计算)
UserGetWindowBorders(winpos.c L894):把样式组合折算成像素边框(基础边框数 ×SM_CXBORDER+ 粗边框追加(SM_CXFRAME - SM_CXDLGFRAME))。UserDrawCaptionButtonWnd(nonclient.c L901)用它把窗口矩形收缩成"内边矩形",进而定位标题栏按钮------绘制侧与计算侧共享同一函数,保证按钮不会画到边框上;co_WinPosGetNonClientSize(winpos.c L2396)→WM_NCCALCSIZE(FALSE)→NC_HandleNCCalcSize:标题栏高度(SM_CYCAPTION/SM_CYSMCAPTION)在此被"吃掉"(Rect->top += ...,L1339-1342),同时置WNDS_HASCAPTION。UserDrawCaptionBar绘制标题栏时再次把标题栏高度从顶部切出------计算与绘制对标题栏高度的理解完全一致;co_WinPosShowWindow(L2630):SW_MINIMIZE/MAXIMIZE/RESTORE的落地者,三按钮行为的最终执行者(8.2/8.3 节)。
10.2 与 menu.c(系统菜单)
标题栏按钮与系统菜单是"一套状态、两个入口":
| 数据 | 系统菜单(menu.c) | 标题栏按钮(nonclient.c) |
|---|---|---|
| 命令执行 | MENU_TrackMenu 选中的 SC_* → WM_SYSCOMMAND |
NC_DoButton 抬起 → WM_SYSCOMMAND |
| 关闭可用性 | MENU_InitSysMenuPopup 置 MF_GRAYED |
IntGetMenuState(SC_CLOSE) 灰化按钮 |
| 图标符号 | MENU_DrawPopupGlyph(Marlett '2'/'0'/'1'/'r') |
UITOOLS95_DrawFrameCaption(Marlett 同字符) |
| 状态刷新 | UserPaintCaption(spwndNotify, DC_BUTTONS)(menu.c L1249) |
重绘按钮 |
IntGetSystemMenu 返回的菜单把 spwndNotify 指向窗口,菜单项状态变化时通过 UserPaintCaption 通知标题栏重绘------这就是 EnableMenuItem(SC_CLOSE, MF_GRAYED) 能实时灰化关闭按钮的机制。
10.3 与 focus.c / msgqueue.c(激活与 NC 鼠标)
- focus.c :激活切换发送
WM_NCACTIVATE(6.4 节),gpqForeground->spwndActive是NC_DoNCPaint激活判定的依据之一; - msgqueue.c :鼠标在 NC 区域时把
WM_MOUSEMOVE/LBUTTONDOWN/...转换为WM_NCMOUSEMOVE/WM_NCLBUTTONDOWN/...并携带命中码;NC_DoButton的跟踪循环用co_IntGetPeekMessage(..., WM_MOUSEFIRST, WM_MOUSELAST, PM_REMOVE, ...)从队列直接取消息------这正是窗口处于"按钮跟踪"模态时的消息消费方式。
10.4 与 defwnd.c(消息分发)
IntDefWindowProc 是标题栏全部行为的入口总线:WM_NCPAINT(L1302)、WM_NCACTIVATE(L1294)、WM_NCCALCSIZE(L1289)、WM_NCLBUTTONDOWN(L807)、WM_NCLBUTTONDBLCLK(L816)、WM_SYSCOMMAND(L723)、WM_SETTEXT(L712,UserPaintCaption(DC_TEXT))、WM_SETICON(L786→DefWndSetIcon,UserPaintCaption(DC_ICON))、WM_SYSCOLORCHANGE(L1195,UserPaintCaption(DC_NC))、WM_CLOSE(L1078)。应用未处理的消息全部汇聚于此,因此标题栏"系统绘制"与"应用接管"(WNDS_NONCPAINT)的分界就在这些 case 的入口判定。
10.5 与 painting.c / draw.c / scrollbar.c
- painting.c :
UserDrawCaption/UserDrawCaptionText是标题栏内容绘制(本册 5.2); - draw.c :
DrawFrameControl/UITOOLS95_DrawFrameCaption是按钮符号绘制(本册 5.5/5.8),DrawEdge供外缘/客户边缘使用; - scrollbar.c :滚动条与尺寸调整框(
DFCS_SCROLLSIZEGRIP)在NC_DoNCPaint尾部绘制(L1262-1303),与标题栏同属一次 NC 重绘,但本册不展开。
11. 源码索引
11.1 本册核心文件
| 文件 | 角色 |
|---|---|
win32ss/user/ntuser/nonclient.c |
标题栏绘制与交互主战场(约 2185 行) |
win32ss/user/ntuser/draw.c |
Marlett 符号绘制 / DrawFrameControl 内核实现 |
win32ss/user/ntuser/painting.c |
标题背景/图标/文字绘制 |
win32ss/user/ntuser/defwnd.c |
NC 消息分发 / UserPaintCaption / SC_* 落地 |
win32ss/user/ntuser/winpos.c |
UserGetWindowBorders / co_WinPosShowWindow / NCCALCSIZE |
win32ss/user/ntuser/menu.c |
系统菜单构建、跟踪、项状态刷新 |
win32ss/user/ntuser/metric.c |
系统度量(SM_*)初始化 |
11.2 本册覆盖的函数清单
| 函数 | 文件:行号 | 本册章节 |
|---|---|---|
NC_GetInsideRect |
nonclient.c:71 | 8.6 |
NC_GetSysPopupPos |
nonclient.c:117 | 8.6 |
NC_IconForWindow |
nonclient.c:736 | 5.3 |
UserDrawSysMenuButton |
nonclient.c:767 | 5.3 / 7.1 |
UserDrawCaptionButton |
nonclient.c:808 | 4.2 / 5.4 |
UserDrawCaptionButtonWnd |
nonclient.c:890 | 4.2 / 7.2 |
NC_DrawFrame |
nonclient.c:909 | 5.6 |
UserDrawCaptionBar |
nonclient.c:955 | 5.1 |
NC_DoNCPaint |
nonclient.c:1097 | 5.7 / 6.1 |
NC_HandleNCCalcSize |
nonclient.c:1307 | 10.1(第 6 册 6.3 已全展开) |
NC_DoNCActive |
nonclient.c:1427 | 6.1 |
NC_HandleNCActivate |
nonclient.c:1445 | 6.1 |
NC_DoButton |
nonclient.c:1502 | 7.2 |
NC_HandleNCLButtonDown |
nonclient.c:1582 | 7.1 |
NC_HandleNCLButtonDblClk |
nonclient.c:1674 | 7.4 |
NC_HandleNCRButtonDown |
nonclient.c:1732 | 第 6 册 11.5 |
UserDrawCaption |
painting.c:2254 | 4.3 / 5.2 |
UserDrawCaptionText |
painting.c:2148 | 4.4 / 5.2 |
NtUserDrawCaptionTemp |
painting.c:2436 | 5.2(用户态 DrawCaption 入口) |
UITOOLS95_DFC_ButtonPush |
draw.c:620 | 5.8 |
UITOOLS95_DFC_ButtonCheckRadio |
draw.c:664 | 5.8 |
UITOOLS_MakeSquareRect |
draw.c:566 | 5.5 |
UITOOLS95_DrawFrameCaption |
draw.c:788 | 5.5 |
UITOOLS95_DrawFrameButton |
draw.c:759 | 5.8 |
DrawFrameControl |
draw.c:1094 | 5.8 |
DrawEdge |
draw.c:1085 | 5.1 / 5.7 |
UserPaintCaption |
defwnd.c:394 | 6.4 / 10.4 |
DefWndSetIcon |
defwnd.c:432 | 5.3 |
DefWndHandleSysCommand |
defwnd.c:123 | 8.1 |
UserGetWindowBorders |
winpos.c:894 | 4.2 / 10.1 |
co_WinPosGetNonClientSize |
winpos.c:2396 | 10.1 |
co_WinPosShowWindow |
winpos.c:2630 | 8.2 / 8.3 |
MENU_InitSysMenuPopup |
menu.c:1362 | 8.5 |
MENU_GetSystemMenu |
menu.c:5304 | 8.5 |
IntGetSystemMenu |
menu.c:5409 | 8.5 |
MENU_TrackMouseMenuBar |
menu.c:4486 | 8.6 |
MENU_TrackKbdMenuBar |
menu.c:4516 | 7.5 |
MENU_DrawPopupGlyph |
menu.c:1398 | 3.6 / 8.5 |
11.3 常量定义位置
| 常量 | 位置 |
|---|---|
SM_CYCAPTION(4)、SM_CXSIZE(30)、SM_CXSMICON(49)、SM_CXFRAME(32) 等 |
sdk/include/psdk/winuser.h L972-1032 |
DFC_CAPTION(1)、DFCS_CAPTIONCLOSE(0)...DFCS_MONO(0x8000) |
sdk/include/psdk/winuser.h L473-511 |
DC_ACTIVE(1)...DC_BUTTONS(0x1000) |
sdk/include/psdk/winuser.h L427-436 |
DC_NOVISIBLE(0x800)、DC_FRAME(0x8000)、DC_DRAWCAPTIONMD(0x10000000) 等 |
sdk/include/reactos/undocuser.h L148-154 |
SC_SIZE(0xF000)...SC_MONITORPOWER(0xF170) |
sdk/include/psdk/winuser.h L2620-2639 |
HTCAPTION(2)、HTMINBUTTON(8)、HTMAXBUTTON(9)、HTCLOSE(20) |
sdk/include/psdk/winuser.h L2512-2536 |
WNDS_HASCAPTION、WNDS_ACTIVEFRAME、WNDS_FORCEMENUDRAW、WNDS_NONCPAINT |
win32ss/include/ntuser.h L605-636 |
WNDS2_CAPTIONTEXTTRUNCATED(0x08000000) |
win32ss/include/ntuser.h L666 |
WS_CAPTION(0xc00000)、WS_SYSMENU(0x80000)、WS_MINIMIZEBOX(0x20000)、WS_MAXIMIZEBOX(0x10000)、CS_NOCLOSE(512) |
sdk/include/psdk/winuser.h L226-662 |
12. 附:关键结论
- 标题栏的"三要素"由三个函数分别负责 :
UserDrawCaptionBar(总装)、UserDrawCaption(背景/图标/文字)、UserDrawCaptionButton×3(按钮)。三者共享同一套系统度量推导,任何一边改动都必须同步,否则绘制与命中(GetNCHitEx)错位。 - 按钮符号的本质是 Marlett 字体的单个字符 :
'0'最小化、'1'最大化、'2'还原、'r'关闭、's'帮助。UITOOLS95_DrawFrameCaption用GreCreateFontIndirectW按按钮尺寸创建 Marlett 字体,GreTextOutW输出单字符;按下 = 3D 边凹陷 + 符号右下移 1px;灰化 =BTNSHADOW符号 +BTNHIGHLIGHT阴影。 - 三按钮的交互状态机(NC_DoButton) :按下画压入外观 → 捕获鼠标 → 移动中
GetNCHitEx实时判定"是否还在按钮内"(移出弹起、移回再压入)→ 在按钮内抬起才发送WM_SYSCOMMAND(SC_*)。这是"按住反悔可取消"的标准按钮语义。 - 按钮灰化是"系统菜单状态"的投影 :关闭按钮的可用性 =
IntGetMenuState(SC_CLOSE)非MF_GRAYED|MF_DISABLED且非CS_NOCLOSE;最小化/最大化按钮的可用性 =WS_MINIMIZEBOX/WS_MAXIMIZEBOX样式。系统菜单项状态变化经spwndNotify → UserPaintCaption(DC_BUTTONS)实时刷新按钮。 - SC_ 命令的落地分两类 *:
SC_MINIMIZE/MAXIMIZE/RESTORE→co_WinPosShowWindow(SW_*)(winpos.c 状态机);SC_CLOSE→WM_CLOSE消息(应用可拦截)→ 未处理时co_UserDestroyWindow。最小化按钮在已最小化时发SC_RESTORE,最大化按钮在已最大化时同样发SC_RESTORE------这就是按钮符号在'0'/'1'与'2'之间切换的行为根源。 - 激活双外观的载体是
DC_ACTIVE标志 :NC_HandleNCActivate维护WNDS_ACTIVEFRAME,NC_DoNCPaint/UserPaintCaption依据gpqForeground同步该位并置DC_ACTIVE,UserDrawCaption/UserDrawCaptionText据此选择激活/非激活系统色。NC_DoNCActive在颜色无差异时跳过重绘(最小重绘优化)。 - 标题栏是"窗口显示"主线的收口点:绘制(WM_NCPAINT 链)、交互(WM_NCLBUTTONDOWN 链)、激活(WM_NCACTIVATE 链)、命令(WM_SYSCOMMAND 链)四条链路在此汇聚,与 winpos(尺寸/显示状态)、menu(系统菜单)、focus(激活)、msgqueue(NC 鼠标消息)四个模块耦合------它是理解"窗口显示"全貌的最佳切入点。
本册完。下一篇可继续沿"窗口显示"主线展开:菜单栏绘制(MENU_DrawMenuBar)或滚动条交互(scrollbar.c),或转入"窗口管理"主线的消息队列(msgqueue.c)专题。