ReactOS 窗口系统分析(29):窗口 Z 顺序的实现 — winpos.c ZOrder 机制深度分析

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.cwin32ss/user/user32/include/ntwrapper.h(用户态封装)、win32ss/include/ntuser.hsdk/include/psdk/winuser.h(数据结构与常量)。
本文所有内容均基于 ReactOS 真实源码分析,不包含虚构信息。行号以 2026 年 8 月工作区源码为准。


目录

  1. 概述
  2. 设计动机
  3. 核心数据结构
  4. 核心函数代码级展开
  5. Z 序调整全流程(重点章节)
  6. TOPMOST 机制
  7. owned popup 约束
  8. 激活与 Z 序
  9. 命中测试与 Z 序
  10. 调用链与模块关系
  11. 源码索引

1. 概述

1.1 本篇回答什么问题

Z 顺序(Z-order)是窗口系统的核心概念之一:屏幕上的窗口可以重叠,"谁盖住谁"由 Z 序决定。Windows 的 Z 序模型是兄弟窗口单链表——每个父窗口(或桌面)把它的直接子窗口串成一条双向链表,链表头部(spwndChild)是屏幕最上层,链表尾部是最底层;绘制、命中测试、可见区域计算全部按这条链表的顺序工作。

本篇逐行拆解的问题包括:

  1. 窗口的 Z 序到底存在哪里?spwndNext/spwndPrev/spwndChild/spwndParent 四个指针如何编码"兄弟次序"与"父子关系"?
  2. SetWindowPos(hwnd, hWndInsertAfter, ...) 里的 hWndInsertAfter 是怎么变成链表操作的?HWND_TOP(0)/HWND_BOTTOM(1)/HWND_TOPMOST(-1)/HWND_NOTOPMOST(-2) 四个伪句柄分别对应什么链表动作?
  3. 0xffff/0xfffeHWND_TOPMOST/HWND_NOTOPMOST 的符号扩展问题——为什么内核要把 (HWND)0xffff 修正为 HWND_TOPMOST
  4. WS_EX_TOPMOST(置顶)窗口带如何维持"永远在普通窗口之上"?IntGetLastTopMostWindow 找的分界点是什么?
  5. owned popup(模态对话框、下拉菜单窗口、工具提示)为什么能始终盖住它的 owner?WinPosDoOwnedPopups 的"插到 owner 前一格"算法细节?
  6. 激活窗口为什么自动跑到最前?WinPosFixupFlags 的"激活抬升"逻辑如何改写调用者的 Z 序意图?
  7. GetWindow(GW_HWNDFIRST/NEXT/PREV/LAST)GetTopWindowEnumWindows 如何按 Z 序遍历?
  8. 鼠标命中测试(WindowFromPoint)为什么是"按 Z 序自上而下"的?
  9. 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 重算(不重复区域运算细节)
重点 bugCORE-6651(父客户区偏移)等CORE-6129/CORE-6554(owned popup)、符号扩展、兄弟校验

本册不再展开:CVR/SMWP 批量移动(_3 第 3.1/3.2 节)、区域四元组与 BitBlt(_3 第 7 章)、窗口吸附(_3 第 8 章)。本册对 co_WinPosSetWindowPos 只展开 L1836-1951 的 Z 序处理段WinPosFixupFlagsZ 序分支(L1608-1693)


2. 设计动机

2.1 为什么用"兄弟单链表"而不是"全局 Z 值"

最直观的 Z 序实现是给每个窗口一个整数 Z 值(越大越靠前)。但 Windows(以及 ReactOS)选择兄弟链表,原因有三:

  1. 局部性:Z 序只在"同一父窗口的兄弟之间"有意义。子窗口的 Z 序与它父窗口的 Z 序互相独立——父窗口被别的窗口盖住时,其所有子窗口(无论内部 Z 序如何)都一起被盖住。链表结构天然把 Z 序限定在兄弟范围内(spwndParent 相同才可比)。
  2. 相对顺序,非绝对数值:Z 序关心的是"谁在谁上面"的相对关系,不关心"具体是第几层"。链表只需改两个指针即可插入/摘除,而整数 Z 值方案需要全局重编号(或预留间隔,久用必满)。
  3. O(1) 摘除/插入IntUnlinkWindow/IntLinkWindow 都是纯指针操作,不涉及数值比较或重排。SetWindowPos(HWND_TOP) 一次调用只需 4-6 次指针赋值。

