ReactOS 窗口系统分析(1):窗口对象与创建 — window.c

ReactOS 窗口系统分析(1):窗口对象与创建 --- window.c

系列:窗口系统三大主线之窗口显示 第 1 册

本文基于 ReactOS 真实源码(截至 2026 年 8 月),所有行号均对应当时仓库状态。

核心文件:win32ss/user/ntuser/window.c(约 4731 行)

配套文件:win32ss/include/ntuser.h(WND/THRDESKHEAD/FNID)、win32ss/user/ntuser/win32.h(THREADINFO/PROCESSINFO)、win32ss/user/ntuser/window.h(内部接口)、win32ss/user/ntuser/object.c(对象系统)、win32ss/user/ntuser/class.c(类解析)、win32ss/user/ntuser/winpos.c(位置/尺寸)、win32ss/user/user32/windows/window.c(用户态 CreateWindowExW)

目录

  1. 概述
  2. 设计动机
  3. 核心数据结构
  4. 核心函数详解
  5. 窗口创建完整流程
  6. 窗口销毁完整流程
  7. 窗口树遍历
  8. 调用链与模块关系
  9. 源码索引
  10. 附录:调试要点与常见问题

1. 概述

本册聚焦"窗口对象"与"窗口创建/销毁"。窗口对象(WND)是 win32k USER 子系统中最核心的内核对象:每个 Win32 窗口在创建时于桌面堆 中分配一个 WND 结构,通过 USER 句柄表获得一个 HWND,并在窗口树(父子/拥有关系)与线程窗口链表中占据位置。window.c 是这一过程的"主战场"。

CreateWindowEx 的完整调用链如下:

复制代码
应用程序 (CreateWindowExW / CreateWindowExA)
  │  user32.dll
  ▼
User32CreateWindowEx (user32/windows/window.c L152)
  │  ① ANSI→Unicode 转换、注册系统控件(RegisterSystemControls)
  │  ② 类版本解析(ClassNameToVersion → 未找到类时加载控件库并重试)
  │  ③ MDI 子窗口预处理(WS_EX_MDICHILD,MDICREATESTRUCT 打包)
  ▼
NtUserCreateWindowEx (ntuser/window.c L2699)  ← 系统调用(UserEnterExclusive 加锁)
  │  ① ProbeAndCaptureLargeString 捕获窗口名/类名/类版本到内核
  │  ② 校验 hMenu 句柄合法性(非子窗口时)
  │  ③ 填充 CREATESTRUCTW Cs
  ▼
co_UserCreateWindowEx (ntuser/window.c L2152)
  │  ① IntGetAndReferenceClass:按类名原子解析窗口类并引用(class.c)
  │  ② 解析 hWndParent → ParentWindow / OwnerWindow(HWND_MESSAGE 特判)
  │  ③ IntFixWindowCoordinates:CW_USEDEFAULT 坐标修正
  │  ④ IntCreateWindow:分配 WND 并初始化(核心!)
  │  ⑤ WH_CBT HCBT_CREATEWND 钩子
  │  ⑥ 样式修正(WS_CLIPSIBLINGS/WS_CAPTION/WS_EX_WINDOWEDGE)
  │  ⑦ WM_GETMINMAXINFO → rcWindow/rcClient 矩形计算
  │  ⑧ WM_NCCREATE(失败回滚)
  │  ⑨ IntLinkHwnd 挂入 Z 序(窗口树)
  │  ⑩ IME 默认窗口 / WM_NCCALCSIZE / WM_CREATE(失败回滚)
  │  ⑪ EVENT_OBJECT_CREATE、WM_SIZE/WM_MOVE、WS_VISIBLE 时 ShowWindow
  │  ⑫ 滚动条、热键、HSHELL_WINDOWCREATED 通知
  ▼
返回 HWND(失败时 co_UserDestroyWindow 回滚并返回 NULL)

对称地,窗口销毁调用链:

复制代码
DestroyWindow (user32) → NtUserDestroyWindow (window.c L3063)
  → co_UserDestroyWindow (L2884)
      ├── WH_CBT HCBT_DESTROYWND 钩子
      ├── IntSendDestroyMsg:WM_DESTROY + 递归销毁子窗口
      ├── IntDestroyOwnedWindows:递归销毁被拥有的弹出窗口
      ├── 焦点/激活/捕获/剪贴板/鼠标跟踪清理
      └── co_UserFreeWindow (L575)
            ├── 解链(IntUnlinkWindow)+ 清属性/滚动条/菜单/计时器/热键
            ├── WM_NCDESTROY、清更新区域、删除 hrgnClip/hrgnUpdate
            ├── UserDeleteObject 释放句柄表项
            └── 引用计数归零后桌面堆回收(FreeDeskThreadObject)

