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. 概述
本册聚焦"窗口对象"与"窗口创建/销毁"。窗口对象(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 把"窗口"抽象为一个内核对象,而不是一组散布在应用进程中的数据结构,原因在于:
- 跨进程共享:窗口属于创建它的线程,但可以被其他进程引用(SendMessage、SetWindowPos、EnumWindows 等)。窗口对象必须存在于所有 GUI 进程都能访问的内核空间(win32k 的桌面堆)。
- 安全性 :HWND 是一个不透明句柄,通过 USER 句柄表(
gHandleTable)间接指向对象,内核可以校验进程是否有权访问(UserGetWindowObject校验类型与销毁状态)。 - 与消息队列解耦 :窗口对象记录
head.pti(所属线程),消息投递时据此找到线程消息队列;窗口销毁时从队列中清除相关消息(MsqRemoveWindowMessagesFromQueue)。 - 统一生命周期管理 :所有 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.c 的 IntGetWindowLong)。写路径(如 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 就是 THRDESKHEAD:head.h 是 HWND,head.cLockObj 是引用计数,head.pti 是创建线程的 THREADINFO,head.rpdesk 是对象所在桌面(决定它被哪个桌面堆分配)。pSelf 在 AllocDeskThreadObject(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初始化时指向自身(IntCreateWindow中WndSetLastActive(pWnd, pWnd)),用于SetForegroundWindow焦点恢复。pSBInfo/pSBInfoex指向桌面堆分配的滚动条信息(scrollbar.c)。strName是LARGE_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_INDESTROY 在 co_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,或重叠窗口)。- 这些宏是
IntGetWindowBorderMeasures与WM_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)
IntIsDesktopWindow:spwndParent == NULL即桌面(窗口树根)。IntWndBelongsToThread:co_UserFreeWindow用它区分"同线程子窗口直接释放 / 跨线程子窗口异步销毁"。IntGetWndThreadId/ProcessId:NtUserQueryWindow的线程/进程 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* 函数采用(如
NtUserDestroyWindow、NtUserGetAncestor)。 - 注意事项 :不加引用计数 。需要跨"可能销毁对象"的代码段时,调用方用
UserRefObjectCo/UserDerefObjectCo(如co_UserDestroyWindow的ASSERT_REFS_CO)。
ValidateHwndNoErr(window.c L108)
c
PWND FASTCALL ValidateHwndNoErr(HWND hWnd)
- 作用 :
UserGetWindowObject的"无错误"变体:UserGetObjectNoErr(gHandleTable, hWnd, TYPE_WINDOW)+WNDS_DESTROYED检查。 - 使用方式 :用于遍历类路径中"找不到就算了"的场景(如
NtUserBuildHwndList的 EnumThreadWindows 分支、IntDestroyOwnedWindows、IntShowOwnedPopups),避免频繁设置 LastError 影响外层错误码。
VerifyWnd(window.c L88)
c
PWND FASTCALL VerifyWnd(PWND pWnd)
- 作用 :对已持有指针 的 PWND 做有效性复核:非空、未
WNDS_DESTROYED、未WNDS2_INDESTROY、且句柄表项未处于HANDLEENTRY_INDESTROY(UserObjectInDestroy)。 - 实现流程 :先做 state/state2 检查,再保存/恢复
EngGetLastError()(因为UserObjectInDestroy失败时可能改写错误码),返回 NULL 表示失效。 - 使用方式 :
IntSendParentNotify在给父窗口发 WM_PARENTNOTIFY 前用它验证父窗口仍然有效。
IntGetWindowObject(window.c L75)
c
PWND FASTCALL IntGetWindowObject(HWND hWnd)
- 作用 :
UserGetWindowObject+ 立即加引用 (Window->head.cLockObj++)。 - 使用方式 :调用方用完必须
UserDereferenceObject(Window)(如IntSetOwner、co_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(禁止环)。 - 使用方式 :
IntSetOwner与IntCreateWindow都在设置 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->style:new = (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。 - 实现流程 :
UserGetWindowObject解析;- 禁用时先发
WM_CANCELMODE(取消模态循环),若窗口有键盘焦点则co_UserSetFocus(NULL)撤焦; IntSetStyle改样式;- 状态变化时
IntNotifyWinEvent(EVENT_OBJECT_STATECHANGE)+ 发送WM_ENABLE; - 返回"原先是否禁用"。
- 注意事项:返回值的语义是"旧状态",与文档一致(非零 = 之前已禁用)。
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)、或具体窗口句柄。 - 实现流程 :
HWND_NOTOPMOST:清WS_EX_TOPMOST,回落为 HWND_TOP 处理;IntUnlinkWindow(Wnd)先解旧链;HWND_BOTTOM:找到链尾插入,清WS_EX_TOPMOST;HWND_TOPMOST:IntLinkWindow(Wnd, NULL)插链首,置WS_EX_TOPMOST;HWND_TOP:找到第一个非置顶窗口(跳过置顶窗口),插到它前面;若遇到 owner 则把 Wnd 升为置顶;- 具体句柄:解析后
IntLinkWindow(Wnd, WndInsertAfter),并根据插入点前后节点的WS_EX_TOPMOST修正自己的置顶位; - 末尾置
Wnd->ExStyle2 |= WS_EX2_LINKED标记已挂接。
- 使用方式 :
co_UserCreateWindowEx(新窗口入链)、co_IntSetParent、co_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 →
IntProcessOwnerSwap→IntValidateOwnerDepth通过则WndSetOwner,否则回滚并设ERROR_INVALID_PARAMETER;返回旧 owner 句柄。 - 注意事项:新 owner 句柄非法时返回 NULL 且不改变现状。
co_IntSetParent(window.c L1157)
c
PWND FASTCALL co_IntSetParent(PWND Wnd, PWND WndNewParent)
- 作用 :
SetParent内核主流程(返回旧父窗口指针)。 - 实现流程 :
- 拒绝消息窗口为父、拒绝把子窗口设为其祖先(
IntIsChildWindow+ 沿父链查环); co_WinPosShowWindow(Wnd, SW_HIDE)先隐藏(记录 WasVisible);- 计算旧父坐标下的位置(含 WS_EX_LAYOUTRTL 镜像修正),
IntScreenToClient转换; IntUnlinkWindow解链 →WndSetParent换父 →IntLinkHwnd按 HWND_TOP/HWND_TOPMOST 重新入链;- 桌面为父时强制
WS_CLIPSIBLINGS;维护线程附加(子窗口跨线程); IntNotifyWinEvent(EVENT_OBJECT_PARENTCHANGE);co_WinPosSetWindowPos置顶并发送 WM_WINDOWPOSCHANGING/CHANGED;- 若之前可见则
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的系统调用入口。 - 实现流程 :
- 非子窗口且 hMenu 非空时,
UserGetMenuObject(hMenu)校验菜单句柄(非法 →ERROR_INVALID_MENU_HANDLE); ProbeAndCaptureLargeString捕获窗口名(用户态→内核池拷贝);- 类名/类版本:
IS_ATOM判断------原子直接作为UNICODE_STRING.Buffer传递(不拷贝),字符串则捕获为LARGE_STRING并截断为UNICODE_STRING(FIXME 注释:LARGE_STRING 被截断到 MAXUSHORT); - 填充
CREATESTRUCTW Cs(style/lpCreateParams/hInstance/hMenu/hwndParent/cx/cy/x/y/lpszName/lpszClass/dwExStyle); UserEnterExclusive()加 USER 排他锁;co_UserCreateWindowEx(&Cs, &ustrClsVersion, plstrWindowName, acbiBuffer, dwFlags);- 成功则
hwnd = UserHMGetHandle(pwnd); UserLeave()解锁,cleanup:释放三个捕获缓冲。
- 非子窗口且 hMenu 非空时,
- 注意事项 :
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。
- 实现流程 (按代码顺序):
- 线程必须已连桌面(
pti->rpdesk非空),引用窗口站WinSta; IntGetAndReferenceClass(ClassName, Cs->hInstance, FALSE)解析并引用类(class.c L1450)------找不到 →ERROR_CANNOT_FIND_WND_CLASS(user32 收到后加载控件库重试);- 解析父/拥有者:
hwndParent为HWND_MESSAGE→ 消息窗口(rpdesk->spwndMessage);非 WS_CHILD 时hWndParent当作 owner;WS_CHILD 无父 →ERROR_TLW_WITH_WSCHILD; - owner 规范化:owner 是桌面 → 清空;owner 存在且父非桌面 → 拒绝(owned 窗口必须是顶级);owner 是子窗口则沿父链上溯到顶级;
IntFixWindowCoordinates修正 CW_USEDEFAULT;IntCreateWindow分配并初始化 WND;UserRefObjectCo(Window, &Ref)建立 CO 引用,解除句柄引用;Cs->lpszName/lpszClass指向内核缓冲,供 WM_NCCREATE/WM_CREATE 使用;- WH_CBT 钩子:分配 CREATESTRUCTW/CBT_CREATEWNDW,
IntMsgCreateStructW打包,co_HOOK_CallHooks(WH_CBT, HCBT_CREATEWND, ...),钩子可改写 x/y/cx/cy/hwndInsertAfter,返回非 0 则中止创建; - 样式修正:非子窗口强制
WS_CLIPSIBLINGS(非 POPUP 还加WS_CAPTION);WS_EX_WINDOWEDGE依IntCheckFrameEdge逻辑修正; - 非子窗口置
WNDS_SENDSIZEMOVEMSGS;子窗口坐标叠加父客户区原点; co_WinPosGetMinMaxInfo+ WM_GETMINMAXINFO 约束尺寸(WS_THICKFRAME 或非 POPUP/CHILD 时);- 计算
rcWindow(含 RTL 镜像修正)与rcClient = rcWindow; - 非 Win31 兼容且类有 CS_PARENTDC 时清除 CLIPSIBLINGS/CLIPCHILDREN;子窗口跨线程
AttachThreadInput; - WM_NCCREATE :
co_IntSendMessage(..., WM_NCCREATE, 0, (LPARAM)Cs),返回 0 → 失败回滚; - 挂接 :WS_CHILD|WS_MAXIMIZE →
IntLinkHwnd(Window, HWND_BOTTOM)(子窗口先沉底),否则按钩子结果hwndInsertAfter入链; - IME:
IS_IMM_MODE()且无默认 IME 且IntWantImeWindow→co_IntCreateDefaultImeWindow并发送 WM_IME_SYSTEM 消息; - WM_NCCALCSIZE :
co_WinPosGetNonClientSize计算客户区,RECTL_vOffsetRect修正窗口位置; - WM_CREATE:返回 -1 → 失败回滚;
IntNotifyWinEvent(EVENT_OBJECT_CREATE, ...);置WNDS2_WMCREATEMSGPROCESSED(此后"创建成功");- 发送 WM_SIZE(SIZE_RESTORED)/WM_MOVE(WNDS_SENDSIZEMOVEMSGS 时);
- 处理 WS_MINIMIZE/WS_MAXIMIZE(
IntSetStyle清零后co_WinPosMinMaximize+co_WinPosSetWindowPos); IntSendParentNotify(Window, WM_CREATE)(WM_PARENTNOTIFY);- 满足条件(无 owner/owner 不可见/非工具窗)且桌面子窗口可见时
co_IntShellHookNotify(HSHELL_WINDOWCREATED); - 显示滚动条(WS_VSCROLL/WS_HSCROLL →
co_UserShowScrollBar); Cs->style & WS_VISIBLE→co_WinPosShowWindow(Window, dwShowMode);MDI 子窗口额外WM_MDIREFRESHMENU+ SetWindowPos 置顶;- 控制台窗口类手动 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"。
- 实现流程 :
- 线程选择:
pdeskCreated ? gptiDesktopThread : GetW32ThreadInfo()(桌面线程创建桌面窗口用); - RTL 布局继承:非 WS_EX_LAYOUTRTL 时,子窗口继承父的 RTL;无父时继承进程
dwLayout & LAYOUT_RTL(对话框 FNID_DIALOG 除外); WS_EX_WINDOWEDGE自动推导(WS_EX_DLGMODALFRAME或(无 STATICEDGE 且 DLGFRAME/THICKFRAME));- Unicode 判定:
bUnicodeWindow = !(Cs->dwExStyle & WS_EX_SETANSICREATOR); UserCreateObject(gHandleTable, pdeskCreated ?: pti->rpdesk, pti, &hWnd, TYPE_WINDOW, sizeof(WND)+Class->cbwndExtra)------桌面堆分配 WND + 附加字节,返回已加引用对象;- 桌面窗口 HACK:
pdeskCreated->DesktopWindow==NULL时把新窗口登记为桌面窗口(win32csr/desktopbg.c 用); - 字段初始化:
WndSetParent/WndSetOwner;fnid=0;WndSetLastActive(pWnd, pWnd)(自指);按dwVer置 WIN31/NT4/WIN2K 兼容位;pcls、hModule、style=Cs->style & ~WS_VISIBLE(创建期强制不可见,稍后由 co_UserCreateWindowEx 决定显示)、ExStyle、cbwndExtra=类值、pActCtx=acbiBuffer; hImc:线程有默认输入上下文(pti->spDefaultImc)且非按钮类时关联;InternalPos.MaxPos/IconPos = -1(未初始化标记);HideFocus/HideAccel继承父;InitializeListHead(&ThreadListEntry);pti->cWindows++;- 小图标缓存:类有大图标无小图标时按
SM_CXSMICON/SM_CYSMICON拷贝生成(co_IntCopyImage),标记CSF_CACHEDSMICON; - 窗口过程解析:
IsCallProcHandle(类的 lpfnWndProc)→ 取pcpd->pfnClientPrevious;类 fnid 在 BUTTON...GHOST 区间时按 Unicode/ANSI 换GETPFNCLIENTW/A;CSF_SERVERSIDEPROC→WNDS_SERVERSIDEWINDOWPROC;WNDS_ANSICREATOR/WNDS_ANSIWINDOWPROC按创建者与类决定(按钮/组合框/编辑框等系统类在 ANSI 创建者下强制 ANSI 过程); - 编辑框类:立即
CreateCallProc并UserAddCallProcToClass(2K/XP 行为兼容); InitializeListHead(&PropListHead);- 标题拷贝:
WindowName非空 → 桌面堆分配Length+sizeof(UNICODE_NULL)并拷贝(strName); - 菜单:非子窗口时------
Cs->hMenu直接IntSetMenu;否则类lpszMenuName存在时co_IntCallLoadMenu加载再IntSetMenu;子窗口把Cs->hMenu存入IDMenu(即子窗口 ID); - owner 深度校验与线程附加(父链上有非消息/非桌面父时);
InsertTailList(&pti->WindowListHead, &pWnd->ThreadListEntry)------挂入线程窗口链表;- CS_CLASSDC:类级 DCE(
DceAllocDCE(pWnd, DCE_CLASS_DC));CS_OWNDC:窗口级 DCE。
- 线程选择:
- 失败路径 :
AllocError(分配失败)设STATUS_INSUFFICIENT_RESOURCES;Error:统一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系统调用入口。 - 实现流程 :
UserEnterExclusive→UserGetWindowObject→UserRefObjectCo保护 →co_UserDestroyWindow(Window)→UserDerefObjectCo→UserLeave。 - 注意事项:销毁期间持 CO 引用,防止回调重入导致对象提前释放(FIXME 注释承认这是保险措施)。
co_UserDestroyWindow(window.c L2884)
c
BOOLEAN co_UserDestroyWindow(PVOID Object) // Object 实为 PWND
- 作用 :销毁主流程(也作为 object.c 中 TYPE_WINDOW 的
ObjectDestroy回调被UserDestroyObjectsForOwner调用)。 - 实现流程 :
- 二次销毁防护:
IntIsWindow(UserHMGetHandle(Window))为假 → 直接返回 TRUE; - 权限:窗口属于其他线程且非桌面销毁(
DF_DESTROYED桌面窗口特例)→ERROR_ACCESS_DENIED; WNDS2_WMCREATEMSGPROCESSED已置 →co_HOOK_CallHooks(WH_CBT, HCBT_DESTROYWND, ...),钩子返回非 0 则取消销毁;- 解除线程附加:非子窗口有 owner →
AttachThreadInput(..., FALSE);子窗口非顶级 → 父线程解除附加; IntSendParentNotify(Window, WM_DESTROY)(子窗口发 WM_PARENTNOTIFY);- 隐藏窗口:子窗口
co_WinPosShowWindow(SW_HIDE)(收 WM_SHOWWINDOW),非子窗口co_WinPosSetWindowPos(SWP_HIDEWINDOW); - 调整
spwndLastActive(owner 链上的引用); - 队列引用清理:
spwndActive/spwndFocus/spwndActivePrev/spwndCapture置 NULL; - 清理 Shell 窗口句柄(
pDeskInfo->hShellWindow);IntEngWindowChanged(WOC_DELETE)通知引擎; - 再次
IntIsWindow检查(回调可能已销毁); IntDestroyOwnedWindows(Window)递归销毁被拥有的弹出窗口;- 构造 WM_MOUSEMOVE 并
co_MsqInsertMouseMessage(鼠标移出被销毁窗口); IntNotifyWinEvent(EVENT_OBJECT_DESTROY);IntSendDestroyMsg:WM_DESTROY + 递归子窗口;- 默认 IME 窗口联动销毁(
IntImeCanDestroyDefIME/forChild); co_UserFreeWindow(Window, ppi, pti, TRUE)释放存储。
- 二次销毁防护:
- 注意事项 :销毁路径全程穿插
IntIsWindow重入检查------WM_DESTROY 里应用可能再调用 DestroyWindow 销毁本窗口或其子窗口。
IntSendDestroyMsg(window.c L462)
c
static void IntSendDestroyMsg(HWND hWnd)
- 作用 :发送 WM_DESTROY 并递归销毁子窗口(先父后子)。
- 实现流程 :
- 焦点/激活善后(Rule #1):被销毁窗口是活动窗口 →
co_WinPosActivateOtherWindow(激活其他窗口);是焦点窗口 → 子窗口撤焦到父、否则co_UserSetFocus(NULL); - 插入符:
MessageQueue->CaretInfo.hWnd == hWnd→co_IntDestroyCaret; - 鼠标跟踪:
rpdesk->spwndTrack == Window→IntRemoveTrackMouseEvent; - 剪贴板:
prpwinsta->spwndClipOwner == Window→UserClipboardRelease; co_IntSendMessage(hWnd, WM_DESTROY, 0, 0);- 返回后
IntIsWindow复查(WM_DESTROY 里可能自我销毁)→IntWinListChildren快照 → 逐个IntSendDestroyMsg递归子窗口。
- 焦点/激活善后(Rule #1):被销毁窗口是活动窗口 →
- 注意事项 :子窗口列表必须在 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 所做一切的逆操作。
- 实现流程 :
WNDS2_INDESTROY置位(防二次进入);清WS_VISIBLE、cVisWindows--;- 先从线程窗口链表摘除(
RemoveEntryList(&ThreadListEntry)),避免销毁线程窗口时重入; IntDeRegisterShellHookWindow;IntWinListChildren快照递归:同线程子窗口直接co_UserFreeWindow,跨线程子窗口发WM_ASYNC_DESTROYWINDOW(异步销毁);SendMessages时:co_UserRedrawWindow(RDW_VALIDATE|...)清更新区域(防止 WM_NCDESTROY 期间生成 WM_PAINT)→ 同线程发 WM_NCDESTROY;UserClipboardFreeWindow、DestroyTimersForWindow、UnregisterWindowHotKeys、MsqRemoveWindowMessagesFromQueue(清队列中所有发往该窗口的消息);state |= WNDS_DESTROYED; fnid |= FNID_FREED(从此刻起句柄失效);- 清理 Shell 窗口/ListView 句柄、默认 IME 的 owner、
spwndDefaultIme(UserAssignmentUnlock); - 队列引用(focus/active/capture)再清理(
IntReleaseCapture); - 更新区域回收:
MsqDecPaintCountQueue+GreDeleteObject(hrgnUpdate)(若>HRGN_WINDOW 且有效),清WNDS_INTERNALPAINT/WNDS_SENDERASEBACKGROUND/WNDS_SENDNCPAINT; - 菜单释放:非子窗口
IDMenu是菜单 →IntDestroyMenuObject;SystemMenu同理; DceFreeWindowDCE(清窗口 DC,防孤儿 DC);IntUnlinkWindow解链;UserRemoveWindowProps清属性;WndSetOwner/Parent/Child/LastActive(NULL)清引用;UserRefObjectCo+UserMarkObjectDestroy(标记句柄表项销毁);IntDestroyScrollBars;控制台窗口类手动 GUI 计数;IntDereferenceClass(类引用计数归还------与 IntCreateWindow/IntGetAndReferenceClass 对称);hrgnClip删除;pti->cWindows--;UserFreeWindowInfo(释放 strName 缓冲、清 CallbackWnd 缓存);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_PARENTNOTIFY (wParam = 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->cbwndExtra→ERROR_INVALID_INDEX;否则读写&Window[1] + Index(64 位下按 Size 区分 32/64 位读写,ReadUnalignedU32/WriteUnalignedU32vsReadUnalignedUlongPtr/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_CLIPSIBLINGS;WS_MINIMIZE不可清除;同步WS_EX_WINDOWEDGE;子窗口↔非子窗口切换时清/释放IDMenu菜单引用;WS_VISIBLE变化维护cVisWindows并DceResetActiveDCEs;写style;发WM_STYLECHANGED。GWLP_WNDPROC:校验窗口属于当前进程且未FNID_FREED(否则ERROR_ACCESS_DENIED),IntSetWindowProc换过程,返回旧过程。GWLP_HINSTANCE:改hModule。GWLP_HWNDPARENT:父是桌面 →IntSetOwner;否则co_UserSetParent。GWLP_ID:改IDMenu。GWLP_USERDATA:改dwUserData。- 其他:
ERROR_INVALID_INDEX。
- 正索引(附加字节) :
- 注意事项 :
bAlter参数来自NtUserAlterWindowStyle(user32SetWindowLong的"改样式不发消息"变体,用于窗口过程内部)。
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_DENIED,IntGetDesktopWindow()比较);分别以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/W→GETPFNSERVER,置/清WNDS_SERVERSIDEWINDOWPROC),维护Unicode/WNDS_ANSIWINDOWPROC。
用户态 GetWindowLong(user32/windows/class.c L1089 IntGetWindowLong)
- 作用 :
GetWindowLongA/W/PtrA/PtrW(L1162-1202)的统一实现,完全在用户态 完成(桌面堆用户态映射直读 WND):GWLP_HWNDPARENT:GetAncestor(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_WNDPROC需TestWindowProcess权限检查后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 结尾)。 - 使用方式 :
IntDestroyOwnedWindows、IntShowOwnedPopups。
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==0或Child->pcls->atomNVClassName == ClassAtom;窗口名可选(WindowName->Buffer != 0时RtlCompareUnicodeString忽略大小写)。内核版本不发 WM_GETTEXT ,直接读strName(注释明确与用户态版本的区别)。NtUserFindWindowEx:实现FindWindow/FindWindowEx。hwndParent==NULL→ 桌面窗口(并允许消息窗口);HWND_MESSAGE→ 消息窗口;类名/窗口名先ProbeForReadUnicodeString探测,IntGetAtomFromStringOrAtom转原子;桌面父分支遍历所有顶级窗口(含子窗口命中时返回顶级窗口 句柄的语义------IntFindWindow(TopLevelWindow, ...)命中即返回 TopLevelWindow);最后消息窗口兜底查找。
UserGetAncestor(window.c L3363)/ NtUserGetAncestor(L3427)
UserGetAncestor(pWnd, uType):GA_PARENT(返回父)、GA_ROOT(沿父链到顶层,消息窗口处截断)、GA_ROOTOWNER(沿IntGetParent到拥有链顶端)。NtUserGetAncestor:UserEnterShared+UserGetWindowObject+ uType 范围校验(1...GA_ROOTOWNER),调UserGetAncestor后转句柄。
4.9 窗口信息服务/杂项族
NtUserQueryWindow(window.c L4214)
c
DWORD_PTR APIENTRY NtUserQueryWindow(HWND hWnd, DWORD Index)
- 作用 :一个函数服务多个查询(
GetWindowThreadProcessId、IsHungAppWindow、激活/焦点窗口、IME 查询等):QUERY_WINDOW_UNIQUE_PROCESS_ID/THREAD_ID:IntGetWndProcessId/IntGetWndThreadId(PsGetProcessId(ppi->peProcess)/PsGetThreadId(pEThread));CSRSS 控制台窗口类从附加字节读控制台领导进程 ID(GWLP_CONSOLE_LEADER_PID/TID偏移 0/4);QUERY_WINDOW_ACTIVE/FOCUS:队列spwndActive/spwndFocus;QUERY_WINDOW_ISHUNG:fnid == FNID_GHOST或MsqIsHung;QUERY_WINDOW_REAL_ID:pEThread->Cid.UniqueProcess;QUERY_WINDOW_FOREGROUND:MessageQueue == gpqForeground;QUERY_WINDOW_DEFAULT_IME/ICONTEXT/ACTIVE_IME:spwndDefaultIme/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)
- 作用 :
RegisterWindowMessage:IntSafeCopyUnicodeStringTerminateNULL捕获消息名 →IntAddAtom返回原子(即消息号)。返回 0 表示失败。
NtUserSetWindowFNID(window.c L4355)
- 作用 :设置窗口 fnid(user32 控件初始化用)。权限:窗口须属于当前进程;只接受
FNID_FIRST..FNID_GHOST(Wnd->fnid须为 0)或FNID_DESTROY;Wnd->fnid |= fnID。 - 注意事项:注释说明"HACK:最小值本应是 FNID_BUTTON,但菜单代码依赖此放宽"。
NtUserDefSetText / DefSetText(window.c L4478 / L4397)
DefSetText(Wnd, WindowText):把窗口标题写入strName(长度适配:够用则原地写,否则重分配桌面堆缓冲),并发co_IntShellHookNotify(HSHELL_REDRAW)。NtUserDefSetText:系统调用包装,处理 ANSI/UnicodeLARGE_STRING(RtlLargeStringToUnicodeString),供DefWindowProc的 WM_SETTEXT 分支调用。- 共同逻辑 :strName 写前先释放旧缓冲(若新长度放不下),写后保证
MaximumLength = Length + sizeof(UNICODE_NULL)。
NtUserInternalGetWindowText(window.c L4612)
- 作用 :
GetWindowText内核回退路径(user32 通常直读用户态 strName):取strName.Length/sizeof(WCHAR),MmCopyToCaller拷贝并补L'\0';nMaxCount<=1→ERROR_INVALID_PARAMETER。
NtUserGetComboBoxInfo(window.c L3491)
c
BOOL APIENTRY NtUserGetComboBoxInfo(HWND hWnd, PCOMBOBOXINFO pcbi)
- 作用 :填充
COMBOBOXINFO(下拉框的编辑框/列表框句柄、按钮与文本矩形、按钮状态)。 - 实现流程 :
UserGetWindowObject解析;ProbeForWrite(pcbi, sizeof(COMBOBOXINFO), 1)探测用户缓冲;pcbi->cbSize < sizeof(COMBOBOXINFO)→ERROR_INVALID_PARAMETER;- 窗口既不是 COMBOBOX 类也不是
FNID_COMBOBOX→ 转发CB_GETCOMBOBOXINFO消息(让控件自己回答); - 否则直接从
((PWND2CBOX)Wnd)->pCBox(HEADCOMBO 结构,window.c L3461-3478)读取textRect/buttonRect/wState/self/hWndEdit/hWndLBox;CBF_BUTTONDOWN→STATE_SYSTEM_PRESSED;按钮矩形为空 →STATE_SYSTEM_INVISIBLE; - 跨进程处理 :
ppi != Wnd->head.pti->ppi时KeAttachProcess附加到窗口所属进程再访问其用户态映射数据,结束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_BOTTOMMOST并co_WinPosSetWindowPos(HWND_BOTTOM)沉底;登记到WinStaObject与pDeskInfo(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)
CreateWindowExW(L478):若系统控件未注册则RegisterSystemControls()(注册"Button"/"Edit"/"Static"等系统类)。WS_EX_MDICHILD预处理(L500):校验父是 MDIClient(pWndParent->fnid == FNID_MDICLIENT);打包MDICREATESTRUCTW到lpParam;修正样式(强制WS_CHILD|WS_VISIBLE|WS_CLIPSIBLINGS|WS_CAPTION|WS_SYSMENU|WS_THICKFRAME|WS_MINIMIZEBOX|WS_MAXIMIZEBOX);MDI_CalcDefaultChildPos计算默认位置;hMenu换成子窗口 ID。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_CLASS时VersionRegisterClass加载控件库后重试一次(这是"未注册的类也能创建"的机制)。
阶段 1:系统调用与参数捕获(window.c L2697)
NtUserCreateWindowEx:菜单句柄校验 →ProbeAndCaptureLargeString捕获窗口名 → 类名/类版本(原子直传 or 捕获)→ 填CREATESTRUCTW→UserEnterExclusive。
阶段 2:类解析(class.c L1450)
IntGetAndReferenceClass:IntGetClassAtom(按原子/名字在进程私有类pclsPrivateList、公共类pclsPublicList、系统类SystemClassList中查找)→IntReferenceClass加引用(cWndReferenceCount++)。
阶段 3:父/拥有者解析 + 坐标修正(window.c L2151 步骤 3-5)
HWND_MESSAGE→spwndMessage;hWndParent在非 WS_CHILD 时成为 owner;owner 规范化(桌面→NULL;子窗口→上溯到顶级;owned 必须顶级)。IntFixWindowCoordinates:CW_USEDEFAULT 展开。
阶段 4:WND 分配与初始化(IntCreateWindow L1805)
UserCreateObject(..., TYPE_WINDOW, sizeof(WND)+cbwndExtra)→AllocDeskThreadObject桌面堆分配 + 零初始化 + 头字段(pSelf/rpdesk/pti)+IntReferenceThreadInfo+ 句柄表UserAllocHandle(句柄引用)→ 创建者引用。- 逐字段填充(见 4.5):窗口过程解析、标题拷贝、菜单、DCE、
InsertTailList(WindowListHead)等。
阶段 5:回调与挂接(co_UserCreateWindowEx 步骤 9-27)
- WH_CBT
HCBT_CREATEWND(可改坐标/插入位置)。 - 样式修正 → WM_GETMINMAXINFO 尺寸约束 →
rcWindow/rcClient计算。 - WM_NCCREATE(失败 → 回滚销毁)。
IntLinkHwnd挂入 Z 序(子窗口沉底 HWND_BOTTOM)。- IME 默认窗口创建。
- WM_NCCALCSIZE (
co_WinPosGetNonClientSize计算客户区)。 - WM_CREATE(返回 -1 → 回滚销毁)。
WNDS2_WMCREATEMSGPROCESSED置位 → WM_SIZE/WM_MOVE → 最小化/最大化处理 → WM_PARENTNOTIFY → HSHELL_WINDOWCREATED → 滚动条 →WS_VISIBLE时co_WinPosShowWindow(首显创建窗口表面,见第 3 分册)→ 热键。
阶段 6:返回
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_UserFreeWindow 是 IntCreateWindow 的完整逆操作: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 |
CreateWindowEx 的 lpParam 原样透传 |
应用自行解释 |
hwndParent |
创建参数中的父句柄 | 注意与解析后的 ParentWindow 可能不同(owner 场景) |
x/y/cx/cy |
WH_CBT 钩子可能改写后的坐标 | WM_CREATE 前已用于 rcWindow 计算 |
回调顺序与语义要点:
WM_NCCREATE(L2424):非客户区创建确认,返回 0 表示应用拒绝 → 整个创建失败回滚。WM_NCCALCSIZE(经co_WinPosGetNonClientSize,L2474):参数FALSE+RECT*,窗口过程计算客户区。WM_CREATE(L2484):客户区创建确认,返回 -1 表示失败 → 回滚销毁。WM_SIZE/WM_MOVE(L2510/2517):创建期总是SIZE_RESTORED(注释:即使 WS_MAXIMIZE 也发 RESTORED,因为窗口尚未显示/最大化);用co_IntSendMessageNoWait(不等待应用处理)。WM_PARENTNOTIFY(WM_CREATE)(L2545):父窗口收到子窗口创建通知。
5.4 线程输入附加(AttachThreadInput)在创建中的作用
co_UserCreateWindowEx 与 IntCreateWindow 在多处调用 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.pi) :UserCreateObject时由分配器写入------AllocDeskThreadObject写pti(线程对象),AllocDeskProcObject写ppi(进程对象)。窗口对象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 后挂入全局 gpwlList;IntFreeHwndList 摘除并把最大的一块存入 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检查)。- 类提供创建所需的一切默认:
lpfnWndProc、cbwndExtra、lpszMenuName、spicn/spicnSm、CS_CLASSDC/CS_OWNDC/CS_PARENTDC、CS_IME、fnid(类 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 排查"窗口创建失败"的标准路径
- 看串口是否出现
Failed to find class------若是,user32 层VersionRegisterClass是否成功(检查控件库 DLL)。 - 若类找到但创建失败,看
NCCREATE message failed/WM_CREATE message failed------应用窗口过程在回调中返回了拒绝值。 - 若分配失败(
Allocation Error),检查桌面堆剩余空间与cbwndExtra是否过大。 - 窗口创建成功但立即消失------检查
co_UserCreateWindowEx步骤 26:Cs->style & WS_VISIBLE决定是否首显;dwShowMode是否被IntFixWindowCoordinates改写。 - 句柄时而有效时而无效------注意句柄代次:销毁后再创建同一槽位窗口,旧句柄因代次不匹配而失效,属正常现象。
10.4 关键设计经验总结
- 窗口销毁是"逐步失效"的 :
WNDS_DESTROYED置位前,任何回调(WM_DESTROY/WM_NCDESTROY)里应用仍能拿到有效窗口句柄;置位后句柄立即失效。依赖这一点,销毁流程可以安全地跨消息回调。 - 引用计数必须成对 :
IntGetWindowObject拿指针必须配UserDereferenceObject;UserRefObjectCo必须配UserDerefObjectCo;WndSet*自动处理树引用。任何不对称都会导致桌面堆泄漏或 use-after-free。 - 窗口树指针只经 WndSet* 修改 :这是引用计数正确性的前提,也是
IntLinkWindow/IntUnlinkWindow能安全摘挂的基础。 - 创建与销毁的消息序列不可调换:WM_NCCREATE → WM_NCCALCSIZE → WM_CREATE(创建);WM_DESTROY(先父后子)→ WM_NCDESTROY(销毁)。应用与控件依赖此顺序做资源初始化/清理。
- 跨线程窗口关系必须伴随 AttachThreadInput:父子/拥有关系的建立与解除都要同步附加/解附加线程输入,否则焦点与激活语义被破坏。
本文档基于 ReactOS 源代码逐行分析,行号对应 2026 年 8 月仓库状态;若源码演进导致行号漂移,请以函数名为准。
下一册:《ReactOS 窗口系统分析(2):窗口类 --- class.c》