2.2 画家算法(Painter’s Algorithm)的支撑

窗口绘制采用"画家算法":从 Z 序最底层的窗口开始逐层往上画,上层窗口覆盖下层。ReactOS 没有独立的合成器(现代 DWM 才每窗口一个纹理),屏幕上的像素由各窗口的可见区域拼接而成——可见区域 = 自己的区域减去所有 Z 序在上的兄弟遮挡的部分vis.cVIS_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=0HWND_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 的用户心智模型是"我点击的窗口应该到最前面"。这个行为不是输入子系统单独做的,而是两条机制叠加

  1. 显式路径:鼠标点击 → WM_MOUSEACTIVATEco_IntMouseActivateWindow(focus.c L1248)→ co_IntSetForegroundAndFocusWindow → 激活成功后 co_WinPosSetWindowPos(Wnd, HWND_TOP, ...)(focus.c L876)把窗口搬到 Z 序顶端
  2. 隐式路径(保底):任何 SetWindowPos 调用,只要目标窗口不是当前前台窗口且未带 SWP_NOACTIVATE/SWP_HIDEWINDOWWinPosFixupFlags 就会把调用者的 Z 序意图改写为"置顶"(L1608-1618)——即使调用者只请求 SWP_NOZORDER 或指定了非顶端插入位置。

第二条是防御性的:它保证"窗口被 SetWindowPos 处理过但没有显式激活"时也不会留在 Z 序底部。Wine/ReactOS 的 msgwin 测试(test_SetFocus/test_SetWindowPos 系列)专门校验这条行为的消息序列。

2.6 “已就位则跳过”:避免无意义的重排与重绘风暴