1.1 本册范围

  • 窗口对象的分配、初始化、挂接、释放(IntCreateWindow / co_UserFreeWindow
  • 窗口树(父/子/拥有者)与 Z 序链表(IntLinkHwnd / IntUnlinkWindow
  • 窗口创建/销毁两条完整流程
  • 窗口句柄解析族(UserGetWindowObject / ValidateHwndNoErr / VerifyWnd
  • 窗口属性读写(NtUserSetWindowLong 族)
  • 窗口树遍历与查找(NtUserBuildHwndList / NtUserFindWindowEx / NtUserGetAncestor
  • 窗口系统服务杂项(NtUserQueryWindow / NtUserSetWindowFNID / NtUserDefSetText 等)

不涉及(留给后续分册):类注册与类对象的细节(_2 分册)、窗口位置/可见性/ShowWindow(_3 分册)、绘制与更新区域(_4 分册)、可见区域/窗口 DC(_5 分册)。


2. 设计动机

2.1 为什么需要"窗口对象"模型

Windows 把"窗口"抽象为一个内核对象,而不是一组散布在应用进程中的数据结构,原因在于:

  1. 跨进程共享:窗口属于创建它的线程,但可以被其他进程引用(SendMessage、SetWindowPos、EnumWindows 等)。窗口对象必须存在于所有 GUI 进程都能访问的内核空间(win32k 的桌面堆)。
  2. 安全性 :HWND 是一个不透明句柄,通过 USER 句柄表(gHandleTable)间接指向对象,内核可以校验进程是否有权访问(UserGetWindowObject 校验类型与销毁状态)。
  3. 与消息队列解耦 :窗口对象记录 head.pti(所属线程),消息投递时据此找到线程消息队列;窗口销毁时从队列中清除相关消息(MsqRemoveWindowMessagesFromQueue)。
  4. 统一生命周期管理 :所有 USER 对象(窗口、菜单、光标、钩子......)共用一套引用计数 + 句柄表机制(object.c),UserCreateObject/UserReferenceObjectByHandle/UserDeleteObject 统一管理。

2.2 句柄引用计数模型

每个 USER 对象头 HEAD 携带 h(句柄)与 cLockObj(引用计数):

  • 对象创建时(UserCreateObject)计 1 次引用("创建者引用");句柄表分配后句柄本身再计 1 次("句柄引用"),共 2。
  • 任何 UserGetWindowObject/UserReferenceObjectByHandle 会递增 cLockObj;对应 UserDereferenceObject 递减。
  • 计数归零时,句柄表项被释放(free_user_entry),对象内存由类型回调 ObjectFree 回收。
  • 对窗口而言,销毁路径是 UserMarkObjectDestroy(标记 HANDLEENTRY_DESTROY)→ co_UserFreeWindow 末尾 UserDerefObjectCo + UserDeleteObject

这套机制保证:窗口在销毁期间即使应用重入调用 DestroyWindow,对象也不会提前释放 ------co_UserDestroyWindow 开头的 ASSERT_REFS_CO(Window)IntIsWindow 二次检查正是为此。

2.3 Z 序链表与窗口树

窗口之间的组织是"树 + 双向链表"混合结构:

  • 父子关系spwndParent / spwndChild(WS_CHILD 子窗口;顶级窗口的父是桌面窗口 pDeskInfo->spwnd)。
  • 拥有关系spwndOwner(WS_POPUP 弹出窗口/对话框的所有者;WS_CHILD 子窗口的拥有者非空时用于 Z 序上"保持在所有者之上")。
  • 兄弟链表(Z 序)spwndNext / spwndPrev 组成同一父窗口下的 Z 序双向链表,spwndChild 指向链首(Z 序最顶端)。窗口绘制时的遮挡关系、命中测试、焦点恢复都依赖这个链表。

IntLinkHwnd 负责把窗口按 HWND_TOP/HWND_BOTTOM/HWND_TOPMOST/HWND_NOTOPMOST/指定窗口 语义插入链表,是 SetWindowPos Z 序操作的底层原语(第 3 分册详述)。

2.4 用户态/内核态"双读"的 HWND

WND 结构位于桌面堆,而桌面堆同时映射到进程用户态usrheap.c),因此 user32 可以直接在用户态读取 WND 字段实现 GetWindowLong/GetWindowText 等快速路径(见 user32/windows/class.cIntGetWindowLong)。写路径(如 SetWindowLong)则必须进内核(NtUserSetWindowLong)以保证原子性与安全检查。这是 ReactOS/Windows USER 分层的核心设计。


3. 核心数据结构

3.1 对象头链:HEAD / THROBJHEAD / THRDESKHEAD(ntuser.h L178-195)

c 复制代码
typedef struct _HEAD
{
    HANDLE h;          // 对象句柄(HWND 等)
    DWORD cLockObj;    // 引用计数
} HEAD, *PHEAD;

typedef struct _THROBJHEAD
{
    HEAD;
    struct _THREADINFO *pti;   // 所属线程
} THROBJHEAD, *PTHROBJHEAD;

typedef struct _THRDESKHEAD
{
    THROBJHEAD;
    struct _DESKTOP *rpdesk;   // 所属桌面
    PVOID pSelf;               // 指向对象自身(自引用,调试/校验用)
} THRDESKHEAD, *PTHRDESKHEAD;

WND.head 就是 THRDESKHEADhead.h 是 HWND,head.cLockObj 是引用计数,head.pti 是创建线程的 THREADINFOhead.rpdesk 是对象所在桌面(决定它被哪个桌面堆分配)。pSelfAllocDeskThreadObject(object.c L78)中被设为对象自身地址,供句柄表/调试校验。

3.2 WND 结构完整字段(ntuser.h L693-767)

c 复制代码
typedef struct _WND
{
    THRDESKHEAD head;                 // 对象头:h(HWND)+ cLockObj + pti + rpdesk + pSelf
    DWORD state;                      // 窗口状态位(WNDS_*,见 3.3)
    DWORD state2;                     // 第二状态位(WNDS2_*,见 3.4)
    DWORD ExStyle;                    // 扩展样式 WS_EX_*
    DWORD style;                      // 窗口样式 WS_*
    HINSTANCE hModule;                // 创建模块句柄(GWLP_HINSTANCE 返回值)
    DWORD fnid;                       // 窗口功能 ID(FNID_*,见 3.5)
    struct _WND *spwndNext;           // 兄弟链表:下一个(Z 序向下)
    struct _WND *spwndPrev;           // 兄弟链表:上一个(Z 序向上)
    struct _WND *spwndParent;         // 父窗口
    struct _WND *spwndChild;          // 第一个子窗口(Z 序最顶)
    struct _WND *spwndOwner;          // 拥有者窗口
    RECT rcWindow;                    // 窗口矩形(屏幕坐标)
    RECT rcClient;                    // 客户区矩形
    WNDPROC lpfnWndProc;              // 窗口过程(用户态回调地址或服务器端过程)
    PCLS pcls;                        // 指向窗口类对象
    HRGN hrgnUpdate;                  // 更新区域(待重绘,WS_PAINT 依据)
    LIST_ENTRY PropListHead;          // 属性链表头(SetProp/GetProp)
    ULONG PropListItems;              // 属性项计数
    PSBINFO pSBInfo;                  // 滚动条信息(经典 SBINFO)
    HMENU SystemMenu;                 // 系统菜单句柄
    UINT_PTR IDMenu;                  // 窗口菜单句柄或子窗口 ID(GWLP_ID)
    HRGN hrgnClip;                    // 裁剪区域(SetWindowRgn 设置)
    HRGN hrgnNewFrame;                // 新窗口框区域(边框变化重绘用)
    LARGE_UNICODE_STRING strName;     // 窗口标题(SetWindowText/GetWindowText 数据源)
    ULONG cbwndExtra;                 // 窗口附加字节数(cbWndExtra)
    struct _WND *spwndLastActive;     // 上一激活窗口(同级内)
    HIMC hImc;                        // 输入上下文(IME 关联)
    LONG_PTR dwUserData;              // GWLP_USERDATA
    PVOID pActCtx;                    // 激活上下文(ACTCTX,创建时传入)
    struct _WND *spwndClipboardListener; // 剪贴板监听窗口链
    DWORD ExStyle2;                   // 第二扩展样式(WS_EX2_*,内部用)
    struct { RECT NormalRect; POINT IconPos; POINT MaxPos; UINT flags; } InternalPos; // 恢复/图标/最大化位置(WPF_*)
    UINT Unicode:1;                   // 是否为 Unicode 窗口
    UINT InternalPosInitialized:1;    // InternalPos 是否已初始化
    UINT HideFocus:1;                 // UISF_HIDEFOCUS(键盘焦点矩形隐藏)
    UINT HideAccel:1;                 // UISF_HIDEACCEL(键盘加速键下划线隐藏)
    PSBINFOEX pSBInfoex;              // 滚动条扩展信息
    LIST_ENTRY ThreadListEntry;       // 线程窗口链表节点(pti->WindowListHead)
    PVOID DialogPointer;              // 对话框指针(对话框专用)
} WND, *PWND;

字段族划分(便于记忆):

字段 说明
对象头 head 句柄/引用/线程/桌面
样式 state、state2、style、ExStyle、ExStyle2、fnid 窗口行为标志
树关系 spwndNext/Prev/Parent/Child/Owner/LastActive/ClipboardListener 窗口树与链表
几何 rcWindow、rcClient、InternalPos 位置与尺寸
过程/类 lpfnWndProc、pcls、hModule、pActCtx、Unicode 行为与上下文
资源 SystemMenu、IDMenu、hImc、pSBInfo、pSBInfoex 菜单/输入法/滚动条
绘制 hrgnUpdate、hrgnClip、hrgnNewFrame 区域
应用数据 dwUserData、PropListHead、PropListItems、strName、cbwndExtra 附加数据
链表节点 ThreadListEntry、DialogPointer 线程归属/对话框

要点

  • cbwndExtra 窗口附加字节紧跟在 WND 结构之后(&Window[1]),SetWindowLong 的正索引就写在那里。
  • spwndLastActive 初始化时指向自身(IntCreateWindowWndSetLastActive(pWnd, pWnd)),用于 SetForegroundWindow 焦点恢复。
  • pSBInfo/pSBInfoex 指向桌面堆分配的滚动条信息(scrollbar.c)。
  • strNameLARGE_UNICODE_STRING(带 bAnsi 位),标题在桌面堆分配;GetWindowText 优先走用户态直读(NtUserInternalGetWindowText 是内核回退路径)。

3.3 state 状态位表(WNDS_*,ntuser.h L604-636)

位值 含义
0x00000001 WNDS_HASMENU 有菜单
0x00000002 WNDS_HASVERTICALSCROOLLBAR 有垂直滚动条
0x00000004 WNDS_HASHORIZONTALSCROLLBAR 有水平滚动条
0x00000008 WNDS_HASCAPTION 有标题栏
0x00000010 WNDS_SENDSIZEMOVEMSGS 需要发送 WM_SIZE/WM_MOVE
0x00000020 WNDS_MSGBOX 消息框窗口
0x00000040 WNDS_ACTIVEFRAME 激活边框
0x00000080 WNDS_HASSPB 有滚动条信息块
0x00000100 WNDS_NONCPAINT 非客户区需重绘
0x00000200 WNDS_SENDERASEBACKGROUND 需要发送 WM_ERASEBKGND
0x00000400 WNDS_ERASEBACKGROUND 擦除背景
0x00000800 WNDS_SENDNCPAINT 需要发送 WM_NCPAINT
0x00001000 WNDS_INTERNALPAINT 内部绘制待处理
0x00002000 WNDS_UPDATEDIRTY 更新区域脏
0x00004000 WNDS_HIDDENPOPUP 隐藏的弹出窗口(ShowOwnedPopups 用)
0x00008000 WNDS_FORCEMENUDRAW 强制菜单重绘
0x00010000 WNDS_DIALOGWINDOW 对话框窗口
0x00020000 WNDS_HASCREATESTRUCTNAME 持有 CREATESTRUCT 名字
0x00040000 WNDS_SERVERSIDEWINDOWPROC 窗口过程在 win32k 内(服务器端过程)
0x00080000 WNDS_ANSIWINDOWPROC ANSI 窗口过程
0x00100000 WNDS_BEINGACTIVATED 正在激活
0x00200000 WNDS_HASPALETTE 有调色板
0x00400000 WNDS_PAINTNOTPROCESSED WM_PAINT 未处理
0x00800000 WNDS_SYNCPAINTPENDING 同步绘制挂起
0x01000000 WNDS_RECEIVEDQUERYSUSPENDMSG 已收到查询挂起消息
0x02000000 WNDS_RECEIVEDSUSPENDMSG 已收到挂起消息
0x04000000 WNDS_TOGGLETOPMOST 切换置顶
0x08000000 WNDS_REDRAWIFHUNG 挂起时重绘
0x10000000 WNDS_REDRAWFRAMEIFHUNG 挂起时重绘边框
0x20000000 WNDS_ANSICREATOR ANSI 创建者(CreateWindowA 创建)
0x40000000 WNDS_MAXIMIZESTOMONITOR 最大化到显示器
0x80000000 WNDS_DESTROYED 已销毁(此后句柄失效)

其中 WNDS_ACTIVEFRAME 0x00000006 是"有标题/滚动条组合"的便捷宏。WNDS_DESTROYED 是句柄校验的关键位UserGetWindowObject/ValidateHwndNoErr 都检查它。

3.4 state2 状态位表(WNDS2_*,ntuser.h L640-670)

位值 含义
0x00000001 WNDS2_WMPAINTSENT WM_PAINT 已发送
0x00000002 WNDS2_ENDPAINTINVALIDATE EndPaint 使无效
0x00000004 WNDS2_STARTPAINT BeginPaint 中
0x00000008 WNDS2_OLDUI 旧式 UI
0x00000010 WNDS2_HASCLIENTEDGE 有客户区边框
0x00000020 WNDS2_BOTTOMMOST 置底窗口(Shell 桌面窗口)
0x00000040 WNDS2_FULLSCREEN 全屏
0x00000080 WNDS2_INDESTROY 销毁中(防止二次销毁)
0x00000100 WNDS2_WIN31COMPAT Win3.1 兼容模式
0x00000200 WNDS2_WIN40COMPAT Win4.0(NT4)兼容模式
0x00000400 WNDS2_WIN50COMPAT Win5.0(2K)兼容模式
0x00000800 WNDS2_MAXIMIZEDMONITORREGION 最大化监视器区域
0x00001000 WNDS2_CLOSEBUTTONDOWN 关闭按钮按下
0x00002000 WNDS2_MAXIMIZEBUTTONDOWN 最大化按钮按下
0x00004000 WNDS2_MINIMIZEBUTTONDOWN 最小化按钮按下
0x00008000 WNDS2_HELPBUTTONDOWN 帮助按钮按下
0x00010000 WNDS2_SCROLLBARLINEUPBTNDOWN 滚动条上箭头按下
0x00020000 WNDS2_SCROLLBARPAGEUPBTNDOWN 滚动条上翻页按下
0x00040000 WNDS2_SCROLLBARPAGEDOWNBTNDOWN 滚动条下翻页按下
0x00080000 WNDS2_SCROLLBARLINEDOWNBTNDOWN 滚动条下箭头按下
0x00100000 WNDS2_ANYSCROLLBUTTONDOWN 任一滚动条按钮按下
0x00200000 WNDS2_SCROLLBARVERTICALTRACKING 垂直滚动条拖动中
0x00400000 WNDS2_FORCENCPAINT 强制 NC 重绘
0x00800000 WNDS2_FORCEFULLNCPAINTCLIPRGN 强制全 NC 裁剪区域
0x01000000 WNDS2_FULLSCREENMODE 全屏模式
0x08000000 WNDS2_CAPTIONTEXTTRUNCATED 标题被截断
0x10000000 WNDS2_NOMINMAXANIMATERECTS 禁止最小/最大动画矩形
0x20000000 WNDS2_SMALLICONFROMWMQUERYDRAG 小图标来自 WM_QUERYDRAGICON
0x40000000 WNDS2_SHELLHOOKREGISTERED 已注册 Shell 钩子
0x80000000 WNDS2_WMCREATEMSGPROCESSED WM_CREATE 已处理(创建成功的标记)

WNDS2_INDESTROYco_UserFreeWindow 开头置位,防止销毁重入;WNDS2_WMCREATEMSGPROCESSED 在创建成功路径置位,销毁时据此决定是否调用 HCBT_DESTROYWND 钩子,同时也是"窗口曾完整创建"的依据。

3.5 fnid 值表(FNID_*,ntuser.h L857-900)

fnid 标识窗口的"功能类型"(是按钮、编辑框、菜单还是普通窗口),驱动消息分派与默认行为。值从 0x029A 起:

含义
0x029A FNID_FIRST / FNID_SCROLLBAR 滚动条
0x029B FNID_ICONTITLE 图标标题
0x029C FNID_MENU 菜单窗口
0x029D FNID_DESKTOP 桌面窗口
0x029E FNID_DEFWINDOWPROC 默认窗口过程
0x029F FNID_MESSAGEWND 消息窗口(仅消息)
0x02A0 FNID_SWITCH 任务切换窗口
0x02A1 FNID_BUTTON 按钮
0x02A2 FNID_COMBOBOX 组合框
0x02A3 FNID_COMBOLBOX 组合框下拉列表
0x02A4 FNID_DIALOG 对话框
0x02A5 FNID_EDIT 编辑框
0x02A6 FNID_LISTBOX 列表框
0x02A7 FNID_MDICLIENT MDI 客户区
0x02A8 FNID_STATIC 静态控件
0x02A9 FNID_IME IME 窗口
0x02AA FNID_GHOST Ghost 窗口(挂起窗口)
0x02AB FNID_CALLWNDPROC 调用窗口过程(钩子辅助)
0x02AC FNID_CALLWNDPROCRET 调用窗口过程返回
0x02AD FNID_HKINLPCWPEXSTRUCT 钩子 EX 结构
0x02AE FNID_HKINLPCWPRETEXSTRUCT 钩子返回 EX 结构
0x02AF FNID_MB_DLGPROC 消息框对话框过程
0x02B0 FNID_MDIACTIVATEDLGPROC MDI 激活对话框过程
0x02B1 FNID_SENDMESSAGE SendMessage
0x02B2 FNID_SENDMESSAGEFF SendMessage 转发
0x02B3 FNID_SENDMESSAGEWTOOPTION SendMessage 超时选项
0x02B4 FNID_SENDMESSAGECALLPROC SendMessage 调用过程
0x02B5 FNID_BROADCASTSYSTEMMESSAGE 广播系统消息
0x02B6 FNID_TOOLTIPS 工具提示
0x02B7 FNID_SENDNOTIFYMESSAGE 发送通知消息
0x02B8 FNID_SENDMESSAGECALLBACK SendMessageCallback
0x2000 FNID_DDEML DDEML 注册
0x4000 FNID_DESTROY 正在销毁(WM_NCDESTROY 时置位)
0x8000 FNID_FREED 已释放

fnid 与系统类原子配合使用:如 GETPFNCLIENTW(fnid) 取用户态客户端窗口过程数组(gpsi->apfnClientW),GETPFNSERVER(fnid) 取服务器端过程。NtUserSetWindowFNID 允许应用进程把窗口标记为按钮等系统类(仅限 FNID_FIRST..FNID_GHOST 区间且 fnid 原本为 0)。

3.6 HWND 句柄语义(object.c L341-478)

c 复制代码
#define FIRST_USER_HANDLE 0x0020   // 低字最小值
#define LAST_USER_HANDLE  0xffef   // 低字最大值

// 句柄→表项:索引 = (低字 - 0x20) >> 1,代次校验 = 高字
static int index = (LOWORD(handle) - FIRST_USER_HANDLE) >> 1;
if (generation == ht->handles[index].generation || !generation || generation == 0xffff) ...
  • HWND 编码:低 16 位是(表索引 << 1)+ 0x20,高 16 位是代次(generation)。索引除以 2 是因为每个表项占用"2 个句柄槽"的间隔(兼容 16 位句柄对齐)。
  • 代次(generation):句柄释放后表项复用,代次递增,旧句柄因代次不匹配而失效------这就是"悬空 HWND 复用后不会误指新窗口"的机制。
  • handle_to_entry 还检查 entry->type != 0(0 表示空闲),防止访问已释放项。
  • 特殊句柄值:HWND_BROADCAST(0xffff)/HWND_TOPMOST(-1)/HWND_NOTOPMOST(-2)/HWND_TOP(0)/HWND_BOTTOM(1)/HWND_MESSAGE(-3) 由 IntIsBroadcastHwnd/IntLinkHwnd 等特殊处理,不进入句柄表。

3.7 WINDOWLIST 窗口句柄列表(window.h L85-92)

c 复制代码
typedef struct tagWINDOWLIST
{
    struct tagWINDOWLIST *pNextList;  // 全局列表链(gpwlList)
    HWND *phwndLast;                  // 已填充末尾指针
    HWND *phwndEnd;                   // 容量末尾指针
    PTHREADINFO pti;                  // 目标线程过滤(NULL=不过滤)
    HWND ahwnd[ANYSIZE_ARRAY];        // 句柄数组,以 HWND_TERMINATOR((HWND)1) 结尾
} WINDOWLIST, *PWINDOWLIST;
  • IntBuildHwndList(window.c L1422)构建这种列表;全局 gpwlList/gpwlCache(L18-19)管理当前列表与缓存。
  • WL_IS_BAD(pwl) = phwndEnd <= phwndLast(列表写满);IntGrowHwndList 每次增长 8 个句柄(GROW_COUNT)。
  • HWND_TERMINATOR((HWND)1)是列表结尾哨兵。
  • 使用场景:EnumWindows/EnumChildWindows/EnumThreadWindows 的内核实现 NtUserBuildHwndList,以及 co_UserFreeWindow 收集子窗口快照。

3.8 THREADINFO / PROCESSINFO 中的窗口相关字段(win32.h L59-164、L224-302)

THREADINFO(每 Win32 线程一个,经 PsGetCurrentThreadWin32Thread() 获取)中与窗口相关:

字段 说明
ppi 所属 PROCESSINFO
rpdesk / pDeskInfo 线程桌面与桌面信息(pDeskInfo->spwnd 即桌面窗口)
MessageQueue 线程消息队列(焦点/激活/捕获窗口的载体)
spwndDefaultIme 默认 IME 窗口
cWindows 本线程窗口数(创建 +1,释放 -1)
cVisWindows 本线程可见窗口数(WS_VISIBLE 变化时调整)
WindowListHead 线程窗口链表(ThreadListEntry 节点挂这里)
TIF_flags 线程标志(TIF_CSRSSTHREAD/TIF_INCLEANUP 等)

PROCESSINFO 提供 pclsPrivateList/pclsPublicList(进程私有/公共类链表)、prpwinsta(窗口站)、dwHotkey(首个窗口热键)、usi(STARTUPINFO 拷贝)等。

3.9 关键内部宏速查(window.h L9-116)

window.h 定义了窗口系统内部使用的一批"样式判定/关系判定"宏,理解它们才能读懂 window.c 与 winpos.c 的分支逻辑:

边框/菜单判定宏
c 复制代码
#define HAS_DLGFRAME(Style, ExStyle) \
    (((ExStyle) & WS_EX_DLGMODALFRAME) || \
    (((Style) & WS_DLGFRAME) && (!((Style) & WS_THICKFRAME))))
#define HAS_THICKFRAME(Style, ExStyle) \
    (((Style) & WS_THICKFRAME) && \
    (!(((Style) & (WS_DLGFRAME | WS_BORDER)) == WS_DLGFRAME)))
#define HAS_THINFRAME(Style, ExStyle) \
    (((Style) & WS_BORDER) || (!((Style) & (WS_CHILD | WS_POPUP))))
#define HAS_CLIENTFRAME(Style, ExStyle) \
    (((ExStyle) & WS_EX_CLIENTEDGE) || (!((Style) & (WS_CHILD | WS_POPUP))))
#define HAS_MENU(pWnd, style) \
    ((((style) & (WS_CHILD | WS_POPUP)) != WS_CHILD) && (pWnd->IDMenu) && IntIsMenu(...))
  • HAS_DLGFRAME:对话框边框(DLGMODALFRAME,或 DLGFRAME 且非 THICKFRAME)。
  • HAS_THICKFRAME:可调大小边框(THICKFRAME,且 DLGFRAME+BORDER 组合不算)。
  • HAS_THINFRAME:细边框(WS_BORDER,或既非子窗口也非弹出窗口------即重叠窗口恒有边框)。
  • HAS_CLIENTFRAME:客户区边框(WS_EX_CLIENTEDGE,或重叠窗口)。
  • 这些宏是 IntGetWindowBorderMeasuresWM_NCCALCSIZE 默认分支的基础。
关系判定宏
c 复制代码
#define IntIsDesktopWindow(WndObj) (WndObj->spwndParent == NULL)          // 桌面窗口
#define IntIsBroadcastHwnd(hWnd) (hWnd == HWND_BROADCAST || hWnd == HWND_TOPMOST)
#define IntWndBelongsToThread(WndObj, W32Thread) \
    ((WndObj->head.pti) && (WndObj->head.pti == W32Thread))               // 线程归属
#define IntGetWndThreadId(WndObj) PsGetThreadId(WndObj->head.pti->pEThread)
#define IntGetWndProcessId(WndObj) PsGetProcessId(WndObj->head.pti->ppi->peProcess)
  • IntIsDesktopWindowspwndParent == NULL 即桌面(窗口树根)。
  • IntWndBelongsToThreadco_UserFreeWindow 用它区分"同线程子窗口直接释放 / 跨线程子窗口异步销毁"。
  • IntGetWndThreadId/ProcessIdNtUserQueryWindow 的线程/进程 ID 查询底层。
窗口类型判定宏
c 复制代码
#define IS_WND_CHILD(pWnd)      ((pWnd)->style & WS_CHILD)
#define IS_WND_MENU(pWnd)       ((pWnd)->pcls->atomClassName == gpsi->atomSysClass[ICLS_MENU])
#define IS_WND_IMELIKE(pWnd) \
    (((pWnd)->pcls->style & CS_IME) || \
     ((pWnd)->pcls->atomClassName == gpsi->atomSysClass[ICLS_IME]))
指针替换内联函数(window.h L128-188)
c 复制代码
static inline VOID ReplaceWndPtr(PWND* ppwnd, PWND pwndNew)
{
    if (pwndNew)  UserReferenceObject(pwndNew);   // 先引用新
    if (*ppwnd)   UserDereferenceObject(*ppwnd);  // 再解引用旧
    *ppwnd = pwndNew;                             // 最后赋值
}
#define WndSetOwner / WndSetParent / WndSetChild / WndSetNext / WndSetPrev / WndSetLastActive
  • 所有窗口树指针的赋值必须经这些包装 ,否则会出现"悬空引用计数":新对象被树引用却没加计数、旧对象被树放弃却没减计数。IntLinkWindow/IntUnlinkWindow/co_UserFreeWindow 均依赖此保证。

3.10 窗口子类型与客户端过程表

WND 的"继承"子类型(ntuser.h L771-792)
c 复制代码
typedef struct _SBWND   { WND wnd; BOOL fVert; UINT wDisableFlags; SBCALC SBCalc; } SBWND;   // 滚动条窗口
typedef struct _MDIWND  { WND wnd; DWORD dwReserved; PVOID pmdi; } MDIWND;                  // MDI 子窗口
typedef struct _MENUWND { WND wnd; PPOPUPMENU ppopupmenu; } MENUWND;                        // 菜单窗口
  • 这些结构把 WND 作为第一个成员 嵌入,分配时 UserCreateObject(..., sizeof(SBWND)) 即得到"带附加数据的窗口对象",(PWND)psbwnd 可直接当普通窗口用。
  • window.c 内还有两处"窗口 + 控件数据"的 ReactOS 本地结构:WND2CBOX(L3481,LPHEADCOMBO pCBox,组合框)、WND2LB(L3594,LB_DESCR *pLBiv,列表框),供 NtUserGetComboBoxInfo/NtUserGetListBoxInfo 直接读控件状态。
PFNCLIENT 客户端过程表(ntuser.h L794-819)
c 复制代码
typedef struct _PFNCLIENT
{
    WNDPROC pfnScrollBarWndProc; pfnTitleWndProc; pfnMenuWndProc; pfnDesktopWndProc;
    pfnDefWindowProc; pfnMessageWindowProc; pfnSwitchWindowProc; pfnButtonWndProc;
    pfnComboBoxWndProc; pfnComboListBoxProc; pfnDialogWndProc; pfnEditWndProc;
    pfnListBoxWndProc; pfnMDIClientWndProc; pfnStaticWndProc; pfnImeWndProc;
    pfnGhostWndProc; pfnHkINLPCWPSTRUCT; pfnHkINLPCWPRETSTRUCT; pfnDispatchHook;
    pfnDispatchDefWindowProc; pfnDispatchMessage; pfnMDIActivateDlgProc;
} PFNCLIENT, *PPFNCLIENT;
  • gpsi->apfnClientA/apfnClientW 是系统类窗口过程的 A/W 两套客户端地址;GETPFNCLIENTW(fnid) 按 fnid 索引。
  • 服务器端过程表 gpsi->aStoCidPfn[FNID_FIRST..FNID_SWITCH] 是 win32k 内部实现(如滚动条/桌面/菜单窗口过程),IntGetWindowProc/IntSetWindowProc 在两端之间转换。

4. 核心函数详解

本节按功能族逐一展开 window.c 中的关键函数。每个函数给出签名、参数、作用、实现流程、使用方式、注意事项。行号基于当前源码。

4.1 句柄 → 对象解析族

窗口 API 的第一步几乎都是"把 HWND 变成 PWND"。ReactOS 提供了一组层层封装的解析函数,区别在于是否设置错误码是否检查销毁状态是否加引用

UserGetObject / UserGetObjectNoErr(object.c L495 / L481)
c 复制代码
PVOID UserGetObject(PUSER_HANDLE_TABLE ht, HANDLE handle, HANDLE_TYPE type);
PVOID UserGetObjectNoErr(PUSER_HANDLE_TABLE ht, HANDLE handle, HANDLE_TYPE type);
  • 作用 :按句柄查 USER 句柄表,返回对象指针;UserGetObject 失败时 EngSetLastError(ERROR_INVALID_HANDLE)NoErr 版本不设置错误。
  • 实现流程handle_to_entry(ht, handle)(索引+代次+空闲校验)→ 检查 entry->type == type(类型必须匹配,防止把菜单句柄当窗口句柄用)→ 返回 entry->ptr
  • 使用方式UserGetWindowObject 内部就调用 UserGetObject(gHandleTable, hWnd, TYPE_WINDOW)
  • 注意事项 :返回值不加引用计数 ,调用者若需长期持有必须再 UserReferenceObject
UserGetWindowObject(window.c L123)
c 复制代码
PWND FASTCALL UserGetWindowObject(HWND hWnd)
  • 作用 :最常用的"HWND→PWND"解析;hWnd==NULL 或解析失败或 WNDS_DESTROYED 时返回 NULL 并设 ERROR_INVALID_WINDOW_HANDLE
  • 实现流程UserGetObject(..., TYPE_WINDOW)额外检查 Window->state & WNDS_DESTROYED------这是句柄复用陷阱的关键防线(对象已标记销毁但句柄表项可能尚未回收)。
  • 使用方式 :几乎被 window.c 内所有 NtUser* 函数采用(如 NtUserDestroyWindowNtUserGetAncestor)。
  • 注意事项不加引用计数 。需要跨"可能销毁对象"的代码段时,调用方用 UserRefObjectCo/UserDerefObjectCo(如 co_UserDestroyWindowASSERT_REFS_CO)。
ValidateHwndNoErr(window.c L108)
c 复制代码
PWND FASTCALL ValidateHwndNoErr(HWND hWnd)
  • 作用UserGetWindowObject 的"无错误"变体:UserGetObjectNoErr(gHandleTable, hWnd, TYPE_WINDOW) + WNDS_DESTROYED 检查。
  • 使用方式 :用于遍历类路径中"找不到就算了"的场景(如 NtUserBuildHwndList 的 EnumThreadWindows 分支、IntDestroyOwnedWindowsIntShowOwnedPopups),避免频繁设置 LastError 影响外层错误码。
VerifyWnd(window.c L88)
c 复制代码
PWND FASTCALL VerifyWnd(PWND pWnd)
  • 作用 :对已持有指针 的 PWND 做有效性复核:非空、未 WNDS_DESTROYED、未 WNDS2_INDESTROY、且句柄表项未处于 HANDLEENTRY_INDESTROYUserObjectInDestroy)。
  • 实现流程 :先做 state/state2 检查,再保存/恢复 EngGetLastError()(因为 UserObjectInDestroy 失败时可能改写错误码),返回 NULL 表示失效。
  • 使用方式IntSendParentNotify 在给父窗口发 WM_PARENTNOTIFY 前用它验证父窗口仍然有效。
IntGetWindowObject(window.c L75)
c 复制代码
PWND FASTCALL IntGetWindowObject(HWND hWnd)
  • 作用UserGetWindowObject + 立即加引用Window->head.cLockObj++)。
  • 使用方式 :调用方用完必须 UserDereferenceObject(Window)(如 IntSetOwnerco_UserFreeWindow 的子窗口遍历)。这是"拿指针即持锁"的显式版本。

4.2 窗口关系/状态查询族

IntIsWindow(window.c L176)
c 复制代码
BOOL FASTCALL IntIsWindow(HWND hWnd)
  • 作用 :等价于 Win32 IsWindow------判断句柄是否指向一个仍存在的窗口。
  • 实现流程UserGetWindowObject(hWnd) 成功即 TRUE(内部已含 WNDS_DESTROYED 检查)。
  • 使用方式 :window.c 内部大量使用,例如 co_UserDestroyWindow 的"二次销毁防护"(if (!IntIsWindow(UserHMGetHandle(Window))))、IntSendDestroyMsg 中 WM_DESTROY 重入后的存活检查、IntDestroyOwnedWindows 的逐个校验。
  • 注意事项 :它是"句柄存在性"检查,不是"指针有效性"检查;对已经 WNDS_DESTROYED 的窗口返回 FALSE,从而让销毁流程安全重入。
IntIsWindowVisible(window.c L189)
c 复制代码
BOOL FASTCALL IntIsWindowVisible(PWND Wnd)
  • 作用 :判断窗口及其所有祖先是否可见(WS_VISIBLE 全链检查)。
  • 实现流程 :沿 spwndParent 向上:任一无 WS_VISIBLE → FALSE;WS_MINIMIZE 且非自身 → FALSE(最小化视为不可见);FNID_DESKTOP → TRUE(桌面是可见性终点);到顶为 NULL → TRUE(无父且可见)。
  • 使用方式ShowWindow/绘制协议判断"是否真正显示在屏幕上"。
IntGetParent(window.c L204)
c 复制代码
PWND FASTCALL IntGetParent(PWND Wnd)
  • 作用 :实现 GetParent 语义:WS_POPUP 返回拥有者WS_CHILD 返回父窗口,其他返回 NULL。
  • 注意事项 :与 spwndParent 直接取值不同------弹出窗口的 spwndParent 是桌面窗口,但 GetParent 语义要的是 owner。这是 Win32 兼容性的经典坑,调用前务必想清楚要用哪一个。
IntIsChildWindow(window.c L928)
c 复制代码
BOOL FASTCALL IntIsChildWindow(PWND Parent, PWND BaseWindow)
  • 作用 :判断 BaseWindow 是否是 Parent 的(真)后代:沿 spwndParent 上溯,途中任一窗口不是 WS_CHILD(即 (WS_POPUP|WS_CHILD)!=WS_CHILD)就返回 FALSE,直到遇到 Parent 返回 TRUE。
  • 使用方式co_IntSetParent 用其拒绝"把子窗口设为父窗口"的非法操作。
IntGetNonChildAncestor(window.c L351)
c 复制代码
PWND FASTCALL IntGetNonChildAncestor(PWND pWnd)
  • 作用:沿父链上溯,跳过所有 WS_CHILD 窗口,返回第一个非子窗口(顶级窗口/弹出窗口)。
  • 使用方式IntCreateWindow 中计算"父窗口链上真正的拥有者";SetParent/owner 深度校验用。
IntIsTopLevelWindow(window.c L359)
c 复制代码
BOOL FASTCALL IntIsTopLevelWindow(PWND pWnd)
  • 作用:父窗口是桌面窗口 → 顶级窗口。
  • 使用方式co_UserCreateWindowEx 判断是否需 AttachThreadInput;销毁时判断是否需要解除线程附加。
IntValidateOwnerDepth(window.c L367)
c 复制代码
BOOL FASTCALL IntValidateOwnerDepth(PWND Wnd, PWND Owner)
  • 作用 :沿 spwndOwner 链检查嵌套深度是否超过 gNestedWindowLimit(window.c L16 全局,默认 50),链上出现 Wnd 自身或超过限制则失败。
  • 实现流程Depth=1 起步,循环沿 owner 上溯:遇到 NULL → 返回 50 >= Depth;遇到 Wnd 自身 → 返回 FALSE(禁止环)。
  • 使用方式IntSetOwnerIntCreateWindow 都在设置 owner 前调用,防止出现"窗口拥有自己"或无限深的 owner 环。
IntGetWindowContextHelpId(window.c L439)
  • 作用 :沿 IntGetParent 链向上查找 atomContextHelpIdProp 属性(UserGetProp),返回第一个非 0 的帮助 ID(GetWindowContextHelpId 语义),到桌面窗口为止。

4.3 样式操作族

IntSetStyle(window.c L143)
c 复制代码
ULONG FASTCALL IntSetStyle(PWND pwnd, ULONG set_bits, ULONG clear_bits)
  • 作用 :原子化地修改 pwnd->stylenew = (old | set_bits) & ~clear_bits;返回样式。
  • 实现流程 :若 WS_VISIBLE 位发生变化,同步调整 pti->cVisWindows 计数并 DceResetActiveDCEs(pwnd)(重置该窗口的活动 DC,防止失效 DC 被复用)。
  • 使用方式IntEnableWindow(置/清 WS_DISABLED)、co_UserCreateWindowEx(清 WS_MAXIMIZE|WS_MINIMIZE 以发送 WM_SIZE)、co_WinPosMinMaximize(置 WS_MINIMIZE)等。
  • 注意事项:不发送任何消息、不重绘------它只是"改位 + 维护计数",配套消息(WM_ENABLE/WM_SIZE 等)由调用方负责。
IntEnableWindow(window.c L219)
c 复制代码
BOOL FASTCALL IntEnableWindow(HWND hWnd, BOOL bEnable)
  • 作用 :实现 EnableWindow:设置/清除 WS_DISABLED。
  • 实现流程
    1. UserGetWindowObject 解析;
    2. 禁用时先发 WM_CANCELMODE(取消模态循环),若窗口有键盘焦点则 co_UserSetFocus(NULL) 撤焦;
    3. IntSetStyle 改样式;
    4. 状态变化时 IntNotifyWinEvent(EVENT_OBJECT_STATECHANGE) + 发送 WM_ENABLE
    5. 返回"原先是否禁用"。
  • 注意事项:返回值的语义是"旧状态",与文档一致(非零 = 之前已禁用)。

4.4 窗口树挂接/解链族(Z 序核心)

IntLinkWindow(window.c L945)
c 复制代码
VOID FASTCALL IntLinkWindow(PWND Wnd, PWND WndInsertAfter /* NULL = 链首 */)
  • 作用 :把 Wnd 插入兄弟链表 (Z 序)------WndInsertAfter==NULL 插到链首(最顶),否则插到其后。父/子指针在插入时维护(WndSetChild)。
  • 实现流程
    • 防自链(Wnd == WndInsertAfter 报错返回);
    • WndSetPrev(Wnd, WndInsertAfter)
    • 有前驱:next = WndInsertAfter->spwndNext,双向修正前后节点;
    • 无前驱(插链首):next = 父->spwndChild,并 WndSetChild(父, Wnd)
  • 使用方式 :仅被 IntLinkHwnd 调用;IntUnlinkWindow 是逆操作。
  • 注意事项 :所有链表指针变更都经 WndSetXxx 包装(window.h L128-188 的 ReplaceWndPtr),它保证"先引用新对象、再解引用旧对象、最后赋值",杜绝悬挂指针。
IntLinkHwnd(window.c L985)
c 复制代码
VOID FASTCALL IntLinkHwnd(PWND Wnd, HWND hWndPrev)
  • 作用 :按 HWND 语义 把窗口插入 Z 序。hWndPrev 取值:HWND_TOP(0)、HWND_BOTTOM(1)、HWND_TOPMOST(-1)、HWND_NOTOPMOST(-2)、或具体窗口句柄。
  • 实现流程
    1. HWND_NOTOPMOST:清 WS_EX_TOPMOST,回落为 HWND_TOP 处理;
    2. IntUnlinkWindow(Wnd) 先解旧链;
    3. HWND_BOTTOM:找到链尾插入,清 WS_EX_TOPMOST
    4. HWND_TOPMOSTIntLinkWindow(Wnd, NULL) 插链首,置 WS_EX_TOPMOST
    5. HWND_TOP:找到第一个非置顶窗口(跳过置顶窗口),插到它前面;若遇到 owner 则把 Wnd 升为置顶;
    6. 具体句柄:解析后 IntLinkWindow(Wnd, WndInsertAfter),并根据插入点前后节点的 WS_EX_TOPMOST 修正自己的置顶位;
    7. 末尾置 Wnd->ExStyle2 |= WS_EX2_LINKED 标记已挂接。
  • 使用方式co_UserCreateWindowEx(新窗口入链)、co_IntSetParentco_WinPosSetWindowPos(第 3 分册)。
  • 注意事项WS_EX2_LINKED 是 ReactOS 内部标记,用于判断窗口是否已在 Z 序链中(HWND_NOTOPMOST 分支借此短路)。
IntUnlinkWindow(window.c L1353)
c 复制代码
VOID FASTCALL IntUnlinkWindow(PWND Wnd)
  • 作用:把 Wnd 从兄弟链表中摘除(父/子指针不动,仅修前后节点)。
  • 实现流程 :后节点的 prev 指向前节点;前节点的 next 指向后节点;若 Wnd 是父的第一个子窗口则更新 spwndChild;最后清空自己的 prev/next。
  • 使用方式IntLinkHwnd 重排前、co_IntSetParent 换父前、co_UserFreeWindow 销毁前。
  • 注意事项 :与 IntLinkWindow 配套,操作后调用方必须重建或释放链表关系。
IntProcessOwnerSwap(window.c L1084)
c 复制代码
VOID FASTCALL IntProcessOwnerSwap(PWND Wnd, PWND WndNewOwner, PWND WndOldOwner)
  • 作用 :owner 变更时维护线程输入附加AttachThreadInput):旧 owner 不同线程则解除附加,新 owner 不同线程则建立附加(满足"被拥有窗口与 owner 必须同输入队列"的约束)。
  • 使用方式IntSetOwner 设置 owner 前后调用(失败时回滚再调用一次)。
  • 注意事项:仅对"窗口本身与 owner 不同线程"的场景操作;这是焦点/激活能跨窗口正确工作的前提之一。
