ReactOS 窗口系统分析(25):标题栏显示与系统按钮 — nonclient.c 标题栏专题

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 行)。


目录

  1. 概述
  2. 设计动机
  3. 核心数据结构
  4. 标题栏几何布局算法
  5. 绘制侧核心函数展开
  6. 激活/非激活双外观
  7. 三按钮交互状态机
  8. 最小化/最大化/关闭行为落地
  9. 调用链总览(mermaid)
  10. 与相关模块的关系
  11. 源码索引
  12. 附:关键结论

1. 概述

1.1 标题栏是什么

标题栏是带 WS_CAPTIONWS_BORDER | WS_DLGFRAME,值 0xc00000)样式的窗口顶部一条横向带状区域,高度由系统度量 SM_CYCAPTION 决定(经典主题默认 19 像素)。它同时承担四种职责:

  1. 标识:显示窗口标题文字与窗口图标,用户据此识别窗口;

  2. 操作入口:承载系统菜单按钮(图标)、最小化/最大化/关闭三个系统按钮;

  3. 移动载体 :按住标题栏空白区拖动可移动窗口(HTCAPTIONSC_MOVE);

  4. 快捷操作 :双击标题栏空白区切换最大化/还原(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 把这些装饰与交互统一收归系统(DefWindowProcNC_DoNCPaint/NC_HandleNCLButtonDown),应用只要声明 WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX 样式组合,系统就自动完成绘制与交互。

这种设计带来三个直接后果:

  1. 外观一致:所有标准窗口的标题栏长得一样,用户不必在每个应用里重新学习"哪个是关闭按钮";
  2. 行为一致GetNCHitEx 输出的 HT 码与 NC_DoButton 执行的 SC_* 命令对所有窗口统一生效,应用无需感知;
  3. 可定制入口仍在 :应用可以通过处理 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_HandleNCLButtonDownHTCAPTION 分支把点击转换成 WM_SYSCOMMAND(SC_MOVE + HTCAPTION),进入 DefWndDoSizeMove 的模态移动循环;双击标题栏(NC_HandleNCLButtonDblClk)则在"最大化↔还原"之间切换。这些行为全部以"标题栏存在"(WNDS_HASCAPTION)为前提。


3. 核心数据结构

3.1 系统度量(SM_*)------标题栏的"计量单位"

标题栏的一切尺寸都来自 UserGetSystemMetrics(内核版 GetSystemMetricsmetric.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_DrawFrameNC_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)

DrawFrameControluType(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/CLOSEDFCS_CAPTIONRESTORE/HELP 同属一个低 4 位字段,因此 UITOOLS95_DrawFrameCaptionswitch(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
HTMINBUTTONHTREDUCE 8 最小化按钮 NC_DoButton→SC_MINIMIZE/SC_RESTORE
HTMAXBUTTONHTZOOM 9 最大化按钮 NC_DoButton→SC_MAXIMIZE/SC_RESTORE
HTCLOSE 20 关闭按钮 NC_DoButton→SC_CLOSE
HTHELP 21 帮助按钮 当前 GetNCHitEx 不产生(无 DFCS_CAPTIONHELP 绘制路径)

HTMINBUTTON/HTMAXBUTTONNC_DoButtonswitch(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_HASCAPTIONNC_HandleNCCalcSize 检测到 WS_CAPTION 时置位,是"有没有标题栏"的权威判据;WNDS_ACTIVEFRAMENC_HandleNCActivate 维护,绘制时经 DC_ACTIVE 传导到颜色选择;WNDS2_CAPTIONTEXTTRUNCATEDUserDrawCaption 调用 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.cMENU_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=0right=窗口宽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 扣除边框 → 把结果交给 UserDrawCaptionButtonNC_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 引擎做尾部省略号。

返回值 RetUserDrawCaption 用来维护 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;                                                // 其余都是可拖动区
    }
}

三个细节值得注意:

  1. 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)与之完全对应;
  2. HTSYSMENU 需要图标NC_IconForWindow(pWnd) 返回非空才划分系统菜单区------没有图标的窗口(如 WS_EX_DLGMODALFRAME)标题栏左端不响应系统菜单点击;
  3. 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_HASMENUNC_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 返回的 NONCLIENTMETRICSWlfCaptionFont/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/SysICntuser.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 节完整展开。补充两点:

  1. UserDrawCaptionButton 入口即检查 if (!(Style & WS_SYSMENU)) return;------没有 WS_SYSMENU 就没有任何系统按钮 (三按钮与系统菜单按钮同源,由 WS_SYSMENU 门控);
  2. 最小化/最大化分支对工具窗口直接 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 边从凸起变凹陷 + 符号向右下移 1pxDFCS_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_INACTIVEBORDERNC_DoNCPaintUserDrawCaptionBar(带 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 决定(UserPaintCaptionWnd->head.pti->MessageQueue == gpqForeground 判定,见 6.4);
  • NC_DoNCPaintFlags == -1WM_NCPAINT 模式的哨兵值(defwnd.c L1306 传入 -1),表示"重绘一切";
  • DC_REDRAWHUNGWNDDC_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_ACTIVE0x1),二者等价(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_CAPTIONDC_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 置位 → UserDrawCaptionuFlags & DC_ACTIVE 选色 → UserDrawCaptionTextuFlags & DC_ACTIVE 选文字色。UserDrawCaptionButton 本身不直接读 DC_ACTIVE------按钮的"激活"体现在符号颜色上(DFCS_INACTIVE 未置位时用 COLOR_BTNTEXT 黑色),与标题栏背景色无关,因此三按钮在激活/非激活窗口上符号颜色相同,仅背景(标题栏底色)不同。

6.3 渐变标题(SPI_GETGRADIENTCAPTIONS)

显示属性"渐变标题栏"开关经 UserSystemParametersInfo(SPI_GETGRADIENTCAPTIONS, ...) 读取(UserDrawCaptionBar L1010 与 NC_DoNCPaint L1177 各读一次)。开启后 Flags |= DC_GRADIENTUserDrawCaptionGreGradientFill(GRADIENT_FILL_RECT_H)COLOR_ACTIVECAPTION 渐变到 COLOR_GRADIENTACTIVECAPTION(非激活同理)。这是"标题栏背景"唯一的第二形态;渐变只作用于背景,图标/文字/按钮不受影响。

6.4 与 focus.c 的关系------WM_NCACTIVATE 从哪来

激活状态变化的源头是 focus.cco_IntSetForegroundWindow/IntSetActiveWindow 等)。激活切换时系统向新旧活动窗口发送 WM_NCACTIVATE

  • 激活窗口WM_NCACTIVATE(TRUE)NC_HandleNCActivateWNDS_ACTIVEFRAME,重绘为激活外观;
  • 失活窗口WM_NCACTIVATE(FALSE) → 清 WNDS_ACTIVEFRAME,重绘为非激活外观。

UserPaintCaption(defwnd.c L394-428)在非 WM_NCACTIVATE 路径(如 WM_SETTEXTWM_SETICONWM_SYSCOLORCHANGE)重绘标题栏时自行判定激活:

c 复制代码
if (pWnd->state & WNDS_HASCAPTION && pWnd->head.pti->MessageQueue == gpqForeground)
    Flags |= DC_ACTIVE;     // 窗口线程队列 == 前台队列 → 视为激活

NC_DoNCPaintWM_NCPAINT 路径用同一个判据(gpqForeground == pWnd->head.pti->MessageQueue),并处理"队列已切前台但 WNDS_ACTIVEFRAME 尚未同步"的竞态(L1184-1193:补齐状态位)。两条判定路径最终都汇入 DC_ACTIVE,保证外观与队列激活状态一致。


7. 三按钮交互状态机

7.1 NC_HandleNCLButtonDown(nonclient.c L1582-1670)------按钮命中分发

WM_NCLBUTTONDOWNmsgqueue.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 :命中码只是选择 ButtonTypeSCMsg 的钥匙;
  • 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)------只有"在按钮内完成按下-抬起"才触发