Z 序链表操作的代价不在于指针赋值本身,而在于连带效应:Z 序变化 → 可见区域重算(vis.c)→ 更新区域生成 → 兄弟窗口重绘。一次"把已经在顶端的窗口移到顶端"的调用如果照做全套,会引发整屏闪烁。因此 WinPosFixupFlagsIntGetWindow(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_TOP0插到"最后一个置顶窗口之后"(置顶带与普通带交界处)保持/按需升级
HWND_BOTTOM1插到链表尾清除
HWND_TOPMOST-1 (0xFFFF)插到链表头置位
HWND_NOTOPMOST-2 (0xFFFE)清位后按 HWND_TOP 处理清除

符号扩展问题:Win32 句柄是 HANDLE(32 位无符号视角),HWND_TOPMOST 被定义为 (HWND)(-1)。当应用用 (HWND)0xffff 字面量传参时(如某些旧代码 SetWindowPos(hwnd, (HWND)0xffff, ...)),进入内核后 0x0000FFFF 不等于 0xFFFFFFFFHWND_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_NOZORDER0x0004保留 Z 序:置位则跳过 IntLinkHwnd(winpos.c L1948)与 WinPosDoOwnedPopups(L1885-1890)
SWP_NOACTIVATE0x0010抑制"激活抬升"(WinPosFixupFlags L1611)与末尾 co_IntSetForegroundWindow
SWP_NOOWNERZORDER0x0200= SWP_NOREPOSITION:跳过 owned popup 约束(L1885)
SWP_NOSENDCHANGING0x0400批量移动 Phase 2 用;owned popup 递归调用时也带(L1513-1514)
SWP_DEFERERASE0x2000owned popup 递归调用时携带,抑制同步擦除
SWP_NOREDRAW0x0008跳过可见区域快照/重绘(Z 序变化但不重画)
SWP_SHOWWINDOW/SWP_HIDEWINDOW0x0040/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 函数全景(本册覆盖)

函数位置职责
系统调用NtUserSetWindowPoswinpos.c L3609SetWindowPos 入口:校验 + 钳制 + 转发
用户态封装SetWindowPosntwrapper.h L433直通 NtUserSetWindowPos
用户态封装BringWindowToTopuser32 window.c L68SetWindowPos(HWND_TOP) + 激活
用户态封装GetTopWindowuser32 window.c L1156GetWindow(GW_CHILD)
心脏co_WinPosSetWindowPos(Z 序段)winpos.c L1836-1951规范化 → owned popup → 链表重排
规范化WinPosFixupFlags(Z 序分支)winpos.c L1608-1693符号扩展/激活抬升/已就位/兄弟校验
约束WinPosDoOwnedPopupswinpos.c L1366owned popup 保底算法
链表IntLinkHwndwindow.c L985HWND_* → 链表动作 + TOPMOST 位
链表IntLinkWindow/IntUnlinkWindowwindow.c L944/L1353指针级插入/摘除
遍历IntGetWindowwindow.c L381GW_HWNDFIRST/NEXT/PREV/LAST/CHILD/OWNER
遍历IntGetLastTopMostWindowwinpos.c L238找最后一个置顶窗口
命中co_WinPosWindowFromPoint/co_WinPosSearchChildrenwinpos.c L2999/L2914按 Z 序自上而下命中
激活NtUserSetActiveWindow/co_IntSetActiveWindow/IntUserSetActiveWindow/co_IntSetForegroundAndFocusWindowfocus.c L1657/L1023/L1169/L920激活置顶(L876)
可见性co_WinPosShowWindow/co_WinPosMinMaximizewinpos.c L2629/L2444显隐/三态中的 Z 序联动
创建co_UserCreateWindowEx 链接段window.c L2431-2438子窗口插 HWND_BOTTOM,其余插 hwndInsertAfter
可见区域VIS_ComputeVisibleRegionvis.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 真实窗口、且非桌面/消息窗口),不做任何语义解释——解释全部留给 WinPosFixupFlagsIntLinkHwnd。坐标钳制(±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_NOACTIVATESWP_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; // 已就位(插到自己之后 / 已在目标之后)→ 不用动
      }
   }
}

两个关键细节:

  1. 兄弟校验hWndInsertAfter 的父必须与本窗口的父相同,否则返回 FALSEco_WinPosSetWindowPos 在 L1878-1882 直接 return TRUE失败但不报错,且不发送任何消息、不做任何几何变更)——这是 Wine 测试确认的 Windows 兼容行为(“跨父窗口的 Z 序操作是静默无操作”)。
  2. 已就位判定:插入参照就是自己,或自己已经在参照的 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 章详述):

  1. 置顶位不随窗口移动,但随"插入到置顶窗口之间"而继承:真句柄分支中,若参照是置顶窗口且其 spwndNext 也是置顶窗口(本窗口被插进置顶带内部),则本窗口自动成为置顶
  2. 插到普通窗口之后 → 强制清置顶位(防止"伪置顶"混入置顶带);
  3. 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 的 GetWindowuser32/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封装
SetWindowPosL433-437:直通 NtUserSetWindowPos(无任何预处理)
SetActiveWindowL451-455:直通 NtUserSetActiveWindow
ShowWindowL439-443:直通 NtUserShowWindow
WindowFromPointuser32 windows/winpos.c L157-162:直通 NtUserWindowFromPoint
GetActiveWindowuser32 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_TOPWH_CBT 钩子可以改写它HCBT_CREATEWNDCBT_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 环)。IntGetNonChildAncestorco_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_WINDOWPOSCHANGINGDestroyWindowIntIsWindow 失败 → 返回 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 场景)IntLinkWindowASSERT(Wnd != WndInsertAfter) 在调试构建崩溃;WinPosDoOwnedPopupsList[i-1] != 自己 防护(L1414)从源头杜绝
⑬ 遮挡重算Z 序改了但 SWP_NOREDRAWVisAfter 不重算、不重绘——调用者承诺自己处理(DeferWindowPos 批量场景常见)