IntSetOwner(window.c L1115)
c 复制代码
static HWND FASTCALL IntSetOwner(HWND hWnd, HWND hWndNewOwner)
  • 作用 :核心的 owner 设置(SetWindowLong(GWLP_HWNDPARENT) 的桌面分支走它)。
  • 实现流程 :解析新旧 owner → IntProcessOwnerSwapIntValidateOwnerDepth 通过则 WndSetOwner,否则回滚并设 ERROR_INVALID_PARAMETER;返回旧 owner 句柄。
  • 注意事项:新 owner 句柄非法时返回 NULL 且不改变现状。
co_IntSetParent(window.c L1157)
c 复制代码
PWND FASTCALL co_IntSetParent(PWND Wnd, PWND WndNewParent)
  • 作用SetParent 内核主流程(返回旧父窗口指针)。
  • 实现流程
    1. 拒绝消息窗口为父、拒绝把子窗口设为其祖先(IntIsChildWindow + 沿父链查环);
    2. co_WinPosShowWindow(Wnd, SW_HIDE) 先隐藏(记录 WasVisible);
    3. 计算旧父坐标下的位置(含 WS_EX_LAYOUTRTL 镜像修正),IntScreenToClient 转换;
    4. IntUnlinkWindow 解链 → WndSetParent 换父 → IntLinkHwnd 按 HWND_TOP/HWND_TOPMOST 重新入链;
    5. 桌面为父时强制 WS_CLIPSIBLINGS;维护线程附加(子窗口跨线程);
    6. IntNotifyWinEvent(EVENT_OBJECT_PARENTCHANGE)co_WinPosSetWindowPos 置顶并发送 WM_WINDOWPOSCHANGING/CHANGED;
    7. 若之前可见则 co_WinPosShowWindow(Wnd, SW_SHOWNORMAL) 恢复。
  • 注意事项:SetParent 会隐藏再显示窗口(含 WM_SHOWWINDOW 消息序列),这是 Win32 定义行为。
