ReactOS 窗口系统分析(29):窗口 Z 顺序的实现 — winpos.c ZOrder 机制深度分析
本文属于"窗口系统三大主线之窗口管理"分册系列(总览见
doc/窗口管理/ReactOS窗口系统架构分析.md第 5 节)。
本册主题:不同窗口之间的 Z 顺序(层叠次序)是怎么实现的——兄弟窗口链表(spwndChild/spwndNext/spwndPrev)如何编码"谁在上面谁在下面"、SetWindowPos(hWndInsertAfter)的插入定位算法、HWND_TOP/BOTTOM/TOPMOST/NOTOPMOST四个伪句柄、TOPMOST 分区机制、owned popup(被拥有的弹出窗)必须压在 owner 之上的约束(CORE-6129/CORE-6554),以及 Z 序与激活、命中测试、可见区域计算的联动。
本册是"窗口系统三大主线之窗口管理"的专题深化篇,接续_3(窗口位置与可见性 winpos.c 整体)。_3已详述的尺寸/移动部分(CVR/SMWP、区域四元组、像素搬运、吸附)本册不重复;本册聚焦_3第 6 章"Z 序管理详解"的逐行级展开与跨模块联动。
核心源码:win32ss/user/ntuser/winpos.c(Z 序参数规范化与 owned popup 约束)、win32ss/user/ntuser/window.c(链表摘除/插入/遍历)、win32ss/user/ntuser/focus.c(激活置顶)、win32ss/user/ntuser/vis.c(Z 序驱动的可见区域合成)、win32ss/user/user32/windows/window.c与win32ss/user/user32/include/ntwrapper.h(用户态封装)、win32ss/include/ntuser.h与sdk/include/psdk/winuser.h(数据结构与常量)。
本文所有内容均基于 ReactOS 真实源码分析,不包含虚构信息。行号以 2026 年 8 月工作区源码为准。
目录
1. 概述
1.1 本篇回答什么问题
Z 顺序(Z-order)是窗口系统的核心概念之一:屏幕上的窗口可以重叠,"谁盖住谁"由 Z 序决定。Windows 的 Z 序模型是兄弟窗口单链表——每个父窗口(或桌面)把它的直接子窗口串成一条双向链表,链表头部(spwndChild)是屏幕最上层,链表尾部是最底层;绘制、命中测试、可见区域计算全部按这条链表的顺序工作。
本篇逐行拆解的问题包括:
- 窗口的 Z 序到底存在哪里?
spwndNext/spwndPrev/spwndChild/spwndParent四个指针如何编码"兄弟次序"与"父子关系"? SetWindowPos(hwnd, hWndInsertAfter, ...)里的hWndInsertAfter是怎么变成链表操作的?HWND_TOP(0)/HWND_BOTTOM(1)/HWND_TOPMOST(-1)/HWND_NOTOPMOST(-2)四个伪句柄分别对应什么链表动作?0xffff/0xfffe与HWND_TOPMOST/HWND_NOTOPMOST的符号扩展问题——为什么内核要把(HWND)0xffff修正为HWND_TOPMOST?WS_EX_TOPMOST(置顶)窗口带如何维持"永远在普通窗口之上"?IntGetLastTopMostWindow找的分界点是什么?- owned popup(模态对话框、下拉菜单窗口、工具提示)为什么能始终盖住它的 owner?
WinPosDoOwnedPopups的"插到 owner 前一格"算法细节? - 激活窗口为什么自动跑到最前?
WinPosFixupFlags的"激活抬升"逻辑如何改写调用者的 Z 序意图? GetWindow(GW_HWNDFIRST/NEXT/PREV/LAST)、GetTopWindow、EnumWindows如何按 Z 序遍历?- 鼠标命中测试(
WindowFromPoint)为什么是"按 Z 序自上而下"的? - Z 序变化后,
vis.c如何重算可见区域、painting.c 如何重绘被遮挡/露出的部分?
1.2 架构图:WND 链表与 Z 序语义
屏幕(显示在最上层的一端)
↑
│ 越靠上(spwndChild 方向)越在屏幕前面
┌─────────────────────────────┼──────────────────────────────┐
│ Desktop 窗口(pDeskInfo->spwnd,窗口树根) │
│ spwndChild ──────────────────────────────────┐ │
│ ↓ spwndNext │ │
│ ┌──────────┐ spwndNext ┌──────────┐ spwndNext ┌──────────┐
│ │ WndA │ ────────────► │ WndB │ ────────────► │ WndC │
│ │ (TOPMOST)│ ◄──────────── │ (TOPMOST)│ ◄──────────── │ (普通) │
│ └──────────┘ spwndPrev └──────────┘ spwndPrev └──────────┘
│ │ │
│ │ spwndChild(WndA 的子窗口也按同一规则串链) │
│ ▼ │
│ ┌──────────┐ spwndNext ┌──────────┐ │
│ │ WndA_1 │ ────────────► │ WndA_2 │ ...(子窗口 Z 序独立) │
│ └──────────┘ ◄──────────── └──────────┘ │
│ │
│ Z 序链表头 = spwndChild(屏幕最上层) │
│ Z 序链表尾 = spwndNext == NULL(屏幕最底层) │
└─────────────────────────────────────────────────────────────────────┘
│
▼
屏幕(显示在最底层的一端)
每个 WND 的五个指针(ntuser.h L711-715):
spwndNext —— 兄弟链表:指向"下面一层"的兄弟(Z 序低一级)
spwndPrev —— 兄弟链表:指向"上面一层"的兄弟(Z 序高一级)
spwndParent —— 父窗口(Z 序链表只在本父的兄弟范围内有效)
spwndChild —— 第一个子窗口(也是子窗口 Z 序链表的表头)
spwndOwner —— 拥有者窗口(owned popup 约束的依据,不参与 Z 序链表)
一句话模型:Z 序 = “以 spwndParent 为根、以 spwndChild 为头的兄弟单向次序(spwndNext 指向更底层)”;spwndPrev 只是为摘除/反向遍历提供的回链。窗口树(父子)是层次,Z 序是同一层内的前后次序,两者正交。
1.3 SetWindowPos(hWndInsertAfter) 的定位算法(总览)
SetWindowPos 的第二个参数 hWndInsertAfter 决定了窗口插入 Z 序链表的位置。算法分两个阶段(详见第 5 章):
阶段一:参数规范化(WinPosFixupFlags,winpos.c L1562)
┌─ 0xffff / 0xfffe 符号扩展修正 → HWND_TOPMOST / HWND_NOTOPMOST
├─ 激活抬升:非前台窗口 + 无 NOACTIVATE/HIDEWINDOW → 改写为 TOPMOST/TOP
├─ 已就位判定:已是目标位置 → 补 SWP_NOZORDER(跳过链表操作)
└─ 真句柄校验:必须同父,否则返回 FALSE(SetWindowPos 不做事但返回 TRUE)
阶段二:链表重排(IntLinkHwnd,window.c L985)
┌─ HWND_TOPMOST → 插链表头 + 置 WS_EX_TOPMOST
├─ HWND_TOP → 插到"最后一个置顶窗口之后"(遇 owner 置顶则自我升级)
├─ HWND_BOTTOM → 插链表尾 + 清 WS_EX_TOPMOST
├─ HWND_NOTOPMOST → 清 WS_EX_TOPMOST,回落到 HWND_TOP 逻辑
└─ 真句柄 H → IntLinkWindow(Wnd, H->spwndPrev),插到 H 之后
阶段零(前置):WinPosDoOwnedPopups(winpos.c L1366)
有 owner 的顶层窗口 → 在桌面子窗口列表中找到 owner,
把 hWndInsertAfter 修正为"owner 前一格",并递归把本窗口的所有
owned popup 逐个搬到本窗口之后(CORE-6129/CORE-6554)。
1.4 与第 3 册的分工
| 维度 | 第 3 册(_3) | 本册(_29) |
|---|---|---|
| 主题 | winpos.c 全部:位置、尺寸、可见性、三态、吸附 | 仅 Z 序:链表、四伪句柄、TOPMOST、owned popup |
| 心脏函数 | co_WinPosSetWindowPos 全流程(600 行) | 其中 Z 序参数规范化 + 链表重排 + owned popup 三段 |
| 区域计算 | CopyRgn/DirtyRgn/ExposedRgn 像素搬运(_3 第 7 章) | 只讲 Z 序变化如何触发 vis.c 重算(不重复区域运算细节) |
| 重点 bug | CORE-6651(父客户区偏移)等 | CORE-6129/CORE-6554(owned popup)、符号扩展、兄弟校验 |
本册不再展开:CVR/SMWP 批量移动(_3 第 3.1/3.2 节)、区域四元组与 BitBlt(_3 第 7 章)、窗口吸附(_3 第 8 章)。本册对 co_WinPosSetWindowPos 只展开 L1836-1951 的 Z 序处理段与 WinPosFixupFlags 的 Z 序分支(L1608-1693)。
2. 设计动机
2.1 为什么用"兄弟单链表"而不是"全局 Z 值"
最直观的 Z 序实现是给每个窗口一个整数 Z 值(越大越靠前)。但 Windows(以及 ReactOS)选择兄弟链表,原因有三:
- 局部性:Z 序只在"同一父窗口的兄弟之间"有意义。子窗口的 Z 序与它父窗口的 Z 序互相独立——父窗口被别的窗口盖住时,其所有子窗口(无论内部 Z 序如何)都一起被盖住。链表结构天然把 Z 序限定在兄弟范围内(
spwndParent相同才可比)。 - 相对顺序,非绝对数值:Z 序关心的是"谁在谁上面"的相对关系,不关心"具体是第几层"。链表只需改两个指针即可插入/摘除,而整数 Z 值方案需要全局重编号(或预留间隔,久用必满)。
- O(1) 摘除/插入:
IntUnlinkWindow/IntLinkWindow都是纯指针操作,不涉及数值比较或重排。SetWindowPos(HWND_TOP)一次调用只需 4-6 次指针赋值。
2.2 画家算法(Painter’s Algorithm)的支撑
窗口绘制采用"画家算法":从 Z 序最底层的窗口开始逐层往上画,上层窗口覆盖下层。ReactOS 没有独立的合成器(现代 DWM 才每窗口一个纹理),屏幕上的像素由各窗口的可见区域拼接而成——可见区域 = 自己的区域减去所有 Z 序在上的兄弟遮挡的部分(vis.c 的 VIS_ComputeVisibleRegion,L12)。因此 Z 序链表是可见性计算的唯一依据:链表顺序一旦错乱,遮挡关系、命中测试、重绘全部跟着错。
vis.c 的算法直接依赖链表遍历(详见 4.14 节):对窗口的每个祖先,从其 spwndChild(最上层)开始沿 spwndNext 走到目标窗口之前,把每一个 WS_VISIBLE 的兄弟的窗口矩形从可见区域里扣除(RGN_DIFF)。遍历的起点(链头)与终点(目标窗口)正是由 Z 序链表决定的。
2.3 四个伪句柄:一次 SetWindowPos 表达"相对与绝对"定位
hwndInsertAfter 参数承载两种语义:
- 绝对语义(相对整条兄弟链表):
HWND_TOP(最上)、HWND_BOTTOM(最下); - 相对语义(相对某窗口):真实句柄——“插到该窗口之后”;
- 跨带语义(相对 TOPMOST 分区):
HWND_TOPMOST(进置顶区)、HWND_NOTOPMOST(出置顶区)。
把"置顶区切换"塞进同一个参数,是 Windows 对 SetWindowPos 的经典设计:SetWindowPos(hwnd, HWND_TOPMOST, 0,0,0,0, SWP_NOMOVE|SWP_NOSIZE) 是"置顶但不移动"的标准写法,HWND_NOTOPMOST 则是"取消置顶"。这两个值 -1/-2(即 0xffff/0xfffe)是负数,与句柄的正值域天然区分,且 HWND_TOP=0、HWND_BOTTOM=1 落在句柄值域的"不可能区间"(句柄从 2 开始编号),四者都不会与真实句柄冲突。
2.4 owned popup 约束:Windows 的"模态"物理基础
对话框的模态性(modal)不只是"消息循环被屏蔽",它还有一个物理层面的保证:模态对话框窗口(WS_POPUP + owner)永远不能被 owner 盖住——否则用户点击 owner 时对话框会消失,模态交互就无从谈起。Windows 把这条规则内建于 Z 序维护中:任何 Z 序操作(SetWindowPos/ShowWindow/激活)都不允许破坏"popup 在 owner 之上"的不变式,必要时自动修正调用者的意图。ReactOS 在 CORE-6129(模态对话框被 owner 盖住)与 CORE-6554(弹出窗链顺序错误)两次回归后,把修复固化在 WinPosDoOwnedPopups(winpos.c L1366)里(详见第 7 章)。
2.5 激活与 Z 序:点击窗口 = 把它带到最前
Windows 的用户心智模型是"我点击的窗口应该到最前面"。这个行为不是输入子系统单独做的,而是两条机制叠加:
- 显式路径:鼠标点击 →
WM_MOUSEACTIVATE→co_IntMouseActivateWindow(focus.c L1248)→co_IntSetForegroundAndFocusWindow→ 激活成功后co_WinPosSetWindowPos(Wnd, HWND_TOP, ...)(focus.c L876)把窗口搬到 Z 序顶端; - 隐式路径(保底):任何
SetWindowPos调用,只要目标窗口不是当前前台窗口且未带SWP_NOACTIVATE/SWP_HIDEWINDOW,WinPosFixupFlags就会把调用者的 Z 序意图改写为"置顶"(L1608-1618)——即使调用者只请求SWP_NOZORDER或指定了非顶端插入位置。
第二条是防御性的:它保证"窗口被 SetWindowPos 处理过但没有显式激活"时也不会留在 Z 序底部。Wine/ReactOS 的 msg 与 win 测试(test_SetFocus/test_SetWindowPos 系列)专门校验这条行为的消息序列。
2.6 “已就位则跳过”:避免无意义的重排与重绘风暴
Z 序链表操作的代价不在于指针赋值本身,而在于连带效应:Z 序变化 → 可见区域重算(vis.c)→ 更新区域生成 → 兄弟窗口重绘。一次"把已经在顶端的窗口移到顶端"的调用如果照做全套,会引发整屏闪烁。因此 WinPosFixupFlags 用 IntGetWindow(GW_HWNDFIRST/LAST) 做已就位判定:目标窗口已经在目标位置(如已是链头且请求 HWND_TOP)→ 补置 SWP_NOZORDER → 后续跳过 IntLinkHwnd(winpos.c L1948)。IntGetWindow 的 O(1) 查询(取 spwndChild/沿 spwndNext 走到底)让这个优化极其廉价。
3. 核心数据结构
3.1 WND 的五个链表指针(ntuser.h L693-767)
typedef struct _WND
{
THRDESKHEAD head; // 对象头:pti(线程)+ rpdesk(桌面)+ 引用计数
DWORD state; // WNDS_* 状态位
DWORD state2;
DWORD ExStyle; // 扩展样式(含 WS_EX_TOPMOST)
DWORD style; // 窗口样式(WS_CHILD/WS_POPUP/WS_VISIBLE...)
...
struct _WND *spwndNext; // L711 兄弟链表:下一层(更底)
struct _WND *spwndPrev; // L712 兄弟链表:上一层(更顶)
struct _WND *spwndParent; // L713 父窗口
struct _WND *spwndChild; // L714 第一个子窗口(子 Z 序链表头)
struct _WND *spwndOwner; // L715 拥有者窗口
RECT rcWindow; // 窗口矩形(屏幕坐标)
RECT rcClient; // 客户区矩形
...
} WND, *PWND;
#define PWND_BOTTOM ((PWND)1) // L769 伪"链尾哨兵"
指针写操作统一走 window.h L155-188 的内联封装(WndSetParent/WndSetChild/WndSetNext/WndSetPrev/WndSetLastActive),它们调用 ReplaceWndPtr——在调试构建中维护窗口对象之间的引用一致性(替换时正确增减对象引用),禁止直接赋值。
语义速查:
| 指针 | 语义 | 谁写它 |
|---|---|---|
spwndParent | 父窗口(桌面窗口的父为 NULL) | WndSetParent(创建/SetParent) |
spwndChild | 第一个子窗口 = 子 Z 序链头 = 最上层子窗口 | WndSetChild(IntLinkWindow/IntUnlinkWindow) |
spwndNext | 兄弟链表中"下面一层" | WndSetNext(IntLinkWindow/IntUnlinkWindow) |
spwndPrev | 兄弟链表中"上面一层"(回链) | WndSetPrev |
spwndOwner | 拥有者(owned popup 用,不在 Z 序链表内) | IntSetOwner(SetWindowLong GWL_HWNDPARENT) |
关键点:spwndChild 是"子窗口链表的头",而头部 = 最上层。所以"把窗口带到最前"= 把它变成父的 spwndChild 或紧跟其后;“把窗口放到底”= 把它放到 spwndNext == NULL 的位置。GetTopWindow(hwnd) 的实现就是 GetWindow(hwnd, GW_CHILD)——取 spwndChild,即最上层的子窗口(user32 window.c L1156-1161)。
3.2 Z 序头尾语义
父窗口 P
spwndChild ──► [A] ◄──spwndPrev── [B] ◄──spwndPrev── [C] ──► NULL
└────spwndNext─────►└────spwndNext─────►
[A] 最上层(屏幕最前);[C] 最底层(屏幕最后)
A->spwndPrev == NULL(链头无回链)
C->spwndNext == NULL(链尾无下链)
P->spwndChild == A(父只记住最上层)
遍历习惯:
- 自上而下(从最上层往下):
for (w = parent->spwndChild; w; w = w->spwndNext) - 自下而上(从最底层往上):先沿
spwndNext走到底再沿spwndPrev回退,或直接从已知窗口w->spwndPrev退。
IntGetWindow(GW_HWNDFIRST)(window.c L400-407)取"父的 spwndChild";GW_HWNDLAST(L423-427)从自身沿 spwndNext 走到底——两者被 WinPosFixupFlags 用来做"已就位判定"(见 4.3 节)。
3.3 四个 HWND_* 伪句柄(psdk/winuser.h L1216-1219)
#define HWND_BOTTOM ((HWND)1)
#define HWND_NOTOPMOST ((HWND)(-2)) // 0xFFFE
#define HWND_TOP ((HWND)0)
#define HWND_TOPMOST ((HWND)(-1)) // 0xFFFF
| 伪句柄 | 值 | 链表语义 | WS_EX_TOPMOST 位 |
|---|---|---|---|
HWND_TOP | 0 | 插到"最后一个置顶窗口之后"(置顶带与普通带交界处) | 保持/按需升级 |
HWND_BOTTOM | 1 | 插到链表尾 | 清除 |
HWND_TOPMOST | -1 (0xFFFF) | 插到链表头 | 置位 |
HWND_NOTOPMOST | -2 (0xFFFE) | 清位后按 HWND_TOP 处理 | 清除 |
符号扩展问题:Win32 句柄是 HANDLE(32 位无符号视角),HWND_TOPMOST 被定义为 (HWND)(-1)。当应用用 (HWND)0xffff 字面量传参时(如某些旧代码 SetWindowPos(hwnd, (HWND)0xffff, ...)),进入内核后 0x0000FFFF 不等于 0xFFFFFFFF(HWND_TOPMOST 的 32 位展开),会误入"真句柄"分支。WinPosFixupFlags L1624-1631 专门修正(见 4.3 节):
if (WinPos->hwndInsertAfter == (HWND)0xffff) WinPos->hwndInsertAfter = HWND_TOPMOST;
else if (WinPos->hwndInsertAfter == (HWND)0xfffe) WinPos->hwndInsertAfter = HWND_NOTOPMOST;
ReactOS 在 NtUserSetWindowPos 入口(L3633-3644)先按完整 32 位值过滤四个伪句柄,再把其余当真实句柄校验(拒绝桌面/消息窗口作为插入参照)。
3.4 SWP_* 标志中与 Z 序相关的位(psdk/winuser.h L1250-1264)
| 标志 | 值 | 与 Z 序的关系 |
|---|---|---|
SWP_NOZORDER | 0x0004 | 保留 Z 序:置位则跳过 IntLinkHwnd(winpos.c L1948)与 WinPosDoOwnedPopups(L1885-1890) |
SWP_NOACTIVATE | 0x0010 | 抑制"激活抬升"(WinPosFixupFlags L1611)与末尾 co_IntSetForegroundWindow |
SWP_NOOWNERZORDER | 0x0200 | = SWP_NOREPOSITION:跳过 owned popup 约束(L1885) |
SWP_NOSENDCHANGING | 0x0400 | 批量移动 Phase 2 用;owned popup 递归调用时也带(L1513-1514) |
SWP_DEFERERASE | 0x2000 | owned popup 递归调用时携带,抑制同步擦除 |
SWP_NOREDRAW | 0x0008 | 跳过可见区域快照/重绘(Z 序变化但不重画) |
SWP_SHOWWINDOW/SWP_HIDEWINDOW | 0x0040/0x0080 | 显隐变化也会触发 owned popup 保底(L1886 条件) |
聚合宏(winpos.c L20-27,SWP_AGG_NOGEOMETRYCHANGE/SWP_AGG_NOPOSCHANGE/SWP_AGG_STATUSFLAGS/SWP_AGG_NOCLIENTCHANGE)把 SWP_NOZORDER 归入"几何无变化"判据——Z 序与尺寸/位置并列,都是"窗口几何"的组成部分。
3.5 WS_EX_TOPMOST 与 WS_EX2_LINKED(置顶区与链表状态)
WS_EX_TOPMOST(0x00000008,psdk/winuser.h):置顶标记。置顶窗口恒位于兄弟链表的前部(置顶带),普通窗口永远排在它们后面。WS_EX2_LINKED(0X00000008,ntuser.h L676):ReactOS 内部扩展位,标记"窗口已按 IntLinkHwnd 语义挂进链表"。IntLinkHwnd(HWND_NOTOPMOST)用它做无操作判定:窗口本非置顶且已链接 → 直接返回(L987-993)。
if (hWndPrev == HWND_NOTOPMOST)
{
if (!(Wnd->ExStyle & WS_EX_TOPMOST) && (Wnd->ExStyle2 & WS_EX2_LINKED))
return; /* nothing to do */
Wnd->ExStyle &= ~WS_EX_TOPMOST;
hWndPrev = HWND_TOP; /* fallback to the HWND_TOP case */
}
3.6 关键辅助结构:IntWinListChildren 的 HWND 数组
WinPosDoOwnedPopups 与命中测试都需要"把兄弟链表按 Z 序拍成数组"再扫描。IntWinListChildren(window.c L274-304)按 spwndChild → spwndNext 顺序(从顶到底)把子窗口句柄填入 ExAllocatePoolWithTag(PagedPool, ..., USERTAG_WINDOWLIST) 分配的数组,NULL 结尾,调用方负责 ExFreePoolWithTag:
HWND* FASTCALL IntWinListChildren(PWND Window)
{
PWND Child;
HWND *List;
UINT Index, NumChildren = 0;
if (!Window) return NULL;
for (Child = Window->spwndChild; Child; Child = Child->spwndNext) ++NumChildren;
List = ExAllocatePoolWithTag(PagedPool, (NumChildren + 1) * sizeof(HWND), USERTAG_WINDOWLIST);
if (!List) { ERR(...); EngSetLastError(ERROR_NOT_ENOUGH_MEMORY); return NULL; }
Index = 0;
for (Child = Window->spwndChild; Child; Child = Child->spwndNext)
List[Index++] = UserHMGetHandle(Child);
List[Index] = NULL;
return List;
}
变体 IntWinListOwnedPopups(window.c L315-349):同样遍历桌面子窗口,但只收集 Child->spwndOwner == Window 且非默认 IME 窗口(IntWndIsDefaultIme)的句柄——IntShowOwnedPopups(window.c L4674,owner 最小化/还原时隐藏/恢复弹出窗)依赖它。
4. 核心函数代码级展开
4.0 函数全景(本册覆盖)
| 层 | 函数 | 位置 | 职责 |
|---|---|---|---|
| 系统调用 | NtUserSetWindowPos | winpos.c L3609 | SetWindowPos 入口:校验 + 钳制 + 转发 |
| 用户态封装 | SetWindowPos | ntwrapper.h L433 | 直通 NtUserSetWindowPos |
| 用户态封装 | BringWindowToTop | user32 window.c L68 | SetWindowPos(HWND_TOP) + 激活 |
| 用户态封装 | GetTopWindow | user32 window.c L1156 | GetWindow(GW_CHILD) |
| 心脏 | co_WinPosSetWindowPos(Z 序段) | winpos.c L1836-1951 | 规范化 → owned popup → 链表重排 |
| 规范化 | WinPosFixupFlags(Z 序分支) | winpos.c L1608-1693 | 符号扩展/激活抬升/已就位/兄弟校验 |
| 约束 | WinPosDoOwnedPopups | winpos.c L1366 | owned popup 保底算法 |
| 链表 | IntLinkHwnd | window.c L985 | HWND_* → 链表动作 + TOPMOST 位 |
| 链表 | IntLinkWindow/IntUnlinkWindow | window.c L944/L1353 | 指针级插入/摘除 |
| 遍历 | IntGetWindow | window.c L381 | GW_HWNDFIRST/NEXT/PREV/LAST/CHILD/OWNER |
| 遍历 | IntGetLastTopMostWindow | winpos.c L238 | 找最后一个置顶窗口 |
| 命中 | co_WinPosWindowFromPoint/co_WinPosSearchChildren | winpos.c L2999/L2914 | 按 Z 序自上而下命中 |
| 激活 | NtUserSetActiveWindow/co_IntSetActiveWindow/IntUserSetActiveWindow/co_IntSetForegroundAndFocusWindow | focus.c L1657/L1023/L1169/L920 | 激活置顶(L876) |
| 可见性 | co_WinPosShowWindow/co_WinPosMinMaximize | winpos.c L2629/L2444 | 显隐/三态中的 Z 序联动 |
| 创建 | co_UserCreateWindowEx 链接段 | window.c L2431-2438 | 子窗口插 HWND_BOTTOM,其余插 hwndInsertAfter |
| 可见区域 | VIS_ComputeVisibleRegion | vis.c L12 | 按 Z 序逐层扣遮挡 |
4.1 NtUserSetWindowPos(winpos.c L3609)— 系统调用入口
BOOL APIENTRY
NtUserSetWindowPos(HWND hWnd, HWND hWndInsertAfter,
int X, int Y, int cx, int cy, UINT uFlags)
{
PWND Window, pWndIA;
BOOL ret = FALSE;
USER_REFERENCE_ENTRY Ref;
UserEnterExclusive(); // 1. USER 排他锁
if (!(Window = UserGetWindowObject(hWnd)) ||
UserIsDesktopWindow(Window) || UserIsMessageWindow(Window))
{ ERR(...); goto Exit; } // 2. 目标窗口校验:拒绝桌面/消息窗口
if ( hWndInsertAfter != HWND_TOP &&
hWndInsertAfter != HWND_BOTTOM &&
hWndInsertAfter != HWND_TOPMOST &&
hWndInsertAfter != HWND_NOTOPMOST )
{
if (!(pWndIA = UserGetWindowObject(hWndInsertAfter)) ||
UserIsDesktopWindow(pWndIA) || UserIsMessageWindow(pWndIA))
{ ERR(...); goto Exit; } // 3. 插入参照校验(真句柄时)
}
/* First make sure that coordinates are valid for WM_WINDOWPOSCHANGING */
if (!(uFlags & SWP_NOMOVE)) { /* x/y 钳制到 ±32767 */ }
if (!(uFlags & SWP_NOSIZE)) { /* cx/cy 钳制到 [0,32767] */ }
UserRefObjectCo(Window, &Ref); // 4. 引用计数(回调期间窗口可能被销毁)
ret = co_WinPosSetWindowPos(Window, hWndInsertAfter, X, Y, cx, cy, uFlags);
UserDerefObjectCo(Window);
Exit:
UserLeave();
return ret;
}
要点:Z 序参数在这里只做"合法性"校验(四伪句柄 or 真实窗口、且非桌面/消息窗口),不做任何语义解释——解释全部留给 WinPosFixupFlags 与 IntLinkHwnd。坐标钳制(±32767)是 Win32 16 位坐标语义,与 Z 序无关但保证 WM_WINDOWPOSCHANGING 看到的参数合法。
4.2 co_WinPosSetWindowPos 的 Z 序处理段(winpos.c L1836-1951)
心脏函数的前半段是 Z 序的主战场。按执行顺序拆解:
/* L1838-1844 组装 WINDOWPOS */
WinPos.hwnd = UserHMGetHandle(Window);
WinPos.hwndInsertAfter = WndInsertAfter; // ← Z 序参数进入 WinPos
WinPos.x = x; WinPos.y = y; WinPos.cx = cx; WinPos.cy = cy;
WinPos.flags = flags;
/* L1846-1865 SWP_ASYNCWINDOWPOS:异步路径(拷贝 WinPos 到分页池,
co_IntSendMessageNoWait(WM_ASYNC_SETWINDOWPOS),由目标线程队列异步执行) */
/* L1867 发 WM_WINDOWPOSCHANGING + 计算初始矩形 */
co_WinPosDoWinPosChanging(Window, &WinPos, &NewWindowRect, &NewClientRect);
/* 应用可在 CHANGING 里改写 WinPos.hwndInsertAfter 与 flags!*/
/* L1870-1875 窗口可能被应用销毁,重新校验 */
if (!IntIsWindow(WinPos.hwnd)) { ... return FALSE; }
/* L1878-1882 ★ Z 序参数规范化(见 4.3) */
if (!WinPosFixupFlags(&WinPos, Window))
{
// See Note. —— 兄弟校验失败:不做事但返回 TRUE(Wine 测试确认 Windows 行为)
return TRUE;
}
/* L1884-1890 ★ owned popup 约束(见 4.4 / 第 7 章) */
Ancestor = UserGetAncestor(Window, GA_PARENT);
if (!(WinPos.flags & SWP_NOOWNERZORDER) &&
(WinPos.flags & (SWP_NOZORDER | SWP_HIDEWINDOW | SWP_SHOWWINDOW)) != SWP_NOZORDER &&
Ancestor && UserHMGetHandle(Ancestor) == IntGetDesktopWindow() )
{
WinPos.hwndInsertAfter = WinPosDoOwnedPopups(Window, WinPos.hwndInsertAfter);
}
/* 触发条件解读:
a) 未带 SWP_NOOWNERZORDER(应用显式放弃约束)
b) 不是"纯 NOZORDER"(即 Z 序要动,或显示状态要变)——
条件 (flags & (NOZORDER|HIDEWINDOW|SHOWWINDOW)) != SWP_NOZORDER 等价于
"NOZORDER 未置 或 HIDEWINDOW/SHOWWINDOW 置位"
c) 窗口的根祖先是桌面(顶层窗口;子窗口由 WinPosDoOwnedPopups 内部直接返回) */
/* ... L1892-1932 变更前可见区域快照(VisBefore / VisBeforeJustClient,
非 NOREDRAW 且几何真会变时调用 VIS_ComputeVisibleRegion)... */
/* L1934-1939 hrgnNewFrame → hrgnClip(SetWindowRgn 的 HACK 挂载点) */
/* L1941 co_WinPosDoNCCALCSize(WM_NCCALCSIZE,客户区重算) */
/* L1947-1951 ★★ 链表重排——Z 序的最终落点 */
/* Validate link windows. (also take into account shell window in hwndShellWindow) */
if (!(WinPos.flags & SWP_NOZORDER) && WinPos.hwnd != UserGetShellWindow())
{
IntLinkHwnd(Window, WinPos.hwndInsertAfter);
}
/* SWP_NOZORDER → 不动链表;
Shell 窗口(UserGetShellWindow)豁免——任务栏/桌面壳的 Z 序由系统管理 */
/* L1953-... 应用几何(OldWindowRect/NewWindowRect 写入 rcWindow/rcClient)、
可见性位切换、区域计算、重绘、WM_WINDOWPOSCHANGED(见 _3 第 4.2 节) */
阶段顺序总结:CHANGING 消息(应用可改写 Z 序意图)→ WinPosFixupFlags(规范化/抬升/已就位)→ WinPosDoOwnedPopups(owner 约束修正)→ IntLinkHwnd(真正的链表操作)→ 可见区域重算与重绘。owned popup 修正发生在 IntLinkHwnd 之前,因此 IntLinkHwnd 拿到的是被约束修正后的 hwndInsertAfter。
4.3 WinPosFixupFlags 的 Z 序分支(winpos.c L1562-1696)
全函数做坐标钳制、可见性互斥、几何去冗余(详见 _3 4.3 节),本篇聚焦 L1608-1693 的 Z 序部分。按代码顺序逐段展开:
4.3.1 激活抬升(L1608-1618)
if ( WinPos->hwnd != UserGetForegroundWindow() && (Wnd->style & (WS_POPUP | WS_CHILD)) != WS_CHILD)
{
/* Bring to the top when activating */
if (!(WinPos->flags & (SWP_NOACTIVATE|SWP_HIDEWINDOW)) &&
(WinPos->flags & SWP_NOZORDER ||
(WinPos->hwndInsertAfter != HWND_TOPMOST && WinPos->hwndInsertAfter != HWND_NOTOPMOST)))
{
WinPos->flags &= ~SWP_NOZORDER;
WinPos->hwndInsertAfter = (0 != (Wnd->ExStyle & WS_EX_TOPMOST) ? HWND_TOPMOST : HWND_TOP);
}
}
解读:
- 触发前提:① 目标窗口不是当前前台窗口(
UserGetForegroundWindow());② 不是子窗口(子窗口不能抢激活,(WS_POPUP|WS_CHILD) != WS_CHILD排除纯 WS_CHILD)。 - 触发条件:③ 未带
SWP_NOACTIVATE与SWP_HIDEWINDOW(调用者明确不想激活);④ 调用者请求了SWP_NOZORDER(“我不动 Z 序”)或插入参照不是HWND_TOPMOST/HWND_NOTOPMOST(即插到中间/底部)。 - 动作:清掉
SWP_NOZORDER,把插入位置改写为HWND_TOPMOST(本窗口已置顶)或HWND_TOP(普通窗口)。 - 语义:SetWindowPos 一个非前台顶层窗口 = 隐含"激活并置顶"。这正是"点击窗口它就到最前"的第二重保险(第一重是 focus.c L876 的显式置顶)。
4.3.2 符号扩展修正(L1623-1631)
/* Fix sign extension */
if (WinPos->hwndInsertAfter == (HWND)0xffff) WinPos->hwndInsertAfter = HWND_TOPMOST;
else if (WinPos->hwndInsertAfter == (HWND)0xfffe) WinPos->hwndInsertAfter = HWND_NOTOPMOST;
见 3.3 节说明。此修正只发生在 SWP_NOZORDER 未置时(外层 if (!(WinPos->flags & SWP_NOZORDER)),L1621)。
4.3.3 四个伪句柄的"已就位判定"(L1633-1658)
if (WinPos->hwndInsertAfter == HWND_TOP)
{
/* Keep it topmost when it's already topmost */
if ((Wnd->ExStyle & WS_EX_TOPMOST) != 0)
WinPos->hwndInsertAfter = HWND_TOPMOST; // 置顶窗口的 TOP 等价于 TOPMOST
if (IntGetWindow(WinPos->hwnd, GW_HWNDFIRST) == WinPos->hwnd)
WinPos->flags |= SWP_NOZORDER; // 已是链头 → 不用动
}
else if (WinPos->hwndInsertAfter == HWND_BOTTOM)
{
if (!(Wnd->ExStyle & WS_EX_TOPMOST) && IntGetWindow(WinPos->hwnd, GW_HWNDLAST) == WinPos->hwnd)
WinPos->flags |= SWP_NOZORDER; // 非置顶且已是最底 → 不用动
}
else if (WinPos->hwndInsertAfter == HWND_TOPMOST)
{
if ((Wnd->ExStyle & WS_EX_TOPMOST) && IntGetWindow(WinPos->hwnd, GW_HWNDFIRST) == WinPos->hwnd)
WinPos->flags |= SWP_NOZORDER; // 已置顶且已是链头 → 不用动
}
else if (WinPos->hwndInsertAfter == HWND_NOTOPMOST)
{
if (!(Wnd->ExStyle & WS_EX_TOPMOST))
WinPos->flags |= SWP_NOZORDER; // 本非置顶 → 无事可做
}
四种情况的共同模式:“目标状态已经满足” → 补 SWP_NOZORDER → 后续 IntLinkHwnd 被跳过。注意 HWND_TOP 分支的特殊性:置顶窗口请求 HWND_TOP 时先升级为 HWND_TOPMOST——因为置顶窗口插到普通带里没有意义(它必须留在置顶带,见第 6 章)。
4.3.4 真句柄的兄弟校验与已就位判定(L1659-1692)
else /* hwndInsertAfter must be a sibling of the window */
{
PWND InsAfterWnd;
InsAfterWnd = ValidateHwndNoErr(WinPos->hwndInsertAfter);
if (!InsAfterWnd) return TRUE; // 句柄已失效:不做事,返回 TRUE(成功)
if (InsAfterWnd->spwndParent != Wnd->spwndParent)
{
/* Note from wine User32 Win test_SetWindowPos:
"Returns TRUE also for windows that are not siblings"
"Does not seem to do anything even without passing flags, still returns TRUE" */
return FALSE; // ★ 不是兄弟 → 整体失败(co_WinPosSetWindowPos 返回 TRUE)
}
else
{
/* We don't need to change the Z order of hwnd if it's already
inserted after hwndInsertAfter or when inserting hwnd after itself. */
if ((WinPos->hwnd == WinPos->hwndInsertAfter) ||
((InsAfterWnd->spwndNext) && (WinPos->hwnd == UserHMGetHandle(InsAfterWnd->spwndNext))))
{
WinPos->flags |= SWP_NOZORDER; // 已就位(插到自己之后 / 已在目标之后)→ 不用动
}
}
}
两个关键细节:
- 兄弟校验:
hWndInsertAfter的父必须与本窗口的父相同,否则返回FALSE→co_WinPosSetWindowPos在 L1878-1882 直接return TRUE(失败但不报错,且不发送任何消息、不做任何几何变更)——这是 Wine 测试确认的 Windows 兼容行为(“跨父窗口的 Z 序操作是静默无操作”)。 - 已就位判定:插入参照就是自己,或自己已经在参照的
spwndNext位置(即已经"插在参照之后")→SWP_NOZORDER。注意InsAfterWnd->spwndNext的判断——若参照本来就是链尾(spwndNext == NULL),则无法通过此法确认,交给IntLinkHwnd再处理。
4.4 WinPosDoOwnedPopups(winpos.c L1366-1523)— owned popup 保底
这是 CORE-6129/CORE-6554 的修复点,函数较长,分三段展开。
4.4.1 前置:子窗口直接返回(L1380-1384)
if (Style & WS_CHILD)
{
TRACE("Window is child\n");
return hWndInsertAfter;
}
owned popup 只针对顶层窗口(owner 关系只存在于顶层窗口之间)。子窗口不参与。
4.4.2 主流程:有 owner 时的"插到 owner 前一格"(L1386-1452)
Owner = (Window->spwndOwner ? UserHMGetHandle(Window->spwndOwner) : NULL);
if (Owner)
{
/* Make sure this popup stays above the owner */
if (hWndInsertAfter != HWND_TOPMOST)
{
DesktopWindow = UserGetDesktopWindow();
List = IntWinListChildren(DesktopWindow); // 桌面子窗口,从顶到底
if (List != NULL)
{
for (i = 0; List[i]; i++)
{
BOOL topmost = FALSE;
ChildObject = ValidateHwndNoErr(List[i]);
if (ChildObject) topmost = (ChildObject->ExStyle & WS_EX_TOPMOST) != 0;
if (List[i] == Owner)
{
/* We found its Owner, so we must handle it here. */
if (i > 0)
{
if (List[i - 1] != UserHMGetHandle(Window))
{
/* 本窗口不是正好在 owner 前一格 →
把 hWndInsertAfter 修正为 owner 前一个窗口
(注意不能等于自己,否则会自链!) */
hWndInsertAfter = List[i - 1];
}
else
{
/* 本窗口已在 owner 前一格 → 已就位,直接返回 */
ExFreePoolWithTag(List, USERTAG_WINDOWLIST);
return hWndInsertAfter;
}
}
else
{
/* owner 是链头(最上层)→ 本窗口必须进置顶带或普通带顶端 */
hWndInsertAfter = topmost ? HWND_TOPMOST : HWND_TOP;
}
break;
}
/* 扫描途中顺带处理"目标位置在置顶带内"的情况 */
if (hWndInsertAfter == HWND_TOP || hWndInsertAfter == HWND_NOTOPMOST)
{
if (!topmost) break; // 遇到第一个非置顶窗口就停(TOP 的插入点在普通带顶端)
}
else if (List[i] == hWndInsertAfter) break;
}
}
else
return hWndInsertAfter;
}
}
算法核心:在桌面子窗口链表中找到 owner 的位置 i,若本窗口不在 i-1 位置,就把 hWndInsertAfter 改为 List[i-1]——“插到 owner 前面一个窗口之后” = “插到 owner 前一格” = owner 的直接上一层。这正是"popup 恒在 owner 之上且尽量贴近 owner"的链表表达。List[i-1] != 自己 的自链防护是 CORE-6554 的教训(若 hWndInsertAfter 变成自己,IntLinkWindow 会把窗口链接到自身,产生环)。
4.4.3 收尾:把"owner 为本窗口的所有 popup"逐个搬到本窗口之后(L1454-1520)
if (hWndInsertAfter == HWND_BOTTOM)
{
ERR("Window is HWND_BOTTOM hwnd %p\n", hWndInsertAfter);
if (List) ExFreePoolWithTag(List, USERTAG_WINDOWLIST);
goto done; // HWND_BOTTOM 特殊:直接放弃 popup 搬运(见第 7 章)
}
if (!List) { DesktopWindow = UserGetDesktopWindow(); List = IntWinListChildren(DesktopWindow); }
if (List != NULL)
{
i = 0;
if (hWndInsertAfter == HWND_TOP || hWndInsertAfter == HWND_NOTOPMOST)
{
if (hWndInsertAfter == HWND_NOTOPMOST || !(Window->ExStyle & WS_EX_TOPMOST))
{
/* skip all the topmost windows */ // 插入点跳过置顶带
while (List[i] &&
(ChildObject = ValidateHwndNoErr(List[i])) &&
(ChildObject->ExStyle & WS_EX_TOPMOST)) i++;
}
}
else if (hWndInsertAfter != HWND_TOPMOST)
{
/* skip windows that are already placed correctly */
for (i = 0; List[i]; i++)
{
if (List[i] == hWndInsertAfter) break;
if (List[i] == UserHMGetHandle(Window))
{
ExFreePoolWithTag(List, USERTAG_WINDOWLIST);
goto done; /* nothing to do if window is moving backwards in z-order */
}
}
}
for (; List[i]; i++)
{
PWND Wnd;
USER_REFERENCE_ENTRY Ref;
if (List[i] == UserHMGetHandle(Window)) break; // 遇到自己为止
if (!(Wnd = ValidateHwndNoErr(List[i]))) continue;
Owner = (Wnd->spwndOwner ? UserHMGetHandle(Wnd->spwndOwner) : NULL);
if (Owner != UserHMGetHandle(Window)) continue; // 只搬"owned by 本窗口"的
UserRefObjectCo(Wnd, &Ref);
TRACE("moving %p owned by %p after %p\n", List[i], UserHMGetHandle(Window), hWndInsertAfter);
co_WinPosSetWindowPos(Wnd, hWndInsertAfter, 0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_NOSENDCHANGING | SWP_DEFERERASE);
UserDerefObjectCo(Wnd);
hWndInsertAfter = List[i]; // ★ 逐个堆叠:后搬的插到先搬的后面
}
ExFreePoolWithTag(List, USERTAG_WINDOWLIST);
}
done:
return hWndInsertAfter;
"逐个堆叠"技巧:第一轮把 owned popup P1 搬到 hWndInsertAfter 之后,然后 hWndInsertAfter = P1;第二轮把 P2 搬到 P1 之后……最终效果是 P1、P2、… 按原相对顺序整体紧跟本窗口。递归调用使用 SWP_NOMOVE|SWP_NOSIZE|SWP_NOACTIVATE|SWP_NOSENDCHANGING|SWP_DEFERERASE——只改 Z 序、不发消息、不激活、不擦背景,最大限度降低连带效应。
4.5 IntLinkHwnd(window.c L985-1082)— 链表重排的执行者
IntLinkHwnd(Wnd, hWndPrev) 是"把 hWndInsertAfter 语义翻译成链表动作"的最终落点。已在前文 3.5 节看过 NOTOPMOST 分支,完整展开:
VOID FASTCALL IntLinkHwnd(PWND Wnd, HWND hWndPrev)
{
if (hWndPrev == HWND_NOTOPMOST)
{
if (!(Wnd->ExStyle & WS_EX_TOPMOST) && (Wnd->ExStyle2 & WS_EX2_LINKED))
return; // 非置顶且已链接 → 无操作
Wnd->ExStyle &= ~WS_EX_TOPMOST; // 清置顶位
hWndPrev = HWND_TOP; // 回落到 HWND_TOP 逻辑
}
IntUnlinkWindow(Wnd); // ★ 先摘除(详见 4.6)
if (hWndPrev == HWND_BOTTOM)
{
/* Link in the bottom of the list */
PWND WndInsertAfter = Wnd->spwndParent->spwndChild;
while (WndInsertAfter && WndInsertAfter->spwndNext)
WndInsertAfter = WndInsertAfter->spwndNext; // 走到链尾
IntLinkWindow(Wnd, WndInsertAfter); // 插到链尾之后
Wnd->ExStyle &= ~WS_EX_TOPMOST; // 置底窗口必非置顶
}
else if (hWndPrev == HWND_TOPMOST)
{
/* Link in the top of the list */
IntLinkWindow(Wnd, NULL); // NULL = 链头(spwndChild)
Wnd->ExStyle |= WS_EX_TOPMOST; // 置顶位
}
else if (hWndPrev == HWND_TOP)
{
/* Link it after the last topmost window */
PWND WndInsertBefore = Wnd->spwndParent->spwndChild;
if (!(Wnd->ExStyle & WS_EX_TOPMOST)) /* put it above the first non-topmost window */
{
while (WndInsertBefore != NULL && WndInsertBefore->spwndNext != NULL)
{
if (!(WndInsertBefore->ExStyle & WS_EX_TOPMOST))
break; // 遇到第一个非置顶窗口 → 插它前面(即置顶带末尾)
if (WndInsertBefore == Wnd->spwndOwner) // ★ owner 约束:owner 是置顶的
{
Wnd->ExStyle |= WS_EX_TOPMOST; // 本窗口也跟着置顶(见第 7 章)
break;
}
WndInsertBefore = WndInsertBefore->spwndNext;
}
}
IntLinkWindow(Wnd, WndInsertBefore ? WndInsertBefore->spwndPrev : NULL);
}
else
{
/* Link it after hWndPrev(真句柄) */
PWND WndInsertAfter = UserGetWindowObject(hWndPrev);
if (WndInsertAfter == NULL) { IntLinkHwnd(Wnd, HWND_TOP); return; } // 兜底
if (Wnd == WndInsertAfter)
{ ERR("Trying to link window 0x%p to itself\n", Wnd); ASSERT(...); }
else
{
IntLinkWindow(Wnd, WndInsertAfter);
}
/* Fix the WS_EX_TOPMOST flag */
if (!(WndInsertAfter->ExStyle & WS_EX_TOPMOST))
{
Wnd->ExStyle &= ~WS_EX_TOPMOST; // 插到普通窗口后 → 非置顶
}
else
{
if (WndInsertAfter->spwndNext &&
(WndInsertAfter->spwndNext->ExStyle & WS_EX_TOPMOST))
{
Wnd->ExStyle |= WS_EX_TOPMOST; // 前后都是置顶 → 保持置顶
}
}
}
Wnd->ExStyle2 |= WS_EX2_LINKED; // 标记已按语义链接
}
TOPMOST 位的三条维护规则(第 6 章详述):
- 置顶位不随窗口移动,但随"插入到置顶窗口之间"而继承:真句柄分支中,若参照是置顶窗口且其
spwndNext也是置顶窗口(本窗口被插进置顶带内部),则本窗口自动成为置顶; - 插到普通窗口之后 → 强制清置顶位(防止"伪置顶"混入置顶带);
HWND_TOP分支中,若 owner 是置顶窗口,本窗口升级为置顶(popup 不能比 owner 低,见第 7 章)。
4.6 IntLinkWindow / IntUnlinkWindow(window.c L944-980 / L1353-1371)— 指针级操作
IntLinkWindow(L944-980)
/* Link the window into siblings list. Children and parent are kept in place. */
VOID FASTCALL
IntLinkWindow(PWND Wnd, PWND WndInsertAfter /* Set to NULL if top sibling */)
{
if (Wnd == WndInsertAfter)
{
ERR("Trying to link window 0x%p to itself\n", Wnd);
ASSERT(WndInsertAfter != Wnd);
return;
}
WndSetPrev(Wnd, WndInsertAfter); // 新窗口的 prev = 参照
if (Wnd->spwndPrev)
{
/* Link after WndInsertAfter */
ASSERT(Wnd != WndInsertAfter->spwndNext);
WndSetNext(Wnd, WndInsertAfter->spwndNext); // 新窗口的 next = 参照的 next
if (Wnd->spwndNext)
WndSetPrev(Wnd->spwndNext, Wnd); // 原 next 的 prev 回指新窗口
ASSERT(Wnd != Wnd->spwndPrev);
WndSetNext(Wnd->spwndPrev, Wnd); // 参照的 next 指向新窗口
}
else
{
/* Link at the top */
ASSERT(Wnd != Wnd->spwndParent->spwndChild);
WndSetNext(Wnd, Wnd->spwndParent->spwndChild); // 新窗口的 next = 原链头
if (Wnd->spwndNext)
WndSetPrev(Wnd->spwndNext, Wnd); // 原链头的 prev 回指新窗口
WndSetChild(Wnd->spwndParent, Wnd); // ★ 父的 spwndChild = 新窗口(成新链头)
}
}
WndInsertAfter == NULL 表示插到链头(成为父的 spwndChild);否则插到参照之后。四步指针操作:改自己的 prev → 改自己的 next → 改后继的 prev → 改前驱的 next。全部走 WndSet* 宏保证引用计数一致。
IntUnlinkWindow(L1353-1371)
VOID FASTCALL
IntUnlinkWindow(PWND Wnd)
{
ASSERT(Wnd != Wnd->spwndNext);
ASSERT(Wnd != Wnd->spwndPrev);
if (Wnd->spwndNext)
WndSetPrev(Wnd->spwndNext, Wnd->spwndPrev); // 后继的 prev 跳过自己
if (Wnd->spwndPrev)
WndSetNext(Wnd->spwndPrev, Wnd->spwndNext); // 前驱的 next 跳过自己
if (Wnd->spwndParent && Wnd->spwndParent->spwndChild == Wnd)
WndSetChild(Wnd->spwndParent, Wnd->spwndNext);// 自己是链头 → 父的 spwndChild 后移
WndSetPrev(Wnd, NULL); // 自己的 prev/next 清空
WndSetNext(Wnd, NULL);
}
摘除是"先摘再插"模式(IntLinkHwnd L995 先调 IntUnlinkWindow),所以链表操作天然幂等:重复 SetWindowPos 到同一位置,先摘除再插回,结果不变。ASSERT 防止自链成环。
4.7 IntGetWindow(window.c L381-437)— GW_* 的 Z 序遍历
HWND FASTCALL
IntGetWindow(HWND hWnd, UINT uCmd)
{
PWND Wnd, FoundWnd;
HWND Ret = NULL;
Wnd = ValidateHwndNoErr(hWnd);
if (!Wnd) return NULL;
FoundWnd = NULL;
switch (uCmd)
{
case GW_OWNER:
if (Wnd->spwndOwner != NULL) FoundWnd = Wnd->spwndOwner;
break;
case GW_HWNDFIRST: // 最上层兄弟(含自己)
if (Wnd->spwndParent != NULL)
{
FoundWnd = Wnd->spwndParent;
if (FoundWnd->spwndChild != NULL)
FoundWnd = FoundWnd->spwndChild; // = 父的链头
}
break;
case GW_HWNDNEXT: // 下一个(更底层)
if (Wnd->spwndNext != NULL) FoundWnd = Wnd->spwndNext;
break;
case GW_HWNDPREV: // 上一个(更上层)
if (Wnd->spwndPrev != NULL) FoundWnd = Wnd->spwndPrev;
break;
case GW_CHILD: // 第一个子窗口 = 子链头 = 最上层子窗口
if (Wnd->spwndChild != NULL) FoundWnd = Wnd->spwndChild;
break;
case GW_HWNDLAST: // 最底层兄弟(含自己)
FoundWnd = Wnd;
while (FoundWnd->spwndNext != NULL)
FoundWnd = FoundWnd->spwndNext; // 沿 spwndNext 走到底
break;
default:
EngSetLastError(ERROR_INVALID_GW_COMMAND);
break;
}
if (FoundWnd != NULL) Ret = UserHMGetHandle(FoundWnd);
return Ret;
}
用户态对应物:user32 的 GetWindow(user32/windows/window.c L1075-1150)是同样逻辑的用户态映射版本——ValidateHwnd 校验句柄后直接读共享映射的 WND 结构(DesktopPtrToUser),多了 GW_ENABLEDPOPUP 分支(经 NtUserCallHwnd(HWND_ROUTINE_DWP_GETENABLEDPOPUP) 查启用弹出窗)。SEH 保护跨映射访问。两套实现并存:user32 的用于应用 API 调用(快速路径),内核 IntGetWindow 用于 win32k 内部(WinPosFixupFlags 的已就位判定,L1639/L1651)。
GetWindow 是只沿兄弟链走的"扁遍历"(不递归子窗口);要全树遍历用 EnumChildWindows(见 4.12 节 IntBuildHwndList)。
4.8 IntGetLastTopMostWindow(winpos.c L238-256)— 置顶带分界点
PWND FASTCALL IntGetLastTopMostWindow(VOID)
{
PWND pWnd;
PDESKTOP rpdesk = gptiCurrent->rpdesk;
if ( rpdesk &&
(pWnd = rpdesk->pDeskInfo->spwnd->spwndChild) && // 桌面链头
pWnd->ExStyle & WS_EX_TOPMOST)
{
for (;;)
{
if (!pWnd->spwndNext) break; // 到链尾
if (!(pWnd->spwndNext->ExStyle & WS_EX_TOPMOST)) break; // 遇到第一个非置顶
pWnd = pWnd->spwndNext;
}
return pWnd; // 最后一个置顶窗口
}
return NULL; // 桌面没有置顶窗口
}
从桌面链头(最上层)沿 spwndNext 走到连续置顶带的末尾——返回"最底层的那个置顶窗口"。用途:
ActivateOtherWindowMin(winpos.c L296-299):当前窗口最小化/隐藏后找激活替补时,从IntGetLastTopMostWindow()->spwndNext开始找——即跳过置顶带,直接从普通带顶端开始找(置顶窗口如工具提示通常不应被选为激活替补);- 语义上它就是"置顶带与普通带的分界点":
IntGetLastTopMostWindow()->spwndNext是第一个普通窗口。
4.9 co_WinPosWindowFromPoint / co_WinPosSearchChildren(winpos.c L2999 / L2914)— 命中测试按 Z 序
WindowFromPoint 的递归实现(NtUserWindowFromPoint L3933 以桌面为 Scope 调用):
static PWND
co_WinPosSearchChildren(IN PWND ScopeWin, IN POINT *Point,
IN OUT USHORT *HitTest, IN BOOL Ignore)
{
HWND *List, *phWnd;
PWND pwndChild = NULL;
/* not visible */
if (!(ScopeWin->style & WS_VISIBLE)) return NULL;
/* not in window or in window region */
if (!IntPtInWindow(ScopeWin, Point->x, Point->y)) return NULL;
/* transparent */
if ((ScopeWin->ExStyle & (WS_EX_LAYERED|WS_EX_TRANSPARENT)) == (WS_EX_LAYERED|WS_EX_TRANSPARENT))
return NULL;
if (!Ignore && (ScopeWin->style & WS_DISABLED))
{ /* disabled child */
if ((ScopeWin->style & (WS_POPUP|WS_CHILD)) == WS_CHILD) return NULL;
*HitTest = HTERROR;
return ScopeWin;
}
/* not minimized and check if point is inside the window */
if (!(ScopeWin->style & WS_MINIMIZE) &&
RECTL_bPointInRect(&ScopeWin->rcClient, Point->x, Point->y))
{
UserReferenceObject(ScopeWin);
List = IntWinListChildren(ScopeWin); // ★ 子窗口从顶到底
if (List)
{
for (phWnd = List; *phWnd; ++phWnd) // ★ 自上而下遍历
{
if (!(pwndChild = ValidateHwndNoErr(*phWnd))) continue;
pwndChild = co_WinPosSearchChildren(pwndChild, Point, HitTest, Ignore);
if (pwndChild != NULL)
{
/* We found a window. Don't send any more WM_NCHITTEST messages */
ExFreePoolWithTag(List, USERTAG_WINDOWLIST);
UserDereferenceObject(ScopeWin);
return pwndChild; // 第一个命中即返回
}
}
ExFreePoolWithTag(List, USERTAG_WINDOWLIST);
}
UserDereferenceObject(ScopeWin);
}
if (ScopeWin->head.pti == PsGetCurrentThreadWin32Thread())
{
*HitTest = (USHORT)co_IntSendMessage(UserHMGetHandle(ScopeWin),
WM_NCHITTEST, 0, MAKELONG(Point->x, Point->y));
if ((*HitTest) == (USHORT)HTTRANSPARENT) return NULL; // 点击穿透
}
else
{
if (*HitTest == HTNOWHERE && pwndChild == NULL) *HitTest = HTCLIENT;
}
return ScopeWin;
}
Z 序语义:IntWinListChildren 返回从顶到底的数组,循环顺序即"最上层先试";找到第一个命中的子窗口就立即返回(递归展开处),不再向更底层的兄弟发 WM_NCHITTEST——注释明确写着 “Don’t send any more WM_NCHITTEST messages”。这就是"Z 序自上而下的命中测试":屏幕上最前面的窗口最先吃到鼠标。
co_WinPosWindowFromPoint(L2999-3030)只是外壳:补桌面 Scope、引用计数、把 HitTest 初始化为 HTNOWHERE,然后调 co_WinPosSearchChildren。
4.10 激活链(focus.c)— 激活时强制置顶
激活与 Z 序的核心交互在 focus.c。完整链条:
NtUserSetActiveWindow(hWnd) focus.c L1657
→ UserSetActiveWindow(Wnd) focus.c L1258 → IntUserSetActiveWindow(L1169)
→ co_IntSetForegroundAndFocusWindow(L920)
→ IntUserSetActiveWindow / 激活消息处理
→ co_IntSetActiveWindow(Wnd, bMouse, bFocus, Async) focus.c L1023
→ 发 WM_ACTIVATE / WM_NCACTIVATE / WM_ACTIVATEAPP(co_IntSendActivateMessages)
→ 更新队列 spwndActive / spwndActivePrev / spwndLastActive
→ 焦点移交(IntSendFocusMessages)
激活置顶的关键行在 co_IntSetForegroundAndFocusWindow 的同一消息队列分支(focus.c L868-877):
if (pumq->spwndActive == Wnd)
{
co_IntSendMessage(UserHMGetHandle(Wnd), WM_NCACTIVATE, TRUE, (LPARAM)UserHMGetHandle(Wnd));
UpdateShellHook(Wnd);
co_WinPosSetWindowPos(Wnd, HWND_TOP, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE);
}
当目标窗口已经(或刚刚)成为活动窗口时,co_WinPosSetWindowPos(Wnd, HWND_TOP, ..., SWP_NOSIZE|SWP_NOMOVE) 把它搬到 Z 序顶端——激活 = 置顶的直接实现。注意这里只传 SWP_NOSIZE|SWP_NOMOVE(不传 SWP_NOACTIVATE),WinPosFixupFlags 的"激活抬升"分支会看到 hwndInsertAfter == HWND_TOP 且窗口已是前台——若已在链头则补 SWP_NOZORDER 跳过重排,形成"点击已在前台的窗口不闪屏"的优化。
co_IntSetActiveWindow 的其他 Z 序相关职责:
WNDS_BEINGACTIVATED状态位(L1124)防重入:激活过程中再触发激活会直接返回;spwndLastActive(同级最近激活窗口)由co_IntSendActivateMessages维护,供ActivateOtherWindowMin找激活替补(见 4.14);- 队列
spwndActive/spwndActivePrev更新(L1094/L1121),gpqForeground/gpqForegroundPrev记录前台队列。
4.11 BringWindowToTop / GetTopWindow / 用户态封装(user32)
BringWindowToTop(user32 windows/window.c L68-78)
BOOL WINAPI BringWindowToTop(HWND hWnd)
{
return NtUserSetWindowPos(hWnd,
HWND_TOP,
0, 0, 0, 0,
SWP_NOSIZE | SWP_NOMOVE);
}
只调一次 SetWindowPos(HWND_TOP)。按 Win32 语义,BringWindowToTop 的"置顶 + 激活"由内核两步完成:① WinPosFixupFlags 的激活抬升(目标非前台窗口且无 SWP_NOACTIVATE → 改写为 HWND_TOPMOST/HWND_TOP,见 4.3.1);② co_WinPosSetWindowPos 步骤 9 的激活联动(非 SWP_NOACTIVATE 且非隐藏 → co_IntSetForegroundWindow,winpos.c L2278 附近)。所以一个 API 调用 = “搬 Z 序 + 抢前台”。
GetTopWindow(user32 windows/window.c L1156-1161)
HWND WINAPI GetTopWindow(HWND hWnd)
{
if (!hWnd) hWnd = GetDesktopWindow();
return GetWindow(hWnd, GW_CHILD);
}
“顶层子窗口” = spwndChild(子 Z 序链头)。桌面窗口的 GW_CHILD 就是最上层的顶级窗口。
其余薄封装(ntwrapper.h)
| API | 封装 |
|---|---|
SetWindowPos | L433-437:直通 NtUserSetWindowPos(无任何预处理) |
SetActiveWindow | L451-455:直通 NtUserSetActiveWindow |
ShowWindow | L439-443:直通 NtUserShowWindow |
WindowFromPoint | user32 windows/winpos.c L157-162:直通 NtUserWindowFromPoint |
GetActiveWindow | user32 windows/winpos.c L138-141:NtUserGetThreadState(THREADSTATE_ACTIVEWINDOW) |
设计要点:用户态对 Z 序零逻辑——所有算法(规范化/抬升/约束/链表)都在内核,user32 只是系统调用转发层。这与"句柄安全由内核统一保证"的 ReactOS 架构一致。
4.12 创建与 SetParent 的 Z 序(window.c)
co_UserCreateWindowEx 的链接段(window.c L2431-2438)
/* Link the window */
if (ParentWindow != NULL)
{
/* Link the window into the siblings list */
if ((Cs->style & (WS_CHILD | WS_MAXIMIZE)) == WS_CHILD)
IntLinkHwnd(Window, HWND_BOTTOM); // ★ 子窗口默认插到最底层
else
IntLinkHwnd(Window, hwndInsertAfter); // 顶层窗口默认 HWND_TOP(见下)
}
创建时的 Z 序默认值(L2282):hwndInsertAfter = HWND_TOP。WH_CBT 钩子可以改写它:HCBT_CREATEWND 的 CBT_CREATEWNDW 结构携带 hwndInsertAfter(L2311),应用在钩子里改它(L2325 写回),创建出的窗口就插到指定位置——这是"创建时控制 Z 序"的唯一入口(CreateWindowEx 本身没有 Z 序参数)。
两个默认值的设计语义:
- 普通子窗口 →
HWND_BOTTOM:新子窗口放到兄弟最底层,不打扰既有子窗口的 Z 序(Windows 兼容行为——先创建的控件在 Z 序上层,Tab 顺序等按创建序排列); - 顶层窗口 →
HWND_TOP:新顶层窗口出现在最上层(用户新建窗口总是在最前面)。 - 特例
(WS_CHILD | WS_MAXIMIZE) == WS_CHILD:最大化子窗口(MDI 子窗口)例外,走hwndInsertAfter(通常仍是 HWND_TOP,MDI 子窗口最大化时要置顶)。
co_IntSetParent(window.c L1157-1292)— SetParent 的 Z 序
/* Even if WndNewParent == WndOldParent continue because the
* child window (Wnd) should be moved to the top of the z-order */
/* Unlink the window from the siblings list */
IntUnlinkWindow(Wnd);
Wnd->ExStyle2 &= ~WS_EX2_LINKED;
WndSetParent(Wnd, WndNewParent);
if (Wnd->style & WS_CHILD && Wnd->spwndOwner && Wnd->spwndOwner->ExStyle & WS_EX_TOPMOST)
{
ERR("SetParent Top Most from Pop up\n");
Wnd->ExStyle |= WS_EX_TOPMOST; // owner 置顶 → 子窗口跟着置顶
}
/* Link the window with its new siblings */
IntLinkHwnd(Wnd,
((0 == (Wnd->ExStyle & WS_EX_TOPMOST) &&
UserIsDesktopWindow(WndNewParent)) ? HWND_TOP : HWND_TOPMOST));
...
co_WinPosSetWindowPos(Wnd,
(0 == (Wnd->ExStyle & WS_EX_TOPMOST) ? HWND_TOP : HWND_TOPMOST),
pt.x, pt.y, 0, 0, swFlags); // 再补一次 SetWindowPos 置顶
SetParent 后窗口必须到新父的 Z 序顶端(注释明说),且旧父/新父是桌面的场景下非置顶窗口用 HWND_TOP(普通带顶端)、否则 HWND_TOPMOST(进置顶带)。注意 owner 置顶时子窗口继承置顶位的规则——与 IntLinkHwnd 的 HWND_TOP 分支(L1031-1035)一致。
4.13 IntWinListChildren 家族与 EnumWindows 的 Z 序遍历(window.c)
IntBuildHwndList / IntPopulateHwndList(window.c L1422 / L1393)
EnumWindows/EnumChildWindows/EnumDesktopWindows/EnumThreadWindows 的用户态 API 最终都汇到 NtUserBuildHwndList(L1513,注释自述 “As best as I can figure, this function is used by EnumWindows, EnumChildWindows, EnumDesktopWindows, & EnumThreadWindows”)。其遍历核心 IntPopulateHwndList:
PWINDOWLIST FASTCALL IntPopulateHwndList(PWINDOWLIST pwl, PWND pwnd, DWORD dwFlags)
{
ASSERT(!WL_IS_BAD(pwl));
for (; pwnd; pwnd = pwnd->spwndNext) // ★ 沿 Z 序兄弟链(自顶向下)
{
if (!pwl->pti || pwl->pti == pwnd->head.pti)
{
*(pwl->phwndLast) = UserHMGetHandle(pwnd);
++(pwl->phwndLast);
if (pwl->phwndLast == pwl->phwndEnd && !IntGrowHwndList(&pwl)) break;
}
if ((dwFlags & IACE_CHILDREN) && pwnd->spwndChild) // 递归进入子链
{
pwl = IntPopulateHwndList(pwl, pwnd->spwndChild, IACE_CHILDREN | IACE_LIST);
if (WL_IS_BAD(pwl)) break;
}
if (!(dwFlags & IACE_LIST)) break;
}
return pwl;
}
遍历顺序:先 spwndChild(最深兄弟链)沿 spwndNext 收集,再递归进第一个子窗口的 spwndChild——即 深度优先、每层按 Z 序自上而下。返回给应用的窗口数组就是"按 Z 序排列的枚举结果"(EnumWindows 的文档语义:枚举顺序为 Z 序)。WINDOWLIST 结构有 gpwlCache 缓存(L1427-1442,避免频繁分配)与 gpwlList 全局链表(跟踪所有未释放列表)。
IntIsTopLevelWindow / IntGetNonChildAncestor / IntValidateOwnerDepth(window.c L359/L351/L367)
BOOL FASTCALL IntIsTopLevelWindow(PWND pWnd)
{
if ( pWnd->spwndParent &&
pWnd->spwndParent == co_GetDesktopWindow(pWnd) ) return TRUE;
return FALSE;
}
PWND FASTCALL IntGetNonChildAncestor(PWND pWnd) // 沿父链找到第一个非 WS_CHILD 的祖先
{
while(pWnd && (pWnd->style & (WS_CHILD | WS_POPUP)) == WS_CHILD)
pWnd = pWnd->spwndParent;
return pWnd;
}
BOOL FASTCALL IntValidateOwnerDepth(PWND Wnd, PWND Owner) // owner 链深度校验
{
INT Depth = 1;
for (;;)
{
if ( !Owner ) return gNestedWindowLimit >= Depth;
if (Owner == Wnd) break;
Owner = Owner->spwndOwner;
Depth++;
}
return FALSE;
}
三者都是 Z 序/拥有链上的"关系查询":是否桌面直属(顶层窗口判定)、非子祖先(激活/命中时的"根")、owner 链深度(gNestedWindowLimit 防止 owner 环)。IntGetNonChildAncestor 被 co_IntSetActiveWindow(focus.c L1140)用于"焦点窗口的非子祖先是否等于活动窗口"的判定。
4.14 vis.c 如何依赖 Z 序计算可见区域(vis.c L12-142)
VIS_ComputeVisibleRegion(Wnd, ClientArea, ClipChildren, ClipSiblings) 是 winpos.c 计算 VisBefore/VisAfter 的唯一来源(co_WinPosSetWindowPos L1900/L1917),它的算法完全建立在 Z 序链表上:
PREGION FASTCALL
VIS_ComputeVisibleRegion(PWND Wnd, BOOLEAN ClientArea,
BOOLEAN ClipChildren, BOOLEAN ClipSiblings)
{
PREGION VisRgn, ClipRgn;
PWND PreviousWindow, CurrentWindow, CurrentSibling;
if (!Wnd || !(Wnd->style & WS_VISIBLE)) return NULL;
VisRgn = ClientArea ? IntSysCreateRectpRgnIndirect(&Wnd->rcClient)
: IntSysCreateRectpRgnIndirect(&Wnd->rcWindow);
/*
* Walk through all parent windows and for each clip the visible region
* to the parent's client area and exclude all siblings that are over
* our window.
*/
PreviousWindow = Wnd;
CurrentWindow = Wnd->spwndParent;
while (CurrentWindow)
{
if (!VerifyWnd(CurrentWindow)) { ... return NULL; }
if (!(CurrentWindow->style & WS_VISIBLE)) { ... return NULL; }
/* ① 裁剪到父客户区 */
ClipRgn = IntSysCreateRectpRgnIndirect(&CurrentWindow->rcClient);
IntGdiCombineRgn(VisRgn, VisRgn, ClipRgn, RGN_AND);
REGION_Delete(ClipRgn);
/* ② 扣除"压在本窗口上面的兄弟"(Z 序关键) */
if ((PreviousWindow->style & WS_CLIPSIBLINGS) ||
(PreviousWindow == Wnd && ClipSiblings))
{
CurrentSibling = CurrentWindow->spwndChild; // ★ 从最上层开始
while ( CurrentSibling != NULL &&
CurrentSibling != PreviousWindow ) // ★ 走到自己为止
{
if ((CurrentSibling->style & WS_VISIBLE) &&
!(CurrentSibling->ExStyle & WS_EX_TRANSPARENT))
{
ClipRgn = IntSysCreateRectpRgnIndirect(&CurrentSibling->rcWindow);
/* 与 hrgnClip(异形窗口区域)求交 */
...
IntGdiCombineRgn(VisRgn, VisRgn, ClipRgn, RGN_DIFF); // ★ 逐个扣除
REGION_Delete(ClipRgn);
}
CurrentSibling = CurrentSibling->spwndNext; // 往更底层走
}
}
PreviousWindow = CurrentWindow;
CurrentWindow = CurrentWindow->spwndParent; // 沿父链上溯
}
/* ③ ClipChildren:扣除自己子窗口占掉的区域 */
...
/* ④ 与自己的 hrgnClip(SetWindowRgn)求交 */
...
return VisRgn;
}
Z 序在其中的角色:兄弟遍历 CurrentWindow->spwndChild 起步、CurrentSibling != PreviousWindow 终止——遍历区间正是"Z 序上位于目标窗口之上的所有兄弟"。被扣除的每个兄弟窗口都是"屏幕上的遮挡者"。这就是"可见区域 = 自己 − 所有更上层的兄弟"的链表实现。若 Z 序链表错乱(如 owned popup 排到 owner 下面),RGN_DIFF 会扣错对象,屏幕出现"弹出窗被主人盖住"的经典 bug(CORE-6129)。
co_VIS_WindowLayoutChanged(vis.c L144-175)是 Z 序变化后的"暴露区域刷新":窗口移走后,NewlyExposed(原来被它盖住、现在露出的区域)被平移到父客户区坐标,co_UserRedrawWindow(Parent, RDW_FRAME|RDW_ERASE|RDW_INVALIDATE|RDW_ALLCHILDREN) 让父与兄弟重画——由 co_WinPosSetWindowPos 步骤 8 的 ExposedRgn 驱动(见 _3 第 7.3 节)。
5. Z 序调整全流程(重点章节)
本节把 SetWindowPos(hwnd, hWndInsertAfter, ...) 从用户调用到链表重新链接的每一步拆开,标注每一步的代码位置、执行者与"如果这里出错会怎样"。
5.1 全流程时间线
用户调用 SetWindowPos(hwnd, hWndInsertAfter, x, y, cx, cy, flags)
│ user32 ntwrapper.h L433-437(纯转发,零逻辑)
▼
NtUserSetWindowPos winpos.c L3609
① 校验 hwnd:非桌面/消息窗口(L3626-3631)
② 校验 hWndInsertAfter:四伪句柄 or 真实窗口(L3633-3644)
③ 坐标钳制 ±32767 / [0,32767](L3646-3660)
④ UserRefObjectCo 持有引用 → co_WinPosSetWindowPos → Deref(L3662-3664)
│
▼
co_WinPosSetWindowPos winpos.c L1798
⑤ 组装 WINDOWPOS:hwndInsertAfter 进入 WinPos.hwndInsertAfter(L1838-1844)
⑥ (SWP_ASYNCWINDOWPOS → WM_ASYNC_SETWINDOWPOS 异步执行,L1846-1865)
⑦ co_WinPosDoWinPosChanging:发 WM_WINDOWPOSCHANGING(L1867)
应用可改写 WinPos.x/y/cx/cy/flags/hwndInsertAfter!
⑧ 窗口可能被应用销毁 → IntIsWindow 重新校验(L1870-1875)
│
▼
WinPosFixupFlags winpos.c L1562(Z 序参数规范化,见 4.3)
⑨a 坐标钳制/可见性互斥/几何去冗余(L1569-1606)
⑨b 激活抬升:非前台 + 无 NOACTIVATE/HIDEWINDOW → 清 NOZORDER、
改 hwndInsertAfter = TOPMOST/TOP(L1608-1618)
⑨c 符号扩展修正:0xffff→HWND_TOPMOST、0xfffe→HWND_NOTOPMOST(L1623-1631)
⑨d 四伪句柄已就位判定:已就位 → 补 SWP_NOZORDER(L1633-1658)
⑨e 真句柄:兄弟校验(不同父 → return FALSE → 外层 return TRUE,L1669-1678)、
已就位判定(L1686-1690)
⑨f 返回值 FALSE → co_WinPosSetWindowPos 直接 return TRUE(L1878-1882,静默无操作)
│
▼
WinPosDoOwnedPopups winpos.c L1366(owned popup 约束,见第 7 章)
⑩ 触发条件:无 NOOWNERZORDER + (NOZORDER 未置 或 SHOW/HIDE 置位)
+ 根祖先是桌面(L1884-1890)
⑩a 有 owner 且非 HWND_TOPMOST:桌面列表找 owner,hWndInsertAfter 修正为
"owner 前一格"(L1392-1439),已就位则提前返回
⑩b 递归把本窗口的 owned popups 逐个搬到 hWndInsertAfter 之后(L1496-1518)
⑩c 返回修正后的 hWndInsertAfter(覆盖 WinPos.hwndInsertAfter)
│
▼
(区域快照/NC 计算省略:L1892-1945,见 _3)
│
▼
IntLinkHwnd window.c L985(★ 链表操作的最终落点)
⑪ 条件:无 SWP_NOZORDER 且 hwnd != UserGetShellWindow()(winpos.c L1947-1951)
⑪a HWND_NOTOPMOST:非置顶且已链接 → return;否则清位回落 HWND_TOP(L987-993)
⑪b IntUnlinkWindow:从旧位置摘除(L995 → window.c L1353)
⑪c 按四伪句柄/真句柄分派:
- HWND_BOTTOM → 走到链尾,IntLinkWindow(Wnd, 链尾),清 TOPMOST(L997-1010)
- HWND_TOPMOST → IntLinkWindow(Wnd, NULL) 插链头,置 TOPMOST(L1011-1016)
- HWND_TOP → 找第一个非置顶窗口(遇 owner 置顶则自我升级),
插到它前面(L1017-1041)
- 真句柄 → IntLinkWindow(Wnd, H),TOPMOST 位按参照继承/清除(L1042-1080)
⑪d Wnd->ExStyle2 |= WS_EX2_LINKED(L1081)
│
▼
几何应用与收尾(winpos.c L1953-2392,见 _3 第 4.2 节步骤 7-9)
⑫ 写入 rcWindow/rcClient、可见性位、区域重算(VisAfter)
⑬ Z 序变化导致的遮挡差:
- 移上层:DirtyRgn = VisAfter − CopyRgn → 自身补画
- 移下层:ExposedRgn = VisBefore(平移) − VisAfter
→ co_VIS_WindowLayoutChanged 让被挡兄弟重画(vis.c L144)
⑭ WM_WINDOWPOSCHANGED(总是携带最终矩形)
⑮ 激活联动:非 NOACTIVATE → co_IntSetForegroundWindow(L2278 附近)
5.2 每一步的"失败模式"对照
| 步骤 | 出错的典型场景 | 结果 |
|---|---|---|
| ② 插入参照校验 | hWndInsertAfter 是已销毁窗口的句柄 | NtUserSetWindowPos 返回 FALSE,SetLastError(ERROR_INVALID_WINDOW_HANDLE)(经 UserGetWindowObject) |
| ⑧ CHANGING 后销毁 | 应用在 WM_WINDOWPOSCHANGING 里 DestroyWindow | IntIsWindow 失败 → 返回 FALSE,不再触碰已销毁对象(引用计数保证对象内存仍有效) |
| ⑨e 兄弟校验 | 跨父窗口指定插入参照 | WinPosFixupFlags 返回 FALSE → co_WinPosSetWindowPos 返回 TRUE——静默成功但什么都没做(Wine 测试确认的 Windows 兼容行为) |
| ⑩a owner 找不到 | 窗口有 spwndOwner 但 owner 不在桌面列表(理论上不可能) | 循环不命中,hWndInsertAfter 保持原值,继续走 IntLinkHwnd |
| ⑪c 真句柄失效 | 插入参照在规范化后已被销毁 | UserGetWindowObject 返回 NULL → 兜底 IntLinkHwnd(Wnd, HWND_TOP)(L1049-1053),插到普通带顶端 |
| ⑪ 自链 | hwndInsertAfter 变成窗口自己(CORE-6554 场景) | IntLinkWindow 的 ASSERT(Wnd != WndInsertAfter) 在调试构建崩溃;WinPosDoOwnedPopups 的 List[i-1] != 自己 防护(L1414)从源头杜绝 |
| ⑬ 遮挡重算 | Z 序改了但 SWP_NOREDRAW | VisAfter 不重算、不重绘——调用者承诺自己处理(DeferWindowPos 批量场景常见) |
5.3 一个完整例子的推演:把普通窗口 W 插到置顶窗口 T 之后
假设桌面子链表当前为 [T(TOPMOST), A(普通), W(普通), B(普通)],应用调用 SetWindowPos(W, T, ..., 0)(无 NOZORDER、无 NOACTIVATE):
NtUserSetWindowPos:W、T 都合法,坐标钳制通过。co_WinPosSetWindowPos组装 WinPos:hwndInsertAfter = T。WinPosFixupFlags:- 激活抬升:W 不是前台窗口、非子窗口、无 NOACTIVATE/HIDEWINDOW、
hwndInsertAfter == T(真句柄,非 TOPMOST/NOTOPMOST)→ 清SWP_NOZORDER(本来就未置)、把hwndInsertAfter改为HWND_TOPMOST(若 W 置顶)或HWND_TOP。W 非置顶 →hwndInsertAfter = HWND_TOP。 - 至此调用者的"插到 T 后面"意图被激活语义覆盖——W 将到普通带顶端。
- 激活抬升:W 不是前台窗口、非子窗口、无 NOACTIVATE/HIDEWINDOW、
WinPosDoOwnedPopups:W 无 owner(或 owner 是桌面),走第二段:hwndInsertAfter == HWND_TOP且 W 非置顶 → 跳过置顶带(i从 T 跳到 A)。然后遍历 A、W、B:无 owned popup → 返回HWND_TOP。IntLinkHwnd(W, HWND_TOP):W 非置顶 → 从链头走:T 是置顶(继续)、T 不是 W 的 owner(继续)→ A 非置顶(停)→IntLinkWindow(W, A->spwndPrev = T)→ 链表变为[T, W, A, B]。- 结果:W 从"普通带中间"被移到"置顶带末尾(普通带顶端)"——比 A、B 都靠前。这正是"SetWindowPos 未带 NOACTIVATE 会激活并置顶"的教科书行为。
若同样的调用带上 SWP_NOACTIVATE:步骤 3 的激活抬升不触发,hwndInsertAfter 保持 T;IntLinkHwnd(W, T) 走真句柄分支 → IntLinkWindow(W, T) → 链表变为 [T, W, A, B]——W 插到 T 之后(置顶带边缘)。注意此时 W 的 WS_EX_TOPMOST 位:参照 T 是置顶、且 T 的 spwndNext(A)非置顶 → 按 L1074-1078 规则,W 不继承置顶位,但它物理上位于 T 与 A 之间(置顶带与普通带交界)。这是"真句柄插入允许窗口紧贴置顶窗口下方"的合法形态——IntGetLastTopMostWindow 会在此返回 T。
5.4 可见性重算与重绘(Z 序变化的直接后果)
IntLinkHwnd 完成后,co_WinPosSetWindowPos 剩余流程按 Z 序变化自动适配(无需特判"Z 序变没变"):
- VisAfter 重算(L2050 附近):
VIS_ComputeVisibleRegion(Window, ...)按新链表顺序重新扣除遮挡; - DirtyRgn(自身补画):
VisAfter − CopyRgn中新增可见的部分 →IntInvalidateWindows(RDW_ERASE|RDW_FRAME|RDW_INVALIDATE|RDW_ALLCHILDREN); - ExposedRgn(兄弟补画):
VisBefore(平移) − VisAfter→co_VIS_WindowLayoutChanged(vis.c L144)→co_UserRedrawWindow(父, RDW_FRAME|RDW_ERASE|RDW_INVALIDATE|RDW_ALLCHILDREN)——被 W 挡住、现在露出的 A/B 区域被标记重绘; - WNDS_SENDNCPAINT:Z 序变化 + 可见 → 置位,painting.c 后续发
WM_NCPAINT(标题栏颜色/状态随激活态变化)。
一句话:Z 序调整本身不画任何像素,它只改变"谁挡谁"的关系,重绘由 vis.c 的区域差与 painting.c 的更新区域机制接力完成。
6. TOPMOST 机制
6.1 置顶带模型
WS_EX_TOPMOST 把兄弟链表分成两个连续区带:
链头(最上) 链尾(最底)
├── 置顶带(TOPMOST band)──┤├── 普通带(normal band)──┤
[T1][T2][T3][T4] [A][B][C][D][E]
▲ ▲
│ │
IntGetLastTopMostWindow() 第一个非置顶窗口
返回 T4(置顶带分界点) = T4->spwndNext
不变量:
- 置顶带永远在普通带之上:任何普通窗口都不能插进置顶带内部(
IntLinkHwnd的 HWND_TOP 分支只走到"第一个非置顶窗口"就停); - 置顶带是连续的:置顶窗口之间没有普通窗口(
IntGetLastTopMostWindow的连续遍历假设); - 置顶位是"入带凭证":插入动作决定位值,位值决定后续插入位置——二者由
IntLinkHwnd在同一个函数里维护,不会出现"位是置顶但排在普通带"或反之。
6.2 WS_EX_TOPMOST 位的三条维护规则(IntLinkHwnd 内)
| 规则 | 代码位置 | 语义 |
|---|---|---|
| 置顶请求置位 | L1015:`Wnd->ExStyle | = WS_EX_TOPMOST` |
| 置底/取消清位 | L1009(BOTTOM)、L991(NOTOPMOST) | 出置顶带 |
| 插入位置继承 | L1067-1079(真句柄分支) | 插到普通窗口之后 → 清位;插到两个置顶窗口之间 → 置位 |
| owner 强制升级 | L1031-1035(HWND_TOP 分支)、L1233-1239(SetParent) | 自己的 owner 是置顶窗口 → 自己也置顶 |
第 4 条规则的详细代码(HWND_TOP 分支):
if (!(Wnd->ExStyle & WS_EX_TOPMOST)) /* put it above the first non-topmost window */
{
while (WndInsertBefore != NULL && WndInsertBefore->spwndNext != NULL)
{
if (!(WndInsertBefore->ExStyle & WS_EX_TOPMOST))
break; // 到普通带边界
if (WndInsertBefore == Wnd->spwndOwner) // ★ owner 是置顶窗口
{
Wnd->ExStyle |= WS_EX_TOPMOST; // 本窗口强制升级进置顶带
break;
}
WndInsertBefore = WndInsertBefore->spwndNext;
}
}
IntLinkWindow(Wnd, WndInsertBefore ? WndInsertBefore->spwndPrev : NULL);
为什么 owner 置顶则 popup 必须置顶:owned popup(如工具提示、下拉菜单)必须盖住 owner(第 7 章);若 owner 在置顶带而 popup 在普通带,popup 会被所有置顶窗口盖住,也就可能被自己的 owner 盖住——约束被破坏。所以"popup 不低于 owner"的强形式是"owner 进置顶带则 popup 跟着进"。
6.3 HWND_TOPMOST / HWND_NOTOPMOST 切换
进入置顶(经典写法):
SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
链路:NtUserSetWindowPos → WinPosFixupFlags(HWND_TOPMOST 分支:已置顶且已是链头 → SWP_NOZORDER,否则继续)→ WinPosDoOwnedPopups(hWndInsertAfter == HWND_TOPMOST 时跳过 owner 修正,直接走第二段)→ IntLinkHwnd(HWND_TOPMOST) → 插链头 + 置位。
退出置顶:
SetWindowPos(hwnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
链路:IntLinkHwnd(HWND_NOTOPMOST) → 清位 → 回落 HWND_TOP → 插到置顶带末尾(普通带顶端)。WinPosFixupFlags 的 HWND_NOTOPMOST 分支(L1654-1657)已判定"本非置顶 → SWP_NOZORDER",所以普通窗口请求 HWND_NOTOPMOST 是无操作。
注意:HWND_NOTOPMOST 与 HWND_TOP 的实际插入位置相同(都是"最后一个置顶窗口之后"),区别只在位值变化(清位 vs 保持/按需升级)。WinPosFixupFlags 的激活抬升(L1616)在窗口置顶时用 HWND_TOPMOST、非置顶时用 HWND_TOP,从不直接产生 HWND_NOTOPMOST——取消置顶是显式动作,不因激活隐式发生。
6.4 IntGetLastTopMostWindow 的分界点用途
if ((pWndTopMost = IntGetLastTopMostWindow()))
pWndChild = pWndTopMost->spwndNext; // 从普通带顶端开始找
else
pWndChild = Wnd->spwndParent->spwndChild; // 无置顶带 → 从整条链的链头开始
ActivateOtherWindowMin(winpos.c L296-299)用它确定"激活替补搜索的起点":跳过置顶带。理由:置顶带里的窗口大多是工具提示(WS_EX_TOOLWINDOW)、任务栏、菜单之类不适合当普通激活目标的窗口;从普通带顶端(用户最可能关注的顶层应用窗口)开始找更合理。这也是为什么 IntGetLastTopMostWindow 被特意实现为"找最后一个置顶窗口"而非第一个——调用方需要的是分界点本身。
7. owned popup 约束
7.1 问题与历史
约束:WS_POPUP 且带 owner 的顶层窗口(模态对话框、下拉菜单、工具提示、属性页)在 Z 序上必须位于 owner 之上。
违反时的症状(CORE-6129/CORE-6554):
- CORE-6129:模态对话框被 owner 盖住——应用调用
SetWindowPos(owner, HWND_TOP)把 owner 提到最前时,对话框没有跟着提升,用户在对话框外点击后对话框消失(模态被绕过); - CORE-6554:多个 owned popup 相互之间的顺序错乱/自链——把 popup 插到"owner 前"的修正逻辑曾把
hwndInsertAfter改成 popup 自己,IntLinkWindow链接成环,窗口树崩溃。
修复策略:不是"禁止应用动 owner 的 Z 序",而是每次 Z 序操作后自动修复 popup 的位置——这就是 WinPosDoOwnedPopups(winpos.c L1366,在第 4.4 节已逐段展开)。
7.2 算法总结(两遍扫描)
WinPosDoOwnedPopups(Window, hWndInsertAfter):
第一遍(仅当 Window 有 owner 且非 HWND_TOPMOST 请求):
在桌面子窗口列表中找到 Window 的 owner 的位置 i
├─ i>0 且 List[i-1] != Window → hWndInsertAfter = List[i-1]
│ (把 Window 的插入点修正为"owner 前一格")
├─ i>0 且 List[i-1] == Window → 已就位,直接返回
└─ i==0(owner 是链头)→ hWndInsertAfter = owner置顶 ? HWND_TOPMOST : HWND_TOP
第二遍(无条件执行,除非提前返回):
从插入点开始向后遍历桌面子窗口列表,
对每个"spwndOwner == Window"的窗口:
递归 co_WinPosSetWindowPos(W, hWndInsertAfter, ...,
NOMOVE|NOSIZE|NOACTIVATE|NOSENDCHANGING|DEFERERASE)
hWndInsertAfter = W // 逐个堆叠,保持 popup 间相对顺序
不变量:“Window 插到 owner 之前” + “所有 owned popups 插到 Window 之后” ⇒ 最终的链表顺序为 [... popupN ... popup1, Window, Owner, ...]——每个 popup 都在 Window 与 Owner 之间、Window 在 Owner 之上、popup 全部在 Window 之下但仍在 Owner 之上。owner 链(owner 也有 owner 时)由递归天然处理:WinPosDoOwnedPopups 只处理直接 owner 关系,但每个 popup 自己也是"Window",它被递归调用时又会把自己的 popups 抬到它上面。
7.3 递归调用的参数选择与代价控制
co_WinPosSetWindowPos(Wnd, hWndInsertAfter, 0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE |
SWP_NOSENDCHANGING | SWP_DEFERERASE);
| 标志 | 作用 |
|---|---|
SWP_NOMOVE|SWP_NOSIZE | 不移动/不缩放(纯 Z 序操作) |
SWP_NOACTIVATE | 不抢激活(popup 搬运不能改变激活窗口) |
SWP_NOSENDCHANGING | 不再发 WM_WINDOWPOSCHANGING(第一遍已发过) |
SWP_DEFERERASE | 延迟擦除,避免每搬一个 popup 同步擦一次屏 |
代价:递归调用的 WinPosFixupFlags 里"激活抬升"不会触发(SWP_NOACTIVATE 已置);owned popup 的 owned popup 会继续递归——链深通常只有 1-2 层(对话框再弹对话框),IntValidateOwnerDepth(window.c L367)用 gNestedWindowLimit 限制 owner 链深度防止极端嵌套。
7.4 与 HWND_BOTTOM 的冲突豁免
WinPosDoOwnedPopups 对 hWndInsertAfter == HWND_BOTTOM 的处理(L1454-1459):
if (hWndInsertAfter == HWND_BOTTOM)
{
ERR("Window is HWND_BOTTOM hwnd %p\n", hWndInsertAfter);
if (List) ExFreePoolWithTag(List, USERTAG_WINDOWLIST);
goto done; // 直接跳过 popup 搬运
}
语义:把窗口放到 Z 序最底(HWND_BOTTOM)与"popup 必须在 owner 之上"本质冲突——owner 也要跟着往下走(否则 popup 压在 owner 上面,但 owner 却到了最底,popup 也得在最底)。ReactOS 的处理是:HWND_BOTTOM 时豁免 popup 约束(ERR 提示但继续执行),窗口连 popup 一起沉底。这是对 Windows 行为的有意近似:Windows 中 SetWindowPos(hwnd, HWND_BOTTOM) 同样会把整组(owner + popups)沉底。
7.5 其他入口对约束的配合
owned popup 约束不止在 WinPosDoOwnedPopups 一处,它是一条横切不变式,各入口以不同方式维护:
| 入口 | 维护方式 |
|---|---|
SetWindowPos/ShowWindow/SetWindowPlacement | WinPosDoOwnedPopups(winpos.c L1889) |
IntLinkHwnd(HWND_TOP) | 遍历时遇到 owner 置顶 → 自己升级置顶(L1031-1035) |
SetParent | owner 置顶 → 子窗口继承置顶位(L1233-1239) |
co_WinPosMinMaximize 最小化/还原 | IntShowOwnedPopups(Wnd, FALSE/TRUE)(L2507/L2534/L2555,window.c L4674)隐藏/恢复 owned popup |
co_WinPosShowWindow | 最小化时 IntShowOwnedPopups(Wnd, FALSE)(L2714);最大化后 ShowOwned 恢复(L2864) |
| 创建 owned popup | co_UserCreateWindowEx 顶层窗口默认 HWND_TOP 插入(本身就在 owner 上方或会触发修正) |
IME 窗口 | IntWinListOwnedPopups 排除默认 IME 窗口(window.c L328),IME 窗口不参与 popup 搬运 |
8. 激活与 Z 序
8.1 "激活置顶"的三重保险
| 层 | 机制 | 代码位置 | 触发条件 |
|---|---|---|---|
| ① 显式置顶 | co_IntSetForegroundAndFocusWindow 中 co_WinPosSetWindowPos(Wnd, HWND_TOP, ..., SWP_NOSIZE|SWP_NOMOVE) | focus.c L876 | 窗口已是(或刚成为)活动窗口,同消息队列分支 |
| ② 隐式抬升 | WinPosFixupFlags 激活抬升:清 SWP_NOZORDER、改 hwndInsertAfter | winpos.c L1608-1618 | 非前台窗口 + 非子窗口 + 无 SWP_NOACTIVATE/HIDEWINDOW |
| ③ 收尾激活 | co_WinPosSetWindowPos 步骤 9 调 co_IntSetForegroundWindow | winpos.c L2278 附近 | 非 SWP_NOACTIVATE + 非隐藏 + 非子窗口 |
①是"激活发生后把窗口带上来",②是"SetWindowPos 隐含激活意图时把 Z 序意图改成置顶",③是"Z 序动了之后把激活状态同步过来"。三者互相配合:
- ①为主:鼠标/键盘交互激活(
WM_MOUSEACTIVATE→co_IntMouseActivateWindow,focus.c L1248)走显式路径; - ②为防:应用代码
SetWindowPos(hwnd, HWND_TOP, ...)(未带 NOACTIVATE)时,②保证即使①没跑(如WM_NCACTIVATE分支被跳过),窗口也会置顶; - ③为尾:Z 序/可见性变化后激活状态必须一致(如
ShowWindow(SW_SHOW)显示一个非活动窗口,③会把它带到前台——除非带SWP_NOACTIVATE)。
8.2 WM_ACTIVATE 联动与 Z 序
co_IntSendActivateMessages(co_IntSetActiveWindow L1133)在激活切换时派发:
- 旧活动窗口收
WM_ACTIVATE(WA_INACTIVE)+WM_NCACTIVATE(FALSE)(标题栏变灰); - 新活动窗口收
WM_ACTIVATE(WA_ACTIVE)+WM_NCACTIVATE(TRUE)(标题栏高亮); - 跨进程时
WM_ACTIVATEAPP; spwndLastActive记录同级最近激活窗口——ActivateOtherWindowMin(winpos.c L304)在旧窗口隐藏/最小化时优先激活它("回到上一个窗口"的用户心智)。
NC 重绘与 Z 序的关系:WM_NCACTIVATE 触发非客户区(标题栏)重绘(nonclient.c),而标题栏颜色表达"激活/非激活"状态。Z 序置顶(focus.c L876)与 WM_NCACTIVATE(TRUE)(L872)在 co_IntSetForegroundAndFocusWindow 中相邻执行——先画标题栏、再搬 Z 序,视觉上"变亮 + 到最前"同时发生。
8.3 隐藏/最小化时的激活替补(Z 序搜索)
co_WinPosShowWindow 隐藏/最小化活动窗口后(L2866-2893),激活必须移交:
if ((Cmd == SW_HIDE) || (Cmd == SW_MINIMIZE))
{
if ( Wnd == pti->MessageQueue->spwndActive && pti->MessageQueue == IntGetFocusMessageQueue() )
{
if (UserIsDesktopWindow(Wnd->spwndParent))
{
if (!ActivateOtherWindowMin(Wnd)) // ① 先试"最小化专用替补"
co_WinPosActivateOtherWindow(Wnd); // ② 兜底:沿 Z 序找
}
else
co_WinPosActivateOtherWindow(Wnd);
}
/* Revert focus to parent */
if (Wnd == pti->MessageQueue->spwndFocus) { ... co_UserSetFocus(Parent); }
}
ActivateOtherWindowMin(winpos.c L285-365)的Z 序搜索顺序:
IntGetLastTopMostWindow()分界点(L296-299)→ 从普通带顶端开始;- 优先
spwndActivePrev(上一个激活窗口,L304); - 否则沿
spwndNext向下扫描:VerifyWnd+ 非WS_EX_NOACTIVATE+WS_VISIBLE且非WS_DISABLED+ 非WS_ICONIC,跳过WS_EX_TOOLWINDOW(工具窗口不当激活替补,L317); - 扫完整个普通带没有 → 换
FindTopWnd轮次从头再扫(允许工具窗口,L326); - 都没有 →
co_IntSetForegroundWindow(pWndTemp)兜底。
co_WinPosActivateOtherWindow(winpos.c L396-477)是更通用的版本,Z 序搜索顺序:
- popup 优先激活 owner(L412-417):
(Wnd->style & WS_POPUP) && (WndTo = Wnd->spwndOwner)→UserGetAncestor(WndTo, GA_ROOT),can_activate_window通过则用——对话框关闭后激活回到主窗口; - 沿
spwndNext找下一个可激活兄弟(L421-426); spwndActivePrev(L434);- 从桌面链头再扫一遍(L439-455),遇到 Wnd 自身则放弃;
- 通过
co_IntSetForegroundWindow(WndTo)或UserSetActiveWindow(WndTo)执行激活。
共同点:替补选择本质是Z 序链上的扫描 + 可激活性过滤(can_activate_window,L372,检查桌面/窗口站/可见性/样式)。Z 序链表在这里既是"候选顺序"也是"优先级"。
8.4 SetActiveWindow 显式调用的 Z 序效果
NtUserSetActiveWindow(hWnd)(focus.c L1657)→ UserSetActiveWindow(L1258)→ IntUserSetActiveWindow(L1169)→ co_IntSetForegroundAndFocusWindow(L1232)→(同队列已激活分支)co_WinPosSetWindowPos(Wnd, HWND_TOP, ...)(L876)。
关键语义:SetActiveWindow 只激活、不主动改 Z 序参数,但激活成功后 L876 的置顶调用必然执行。也就是说:任何成功的激活(显式或隐式)最终都会把窗口带到 Z 序顶端——除非窗口已经在前台(WinPosFixupFlags 已就位判定 → SWP_NOZORDER,重排被跳过)。
9. 命中测试与 Z 序
9.1 自上而下的链表遍历
WindowFromPoint(user32)→ NtUserWindowFromPoint(winpos.c L3933)→ co_WinPosWindowFromPoint(L2999)→ co_WinPosSearchChildren(L2914)。
co_WinPosSearchChildren 的 Z 序语义(4.9 节已展开代码):对每个窗口,先粗筛(可见/区域内/非透明/非禁用),然后从最上层子窗口开始递归(IntWinListChildren 自顶向下),第一个命中的子窗口立即胜出。整棵递归树的遍历顺序 = 屏幕上的视觉覆盖顺序:最前面的窗口最先被测试、最先命中。这意味着:
- 鼠标点在一个被完全遮挡的窗口上 → 命中上层窗口(遮挡者),下层窗口收不到鼠标;
- 点击穿透(
HTTRANSPARENT/WS_EX_TRANSPARENT)→ 当前窗口放弃命中,递归继续测试更下层兄弟; - 禁用顶层窗口 → 命中但返回
HTERROR(co_WinPosSearchChildrenL2943-2948),由Ignore参数决定是否忽略。
9.2 捕获窗口对 Z 序的覆盖
鼠标捕获(SetCapture,focus.c)存在时,命中测试跳过 Z 序:队列 spwndCapture 直接接收所有鼠标消息(mouse.c 的 UserProcessMouseInput 先查捕获)。这与 Z 序无关,但影响"命中测试是否被绕过"——拖拽(标题栏拖动、滚动条拖动)期间 Z 序不参与输入定向。
9.3 命中测试结果的 Z 序一致性
co_WinPosWindowFromPoint 在 UserEnterExclusive() 锁内执行(NtUserWindowFromPoint L3943)——遍历期间 Z 序链表不会被并发修改,保证"命中结果对应的 Z 序状态"在返回时依然真实。窗口句柄引用(UserReferenceObject/UserRefObjectCo)防止递归/回调期间窗口被销毁。
9.4 菜单与 Z 序
菜单弹出(TrackPopupMenuEx,menu.c)创建的是owned popup 窗口(owner 为调用窗口),它的置顶与盖住行为完全由第 7 章的约束保证:菜单窗口恒在 owner 之上,owner 被激活时菜单跟着被抬升(WinPosDoOwnedPopups 第二遍扫描会搬运"owner 为本窗口的 popup",菜单窗口在其中)。菜单关闭(WM_CANCELMODE/点击外部)时 IntShowOwnedPopups(FALSE) 隐藏菜单窗口。
10. 调用链与模块关系
10.1 调用链(mermaid)
链 1:SetWindowPos Z 序主链
链 2:TOPMOST 切换链
链 3:BringWindowToTop / 激活置顶链
链 4:命中测试链(按 Z 序)
链 5:激活搜索链(隐藏/最小化时找替补)
10.2 与相邻模块的关系
| 模块 | 协作点 |
|---|---|
| window.c | Z 序链表操作核心:IntLinkHwnd/IntLinkWindow/IntUnlinkWindow(L944-1082/L1353);遍历 IntGetWindow(L381)/IntWinListChildren(L274)/IntWinListOwnedPopups(L315)/IntBuildHwndList(L1422);创建期 IntLinkHwnd(Window, HWND_BOTTOM/hwndInsertAfter)(L2431-2438);SetParent 的摘除重挂(L1227-1244);IntShowOwnedPopups(L4674) |
| focus.c | 激活置顶 co_WinPosSetWindowPos(Wnd, HWND_TOP, ...)(L876);激活链 co_IntSetActiveWindow(L1023)/IntUserSetActiveWindow(L1169)/co_IntSetForegroundAndFocusWindow(L920);NtUserSetActiveWindow(L1657) |
| vis.c | VIS_ComputeVisibleRegion(L12)按 Z 序扣除兄弟遮挡;co_VIS_WindowLayoutChanged(L144)暴露区域刷新 |
| painting.c | IntInvalidateWindows/co_UserRedrawWindow/UserSyncAndPaintWindows 消费 Z 序变化产生的更新区域;WNDS_SENDNCPAINT 位协议 |
| msgqueue.c | 队列 spwndActive/spwndActivePrev/spwndFocus/spwndCapture 状态(激活/焦点/捕获的"谁");MsqSetStateWindow(L2517)写队列状态;激活消息 WM_ASYNC_SETACTIVEWINDOW 异步通道 |
| simplecall.c | NtUserCallOneParam 路由(IntShowOwnedPopups L494、激活 L535-539、前台 L656-664) |
| menu.c | TrackPopupMenuEx 的 popup 窗口依赖 owned popup 约束(第 7 章) |
| user32.dll | 薄封装:SetWindowPos(ntwrapper.h L433)/BringWindowToTop(window.c L68)/GetTopWindow(L1156)/GetWindow(L1075)/WindowFromPoint(winpos.c L157)——无 Z 序逻辑 |
| psdk/winuser.h | HWND_*(L1216-1219)、SWP_*(L1250-1264)常量;WINDOWPOS 结构 |
| ime.c | IME 窗口 WS_EX_TOPMOST 继承(L1415-1417);默认 IME 窗口排除出 owned popup 列表(window.c L328) |
10.3 并发与锁
- 排他锁:所有写 Z 序的路径(
NtUserSetWindowPos/ShowWindow/SetActiveWindow/DeferWindowPos/SetParent)都在UserEnterExclusive()内——链表结构在变更期间不可被其他线程读取到中间态; - 引用计数:
IntLinkHwnd前后持有UserRefObjectCo(co_WinPosSetWindowPosL3662/L3664);WinPosDoOwnedPopups递归前UserRefObjectCo(Wnd)(L1511)——防止搬运 popup 时应用在回调中销毁它;ReplaceWndPtr(WndSet*)在赋值时维护对象引用一致性; - Shell 窗口豁免:
IntLinkHwnd条件里WinPos.hwnd != UserGetShellWindow()(winpos.c L1948)——任务栏/Shell 的 Z 序由系统独占管理,应用 SetWindowPos 不重排它(配合 window.c L3920-3926 的SetWindowLong对 Shell 窗口WS_EX_TOPMOST的剥离逻辑); - 单线程化:win32k 全局互斥模型下,
gptiCurrent->rpdesk(IntGetLastTopMostWindowL241 用)与桌面pDeskInfo->spwnd的读取都是安全的。
10.4 调试指南
- 调试通道:
DBG_DEFAULT_CHANNEL(UserWinpos)(winpos.c L11)。启用后可看到WinPosDoOwnedPopups的TRACE("(%p) hInsertAfter = %p")(L1376)、WinPosFixupFlags的坐标换算、IntLinkHwnd的调用上下文; - Z 序错乱的症状与定位:
- 弹窗被 owner 盖住 →
WinPosDoOwnedPopups是否被跳过?检查SWP_NOOWNERZORDER是否被意外传入、Ancestor是否桌面(L1884-1890 条件); - 置顶窗口跑到普通窗口下面 → 检查
IntLinkHwnd的 TOPMOST 位维护(L1067-1079)——是否插到了两个置顶窗口之间却没置位; - 窗口树崩溃/自链 →
ASSERT(Wnd != WndInsertAfter)(IntLinkWindow L951-955)——检查hwndInsertAfter是否被WinPosDoOwnedPopups改成自己(CORE-6554 场景,L1414 防护); - 点击窗口不到最前 → 检查
WinPosFixupFlags激活抬升(L1608-1618)与co_IntSetForegroundAndFocusWindow(focus.c L876)是否被SWP_NOACTIVATE抑制; - 激活后窗口闪烁 → 检查已就位判定(L1633-1658)是否漏掉导致重复
IntLinkHwnd+ 全屏重绘;
- 弹窗被 owner 盖住 →
- 崩溃现场:
ASSERT_REFS_CO失败说明回调路径上引用计数不对称——重点检查WinPosDoOwnedPopups递归分支的 Ref/Deref 配对(L1511-1516)。
11. 源码索引
winpos.c(win32ss/user/ntuser/winpos.c)
| 函数/段 | 行号 | 本册覆盖 |
|---|---|---|
IsChildVisible | 226 | 级联可见性(父子链上溯) |
IntGetLastTopMostWindow | 238 | ★ 置顶带分界点 |
ActivateOtherWindowMin | 285 | 最小化激活替补(Z 序扫描) |
can_activate_window | 372 | 可激活性过滤 |
co_WinPosActivateOtherWindow | 396 | 通用激活替补(popup→owner 优先) |
WinPosFindIconPos | 786 | 图标排布(兄弟遍历避让) |
WinPosDoOwnedPopups | 1366 | ★★ owned popup 约束(CORE-6129/6554) |
WinPosInternalMoveWindow | 1532 | 窗口+子孙矩形平移(子窗口随父) |
WinPosFixupFlags | 1562 | ★★ Z 序参数规范化(符号扩展/抬升/已就位/兄弟校验) |
co_WinPosSetWindowPos | 1798 | ★ 心脏函数(Z 序段 L1836-1951) |
co_WinPosMinMaximize | 2444 | 三态切换(IntShowOwnedPopups 联动) |
co_WinPosShowWindow | 2629 | 显隐(子窗口 NOZORDER、置顶 HWND_TOPMOST) |
co_WinPosSearchChildren | 2914 | ★ 命中测试递归(Z 序自上而下) |
co_WinPosWindowFromPoint | 2999 | 命中测试入口 |
NtUserSetWindowPos | 3609 | ★ 系统调用入口(hwndInsertAfter 校验) |
NtUserWindowFromPoint | 3933 | WindowFromPoint 系统调用 |
window.c(win32ss/user/ntuser/window.c)
| 函数 | 行号 | 本册覆盖 |
|---|---|---|
IntWinListChildren | 274 | 兄弟句柄数组(自顶向下) |
IntWinListOwnedPopups | 315 | owned popup 句柄数组(排除 IME) |
IntGetNonChildAncestor | 351 | 非子祖先 |
IntIsTopLevelWindow | 359 | 顶层窗口判定 |
IntValidateOwnerDepth | 367 | owner 链深度限制 |
IntGetWindow | 381 | ★ GW_HWNDFIRST/NEXT/PREV/LAST/CHILD/OWNER |
IntLinkWindow | 944 | ★ 指针级插入(NULL=链头) |
IntLinkHwnd | 985 | ★★ HWND_* → 链表动作 + TOPMOST 位 |
IntUnlinkWindow | 1353 | ★ 指针级摘除 |
IntPopulateHwndList | 1393 | Z 序深度遍历(Enum 家族) |
IntBuildHwndList | 1422 | WINDOWLIST 构建 + 缓存 |
NtUserBuildHwndList | 1513 | EnumWindows 系统调用 |
co_UserCreateWindowEx 链接段 | 2431-2438 | 创建时 Z 序(子窗口 HWND_BOTTOM) |
co_IntSetParent | 1157 | SetParent 摘除重挂(L1227-1244)+ 置顶(L1285-1287) |
IntShowOwnedPopups | 4674 | owner 最小化/还原时隐藏/恢复 popup |
focus.c(win32ss/user/ntuser/focus.c)
| 函数 | 行号 | 本册覆盖 |
|---|---|---|
co_IntSetForegroundAndFocusWindow | 920 | ★ 激活主入口(L876 置顶) |
co_IntSetActiveWindow | 1023 | 激活状态机(BEINGACTIVATED 防重入) |
IntUserSetActiveWindow | 1169 | 前台规则(IsAllowedFGActive) |
co_IntMouseActivateWindow | 1248 | 鼠标点击激活 |
UserSetActiveWindow | 1258 | 激活 API 公共路径 |
co_IntSetForegroundWindow | 1545 | 前台窗口入口 |
NtUserSetActiveWindow | 1657 | SetActiveWindow 系统调用 |
其他文件
| 文件 | 位置 | 内容 |
|---|---|---|
win32ss/user/ntuser/vis.c | L12 / L144 | VIS_ComputeVisibleRegion(Z 序扣遮挡)/co_VIS_WindowLayoutChanged(暴露刷新) |
win32ss/user/ntuser/window.h | L155-188 | WndSetChild/WndSetNext/WndSetPrev/WndSetParent/WndSetLastActive 宏 |
win32ss/include/ntuser.h | L693-767 / L676 | WND 结构(spwndNext/Prev/Parent/Child/Owner)/WS_EX2_LINKED |
sdk/include/psdk/winuser.h | L1216-1219 / L1250-1264 | HWND_TOP/BOTTOM/TOPMOST/NOTOPMOST / SWP_* 标志 |
win32ss/user/user32/windows/window.c | L68 / L1075 / L1156 | BringWindowToTop / GetWindow / GetTopWindow |
win32ss/user/user32/include/ntwrapper.h | L433-437 | SetWindowPos 直通封装 |
win32ss/user/user32/windows/winpos.c | L138-162 | GetActiveWindow/WindowFromPoint 封装 |
win32ss/user/ntuser/msgqueue.c | L2517 | MsqSetStateWindow(队列激活/焦点/捕获状态) |
本文基于 ReactOS 源代码撰写,所有行号指向当前工作区(d:\reactos)的真实代码。
下一篇建议:_30可深入"窗口树遍历与 EnumWindows 家族"(IntBuildHwndList/IntPopulateHwndList/IntGetWindow的完整枚举语义),或转入菜单循环(_11/_12)——owned popup 约束在那里有大量实战场景。
1883

被折叠的 条评论
为什么被折叠?