5.3 一个完整例子的推演:把普通窗口 W 插到置顶窗口 T 之后

假设桌面子链表当前为 [T(TOPMOST), A(普通), W(普通), B(普通)],应用调用 SetWindowPos(W, T, ..., 0)(无 NOZORDER、无 NOACTIVATE):

  1. NtUserSetWindowPos:W、T 都合法,坐标钳制通过。
  2. co_WinPosSetWindowPos 组装 WinPos:hwndInsertAfter = T
  3. WinPosFixupFlags
    • 激活抬升:W 不是前台窗口、非子窗口、无 NOACTIVATE/HIDEWINDOW、hwndInsertAfter == T(真句柄,非 TOPMOST/NOTOPMOST)→ SWP_NOZORDER(本来就未置)、把 hwndInsertAfter 改为 HWND_TOPMOST(若 W 置顶)或 HWND_TOP。W 非置顶 → hwndInsertAfter = HWND_TOP
    • 至此调用者的"插到 T 后面"意图被激活语义覆盖——W 将到普通带顶端。
  4. WinPosDoOwnedPopups:W 无 owner(或 owner 是桌面),走第二段:hwndInsertAfter == HWND_TOP 且 W 非置顶 → 跳过置顶带(i 从 T 跳到 A)。然后遍历 A、W、B:无 owned popup → 返回 HWND_TOP
  5. IntLinkHwnd(W, HWND_TOP):W 非置顶 → 从链头走:T 是置顶(继续)、T 不是 W 的 owner(继续)→ A 非置顶(停)→ IntLinkWindow(W, A->spwndPrev = T) → 链表变为 [T, W, A, B]
  6. 结果:W 从"普通带中间"被移到"置顶带末尾(普通带顶端)"——比 A、B 都靠前。这正是"SetWindowPos 未带 NOACTIVATE 会激活并置顶"的教科书行为。

若同样的调用带上 SWP_NOACTIVATE:步骤 3 的激活抬升不触发,hwndInsertAfter 保持 TIntLinkHwnd(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(平移) − VisAfterco_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

不变量:

  1. 置顶带永远在普通带之上:任何普通窗口都不能插进置顶带内部(IntLinkHwnd 的 HWND_TOP 分支只走到"第一个非置顶窗口"就停);
  2. 置顶带是连续的:置顶窗口之间没有普通窗口(IntGetLastTopMostWindow 的连续遍历假设);
  3. 置顶位是"入带凭证":插入动作决定位值,位值决定后续插入位置——二者由 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);

链路:NtUserSetWindowPosWinPosFixupFlagsHWND_TOPMOST 分支:已置顶且已是链头 → SWP_NOZORDER,否则继续)→ WinPosDoOwnedPopupshWndInsertAfter == HWND_TOPMOST 时跳过 owner 修正,直接走第二段)→ IntLinkHwnd(HWND_TOPMOST) → 插链头 + 置位。

退出置顶

SetWindowPos(hwnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);

链路:IntLinkHwnd(HWND_NOTOPMOST) → 清位 → 回落 HWND_TOP → 插到置顶带末尾(普通带顶端)。WinPosFixupFlagsHWND_NOTOPMOST 分支(L1654-1657)已判定"本非置顶 → SWP_NOZORDER",所以普通窗口请求 HWND_NOTOPMOST 是无操作。

注意HWND_NOTOPMOSTHWND_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 的冲突豁免