co_UserSetParent(window.c L1294)与 NtUserSetParent(L3671)
  • co_UserSetParent:系统调用级包装------拒绝广播句柄/桌面窗口,解析父/子窗口,UserRefObjectCo 双引用保护后调 co_IntSetParent,返回旧父句柄。
  • NtUserSetParent(L3671):UserEnterExclusive 加锁,处理 hWndNewParent==NULL(→桌面窗口)与 HWND_MESSAGE(→消息窗口)特判,再调 co_UserSetParent

4.5 创建链

NtUserCreateWindowEx(window.c L2697)
c 复制代码
HWND NTAPI NtUserCreateWindowEx(
    DWORD dwExStyle, PLARGE_STRING plstrClassName, PLARGE_STRING plstrClsVersion,
    PLARGE_STRING plstrWindowName, DWORD dwStyle, int x, int y, int nWidth, int nHeight,
    HWND hWndParent, HMENU hMenu, HINSTANCE hInstance, LPVOID lpParam,
    DWORD dwFlags, PVOID acbiBuffer);
  • 作用CreateWindowEx 的系统调用入口。
  • 实现流程
    1. 非子窗口且 hMenu 非空时,UserGetMenuObject(hMenu) 校验菜单句柄(非法 → ERROR_INVALID_MENU_HANDLE);
    2. ProbeAndCaptureLargeString 捕获窗口名(用户态→内核池拷贝);
    3. 类名/类版本:IS_ATOM 判断------原子直接作为 UNICODE_STRING.Buffer 传递(不拷贝),字符串则捕获为 LARGE_STRING 并截断为 UNICODE_STRING(FIXME 注释:LARGE_STRING 被截断到 MAXUSHORT);
    4. 填充 CREATESTRUCTW Cs(style/lpCreateParams/hInstance/hMenu/hwndParent/cx/cy/x/y/lpszName/lpszClass/dwExStyle);
    5. UserEnterExclusive() 加 USER 排他锁;
    6. co_UserCreateWindowEx(&Cs, &ustrClsVersion, plstrWindowName, acbiBuffer, dwFlags)
    7. 成功则 hwnd = UserHMGetHandle(pwnd)
    8. UserLeave() 解锁,cleanup: 释放三个捕获缓冲。
  • 注意事项plstrClsVersion 是 ReactOS 扩展(类版本,用于 VersionRegisterClass 加载控件库);acbiBuffer 是激活上下文(ACTCTX),透传给 IntCreateWindow 存入 pActCtx
ProbeAndCaptureLargeString(window.c L2640)
c 复制代码
NTSTATUS NTAPI ProbeAndCaptureLargeString(OUT PLARGE_STRING plstrSafe, IN PLARGE_STRING plstrUnsafe)
  • 作用 :把用户态 LARGE_STRING(带 bAnsi 位的字符串)安全拷贝到内核分页池。
  • 实现流程ProbeForRead 探测结构 → ExAllocatePoolWithTag(PagedPool, Length, TAG_STRING)ProbeForRead(Buffer, Length, sizeof(WCHAR)) + RtlCopyMemory → 输出。Length==0 时 Buffer 保持 NULL。
  • 注意事项 :全程 _SEH2 异常保护,任何探测失败都会清理缓冲并返回异常码。
co_UserCreateWindowEx(window.c L2151)
c 复制代码
PWND FASTCALL co_UserCreateWindowEx(CREATESTRUCTW* Cs, PUNICODE_STRING ClassName,
    PLARGE_STRING WindowName, PVOID acbiBuffer, DWORD dwVer)
  • 作用创建窗口的主路径(在 USER 锁保护下运行)。返回 PWND,失败返回 NULL。
  • 实现流程 (按代码顺序):
    1. 线程必须已连桌面(pti->rpdesk 非空),引用窗口站 WinSta
    2. IntGetAndReferenceClass(ClassName, Cs->hInstance, FALSE) 解析并引用类(class.c L1450)------找不到 → ERROR_CANNOT_FIND_WND_CLASS(user32 收到后加载控件库重试);
    3. 解析父/拥有者:hwndParentHWND_MESSAGE → 消息窗口(rpdesk->spwndMessage);非 WS_CHILD 时 hWndParent 当作 owner;WS_CHILD 无父 → ERROR_TLW_WITH_WSCHILD
    4. owner 规范化:owner 是桌面 → 清空;owner 存在且父非桌面 → 拒绝(owned 窗口必须是顶级);owner 是子窗口则沿父链上溯到顶级;
    5. IntFixWindowCoordinates 修正 CW_USEDEFAULT;
    6. IntCreateWindow 分配并初始化 WND;
    7. UserRefObjectCo(Window, &Ref) 建立 CO 引用,解除句柄引用;
    8. Cs->lpszName/lpszClass 指向内核缓冲,供 WM_NCCREATE/WM_CREATE 使用;
    9. WH_CBT 钩子:分配 CREATESTRUCTW/CBT_CREATEWNDW,IntMsgCreateStructW 打包,co_HOOK_CallHooks(WH_CBT, HCBT_CREATEWND, ...),钩子可改写 x/y/cx/cy/hwndInsertAfter,返回非 0 则中止创建;
    10. 样式修正:非子窗口强制 WS_CLIPSIBLINGS(非 POPUP 还加 WS_CAPTION);WS_EX_WINDOWEDGEIntCheckFrameEdge 逻辑修正;
    11. 非子窗口置 WNDS_SENDSIZEMOVEMSGS;子窗口坐标叠加父客户区原点;
    12. co_WinPosGetMinMaxInfo + WM_GETMINMAXINFO 约束尺寸(WS_THICKFRAME 或非 POPUP/CHILD 时);
    13. 计算 rcWindow(含 RTL 镜像修正)与 rcClient = rcWindow
    14. 非 Win31 兼容且类有 CS_PARENTDC 时清除 CLIPSIBLINGS/CLIPCHILDREN;子窗口跨线程 AttachThreadInput
    15. WM_NCCREATEco_IntSendMessage(..., WM_NCCREATE, 0, (LPARAM)Cs),返回 0 → 失败回滚;
    16. 挂接 :WS_CHILD|WS_MAXIMIZE → IntLinkHwnd(Window, HWND_BOTTOM)(子窗口先沉底),否则按钩子结果 hwndInsertAfter 入链;
    17. IME:IS_IMM_MODE() 且无默认 IME 且 IntWantImeWindowco_IntCreateDefaultImeWindow 并发送 WM_IME_SYSTEM 消息;
    18. WM_NCCALCSIZEco_WinPosGetNonClientSize 计算客户区,RECTL_vOffsetRect 修正窗口位置;
    19. WM_CREATE:返回 -1 → 失败回滚;
    20. IntNotifyWinEvent(EVENT_OBJECT_CREATE, ...);置 WNDS2_WMCREATEMSGPROCESSED(此后"创建成功");
    21. 发送 WM_SIZE(SIZE_RESTORED)/WM_MOVE(WNDS_SENDSIZEMOVEMSGS 时);
    22. 处理 WS_MINIMIZE/WS_MAXIMIZE(IntSetStyle 清零后 co_WinPosMinMaximize + co_WinPosSetWindowPos);
    23. IntSendParentNotify(Window, WM_CREATE)(WM_PARENTNOTIFY);
    24. 满足条件(无 owner/owner 不可见/非工具窗)且桌面子窗口可见时 co_IntShellHookNotify(HSHELL_WINDOWCREATED)
    25. 显示滚动条(WS_VSCROLL/WS_HSCROLL → co_UserShowScrollBar);
    26. Cs->style & WS_VISIBLEco_WinPosShowWindow(Window, dwShowMode);MDI 子窗口额外 WM_MDIREFRESHMENU + SetWindowPos 置顶;
    27. 控制台窗口类手动 GUI 计数;dwHotkey 交给第一个合适窗口(WM_SETHOTKEY)。
  • 失败回滚 (cleanup):已创建 Window → co_UserDestroyWindow(Window)(类引用随之释放);未创建 → IntDereferenceClass。最后释放钩子缓冲、UserDerefObjectCo、解父引用。成功时 EngSetLastError(ERROR_SUCCESS)(CORE-13717:成功不残留旧错误)。
IntCreateWindow(window.c L1805)
c 复制代码
PWND FASTCALL IntCreateWindow(CREATESTRUCTW* Cs, PLARGE_STRING WindowName, PCLS Class,
    PWND ParentWindow, PWND OwnerWindow, PVOID acbiBuffer, PDESKTOP pdeskCreated, DWORD dwVer)
  • 作用分配并初始化 WND 对象(不发送任何消息)。"Allocates and initializes a window"。
  • 实现流程
    1. 线程选择:pdeskCreated ? gptiDesktopThread : GetW32ThreadInfo()(桌面线程创建桌面窗口用);
    2. RTL 布局继承:非 WS_EX_LAYOUTRTL 时,子窗口继承父的 RTL;无父时继承进程 dwLayout & LAYOUT_RTL(对话框 FNID_DIALOG 除外);
    3. WS_EX_WINDOWEDGE 自动推导(WS_EX_DLGMODALFRAME 或(无 STATICEDGE 且 DLGFRAME/THICKFRAME));
    4. Unicode 判定:bUnicodeWindow = !(Cs->dwExStyle & WS_EX_SETANSICREATOR)
    5. UserCreateObject(gHandleTable, pdeskCreated ?: pti->rpdesk, pti, &hWnd, TYPE_WINDOW, sizeof(WND)+Class->cbwndExtra) ------桌面堆分配 WND + 附加字节,返回已加引用对象;
    6. 桌面窗口 HACK:pdeskCreated->DesktopWindow==NULL 时把新窗口登记为桌面窗口(win32csr/desktopbg.c 用);
    7. 字段初始化:WndSetParent/WndSetOwnerfnid=0WndSetLastActive(pWnd, pWnd)(自指);按 dwVer 置 WIN31/NT4/WIN2K 兼容位;pclshModulestyle=Cs->style & ~WS_VISIBLE(创建期强制不可见,稍后由 co_UserCreateWindowEx 决定显示)、ExStylecbwndExtra=类值pActCtx=acbiBuffer
    8. hImc:线程有默认输入上下文(pti->spDefaultImc)且非按钮类时关联;
    9. InternalPos.MaxPos/IconPos = -1(未初始化标记);HideFocus/HideAccel 继承父;InitializeListHead(&ThreadListEntry)pti->cWindows++
    10. 小图标缓存:类有大图标无小图标时按 SM_CXSMICON/SM_CYSMICON 拷贝生成(co_IntCopyImage),标记 CSF_CACHEDSMICON
    11. 窗口过程解析:IsCallProcHandle(类的 lpfnWndProc) → 取 pcpd->pfnClientPrevious;类 fnid 在 BUTTON...GHOST 区间时按 Unicode/ANSI 换 GETPFNCLIENTW/ACSF_SERVERSIDEPROCWNDS_SERVERSIDEWINDOWPROCWNDS_ANSICREATOR/WNDS_ANSIWINDOWPROC 按创建者与类决定(按钮/组合框/编辑框等系统类在 ANSI 创建者下强制 ANSI 过程);
    12. 编辑框类:立即 CreateCallProcUserAddCallProcToClass(2K/XP 行为兼容);
    13. InitializeListHead(&PropListHead)
    14. 标题拷贝:WindowName 非空 → 桌面堆分配 Length+sizeof(UNICODE_NULL) 并拷贝(strName);
    15. 菜单:非子窗口时------Cs->hMenu 直接 IntSetMenu;否则类 lpszMenuName 存在时 co_IntCallLoadMenu 加载再 IntSetMenu;子窗口把 Cs->hMenu 存入 IDMenu(即子窗口 ID);
    16. owner 深度校验与线程附加(父链上有非消息/非桌面父时);
    17. InsertTailList(&pti->WindowListHead, &pWnd->ThreadListEntry) ------挂入线程窗口链表
    18. CS_CLASSDC:类级 DCE(DceAllocDCE(pWnd, DCE_CLASS_DC));CS_OWNDC:窗口级 DCE。
  • 失败路径AllocError(分配失败)设 STATUS_INSUFFICIENT_RESOURCESError: 统一 UserDereferenceObject(pWnd) 后返回 NULL。
IntFixWindowCoordinates(window.c L1710)
c 复制代码
void FASTCALL IntFixWindowCoordinates(CREATESTRUCTW* Cs, PWND ParentWindow, DWORD* dwShowMode)
  • 作用 :处理 CW_USEDEFAULT/(SHORT)0x8000 默认坐标。
  • 实现流程 :重叠窗口(非 POPUP/CHILD)------取进程 hMonitor 或主监视器;STARTF_USEPOSITION 时用进程参数,否则用监视器 cWndStack 级联偏移(SM_CXSIZE+SM_CXFRAME 步进,超工作区 1/4 则复位);尺寸默认 3/4 工作区;无监视器时兜底 800×600。非重叠窗口:默认值一律归零。
  • 注意事项dwShowMode 参数记录"y 为默认值时代入的显示模式",供后续 ShowWindow 使用。

4.6 销毁链

NtUserDestroyWindow(window.c L3062)
c 复制代码
BOOLEAN APIENTRY NtUserDestroyWindow(HWND Wnd)
  • 作用DestroyWindow 系统调用入口。
  • 实现流程UserEnterExclusiveUserGetWindowObjectUserRefObjectCo 保护 → co_UserDestroyWindow(Window)UserDerefObjectCoUserLeave
  • 注意事项:销毁期间持 CO 引用,防止回调重入导致对象提前释放(FIXME 注释承认这是保险措施)。
co_UserDestroyWindow(window.c L2884)
c 复制代码
BOOLEAN co_UserDestroyWindow(PVOID Object)   // Object 实为 PWND
  • 作用 :销毁主流程(也作为 object.c 中 TYPE_WINDOW 的 ObjectDestroy 回调被 UserDestroyObjectsForOwner 调用)。
  • 实现流程
    1. 二次销毁防护:IntIsWindow(UserHMGetHandle(Window)) 为假 → 直接返回 TRUE;
    2. 权限:窗口属于其他线程且非桌面销毁(DF_DESTROYED 桌面窗口特例)→ ERROR_ACCESS_DENIED
    3. WNDS2_WMCREATEMSGPROCESSED 已置 → co_HOOK_CallHooks(WH_CBT, HCBT_DESTROYWND, ...),钩子返回非 0 则取消销毁
    4. 解除线程附加:非子窗口有 owner → AttachThreadInput(..., FALSE);子窗口非顶级 → 父线程解除附加;
    5. IntSendParentNotify(Window, WM_DESTROY)(子窗口发 WM_PARENTNOTIFY);
    6. 隐藏窗口:子窗口 co_WinPosShowWindow(SW_HIDE)(收 WM_SHOWWINDOW),非子窗口 co_WinPosSetWindowPos(SWP_HIDEWINDOW)
    7. 调整 spwndLastActive(owner 链上的引用);
    8. 队列引用清理:spwndActive/spwndFocus/spwndActivePrev/spwndCapture 置 NULL;
    9. 清理 Shell 窗口句柄(pDeskInfo->hShellWindow);IntEngWindowChanged(WOC_DELETE) 通知引擎;
    10. 再次 IntIsWindow 检查(回调可能已销毁);
    11. IntDestroyOwnedWindows(Window) 递归销毁被拥有的弹出窗口;
    12. 构造 WM_MOUSEMOVE 并 co_MsqInsertMouseMessage(鼠标移出被销毁窗口);
    13. IntNotifyWinEvent(EVENT_OBJECT_DESTROY)
    14. IntSendDestroyMsg:WM_DESTROY + 递归子窗口;
    15. 默认 IME 窗口联动销毁(IntImeCanDestroyDefIME/forChild);
    16. co_UserFreeWindow(Window, ppi, pti, TRUE) 释放存储。
  • 注意事项 :销毁路径全程穿插 IntIsWindow 重入检查------WM_DESTROY 里应用可能再调用 DestroyWindow 销毁本窗口或其子窗口。