三个细节:

  1. SC_CLOSE 的 lParam 原样传递 ,其余命令传 MAKELONG(Msg.pt.x, Msg.pt.y)(抬起位置)------SC_CLOSE 的 lParam 是按下时的坐标(供 WM_CLOSE 使用),其余命令的坐标仅供系统菜单等场景参考;
  2. 移出移回Pressed 在移动中反复翻转,GetNCHitEx 每次都实时判定,实现"按住不放、移出取消、移回恢复"的完整语义;
  3. 没有悬停态(HOT) :经典主题没有 DFCS_HOT 悬停高亮,NC_DoButton 只维护 按下/弹起 两态;DFCS_HOT 供主题引擎使用(本路径不置位)。

7.3 HTSYSMENU 的按下与弹出

系统菜单按钮(图标区)不走 NC_DoButton------它直接进系统菜单跟踪(见 7.1 的 HTSYSMENU 分支):按下画图标偏移 → 发送 SC_MOUSEMENU + HTSYSMENUDefWndHandleSysCommandSC_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)

标题栏按钮不只有鼠标路径。DefWndHandleSysCommandSC_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 的落地分两步:

  1. 隐藏 owned popup :窗口是活动窗口时 IntShowOwnedPopups(pWnd, FALSE) 把它的属主弹窗一并隐藏------最小化一个窗口时其附属对话框不应残留在屏幕上;
  2. ShowWindow 路径co_WinPosShowWindow(pWnd, SW_MINIMIZE)(winpos.c L2630)进入 winpos.c 的窗口位置/尺寸状态机:窗口尺寸变为 SM_CXMINIMIZED × SM_CYMINIMIZED(任务栏按钮形态,NC_HandleNCCalcSize 最小化分支据此固定客户区),WS_MINIMIZEWS_ICONIC)样式被置位,随后产生 WM_SIZE(SIZE_MINIMIZED)WM_MOVE 等消息(defwnd.c DefWndHandleWindowPosChanged L100-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_DoButtonSCMsg 选择):最小化按钮在窗口已最小化 时发 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_InitSysMenuPopupMENU_InitPopup(L3404)与 co_UserTrackSystemMenu(L4620)路径上被调用------每次弹出系统菜单前刷新状态。这也是关闭按钮灰化的"数据源":NC_DoButton/UserDrawCaptionButton 通过 IntGetMenuState(SC_CLOSE) 读取的就是这里设置的 MF_GRAYED

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_HASCAPTIONUserDrawCaptionBar 绘制标题栏时再次把标题栏高度从顶部切出------计算与绘制对标题栏高度的理解完全一致;
  • co_WinPosShowWindow(L2630):SW_MINIMIZE/MAXIMIZE/RESTORE 的落地者,三按钮行为的最终执行者(8.2/8.3 节)。