WinPosDoOwnedPopupshWndInsertAfter == 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/SetWindowPlacementWinPosDoOwnedPopups(winpos.c L1889)
IntLinkHwnd(HWND_TOP)遍历时遇到 owner 置顶 → 自己升级置顶(L1031-1035)
SetParentowner 置顶 → 子窗口继承置顶位(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 popupco_UserCreateWindowEx 顶层窗口默认 HWND_TOP 插入(本身就在 owner 上方或会触发修正)
IME 窗口IntWinListOwnedPopups 排除默认 IME 窗口(window.c L328),IME 窗口不参与 popup 搬运

8. 激活与 Z 序

8.1 "激活置顶"的三重保险

机制代码位置触发条件
① 显式置顶co_IntSetForegroundAndFocusWindowco_WinPosSetWindowPos(Wnd, HWND_TOP, ..., SWP_NOSIZE|SWP_NOMOVE)focus.c L876窗口已是(或刚成为)活动窗口,同消息队列分支
② 隐式抬升WinPosFixupFlags 激活抬升:清 SWP_NOZORDER、改 hwndInsertAfterwinpos.c L1608-1618非前台窗口 + 非子窗口 + 无 SWP_NOACTIVATE/HIDEWINDOW
③ 收尾激活co_WinPosSetWindowPos 步骤 9 调 co_IntSetForegroundWindowwinpos.c L2278 附近SWP_NOACTIVATE + 非隐藏 + 非子窗口

①是"激活发生后把窗口带上来",②是"SetWindowPos 隐含激活意图时把 Z 序意图改成置顶",③是"Z 序动了之后把激活状态同步过来"。三者互相配合:

  • ①为主:鼠标/键盘交互激活(WM_MOUSEACTIVATEco_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 序搜索顺序

  1. IntGetLastTopMostWindow() 分界点(L296-299)→ 从普通带顶端开始;
  2. 优先 spwndActivePrev(上一个激活窗口,L304);
  3. 否则沿 spwndNext 向下扫描:VerifyWnd + 非 WS_EX_NOACTIVATE + WS_VISIBLE 且非 WS_DISABLED + 非 WS_ICONIC跳过 WS_EX_TOOLWINDOW(工具窗口不当激活替补,L317);
  4. 扫完整个普通带没有 → 换 FindTopWnd 轮次从头再扫(允许工具窗口,L326);
  5. 都没有 → co_IntSetForegroundWindow(pWndTemp) 兜底。

co_WinPosActivateOtherWindow(winpos.c L396-477)是更通用的版本,Z 序搜索顺序:

  1. popup 优先激活 owner(L412-417):(Wnd->style & WS_POPUP) && (WndTo = Wnd->spwndOwner)UserGetAncestor(WndTo, GA_ROOT)can_activate_window 通过则用——对话框关闭后激活回到主窗口;
  2. 沿 spwndNext 找下一个可激活兄弟(L421-426);
  3. spwndActivePrev(L434);
  4. 从桌面链头再扫一遍(L439-455),遇到 Wnd 自身则放弃;
  5. 通过 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)→ 当前窗口放弃命中,递归继续测试更下层兄弟;
  • 禁用顶层窗口 → 命中但返回 HTERRORco_WinPosSearchChildren L2943-2948),由 Ignore 参数决定是否忽略。

9.2 捕获窗口对 Z 序的覆盖

鼠标捕获(SetCapture,focus.c)存在时,命中测试跳过 Z 序:队列 spwndCapture 直接接收所有鼠标消息(mouse.c 的 UserProcessMouseInput 先查捕获)。这与 Z 序无关,但影响"命中测试是否被绕过"——拖拽(标题栏拖动、滚动条拖动)期间 Z 序不参与输入定向。

9.3 命中测试结果的 Z 序一致性