IntSendDestroyMsg(window.c L462)
c 复制代码
static void IntSendDestroyMsg(HWND hWnd)
  • 作用 :发送 WM_DESTROY递归销毁子窗口(先父后子)。
  • 实现流程
    1. 焦点/激活善后(Rule #1):被销毁窗口是活动窗口 → co_WinPosActivateOtherWindow(激活其他窗口);是焦点窗口 → 子窗口撤焦到父、否则 co_UserSetFocus(NULL)
    2. 插入符:MessageQueue->CaretInfo.hWnd == hWndco_IntDestroyCaret
    3. 鼠标跟踪:rpdesk->spwndTrack == WindowIntRemoveTrackMouseEvent
    4. 剪贴板:prpwinsta->spwndClipOwner == WindowUserClipboardRelease
    5. co_IntSendMessage(hWnd, WM_DESTROY, 0, 0)
    6. 返回后 IntIsWindow 复查(WM_DESTROY 里可能自我销毁)→ IntWinListChildren 快照 → 逐个 IntSendDestroyMsg 递归子窗口。
  • 注意事项 :子窗口列表必须在 WM_DESTROY 之后枚举(WM_DESTROY 里应用可能新建子窗口,也应被销毁)。
co_UserFreeWindow(window.c L575)
c 复制代码
LRESULT co_UserFreeWindow(PWND Window, PPROCESSINFO ProcessData,
                          PTHREADINFO ThreadData, BOOLEAN SendMessages)
  • 作用释放窗口的一切存储------"functional DestroyWindows"(Internals p.358),即 CreateWindow 所做一切的逆操作。
  • 实现流程
    1. WNDS2_INDESTROY 置位(防二次进入);清 WS_VISIBLEcVisWindows--
    2. 先从线程窗口链表摘除(RemoveEntryList(&ThreadListEntry)),避免销毁线程窗口时重入;
    3. IntDeRegisterShellHookWindowIntWinListChildren 快照递归:同线程子窗口直接 co_UserFreeWindow,跨线程子窗口发 WM_ASYNC_DESTROYWINDOW(异步销毁);
    4. SendMessages 时:co_UserRedrawWindow(RDW_VALIDATE|...) 清更新区域(防止 WM_NCDESTROY 期间生成 WM_PAINT)→ 同线程发 WM_NCDESTROY
    5. UserClipboardFreeWindowDestroyTimersForWindowUnregisterWindowHotKeysMsqRemoveWindowMessagesFromQueue(清队列中所有发往该窗口的消息);
    6. state |= WNDS_DESTROYED; fnid |= FNID_FREED从此刻起句柄失效);
    7. 清理 Shell 窗口/ListView 句柄、默认 IME 的 owner、spwndDefaultIme(UserAssignmentUnlock);
    8. 队列引用(focus/active/capture)再清理(IntReleaseCapture);
    9. 更新区域回收:MsqDecPaintCountQueue + GreDeleteObject(hrgnUpdate)(若>HRGN_WINDOW 且有效),清 WNDS_INTERNALPAINT/WNDS_SENDERASEBACKGROUND/WNDS_SENDNCPAINT
    10. 菜单释放:非子窗口 IDMenu 是菜单 → IntDestroyMenuObjectSystemMenu 同理;
    11. DceFreeWindowDCE(清窗口 DC,防孤儿 DC);IntUnlinkWindow 解链;
    12. UserRemoveWindowProps 清属性;WndSetOwner/Parent/Child/LastActive(NULL) 清引用;
    13. UserRefObjectCo + UserMarkObjectDestroy(标记句柄表项销毁);IntDestroyScrollBars;控制台窗口类手动 GUI 计数;
    14. IntDereferenceClass(类引用计数归还------与 IntCreateWindow/IntGetAndReferenceClass 对称);hrgnClip 删除;
    15. pti->cWindows--UserFreeWindowInfo(释放 strName 缓冲、清 CallbackWnd 缓存);
    16. UserDerefObjectCo + UserDeleteObject(UserHMGetHandle(Window), TYPE_WINDOW) ------ 句柄表项进入空闲链表;当所有引用归零时 FreeDeskThreadObject 把 WND 归还桌面堆。
IntDestroyOwnedWindows(window.c L2849)
  • 作用 :枚举 IntWinListOwnedPopups(Window) 的每个被拥有窗口:先 WndSetOwner(NULL) 解除 owner,再对同线程窗口 co_UserDestroyWindow 递归销毁。
  • 注意事项 :列表先快照(IntWinListOwnedPopups 分配池数组),销毁过程中 owner 链变化不影响枚举。
IntShowOwnedPopups(window.c L4673)
  • 作用ShowOwnedPopups 语义------显示/隐藏 owner 的所有弹出窗口,配合 WNDS_HIDDENPOPUP 状态与 WM_SHOWWINDOW(SW_PARENTOPENING/SW_PARENTCLOSING) 消息。
IntSendParentNotify(window.c L1689)
  • 作用 :子窗口(非 WS_EX_NOPARENTNOTIFY)创建/销毁时给父窗口发 WM_PARENTNOTIFYwParam = MAKEWPARAM(msg, IDMenu)lParam = 子窗口句柄)。
  • 使用方式 :创建路径 IntSendParentNotify(Window, WM_CREATE)、销毁路径 IntSendParentNotify(Window, WM_DESTROY)

4.7 窗口属性读写族(SetWindowLong / GetWindowLong)

co_IntSetWindowLongPtr(window.c L3849)
c 复制代码
static LONG_PTR co_IntSetWindowLongPtr(HWND hWnd, DWORD Index, LONG_PTR NewValue,
                                       BOOL Ansi, ULONG Size, BOOL bAlter)
  • 作用SetWindowLong 系列的内核实现总入口(写 WND 字段或窗口附加字节),返回旧值。
  • 实现流程
    • 正索引(附加字节)(Index + Size) > Window->cbwndExtraERROR_INVALID_INDEX;否则读写 &Window[1] + Index(64 位下按 Size 区分 32/64 位读写,ReadUnalignedU32/WriteUnalignedU32 vs ReadUnalignedUlongPtr/WriteUnalignedUlongPtr)。
    • 负索引(标准字段) ,switch:
      • GWL_EXSTYLE:构造 STYLESTRUCT,发 WM_STYLECHANGING;Shell 窗口强制清 WS_EX_TOPMOST;按 IntCheckFrameEdge 修正 WS_EX_WINDOWEDGE;非分层窗口 SetLayeredStatus;写 ExStyle;发 WM_STYLECHANGED
      • GWL_STYLE:发 WM_STYLECHANGING(bAlter 时跳过);顶层窗口强制保留 WS_CLIPSIBLINGSWS_MINIMIZE 不可清除;同步 WS_EX_WINDOWEDGE;子窗口↔非子窗口切换时清/释放 IDMenu 菜单引用;WS_VISIBLE 变化维护 cVisWindowsDceResetActiveDCEs;写 style;发 WM_STYLECHANGED
      • GWLP_WNDPROC:校验窗口属于当前进程且未 FNID_FREED(否则 ERROR_ACCESS_DENIED),IntSetWindowProc 换过程,返回旧过程。
      • GWLP_HINSTANCE:改 hModule
      • GWLP_HWNDPARENT:父是桌面 → IntSetOwner;否则 co_UserSetParent
      • GWLP_ID:改 IDMenuGWLP_USERDATA:改 dwUserData
      • 其他:ERROR_INVALID_INDEX
  • 注意事项bAlter 参数来自 NtUserAlterWindowStyle(user32 SetWindowLong 的"改样式不发消息"变体,用于窗口过程内部)。
co_UserSetWindowLong / co_UserSetWindowLongPtr(window.c L4046 / L4052)
  • 薄封装:co_IntSetWindowLongPtr(hWnd, Index, NewValue, Ansi, sizeof(LONG)/sizeof(LONG_PTR), FALSE)
NtUserSetWindowLong(window.c L4069)/ NtUserSetWindowLongPtr(L4091)/ NtUserAlterWindowStyle(L4113)
  • 三个系统调用入口:UserEnterExclusive 加锁;禁止对桌面窗口设置ERROR_ACCESS_DENIEDIntGetDesktopWindow() 比较);分别以 sizeof(LONG)/sizeof(LONG_PTR)/bAlter=TRUE 调 co_IntSetWindowLongPtr
NtUserSetWindowWord(window.c L4144)
  • 作用 :16 位遗留接口(SetWindowWord),GWL_ID/GWL_HINSTANCE/GWL_HWNDPARENT 转发给 co_UserSetWindowLong,其余按 WORD 读写附加字节(越界 ERROR_INVALID_INDEX)。
IntGetWindowProc / IntSetWindowProc(window.c L781 / L841)
  • IntGetWindowProc(pWnd, Ansi):按窗口/类的 fnid 与 Unicode/ANSI 标志取正确的窗口过程。服务器端过程(WNDS_SERVERSIDEWINDOWPROC)→ GETPFNSERVER 反查 GETPFNCLIENTA/W;系统类(BUTTON...GHOST)按相反编码映射;ANSI 转换经 UserGetCPD(CallProc 数据)实现 IntGetCPDA2U/IntGetCPDU2A
  • IntSetWindowProc(pWnd, NewWndProc, Ansi):返回旧过程;处理 IsCallProcHandle(取 pfnClientPrevious);客户端过程与服务器端过程互转(匹配 GETPFNCLIENTA/WGETPFNSERVER,置/清 WNDS_SERVERSIDEWINDOWPROC),维护 Unicode/WNDS_ANSIWINDOWPROC
用户态 GetWindowLong(user32/windows/class.c L1089 IntGetWindowLong)
  • 作用GetWindowLongA/W/PtrA/PtrW(L1162-1202)的统一实现,完全在用户态 完成(桌面堆用户态映射直读 WND):
    • GWLP_HWNDPARENTGetAncestor(GA_PARENT),是桌面则返回 GetWindow(GW_OWNER)
    • ValidateHwnd 校验;正索引读附加字节(越界 ERROR_INVALID_INDEX);
    • 负索引:GWLP_USERDATA/GWL_STYLE/GWL_EXSTYLE(仅 PUBLIC_EXSTYLE 位,L1082-1087 定义的公开位掩码)/GWLP_ID/GWLP_HINSTANCE-1 返回 &wndPtr->state(兼容 WW* 布局);GWLP_WNDPROCTestWindowProcess 权限检查后 IntGetWndProc 转换编码。
  • 设计要点:读操作用户态直读(快),写操作必须进内核(原子性 + 消息通知 + 权限)。这就是"双读"架构的体现。

4.8 遍历/查找族

IntWinListChildren(window.c L274)
c 复制代码
HWND* FASTCALL IntWinListChildren(PWND Window)
  • 作用 :把窗口的所有直接子窗口句柄编译为 HWND 数组(ExAllocatePoolWithTag(PagedPool, ..., USERTAG_WINDOWLIST)),末尾 NULL
  • 实现流程 :先数 spwndChild 链长度,分配 (N+1)*sizeof(HWND),再沿链填 UserHMGetHandle(Child)
  • 使用方式IntSendDestroyMsg(WM_DESTROY 后枚举子窗口)、co_UserFreeWindow(子窗口快照)、NtUserBuildHwndList(EnumThreadWindows 分支)、IntFindWindow
  • 注意事项 :调用者负责 ExFreePoolWithTag(List, USERTAG_WINDOWLIST)
IntWinListOwnedPopups(window.c L315)
  • 作用 :枚举桌面的直接子窗口中 spwndOwner == Window被拥有弹出窗口 (跳过默认 IME 窗口 IntWndIsDefaultIme)。与 IntWinListChildren 结构相同(数组 + NULL 结尾)。
  • 使用方式IntDestroyOwnedWindowsIntShowOwnedPopups
IntGetWindow(window.c L381)
c 复制代码
HWND FASTCALL IntGetWindow(HWND hWnd, UINT uCmd)
  • 作用 :实现 GetWindow 的 GW_OWNER/GW_HWNDFIRST/GW_HWNDNEXT/GW_HWNDPREV/GW_CHILD/GW_HWNDLAST。
  • 实现流程ValidateHwndNoErr 后按命令取对应链表指针,UserHMGetHandle 转句柄;无效命令 ERROR_INVALID_GW_COMMAND
  • 注意事项GW_HWNDFIRST 返回父的 spwndChild(Z 序最顶);GW_HWNDLAST 沿 spwndNext 走到链尾。
IntBuildHwndList(window.c L1422)/ IntPopulateHwndList(L1393)/ IntGrowHwndList(L1373)/ IntFreeHwndList(L1467)
  • IntBuildHwndList(pwnd, dwFlags, pti):构建 WINDOWLIST。优先复用 gpwlCache 缓存,否则分配 sizeof(WINDOWLIST)+(32-1)*sizeof(HWND)IntPopulateHwndList 深度优先填充(IACE_CHILDREN 递归子窗口;IACE_LIST 控制兄弟遍历),按 pti 过滤线程;写满自动 IntGrowHwndList(+8 句柄);尾部写 HWND_TERMINATOR;挂入 gpwlList 全局链。
  • IntFreeHwndList(pwlTarget):从 gpwlList 摘除;与 gpwlCache 容量比较决定缓存还是直接释放(缓存只保留最大的一块)。
NtUserBuildHwndList(window.c L1513)
c 复制代码
NTSTATUS NTAPI NtUserBuildHwndList(HDESK hDesktop, HWND hwndParent, BOOLEAN bChildren,
    ULONG dwThreadId, ULONG cHwnd, HWND* phwndList, ULONG* pcHwndNeeded)
  • 作用EnumWindows/EnumChildWindows/EnumDesktopWindows/EnumThreadWindows 的内核数据源(user32 按 cHwndNeeded 分配缓冲后二次调用取数据)。
  • 实现流程
    • hwndParent || !dwThreadId 分支(桌面/父窗口遍历):父为 NULL → 取活动桌面或按 hDesktop 校验(IntValidateDesktopHandle),父缺省为桌面窗口;从 Parent->spwndChild 开始深度优先 (先下后右)遍历,bChildren 决定是否下钻子窗口;ProbeForWrite 逐项写 phwndList;计数进 *pcHwndNeeded
    • 否则(EnumThreadWindows):PsLookupThreadByThreadId 找线程 → 取 Tcb.Win32Thread(非 GUI 线程 → STATUS_INVALID_PARAMETER)→ IntWinListChildren(UserGetDesktopWindow()) 遍历全部顶级窗口,按 head.pti == W32Thread 过滤。注释说明不用线程窗口链表WindowListHead)而用桌面子窗口遍历,是为了规避 co_UserFreeWindow 期间链表被摘除的问题(修复 CreateWindowEx 测试)。
  • 注意事项 :全程 UserEnterShared() 共享锁;调用者需先以 cHwnd=0 查询数量。
IntFindWindow(window.c L3086)/ NtUserFindWindowEx(L3168)
  • IntFindWindow(Parent, ChildAfter, ClassAtom, WindowName):在父的直系子窗口中顺序查找(跳过 ChildAfter),匹配条件:ClassAtom==0Child->pcls->atomNVClassName == ClassAtom;窗口名可选(WindowName->Buffer != 0RtlCompareUnicodeString 忽略大小写)。内核版本不发 WM_GETTEXT ,直接读 strName(注释明确与用户态版本的区别)。
  • NtUserFindWindowEx:实现 FindWindow/FindWindowExhwndParent==NULL → 桌面窗口(并允许消息窗口);HWND_MESSAGE → 消息窗口;类名/窗口名先 ProbeForReadUnicodeString 探测,IntGetAtomFromStringOrAtom 转原子;桌面父分支遍历所有顶级窗口(含子窗口命中时返回顶级窗口 句柄的语义------IntFindWindow(TopLevelWindow, ...) 命中即返回 TopLevelWindow);最后消息窗口兜底查找。
UserGetAncestor(window.c L3363)/ NtUserGetAncestor(L3427)
  • UserGetAncestor(pWnd, uType):GA_PARENT(返回父)、GA_ROOT(沿父链到顶层,消息窗口处截断)、GA_ROOTOWNER(沿 IntGetParent 到拥有链顶端)。
  • NtUserGetAncestorUserEnterShared + UserGetWindowObject + uType 范围校验(1...GA_ROOTOWNER),调 UserGetAncestor 后转句柄。

4.9 窗口信息服务/杂项族

NtUserQueryWindow(window.c L4214)
c 复制代码
DWORD_PTR APIENTRY NtUserQueryWindow(HWND hWnd, DWORD Index)
  • 作用 :一个函数服务多个查询(GetWindowThreadProcessIdIsHungAppWindow、激活/焦点窗口、IME 查询等):
    • QUERY_WINDOW_UNIQUE_PROCESS_ID/THREAD_IDIntGetWndProcessId/IntGetWndThreadIdPsGetProcessId(ppi->peProcess)/PsGetThreadId(pEThread));CSRSS 控制台窗口类从附加字节读控制台领导进程 ID(GWLP_CONSOLE_LEADER_PID/TID 偏移 0/4);
    • QUERY_WINDOW_ACTIVE/FOCUS:队列 spwndActive/spwndFocus
    • QUERY_WINDOW_ISHUNGfnid == FNID_GHOSTMsqIsHung
    • QUERY_WINDOW_REAL_IDpEThread->Cid.UniqueProcess
    • QUERY_WINDOW_FOREGROUNDMessageQueue == gpqForeground
    • QUERY_WINDOW_DEFAULT_IME/ICONTEXT/ACTIVE_IMEspwndDefaultIme/spDefaultImc(前台队列活动窗口线程的默认 IME)。