标题栏按钮与系统菜单是"一套状态、两个入口":

数据 系统菜单(menu.c) 标题栏按钮(nonclient.c)
命令执行 MENU_TrackMenu 选中的 SC_* → WM_SYSCOMMAND NC_DoButton 抬起 → WM_SYSCOMMAND
关闭可用性 MENU_InitSysMenuPopupMF_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->spwndActiveNC_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→DefWndSetIconUserPaintCaption(DC_ICON))、WM_SYSCOLORCHANGE(L1195,UserPaintCaption(DC_NC))、WM_CLOSE(L1078)。应用未处理的消息全部汇聚于此,因此标题栏"系统绘制"与"应用接管"(WNDS_NONCPAINT)的分界就在这些 case 的入口判定。

10.5 与 painting.c / draw.c / scrollbar.c

  • painting.cUserDrawCaption/UserDrawCaptionText 是标题栏内容绘制(本册 5.2);
  • draw.cDrawFrameControl/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_HASCAPTIONWNDS_ACTIVEFRAMEWNDS_FORCEMENUDRAWWNDS_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. 附:关键结论

  1. 标题栏的"三要素"由三个函数分别负责UserDrawCaptionBar(总装)、UserDrawCaption(背景/图标/文字)、UserDrawCaptionButton×3(按钮)。三者共享同一套系统度量推导,任何一边改动都必须同步,否则绘制与命中(GetNCHitEx)错位。
  2. 按钮符号的本质是 Marlett 字体的单个字符'0' 最小化、'1' 最大化、'2' 还原、'r' 关闭、's' 帮助。UITOOLS95_DrawFrameCaptionGreCreateFontIndirectW 按按钮尺寸创建 Marlett 字体,GreTextOutW 输出单字符;按下 = 3D 边凹陷 + 符号右下移 1px;灰化 = BTNSHADOW 符号 + BTNHIGHLIGHT 阴影。
  3. 三按钮的交互状态机(NC_DoButton) :按下画压入外观 → 捕获鼠标 → 移动中 GetNCHitEx 实时判定"是否还在按钮内"(移出弹起、移回再压入)→ 在按钮内抬起才发送 WM_SYSCOMMAND(SC_*)。这是"按住反悔可取消"的标准按钮语义。
  4. 按钮灰化是"系统菜单状态"的投影 :关闭按钮的可用性 = IntGetMenuState(SC_CLOSE)MF_GRAYED|MF_DISABLED 且非 CS_NOCLOSE;最小化/最大化按钮的可用性 = WS_MINIMIZEBOX/WS_MAXIMIZEBOX 样式。系统菜单项状态变化经 spwndNotify → UserPaintCaption(DC_BUTTONS) 实时刷新按钮。
  5. SC_ 命令的落地分两类 *:SC_MINIMIZE/MAXIMIZE/RESTOREco_WinPosShowWindow(SW_*)(winpos.c 状态机);SC_CLOSEWM_CLOSE 消息(应用可拦截)→ 未处理时 co_UserDestroyWindow。最小化按钮在已最小化时发 SC_RESTORE,最大化按钮在已最大化时同样发 SC_RESTORE------这就是按钮符号在 '0'/'1''2' 之间切换的行为根源。
  6. 激活双外观的载体是 DC_ACTIVE 标志NC_HandleNCActivate 维护 WNDS_ACTIVEFRAMENC_DoNCPaint/UserPaintCaption 依据 gpqForeground 同步该位并置 DC_ACTIVEUserDrawCaption/UserDrawCaptionText 据此选择激活/非激活系统色。NC_DoNCActive 在颜色无差异时跳过重绘(最小重绘优化)。
  7. 标题栏是"窗口显示"主线的收口点:绘制(WM_NCPAINT 链)、交互(WM_NCLBUTTONDOWN 链)、激活(WM_NCACTIVATE 链)、命令(WM_SYSCOMMAND 链)四条链路在此汇聚,与 winpos(尺寸/显示状态)、menu(系统菜单)、focus(激活)、msgqueue(NC 鼠标消息)四个模块耦合------它是理解"窗口显示"全貌的最佳切入点。