co_WinPosWindowFromPointUserEnterExclusive() 锁内执行(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 序主链

SetWindowPos hWndInsertAfter

NtUserSetWindowPos

校验+钳制+引用

WM_WINDOWPOSCHANGING
应用可改写 hwndInsertAfter

符号扩展 0xffff/0xfffe

激活抬升

已就位判定

兄弟校验失败

无 NOOWNERZORDER 且 Z 序/显隐要动

owner 修正插入点

递归搬运 owned popups

无 NOZORDER 且非 Shell 窗口

IntUnlinkWindow 摘除

HWND_BOTTOM

HWND_TOPMOST

HWND_TOP

真句柄

VisAfter 重算

ExposedRgn

WM_WINDOWPOSCHANGED

应用程序

user32 SetWindowPos
ntwrapper.h L433

NtUserSetWindowPos
winpos.c L3609

co_WinPosSetWindowPos
winpos.c L1798

WinPosFixupFlags
winpos.c L1562

修正为 TOPMOST/NOTOPMOST
L1624-1631

改写为 TOPMOST/TOP
L1608-1618

补 SWP_NOZORDER
L1633-1658

return FALSE
L1669-1678

WinPosDoOwnedPopups
winpos.c L1366

插到 owner 前一格
L1392-1439

co_WinPosSetWindowPos 递归
L1496-1518

IntLinkHwnd
window.c L985

window.c L1353

插链尾+清 TOPMOST
L997-1010

插链头+置 TOPMOST
L1011-1016

插到最后一个置顶窗口之后
L1017-1041

IntLinkWindow 插到参照后
L1042-1080

VIS_ComputeVisibleRegion
vis.c L12

co_VIS_WindowLayoutChanged
vis.c L144

链 2:TOPMOST 切换链
渲染错误: Mermaid 渲染失败: Parse error on line 9: ...k1 -->|IntLinkWindow(Wnd, NULL)| head1[父 -----------------------^ Expecting 'SQE', 'DOUBLECIRCLEEND', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'PS'
链 3:BringWindowToTop / 激活置顶链
渲染错误: Mermaid 渲染失败: Parse error on line 3: ...P_NOSIZE\|SWP_NOMOVE| kSWP[NtUserSetWind -----------------------^ Expecting 'SEMI', 'NEWLINE', 'SPACE', 'EOF', 'SQS', 'SHAPE_DATA', 'AMP', 'STYLE_SEPARATOR', 'DOUBLECIRCLESTART', 'PS', '(-', 'STADIUMSTART', 'SUBROUTINESTART', 'VERTEX_WITH_PROPS_START', 'COLON', 'CYLINDERSTART', 'DIAMOND_START', 'TAGEND', 'TRAPSTART', 'INVTRAPSTART', 'START_LINK', 'LINK', 'LINK_ID', 'DOWN', 'DEFAULT', 'NUM', 'COMMA', 'NODE_STRING', 'BRKT', 'MINUS', 'MULT', 'UNICODE_TEXT', got 'PIPE'
链 4:命中测试链(按 Z 序)

屏幕坐标

IntWinListChildren 从顶到底

递归子窗口
第一个命中即返回

WM_NCHITTEST

鼠标输入 mouse.c

NtUserWindowFromPoint
winpos.c L3933

co_WinPosWindowFromPoint
winpos.c L2999

co_WinPosSearchChildren
winpos.c L2914

window.c L274

命中窗口 + htEx

应用窗口过程

链 5:激活搜索链(隐藏/最小化时找替补)

活动窗口被隐藏

IntGetLastTopMostWindow 分界点

spwndActivePrev 优先

沿 spwndNext 扫描

失败

popup 优先 owner

沿 spwndNext / 链头再扫

执行

ShowWindow SW_HIDE/SW_MINIMIZE
winpos.c L2866

ActivateOtherWindowMin
winpos.c L285

winpos.c L238

上一个激活窗口

跳过 TOOLWINDOW
L310-335

co_WinPosActivateOtherWindow
winpos.c L396

spwndOwner → GA_ROOT

候选窗口

co_IntSetForegroundWindow / UserSetActiveWindow

10.2 与相邻模块的关系

模块协作点
window.cZ 序链表操作核心: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.cVIS_ComputeVisibleRegion(L12)按 Z 序扣除兄弟遮挡;co_VIS_WindowLayoutChanged(L144)暴露区域刷新
painting.cIntInvalidateWindows/co_UserRedrawWindow/UserSyncAndPaintWindows 消费 Z 序变化产生的更新区域;WNDS_SENDNCPAINT 位协议
msgqueue.c队列 spwndActive/spwndActivePrev/spwndFocus/spwndCapture 状态(激活/焦点/捕获的"谁");MsqSetStateWindow(L2517)写队列状态;激活消息 WM_ASYNC_SETACTIVEWINDOW 异步通道
simplecall.cNtUserCallOneParam 路由(IntShowOwnedPopups L494、激活 L535-539、前台 L656-664)
menu.cTrackPopupMenuEx 的 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.hHWND_*(L1216-1219)、SWP_*(L1250-1264)常量;WINDOWPOS 结构
ime.cIME 窗口 WS_EX_TOPMOST 继承(L1415-1417);默认 IME 窗口排除出 owned popup 列表(window.c L328)

10.3 并发与锁

  • 排他锁:所有写 Z 序的路径(NtUserSetWindowPos/ShowWindow/SetActiveWindow/DeferWindowPos/SetParent)都在 UserEnterExclusive() 内——链表结构在变更期间不可被其他线程读取到中间态;
  • 引用计数IntLinkHwnd 前后持有 UserRefObjectCoco_WinPosSetWindowPos L3662/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->rpdeskIntGetLastTopMostWindow L241 用)与桌面 pDeskInfo->spwnd 的读取都是安全的。

10.4 调试指南

  • 调试通道DBG_DEFAULT_CHANNEL(UserWinpos)(winpos.c L11)。启用后可看到 WinPosDoOwnedPopupsTRACE("(%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 + 全屏重绘;
  • 崩溃现场ASSERT_REFS_CO 失败说明回调路径上引用计数不对称——重点检查 WinPosDoOwnedPopups 递归分支的 Ref/Deref 配对(L1511-1516)。

11. 源码索引

winpos.c(win32ss/user/ntuser/winpos.c)

函数/段行号本册覆盖
IsChildVisible226级联可见性(父子链上溯)
IntGetLastTopMostWindow238★ 置顶带分界点
ActivateOtherWindowMin285最小化激活替补(Z 序扫描)
can_activate_window372可激活性过滤
co_WinPosActivateOtherWindow396通用激活替补(popup→owner 优先)
WinPosFindIconPos786图标排布(兄弟遍历避让)
WinPosDoOwnedPopups1366★★ owned popup 约束(CORE-6129/6554)
WinPosInternalMoveWindow1532窗口+子孙矩形平移(子窗口随父)
WinPosFixupFlags1562★★ Z 序参数规范化(符号扩展/抬升/已就位/兄弟校验)
co_WinPosSetWindowPos1798★ 心脏函数(Z 序段 L1836-1951)
co_WinPosMinMaximize2444三态切换(IntShowOwnedPopups 联动)
co_WinPosShowWindow2629显隐(子窗口 NOZORDER、置顶 HWND_TOPMOST)
co_WinPosSearchChildren2914★ 命中测试递归(Z 序自上而下)
co_WinPosWindowFromPoint2999命中测试入口
NtUserSetWindowPos3609★ 系统调用入口(hwndInsertAfter 校验)
NtUserWindowFromPoint3933WindowFromPoint 系统调用

window.c(win32ss/user/ntuser/window.c)

函数行号本册覆盖
IntWinListChildren274兄弟句柄数组(自顶向下)
IntWinListOwnedPopups315owned popup 句柄数组(排除 IME)
IntGetNonChildAncestor351非子祖先
IntIsTopLevelWindow359顶层窗口判定
IntValidateOwnerDepth367owner 链深度限制
IntGetWindow381★ GW_HWNDFIRST/NEXT/PREV/LAST/CHILD/OWNER
IntLinkWindow944★ 指针级插入(NULL=链头)
IntLinkHwnd985★★ HWND_* → 链表动作 + TOPMOST 位
IntUnlinkWindow1353★ 指针级摘除
IntPopulateHwndList1393Z 序深度遍历(Enum 家族)
IntBuildHwndList1422WINDOWLIST 构建 + 缓存
NtUserBuildHwndList1513EnumWindows 系统调用
co_UserCreateWindowEx 链接段2431-2438创建时 Z 序(子窗口 HWND_BOTTOM)
co_IntSetParent1157SetParent 摘除重挂(L1227-1244)+ 置顶(L1285-1287)
IntShowOwnedPopups4674owner 最小化/还原时隐藏/恢复 popup

focus.c(win32ss/user/ntuser/focus.c)

函数行号本册覆盖
co_IntSetForegroundAndFocusWindow920★ 激活主入口(L876 置顶)
co_IntSetActiveWindow1023激活状态机(BEINGACTIVATED 防重入)
IntUserSetActiveWindow1169前台规则(IsAllowedFGActive)
co_IntMouseActivateWindow1248鼠标点击激活
UserSetActiveWindow1258激活 API 公共路径
co_IntSetForegroundWindow1545前台窗口入口
NtUserSetActiveWindow1657SetActiveWindow 系统调用

其他文件

文件位置内容
win32ss/user/ntuser/vis.cL12 / L144VIS_ComputeVisibleRegion(Z 序扣遮挡)/co_VIS_WindowLayoutChanged(暴露刷新)
win32ss/user/ntuser/window.hL155-188WndSetChild/WndSetNext/WndSetPrev/WndSetParent/WndSetLastActive
win32ss/include/ntuser.hL693-767 / L676WND 结构(spwndNext/Prev/Parent/Child/Owner)/WS_EX2_LINKED
sdk/include/psdk/winuser.hL1216-1219 / L1250-1264HWND_TOP/BOTTOM/TOPMOST/NOTOPMOST / SWP_* 标志
win32ss/user/user32/windows/window.cL68 / L1075 / L1156BringWindowToTop / GetWindow / GetTopWindow
win32ss/user/user32/include/ntwrapper.hL433-437SetWindowPos 直通封装
win32ss/user/user32/windows/winpos.cL138-162GetActiveWindow/WindowFromPoint 封装
win32ss/user/ntuser/msgqueue.cL2517MsqSetStateWindow(队列激活/焦点/捕获状态)

本文基于 ReactOS 源代码撰写,所有行号指向当前工作区(d:\reactos)的真实代码。
下一篇建议:_30 可深入"窗口树遍历与 EnumWindows 家族"(IntBuildHwndList/IntPopulateHwndList/IntGetWindow 的完整枚举语义),或转入菜单循环(_11/_12)——owned popup 约束在那里有大量实战场景。

内容概要:本文针对不对称电网故障下T型三电平逆变器的低电压穿越(LVRT)问题,提出了一种多目标协同控制策略,并通过Simulink进行仿真实现。该策略综合考虑了有功功率、无功功率、负序电流、中点电位平衡及谐波抑制等多个控制目标,采用正负序分离、双闭环调节与多目标优化算法协同作用,实现了故障期间并网电流的精确控制与系统稳定运行。研究重点在于提升逆变器在电网电压跌落与不平衡等恶劣工况下的适应能力,确保其符合并网技术规范。仿真结果表明,该策略在动态响应速度、电能质量改善和系统鲁棒性方面均表现出优越性能; 适合人群:具备电力电子、新能源并网或自动控制等相关专业背景,从事逆变器控制、微电网或柔性输电系统研究的研发人员及研究生;熟悉Simulink仿真工具者更佳; 使用场景及目标:①研究不对称电网故障下三电平逆变器的低电压穿越控制方法;②掌握多目标协同控制策略的设计思路与实现手段;③通过Simulink仿真平台复现并验证先进控制算法,服务于科研论文撰写、项目开发或工程优化; 阅读建议:建议结合Simulink仿真模型同步学习,重点关注正负序分离锁相、多目标权重分配与中点电位控制模块的实现细节,深入理解控制策略在暂态过程中的协同机制,并尝试调整故障条件与参数以评估系统鲁棒性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

caimouse

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值