GetWindowThreadProcessId(user32/windows/window.c L1409)
  • 作用 :用户态实现:ValidateHwnd 后取 pWnd->head.pti;本线程则直接读 TEB(NtCurrentTeb()->ClientId),跨线程走 NtUserQueryWindow(QUERY_WINDOW_UNIQUE_PROCESS_ID/THREAD_ID)
NtUserRegisterWindowMessage(window.c L4319)
  • 作用RegisterWindowMessageIntSafeCopyUnicodeStringTerminateNULL 捕获消息名 → IntAddAtom 返回原子(即消息号)。返回 0 表示失败。
NtUserSetWindowFNID(window.c L4355)
  • 作用 :设置窗口 fnid(user32 控件初始化用)。权限:窗口须属于当前进程;只接受 FNID_FIRST..FNID_GHOSTWnd->fnid 须为 0)或 FNID_DESTROYWnd->fnid |= fnID
  • 注意事项:注释说明"HACK:最小值本应是 FNID_BUTTON,但菜单代码依赖此放宽"。
NtUserDefSetText / DefSetText(window.c L4478 / L4397)
  • DefSetText(Wnd, WindowText):把窗口标题写入 strName(长度适配:够用则原地写,否则重分配桌面堆缓冲),并发 co_IntShellHookNotify(HSHELL_REDRAW)
  • NtUserDefSetText:系统调用包装,处理 ANSI/Unicode LARGE_STRINGRtlLargeStringToUnicodeString),供 DefWindowProc 的 WM_SETTEXT 分支调用。
  • 共同逻辑 :strName 写前先释放旧缓冲(若新长度放不下),写后保证 MaximumLength = Length + sizeof(UNICODE_NULL)
NtUserInternalGetWindowText(window.c L4612)
  • 作用GetWindowText 内核回退路径(user32 通常直读用户态 strName):取 strName.Length/sizeof(WCHAR)MmCopyToCaller 拷贝并补 L'\0'nMaxCount<=1ERROR_INVALID_PARAMETER
NtUserGetComboBoxInfo(window.c L3491)
c 复制代码
BOOL APIENTRY NtUserGetComboBoxInfo(HWND hWnd, PCOMBOBOXINFO pcbi)
  • 作用 :填充 COMBOBOXINFO(下拉框的编辑框/列表框句柄、按钮与文本矩形、按钮状态)。
  • 实现流程
    1. UserGetWindowObject 解析;ProbeForWrite(pcbi, sizeof(COMBOBOXINFO), 1) 探测用户缓冲;pcbi->cbSize < sizeof(COMBOBOXINFO)ERROR_INVALID_PARAMETER
    2. 窗口既不是 COMBOBOX 类也不是 FNID_COMBOBOX → 转发 CB_GETCOMBOBOXINFO 消息(让控件自己回答);
    3. 否则直接从 ((PWND2CBOX)Wnd)->pCBox(HEADCOMBO 结构,window.c L3461-3478)读取 textRect/buttonRect/wState/self/hWndEdit/hWndLBoxCBF_BUTTONDOWNSTATE_SYSTEM_PRESSED;按钮矩形为空 → STATE_SYSTEM_INVISIBLE
    4. 跨进程处理ppi != Wnd->head.pti->ppiKeAttachProcess 附加到窗口所属进程再访问其用户态映射数据,结束 KeDetachProcess
  • 设计要点:这是"窗口对象直接携带控件状态(HEADCOMBO 是 WND2CBOX 的成员)"的典型例子------组合框下拉部分与编辑框的关系保存在窗口附加结构中,内核可以直接读取而无需发消息。