本册完。下一篇可继续沿"窗口显示"主线展开:菜单栏绘制(MENU_DrawMenuBar)或滚动条交互(scrollbar.c),或转入"窗口管理"主线的消息队列(msgqueue.c)专题。

相关推荐
诺伦1 小时前
Rust 错误处理实战:从 unwrap 到优雅 Result 的进阶之路
开发语言·后端·rust
caimouse2 小时前
ReactOS 窗口系统分析(14):计时器/属性/加速键/热键 — timer.c + prop.c + accelerator.c + hotkey.c
c语言·开发语言·reactos
circuitsosk2 小时前
Python 模块与包管理:import 机制、虚拟环境与 pip 完全指南
开发语言·python·pip·依赖管理·模块与包
就叫飞六吧2 小时前
两道门:X-Frame-Options 和 SameSite 到底谁管什么
开发语言·chrome·ai编程
水饺编程2 小时前
第5章,[Win32 章节] :贝塞尔样条曲线(一)
c语言·c++·windows·visual studio
fl1768312 小时前
基于C#WPF实现的内存加速球清理类似360加速球内存清理
开发语言·c#·wpf
weixin_440730502 小时前
python+request实现接口-小结
开发语言·python
ZJU_统一阿萨姆2 小时前
【算子开发】扫描(Scan)与前缀和
开发语言·arm开发·架构·系统架构·硬件架构
-凌凌漆-2 小时前
【C语言】结构体typedef struct与struct的区别
c语言·开发语言