NtUserGetListBoxInfo(window.c L3602)
c 复制代码
DWORD APIENTRY NtUserGetListBoxInfo(HWND hWnd)
  • 作用 :返回列表框的页大小(LB_GETLISTBOXINFO 的数据源,供 user32 计算滚动量)。
  • 实现流程 :非 LISTBOX 类/非 FNID_LISTBOX → 发 LB_GETLISTBOXINFO 消息;否则跨进程附加后读 ((PWND2LB)Wnd)->pLBiv->page_size(LB_DESCR,window.c L3575-3591),未找到 → 0。
  • 注意事项 :注释强调 wine 测试要求"真实列表框直接读结构、非列表框才发消息",LB_DESCR 布局必须与 user32 控件侧一致(// See Controls ListBox.c:LB_GETLISTBOXINFO must match...)。
UserGetShellWindow(window.c L3706)/ NtUserSetShellWindowEx(L3738)
  • UserGetShellWindow:校验进程窗口站句柄后返回 WinStaObject->ShellWindow
  • NtUserSetShellWindowEx(hwndShell, hwndListView):设置窗口站级 Shell 窗口(Explorer)。约束:Shell 窗口只能设置一次(已有则失败);Shell/ListView 不得 WS_EX_TOPMOST;置 WNDS2_BOTTOMMOSTco_WinPosSetWindowPos(HWND_BOTTOM) 沉底;登记到 WinStaObjectpDeskInfo(hShellWindow/spwndShell/spwndBkGnd/ppiShellProcess);UserRegisterHotKey(WndShell, SC_TASKLIST, MOD_CONTROL, VK_ESCAPE)
UserUpdateUiState(window.c L40)
  • 作用WM_UPDATEUISTATE 的内核处理:UIS_SET/UIS_CLEAR 设置/清除 HideFocus/HideAccel(UISF_HIDEFOCUS/UISF_HIDEACCEL);UIS_INITIALIZE 与非法标志 → ERROR_INVALID_PARAMETER
IntCheckFrameEdge(window.c L3838)
  • 作用 :判断样式组合是否隐含窗口边框(WS_EX_DLGMODALFRAME 或(无 STATICEDGE 且 WS_DLGFRAME|WS_THICKFRAME))→ 决定 WS_EX_WINDOWEDGE 的置位。
IntReAllocatePoolWithTag(window.c L23)
  • 作用 :带标签的池重分配(分配新块、拷贝 min(旧,新) 字节、释放旧块)。IntGrowHwndList 用其扩容。

4.10 与位置/尺寸相关的协作函数(winpos.c)

window.c 自身不计算边框尺寸,而是委托 winpos.c:

IntGetWindowBorderMeasures(winpos.c L867)
c 复制代码
VOID FASTCALL IntGetWindowBorderMeasures(PWND Wnd, UINT *cx, UINT *cy)
  • 作用 :按样式返回窗口边框尺寸:HAS_DLGFRAME(未最小化)→ SM_CXDLGFRAME/SM_CYDLGFRAME;HAS_THICKFRAME → SM_CXFRAME/SM_CYFRAME;HAS_THINFRAME → SM_CXBORDER/SM_CYBORDER;否则 0。
  • 使用方式 :供非客户区计算(WM_NCCALCSIZE 默认分支、标题栏/菜单栏布局)。
co_WinPosGetNonClientSize(winpos.c L2396)
c 复制代码
LRESULT co_WinPosGetNonClientSize(PWND Window, RECT* WindowRect, RECT* ClientRect)
  • 作用 :以 WindowRect 为输入,发送 WM_NCCALCSIZE (FALSE 参数)让窗口过程计算客户区,FixClientRect 修正后输出 ClientRect
  • 使用方式co_UserCreateWindowEx 创建时调用(步骤 18),ShowWindow/SetWindowPos 也会调用。

5. 窗口创建完整流程

将第 4 节的函数按时间线串起来,一次 CreateWindowExW 的完整旅程:

阶段 0:用户态准备(user32/windows/window.c)

  1. CreateWindowExW(L478):若系统控件未注册则 RegisterSystemControls()(注册"Button"/"Edit"/"Static"等系统类)。
  2. WS_EX_MDICHILD 预处理(L500):校验父是 MDIClient(pWndParent->fnid == FNID_MDICLIENT);打包 MDICREATESTRUCTWlpParam;修正样式(强制 WS_CHILD|WS_VISIBLE|WS_CLIPSIBLINGS|WS_CAPTION|WS_SYSMENU|WS_THICKFRAME|WS_MINIMIZEBOX|WS_MAXIMIZEBOX);MDI_CalcDefaultChildPos 计算默认位置;hMenu 换成子窗口 ID。
  3. User32CreateWindowEx(L152):
    • dwFlagsVer = RtlGetExpWinVer(hInstance)------记录调用模块的"期望 Windows 版本"(驱动 WNDS2_WIN31/40/50COMPAT 兼容模式);
    • ANSI 类名 → RtlCreateUnicodeStringFromAsciiz;窗口名 RtlInitLargeString + ANSI→Unicode 转换;
    • 非子窗口且无 hMenu 时,GetClassInfoEx 查类默认菜单并 LoadMenu
    • ANSI 创建者置 WS_EX_SETANSICREATOR(内核据此判定 bUnicodeWindow);
    • ClassNameToVersion 解析类版本/控件库名;
    • for(;😉 重试循环 :调 NtUserCreateWindowEx;失败且 GetLastError()==ERROR_CANNOT_FIND_WND_CLASSVersionRegisterClass 加载控件库后重试一次(这是"未注册的类也能创建"的机制)。

阶段 1:系统调用与参数捕获(window.c L2697)

  1. NtUserCreateWindowEx:菜单句柄校验 → ProbeAndCaptureLargeString 捕获窗口名 → 类名/类版本(原子直传 or 捕获)→ 填 CREATESTRUCTWUserEnterExclusive

阶段 2:类解析(class.c L1450)

  1. IntGetAndReferenceClassIntGetClassAtom(按原子/名字在进程私有类 pclsPrivateList、公共类 pclsPublicList、系统类 SystemClassList 中查找)→ IntReferenceClass 加引用(cWndReferenceCount++)。

阶段 3:父/拥有者解析 + 坐标修正(window.c L2151 步骤 3-5)

  1. HWND_MESSAGEspwndMessagehWndParent 在非 WS_CHILD 时成为 owner;owner 规范化(桌面→NULL;子窗口→上溯到顶级;owned 必须顶级)。
  2. IntFixWindowCoordinates:CW_USEDEFAULT 展开。

阶段 4:WND 分配与初始化(IntCreateWindow L1805)

  1. UserCreateObject(..., TYPE_WINDOW, sizeof(WND)+cbwndExtra)AllocDeskThreadObject 桌面堆分配 + 零初始化 + 头字段(pSelf/rpdesk/pti)+ IntReferenceThreadInfo + 句柄表 UserAllocHandle(句柄引用)→ 创建者引用。
  2. 逐字段填充(见 4.5):窗口过程解析、标题拷贝、菜单、DCE、InsertTailList(WindowListHead) 等。

阶段 5:回调与挂接(co_UserCreateWindowEx 步骤 9-27)

  1. WH_CBT HCBT_CREATEWND(可改坐标/插入位置)。
  2. 样式修正 → WM_GETMINMAXINFO 尺寸约束 → rcWindow/rcClient 计算。
  3. WM_NCCREATE(失败 → 回滚销毁)。
  4. IntLinkHwnd 挂入 Z 序(子窗口沉底 HWND_BOTTOM)。
  5. IME 默认窗口创建。
  6. WM_NCCALCSIZEco_WinPosGetNonClientSize 计算客户区)。
  7. WM_CREATE(返回 -1 → 回滚销毁)。
  8. WNDS2_WMCREATEMSGPROCESSED 置位 → WM_SIZE/WM_MOVE → 最小化/最大化处理 → WM_PARENTNOTIFY → HSHELL_WINDOWCREATED → 滚动条 → WS_VISIBLEco_WinPosShowWindow(首显创建窗口表面,见第 3 分册)→ 热键。

阶段 6:返回

  1. hwnd = UserHMGetHandle(pwnd)UserLeave;释放捕获缓冲;user32 返回 HWND。

5.1 创建期关键状态位时间线

时刻 state/state2 意义
UserCreateObject 后 全 0 对象就绪但未初始化
IntCreateWindow 中 WNDS_ANSICREATOR/WNDS_ANSIWINDOWPROC/WNDS_SERVERSIDEWINDOWPROC/WNDS2_WIN31/40/50COMPAT 编码与兼容性确定
链接后 WS_EX2_LINKED(ExStyle2) 已在 Z 序链
WM_CREATE 成功后 WNDS2_WMCREATEMSGPROCESSED "完整创建"标记(销毁时据此调用 CBT 钩子)

5.2 失败回滚的对称性

co_UserCreateWindowEx 的 cleanup 保证类引用只在两种路径之一被释放

  • IntCreateWindow 失败(Window==NULL)→ IntDereferenceClass
  • 回调失败(Window 已创建)→ co_UserDestroyWindow(Window)co_UserFreeWindow 内的 IntDereferenceClass

同理,co_UserFreeWindowIntCreateWindow 的完整逆操作:WND 分配 ↔ 桌面堆回收、线程链表插入 ↔ 摘除、cWindows++cWindows--、类引用 ↔ 类解引用、strName 分配 ↔ 释放、菜单/DCE/滚动条/属性建立 ↔ 销毁。

5.3 WM_NCCREATE / WM_CREATE 的 CREATESTRUCTW 语义

创建回调把 CREATESTRUCTW(或其指针)作为 lParam 传给窗口过程。回调期间该结构中的指针必须仍指向有效的内核缓冲 ------这就是 co_UserCreateWindowEx 在回调前改写 Cs->lpszName/lpszClass(L2289-2290)的原因:

字段 回调期间的值 说明
lpszClass 类名 UNICODE_STRING.Buffer(原子原值或捕获后的缓冲) 原子时直接是 MAKEINTATOM
lpszName 窗口名 LARGE_STRING.Buffer(捕获后的内核池缓冲) 用户态原始指针已不可用
style / dwExStyle 经样式修正(CLIPSIBLINGS/CAPTION/WINDOWEDGE)后的值 与最终 Wnd->style/ExStyle 基本一致
hMenu 菜单句柄(子窗口为子窗口 ID) 子窗口的 hMenu 参数语义是 ID
hInstance 创建模块句柄 存入 Wnd->hModule
lpCreateParams CreateWindowExlpParam 原样透传 应用自行解释
hwndParent 创建参数中的父句柄 注意与解析后的 ParentWindow 可能不同(owner 场景)
x/y/cx/cy WH_CBT 钩子可能改写后的坐标 WM_CREATE 前已用于 rcWindow 计算

回调顺序与语义要点

  1. WM_NCCREATE(L2424):非客户区创建确认,返回 0 表示应用拒绝 → 整个创建失败回滚。
  2. WM_NCCALCSIZE(经 co_WinPosGetNonClientSize,L2474):参数 FALSE + RECT*,窗口过程计算客户区。
  3. WM_CREATE(L2484):客户区创建确认,返回 -1 表示失败 → 回滚销毁。
  4. WM_SIZE/WM_MOVE(L2510/2517):创建期总是 SIZE_RESTORED(注释:即使 WS_MAXIMIZE 也发 RESTORED,因为窗口尚未显示/最大化);用 co_IntSendMessageNoWait(不等待应用处理)。
  5. WM_PARENTNOTIFY(WM_CREATE)(L2545):父窗口收到子窗口创建通知。

5.4 线程输入附加(AttachThreadInput)在创建中的作用

co_UserCreateWindowExIntCreateWindow 在多处调用 UserAttachThreadInput

场景 代码位置 原因
子窗口父窗口跨线程 L2418 父子必须共享输入状态(焦点/激活/捕获)
窗口 owner 跨线程 L2120 被拥有窗口与 owner 共享输入队列
SetParent 换父 co_IntSetParent L1262/1270 同上
销毁时 co_UserDestroyWindow L2936/2978 对称解除附加

这保证"一个输入队列"(attach 组)内的窗口才能互相激活/传递焦点------Win32 语义的硬约束。

6. 窗口销毁完整流程

6.1 完整时间线

复制代码
DestroyWindow(hWnd)                    // user32
  → NtUserDestroyWindow (L3063)        // UserEnterExclusive
      → UserGetWindowObject           // 句柄→PWND(校验 WNDS_DESTROYED)
      → UserRefObjectCo(Window)       // CO 引用保护
      → co_UserDestroyWindow (L2884)
          ├─ 1. 二次销毁检查(IntIsWindow)
          ├─ 2. 线程归属检查(ERROR_ACCESS_DENIED)
          ├─ 3. WH_CBT HCBT_DESTROYWND(可取消!返回非 0 → 中止销毁)
          ├─ 4. 解除线程输入附加(owner/父线程)
          ├─ 5. WM_PARENTNOTIFY(WM_DESTROY)(子窗口)
          ├─ 6. 隐藏窗口(子窗口 SW_HIDE / 非子窗口 SWP_HIDEWINDOW)
          ├─ 7. spwndLastActive 修正
          ├─ 8. 队列引用清空(active/focus/activePrev/capture)
          ├─ 9. Shell 窗口句柄清空 + IntEngWindowChanged(WOC_DELETE)
          ├─ 10. IntDestroyOwnedWindows:递归销毁被拥有弹出窗口
          ├─ 11. 注入 WM_MOUSEMOVE(鼠标脱离被销毁窗口)
          ├─ 12. IntNotifyWinEvent(EVENT_OBJECT_DESTROY)
          ├─ 13. IntSendDestroyMsg (L462)
          │       ├─ 焦点/激活善后(co_WinPosActivateOtherWindow / co_UserSetFocus)
          │       ├─ 插入符/鼠标跟踪/剪贴板释放
          │       ├─ WM_DESTROY
          │       └─ 递归 IntSendDestroyMsg 子窗口(先父后子)
          ├─ 14. 默认 IME 窗口联动销毁
          └─ 15. co_UserFreeWindow (L575)
                  ├─ WNDS2_INDESTROY 置位;清 WS_VISIBLE
                  ├─ 线程窗口链表摘除
                  ├─ 递归释放子窗口(跨线程发 WM_ASYNC_DESTROYWINDOW)
                  ├─ 清更新区域(RDW_VALIDATE)→ WM_NCDESTROY
                  ├─ 剪贴板/计时器/热键/队列消息清理
                  ├─ state |= WNDS_DESTROYED; fnid |= FNID_FREED   ★句柄失效点
                  ├─ Shell/IME/队列引用收尾;更新区域与 hrgnClip 删除
                  ├─ 菜单/SystemMenu 销毁;DCE 释放;IntUnlinkWindow 解链
                  ├─ 属性清空;Owner/Parent/Child/LastActive 引用清空
                  ├─ UserMarkObjectDestroy(句柄表项标记销毁)
                  ├─ IntDereferenceClass(类引用归还)
                  ├─ UserFreeWindowInfo(strName 释放)
                  └─ UserDerefObjectCo + UserDeleteObject
                      → 引用归零 → FreeDeskThreadObject → 桌面堆回收

6.2 消息顺序(WM_DESTROY / WM_NCDESTROY)

顺序 消息 发送者 说明
1 WM_PARENTNOTIFY(WM_DESTROY) co_UserDestroyWindow 子窗口销毁前通知父
2 WM_SHOWWINDOW(SW_HIDE) co_WinPosShowWindow 仅子窗口
3 WM_DESTROY IntSendDestroyMsg 应用善后(释放资源)
4 (子窗口的 WM_DESTROY...) IntSendDestroyMsg 递归 先父后子
5 WM_NCDESTROY co_UserFreeWindow 最后一步窗口过程回调
6 WM_ASYNC_DESTROYWINDOW co_UserFreeWindow 跨线程子窗口的异步销毁

6.3 重入防护总结

防护机制 位置 说明
IntIsWindow 复查 co_UserDestroyWindow / IntSendDestroyMsg WM_DESTROY 中自我销毁/子窗口被销毁
WNDS2_INDESTROY co_UserFreeWindow 开头 二次 FreeWindow 直接返回
CO 引用(UserRefObjectCo NtUserDestroyWindow / co_UserCreateWindowEx 销毁期间对象不被回收
WNDS_DESTROYED UserGetWindowObject 等 句柄在 FreeWindow 中途即失效
FNID_FREED co_UserFreeWindow 过程替换/查询拒绝已释放窗口
句柄代次 handle_to_entry 复用后的旧句柄失效

6.4 线程/进程退出时的自动清理

窗口不只由应用显式销毁------线程退出、进程退出时 win32k 必须回收该线程/进程创建的全部窗口。机制在 object.c:

复制代码
ExitThreadCallback / ExitProcessCallback(main.c)
  → UserDestroyObjectsForOwner(gHandleTable, pti/ppi)   // object.c L751
      遍历整个句柄表
      对 Entry->pi == Owner(句柄属主=线程/进程)的每个对象
      调用 ObjectCallbacks[type].ObjectDestroy(Entry->ptr)
      TYPE_WINDOW 的回调 = co_UserDestroyWindow
  • 句柄属主(USER_HANDLE_ENTRY.piUserCreateObject 时由分配器写入------AllocDeskThreadObjectpti(线程对象),AllocDeskProcObjectppi(进程对象)。窗口对象 TYPE_WINDOW 属于线程,因此线程退出即销毁其窗口。
  • co_UserFreeWindow 中的一句关键代码与之呼应:先从线程窗口链表摘除 (L601-602),注释说明"防止销毁线程窗口时我们还在 co_UserFreeWindow 里而陷入麻烦"------即退出清理是逐窗口调用 co_UserDestroyWindow 的,必须先摘链避免重入遍历。
  • 跨线程子窗口的 WM_ASYNC_DESTROYWINDOW(L619)则保证:A 线程的窗口 B 线程的子窗口时,父退出不会同步销毁属于 B 的窗口,而是投递异步销毁消息。

6.5 WM_NCDESTROY 之后的"句柄失效"实验视角

从应用视角看,DestroyWindow 返回后句柄已不可用;但从内核视角,"失效"是一个过程

时刻 句柄状态 还能做什么
调用 NtUserDestroyWindow 前 有效 一切
WM_DESTROY 回调期间 有效 仍可 SetWindowLong/发消息(WNDS_DESTROYED 未置位)
WM_NCDESTROY 回调期间 有效 只能做最后清理(WNDS_DESTROYED 尚未置位,但 WNDS2_INDESTROY 已置)
co_UserFreeWindow 置 WNDS_DESTROYED 失效 UserGetWindowObject 返回 NULL
UserDeleteObject 后 表项空闲、代次递增 任何解析均失败

这解释了为什么 co_UserDestroyWindow 在每个回调之后都要 IntIsWindow 复查------回调期间窗口随时可能被应用再次销毁,指针可能已悬空。

7. 窗口树遍历

7.1 数据结构回顾

复制代码
桌面窗口 (pDeskInfo->spwnd, FNID_DESKTOP, spwndParent==NULL)
  └─ spwndChild ──► [顶级窗口A] ⇄ spwndNext ⇄ [顶级窗口B] ⇄ ...(Z 序链,链首最顶)
                      │ spwndChild                       │
                      ▼                                 ▼
                   [子窗口A1] ⇄ spwndNext ⇄ [子窗口A2]   [子窗口B1]
                      │
                      ▼
                   [孙窗口...]
  • 兄弟间用 spwndNext/spwndPrev 双向链;父持 spwndChild(链首)。
  • 拥有关系 spwndOwner 是"逻辑父"(弹出窗口/对话框),不影响树形遍历(遍历只走 spwndParent/spwndChild/spwndNext)。

7.2 四种遍历模式

需求 内核函数 算法
枚举子窗口/桌面窗口 IntWinListChildren(L274) 单层兄弟链快照
枚举被拥有窗口 IntWinListOwnedPopups(L315) 桌面子窗口中按 spwndOwner 过滤
EnumWindows/EnumChildWindows/EnumDesktopWindows NtUserBuildHwndList(L1513) 深度优先(先下钻子窗口,再右移兄弟)
EnumThreadWindows NtUserBuildHwndList 线程分支 全部顶级窗口按 head.pti 过滤
单窗口查找 IntGetWindow(L381) GW_* 命令直接沿链取值
类名/标题查找 IntFindWindow(L3086)/ NtUserFindWindowEx(L3168) 兄弟链顺序扫描 + atomNVClassName/strName 匹配
祖先链 UserGetAncestor(L3363) 沿 spwndParent / IntGetParent 上溯

7.3 NtUserBuildHwndList 的深度优先序

复制代码
IntPopulateHwndList(pwl, pwnd, dwFlags):
  for (; pwnd; pwnd = pwnd->spwndNext)          // 兄弟链
  {
      写入 UserHMGetHandle(pwnd);                // 先输出自己
      if (dwFlags & IACE_CHILDREN && pwnd->spwndChild)
          IntPopulateHwndList(pwl, pwnd->spwndChild, IACE_CHILDREN|IACE_LIST);  // 下钻
      if (!(dwFlags & IACE_LIST)) break;         // 非列表模式只处理第一个
  }

这与 NtUserBuildHwndList 中手写的遍历(bGoDown 标志:有子窗口且 bChildren 就下钻,否则右移,无右则上溯回父)逻辑一致,都产生深度优先的枚举序(与 Windows 文档中 EnumChildWindows 的 Z 序一致:链首→链尾,先子后兄)。

7.4 WINDOWLIST 缓存

IntBuildHwndList 分配 WINDOWLIST 后挂入全局 gpwlListIntFreeHwndList 摘除并把最大的一块存入 gpwlCache 供下次复用(WL_CAPACITY 比较)。这是内核侧为频繁枚举做的池化优化。注意 WINDOWLIST 句柄数组以 HWND_TERMINATOR ((HWND)1) 结束,遍历时以此为哨兵。

7.5 遍历实例:GetWindow 的链式导航

以典型的"遍历顶级窗口"为例对比两种机制:

复制代码
GetWindow(hwndTopA, GW_HWNDNEXT)     // 单步导航(IntGetWindow L408)
  → ValidateHwndNoErr(hwndTopA)
  → 返回 Wnd->spwndNext 的句柄(顶级窗口 B)

EnumWindows(cb, lParam)              // 全量枚举(user32 → NtUserBuildHwndList L1513)
  → 第一次调用:cHwnd=0,得到 *pcHwndNeeded = N
  → 分配 N 个 HWND 的数组
  → 第二次调用:内核深度优先填满数组
  → user32 逐个回调 cb(hwnd, lParam)
  • GetWindow 家族是 O(1) 链表导航,不分配内存、不需要锁内长停留,适合"相邻窗口"查询(任务切换器、MDI 循环)。
  • EnumWindows 家族是 O(窗口数) 快照枚举 ,先计数后拷贝,保证回调期间窗口树变化不破坏枚举(句柄数组是快照,回调前逐一 IntIsWindow 过滤已销毁窗口)。
  • NtUserBuildHwndList 的深度优先序决定了 EnumChildWindows 的访问顺序:同层从左到右(Z 序链首→链尾),先子后兄------这正是 Win32 文档承诺的枚举顺序。

8. 调用链与模块关系

8.1 创建/销毁调用链(mermaid)

#mermaid-svg-qk5ETpNMFPWbtxoS{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-qk5ETpNMFPWbtxoS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-qk5ETpNMFPWbtxoS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-qk5ETpNMFPWbtxoS .error-icon{fill:#552222;}#mermaid-svg-qk5ETpNMFPWbtxoS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-qk5ETpNMFPWbtxoS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-qk5ETpNMFPWbtxoS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-qk5ETpNMFPWbtxoS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-qk5ETpNMFPWbtxoS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-qk5ETpNMFPWbtxoS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-qk5ETpNMFPWbtxoS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-qk5ETpNMFPWbtxoS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-qk5ETpNMFPWbtxoS .marker.cross{stroke:#333333;}#mermaid-svg-qk5ETpNMFPWbtxoS svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-qk5ETpNMFPWbtxoS p{margin:0;}#mermaid-svg-qk5ETpNMFPWbtxoS .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-qk5ETpNMFPWbtxoS .cluster-label text{fill:#333;}#mermaid-svg-qk5ETpNMFPWbtxoS .cluster-label span{color:#333;}#mermaid-svg-qk5ETpNMFPWbtxoS .cluster-label span p{background-color:transparent;}#mermaid-svg-qk5ETpNMFPWbtxoS .label text,#mermaid-svg-qk5ETpNMFPWbtxoS span{fill:#333;color:#333;}#mermaid-svg-qk5ETpNMFPWbtxoS .node rect,#mermaid-svg-qk5ETpNMFPWbtxoS .node circle,#mermaid-svg-qk5ETpNMFPWbtxoS .node ellipse,#mermaid-svg-qk5ETpNMFPWbtxoS .node polygon,#mermaid-svg-qk5ETpNMFPWbtxoS .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-qk5ETpNMFPWbtxoS .rough-node .label text,#mermaid-svg-qk5ETpNMFPWbtxoS .node .label text,#mermaid-svg-qk5ETpNMFPWbtxoS .image-shape .label,#mermaid-svg-qk5ETpNMFPWbtxoS .icon-shape .label{text-anchor:middle;}#mermaid-svg-qk5ETpNMFPWbtxoS .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-qk5ETpNMFPWbtxoS .rough-node .label,#mermaid-svg-qk5ETpNMFPWbtxoS .node .label,#mermaid-svg-qk5ETpNMFPWbtxoS .image-shape .label,#mermaid-svg-qk5ETpNMFPWbtxoS .icon-shape .label{text-align:center;}#mermaid-svg-qk5ETpNMFPWbtxoS .node.clickable{cursor:pointer;}#mermaid-svg-qk5ETpNMFPWbtxoS .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-qk5ETpNMFPWbtxoS .arrowheadPath{fill:#333333;}#mermaid-svg-qk5ETpNMFPWbtxoS .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-qk5ETpNMFPWbtxoS .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-qk5ETpNMFPWbtxoS .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qk5ETpNMFPWbtxoS .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-qk5ETpNMFPWbtxoS .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qk5ETpNMFPWbtxoS .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-qk5ETpNMFPWbtxoS .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-qk5ETpNMFPWbtxoS .cluster text{fill:#333;}#mermaid-svg-qk5ETpNMFPWbtxoS .cluster span{color:#333;}#mermaid-svg-qk5ETpNMFPWbtxoS div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-qk5ETpNMFPWbtxoS .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-qk5ETpNMFPWbtxoS rect.text{fill:none;stroke-width:0;}#mermaid-svg-qk5ETpNMFPWbtxoS .icon-shape,#mermaid-svg-qk5ETpNMFPWbtxoS .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qk5ETpNMFPWbtxoS .icon-shape p,#mermaid-svg-qk5ETpNMFPWbtxoS .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-qk5ETpNMFPWbtxoS .icon-shape .label rect,#mermaid-svg-qk5ETpNMFPWbtxoS .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qk5ETpNMFPWbtxoS .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-qk5ETpNMFPWbtxoS .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-qk5ETpNMFPWbtxoS :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 应用程序
CreateWindowExW (user32)
User32CreateWindowEx (user32/window.c L152)
NtUserCreateWindowEx (ntuser/window.c L2699)
co_UserCreateWindowEx (L2152)
IntGetAndReferenceClass (class.c L1450)
IntCreateWindow (L1805)
UserCreateObject (object.c L567)
AllocDeskThreadObject (object.c L54)
IntLinkHwnd (L985) / IntLinkWindow (L945)
WM_NCCREATE / WM_NCCALCSIZE / WM_CREATE (co_IntSendMessage)
co_WinPosShowWindow (winpos.c L2630)
应用程序
DestroyWindow (user32)
NtUserDestroyWindow (L3063)
co_UserDestroyWindow (L2884)
IntSendDestroyMsg (L462) → WM_DESTROY
IntDestroyOwnedWindows (L2849)
co_UserFreeWindow (L575) → WM_NCDESTROY
IntUnlinkWindow (L1353)
UserDeleteObject (object.c L716)
FreeDeskThreadObject (object.c L89)

SetParent 调用链(4.4 族函数的串联):
#mermaid-svg-oqylq0w2wPzbe12U{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-oqylq0w2wPzbe12U .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-oqylq0w2wPzbe12U .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-oqylq0w2wPzbe12U .error-icon{fill:#552222;}#mermaid-svg-oqylq0w2wPzbe12U .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-oqylq0w2wPzbe12U .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-oqylq0w2wPzbe12U .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-oqylq0w2wPzbe12U .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-oqylq0w2wPzbe12U .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-oqylq0w2wPzbe12U .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-oqylq0w2wPzbe12U .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-oqylq0w2wPzbe12U .marker{fill:#333333;stroke:#333333;}#mermaid-svg-oqylq0w2wPzbe12U .marker.cross{stroke:#333333;}#mermaid-svg-oqylq0w2wPzbe12U svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-oqylq0w2wPzbe12U p{margin:0;}#mermaid-svg-oqylq0w2wPzbe12U .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-oqylq0w2wPzbe12U .cluster-label text{fill:#333;}#mermaid-svg-oqylq0w2wPzbe12U .cluster-label span{color:#333;}#mermaid-svg-oqylq0w2wPzbe12U .cluster-label span p{background-color:transparent;}#mermaid-svg-oqylq0w2wPzbe12U .label text,#mermaid-svg-oqylq0w2wPzbe12U span{fill:#333;color:#333;}#mermaid-svg-oqylq0w2wPzbe12U .node rect,#mermaid-svg-oqylq0w2wPzbe12U .node circle,#mermaid-svg-oqylq0w2wPzbe12U .node ellipse,#mermaid-svg-oqylq0w2wPzbe12U .node polygon,#mermaid-svg-oqylq0w2wPzbe12U .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-oqylq0w2wPzbe12U .rough-node .label text,#mermaid-svg-oqylq0w2wPzbe12U .node .label text,#mermaid-svg-oqylq0w2wPzbe12U .image-shape .label,#mermaid-svg-oqylq0w2wPzbe12U .icon-shape .label{text-anchor:middle;}#mermaid-svg-oqylq0w2wPzbe12U .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-oqylq0w2wPzbe12U .rough-node .label,#mermaid-svg-oqylq0w2wPzbe12U .node .label,#mermaid-svg-oqylq0w2wPzbe12U .image-shape .label,#mermaid-svg-oqylq0w2wPzbe12U .icon-shape .label{text-align:center;}#mermaid-svg-oqylq0w2wPzbe12U .node.clickable{cursor:pointer;}#mermaid-svg-oqylq0w2wPzbe12U .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-oqylq0w2wPzbe12U .arrowheadPath{fill:#333333;}#mermaid-svg-oqylq0w2wPzbe12U .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-oqylq0w2wPzbe12U .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-oqylq0w2wPzbe12U .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-oqylq0w2wPzbe12U .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-oqylq0w2wPzbe12U .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-oqylq0w2wPzbe12U .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-oqylq0w2wPzbe12U .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-oqylq0w2wPzbe12U .cluster text{fill:#333;}#mermaid-svg-oqylq0w2wPzbe12U .cluster span{color:#333;}#mermaid-svg-oqylq0w2wPzbe12U div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-oqylq0w2wPzbe12U .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-oqylq0w2wPzbe12U rect.text{fill:none;stroke-width:0;}#mermaid-svg-oqylq0w2wPzbe12U .icon-shape,#mermaid-svg-oqylq0w2wPzbe12U .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-oqylq0w2wPzbe12U .icon-shape p,#mermaid-svg-oqylq0w2wPzbe12U .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-oqylq0w2wPzbe12U .icon-shape .label rect,#mermaid-svg-oqylq0w2wPzbe12U .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-oqylq0w2wPzbe12U .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-oqylq0w2wPzbe12U .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-oqylq0w2wPzbe12U :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} SetParent (user32)
NtUserSetParent (L3672)
co_UserSetParent (L1294)
co_IntSetParent (L1157)
co_WinPosShowWindow (SW_HIDE)
IntUnlinkWindow (L1353)
IntLinkHwnd (L985)
co_WinPosSetWindowPos (置顶 + WM_WINDOWPOSCHANGING/CHANGED)
co_WinPosShowWindow (SW_SHOWNORMAL 恢复)

8.2 与 class.c 的关系

  • IntGetAndReferenceClass(class.c)在创建前解析类并加引用co_UserFreeWindow 在销毁时 IntDereferenceClass 归还引用 ------类对象 cWndReferenceCount 保证"有窗口引用时类不可注销"(UserUnregisterClass 检查)。
  • 类提供创建所需的一切默认:lpfnWndProccbwndExtralpszMenuNamespicn/spicnSmCS_CLASSDC/CS_OWNDC/CS_PARENTDCCS_IMEfnid(类 fnid 决定窗口过程映射区间)。
  • 窗口与类的双向指针:Wnd->pcls ↔ 类维护窗口引用计数;atomNVClassName 用于 FindWindow 的类匹配。

8.3 与 winpos.c 的关系

window.c 委托 winpos.c 实现 用途
边框尺寸 IntGetWindowBorderMeasures(L867) 非客户区布局
客户区计算 co_WinPosGetNonClientSize(L2396) WM_NCCALCSIZE
最小/最大 co_WinPosGetMinMaxInfo(L940) 创建期尺寸约束
显示/隐藏 co_WinPosShowWindow(L2630) 创建时首显 / 销毁前隐藏
位置置顶 co_WinPosSetWindowPos(L1799) 创建/SetParent/销毁
最小化/最大化 co_WinPosMinMaximize(L2445) 创建时样式处理
Z 序原语 IntLinkHwnd/IntUnlinkWindow(window.c) winpos.c 反向调用 window.c

8.4 与 object.c 的关系

  • 句柄表 gHandleTable:窗口句柄的分配(UserAllocHandle)、校验(handle_to_entry)、释放(free_user_entry)。
  • 对象生命周期UserCreateObject(分配+双引用)→ UserReferenceObject/UserDereferenceObject(引用计数)→ UserMarkObjectDestroy/UserDeleteObject(销毁标记/释放)。
  • 类型回调表 ObjectCallbacks[TYPE_WINDOW] = { AllocDeskThreadObject, co_UserDestroyWindow, FreeDeskThreadObject }UserDestroyObjectsForOwner(进程/线程退出清理)枚举句柄表,对每个窗口调用 co_UserDestroyWindow------这就是"线程退出自动销毁其窗口"的机制。

8.5 与用户态(user32)的分工

操作 用户态 内核态
读 WND 字段(GetWindowLong/GetWindowText) 桌面堆映射直读(class.c IntGetWindowLong) 回退:NtUserQueryWindow/NtUserInternalGetWindowText
写 WND 字段 --- NtUserSetWindowLong(锁 + 消息通知 + 权限)
创建 参数打包/类版本/控件库加载 NtUserCreateWindowEx 全流程
销毁 DestroyWindow 转发 NtUserDestroyWindow 全流程
枚举 分配缓冲、两次调用 NtUserBuildHwndList 深度优先

9. 源码索引

window.c 函数索引(行号按当前源码)

行号 函数
L23 IntReAllocatePoolWithTag 辅助
L40 UserUpdateUiState 杂项
L75 IntGetWindowObject 解析
L88 VerifyWnd 解析
L108 ValidateHwndNoErr 解析
L123 UserGetWindowObject 解析
L143 IntSetStyle 样式
L176 IntIsWindow 查询
L189 IntIsWindowVisible 查询
L204 IntGetParent 查询
L219 IntEnableWindow 样式
L274 IntWinListChildren 遍历
L315 IntWinListOwnedPopups 遍历
L351 IntGetNonChildAncestor 查询
L359 IntIsTopLevelWindow 查询
L367 IntValidateOwnerDepth 查询
L381 IntGetWindow 遍历
L439 IntGetWindowContextHelpId 查询
L462 IntSendDestroyMsg 销毁
L541 UserFreeWindowInfo 销毁
L575 co_UserFreeWindow 销毁
L781 IntGetWindowProc 属性
L841 IntSetWindowProc 属性
L928 IntIsChildWindow 查询
L945 IntLinkWindow 挂接
L985 IntLinkHwnd 挂接
L1084 IntProcessOwnerSwap 挂接
L1115 IntSetOwner 挂接
L1157 co_IntSetParent 挂接
L1294 co_UserSetParent 挂接
L1353 IntUnlinkWindow 挂接
L1373 IntGrowHwndList 遍历
L1393 IntPopulateHwndList 遍历
L1422 IntBuildHwndList 遍历
L1467 IntFreeHwndList 遍历
L1513 NtUserBuildHwndList 遍历
L1689 IntSendParentNotify 创建/销毁
L1710 IntFixWindowCoordinates 创建
L1805 IntCreateWindow 创建
L2151 co_UserCreateWindowEx 创建
L2640 ProbeAndCaptureLargeString 创建
L2697 NtUserCreateWindowEx 创建
L2849 IntDestroyOwnedWindows 销毁
L2884 co_UserDestroyWindow 销毁
L3062 NtUserDestroyWindow 销毁
L3086 IntFindWindow 遍历
L3168 NtUserFindWindowEx 遍历
L3363 UserGetAncestor 遍历
L3427 NtUserGetAncestor 遍历
L3491 NtUserGetComboBoxInfo 杂项(控件)
L3602 NtUserGetListBoxInfo 杂项(控件)
L3671 NtUserSetParent 挂接
L3706 UserGetShellWindow 杂项
L3738 NtUserSetShellWindowEx 杂项
L3838 IntCheckFrameEdge 样式
L3849 co_IntSetWindowLongPtr 属性
L4046 co_UserSetWindowLong 属性
L4052 co_UserSetWindowLongPtr 属性
L4069 NtUserSetWindowLong 属性
L4091 NtUserSetWindowLongPtr 属性
L4113 NtUserAlterWindowStyle 属性
L4144 NtUserSetWindowWord 属性
L4214 NtUserQueryWindow 杂项
L4319 NtUserRegisterWindowMessage 杂项
L4355 NtUserSetWindowFNID 杂项
L4397 DefSetText 杂项
L4478 NtUserDefSetText 杂项
L4612 NtUserInternalGetWindowText 杂项
L4673 IntShowOwnedPopups 销毁

关联文件索引

文件 关键内容
win32ss/include/ntuser.h WND(L693)、THRDESKHEAD(L190)、state/state2 标志(L604-670)、FNID(L857-900)、句柄类型(L38)、HANDLE 常量(L10)
win32ss/user/ntuser/window.h IntCreateWindow/co_UserCreateWindowEx/co_UserFreeWindow 声明(L59-81)、WINDOWLIST(L85)、ReplaceWndPtr/WndSet* 内联(L128-188)、IS_WND_* 宏
win32ss/user/ntuser/win32.h W32THREAD(L59)、THREADINFO(L80/83)、USERSTARTUPINFO(L212)、W32PROCESS(L224)、PROCESSINFO(L250)
win32ss/user/ntuser/object.c AllocDeskThreadObject(L54)、ObjectCallbacks(L249-278)、handle_to_entry(L341)、UserAllocHandle(L458)、UserCreateObject(L565)、UserMarkObjectDestroy(L619)、UserDereferenceObject(L642)、UserDeleteObject(L716)、UserReferenceObjectByHandle(L739)
win32ss/user/ntuser/class.c IntDereferenceClass(L820)、IntGetClassAtom(L1377)、IntGetAndReferenceClass(L1450)、UserRegisterClass(L1504)
win32ss/user/ntuser/winpos.c IntGetWindowBorderMeasures(L867)、co_WinPosGetMinMaxInfo(L940)、co_WinPosSetWindowPos(L1799)、co_WinPosGetNonClientSize(L2396)、co_WinPosShowWindow(L2630)
win32ss/user/user32/windows/window.c User32CreateWindowEx(L152)、CreateWindowExA(L352)、CreateWindowExW(L478)、GetWindowThreadProcessId(L1409)
win32ss/user/user32/windows/class.c IntGetWindowLong(L1089)、GetWindowLongA/W(L1164/1174)、GetWindowLongPtrA/W(L1184/1195)

10. 附录:调试要点与常见问题

10.1 调试渠道

  • window.c 的调试通道是 DBG_DEFAULT_CHANNEL(UserWnd)(L14),串口日志前缀为 err:UserWnd/trace:UserWnd
  • 创建/销毁的关键路径均有 TRACE:"Created window object with handle %p"(L1875)、"co_UserCreateWindowEx(%wZ): Created window %p"(L2608)、"co_UserDestroyWindow(Window = 0x%p, hWnd = 0x%p)"(L2906)。
  • 常见错误日志及其含义:
日志 含义
IntCreateWindow Allocation Error + STATUS_INSUFFICIENT_RESOURCES WND 分配失败(桌面堆耗尽)
Failed to find class %wZ 类不存在(user32 会尝试加载控件库重试)
co_UserCreateWindowEx(%wZ): NCCREATE message failed WM_NCCREATE 返回 0,创建回滚
co_UserCreateWindowEx(%wZ): WM_CREATE message failed WM_CREATE 返回 -1,创建回滚
Tried to call co_UserFreeWindow() twice 二次释放防护触发(TRACE)
Tried to destroy a window twice 二次销毁防护触发(TRACE)
Window 0x%p has been linked too early! Z 序链接时机错误(调试断言)
Trying to link window 0x%p to itself 自链接错误(ERR + ASSERT)
Destroying the ShellWindow! 销毁了 Shell 窗口(非预期路径)

10.2 常见失败错误码速查

错误码 触发场景 排查方向
ERROR_CANNOT_FIND_WND_CLASS 类未注册 控件库是否加载(VersionRegisterClass 重试)
ERROR_TLW_WITH_WSCHILD WS_CHILD 无父窗口 创建子窗口必须给 hWndParent
ERROR_INVALID_WINDOW_HANDLE 句柄无效或 WNDS_DESTROYED 句柄是否已被销毁/代次失效
ERROR_INVALID_MENU_HANDLE 创建时 hMenu 非法 非子窗口的 hMenu 必须是有效菜单
ERROR_ACCESS_DENIED 跨线程销毁 / 设置桌面窗口 线程归属检查
ERROR_INVALID_INDEX SetWindowLong 索引越界 与 cbwndExtra 比较
ERROR_INVALID_PARAMETER owner 深度超限 / 非法 SetParent 检查窗口树关系

10.3 排查"窗口创建失败"的标准路径

  1. 看串口是否出现 Failed to find class------若是,user32 层 VersionRegisterClass 是否成功(检查控件库 DLL)。
  2. 若类找到但创建失败,看 NCCREATE message failed/WM_CREATE message failed------应用窗口过程在回调中返回了拒绝值。
  3. 若分配失败(Allocation Error),检查桌面堆剩余空间与 cbwndExtra 是否过大。
  4. 窗口创建成功但立即消失------检查 co_UserCreateWindowEx 步骤 26:Cs->style & WS_VISIBLE 决定是否首显;dwShowMode 是否被 IntFixWindowCoordinates 改写。
  5. 句柄时而有效时而无效------注意句柄代次:销毁后再创建同一槽位窗口,旧句柄因代次不匹配而失效,属正常现象。

10.4 关键设计经验总结

  1. 窗口销毁是"逐步失效"的WNDS_DESTROYED 置位前,任何回调(WM_DESTROY/WM_NCDESTROY)里应用仍能拿到有效窗口句柄;置位后句柄立即失效。依赖这一点,销毁流程可以安全地跨消息回调。
  2. 引用计数必须成对IntGetWindowObject 拿指针必须配 UserDereferenceObjectUserRefObjectCo 必须配 UserDerefObjectCoWndSet* 自动处理树引用。任何不对称都会导致桌面堆泄漏或 use-after-free。
  3. 窗口树指针只经 WndSet* 修改 :这是引用计数正确性的前提,也是 IntLinkWindow/IntUnlinkWindow 能安全摘挂的基础。
  4. 创建与销毁的消息序列不可调换:WM_NCCREATE → WM_NCCALCSIZE → WM_CREATE(创建);WM_DESTROY(先父后子)→ WM_NCDESTROY(销毁)。应用与控件依赖此顺序做资源初始化/清理。
  5. 跨线程窗口关系必须伴随 AttachThreadInput:父子/拥有关系的建立与解除都要同步附加/解附加线程输入,否则焦点与激活语义被破坏。

本文档基于 ReactOS 源代码逐行分析,行号对应 2026 年 8 月仓库状态;若源码演进导致行号漂移,请以函数名为准。

下一册:《ReactOS 窗口系统分析(2):窗口类 --- class.c》

相关推荐
Code额2 小时前
Python 连接 DeepSeek API,OpenAI
开发语言·python·ai·ai编程
-凌凌漆-2 小时前
【freertos】Task创建(v2)
java·开发语言·算法
牛油果子哥q2 小时前
C++内存模型与深浅拷贝万字详解:栈堆静态内存布局、深浅拷贝底层差异、内存泄漏根治、拷贝崩溃踩坑、手写深拷贝实战
java·开发语言·c++
曹牧2 小时前
C#:函数参数指定默认值
开发语言·c#
智者知已应修善业2 小时前
【51单片机数码管逐位显示1-8余位不亮】2024-10-23
c语言·经验分享·笔记·嵌入式硬件·51单片机
AAA@峥3 小时前
Python 基础开篇:认识 Python、版本选择、环境部署与首个 HelloWorld
开发语言·python
caimouse4 小时前
ReactOS 图形系统分析(47):GDI 批处理 — gdibatch.c
c语言·开发语言
qq21084629534 小时前
在python中什么是 self 和 cls?
开发语言·前端·python
杨充4 小时前
05.多用组合和少继承
开发语言·bash