CreateWindowEx 消息顺序修复 —— WM_SIZE/WM_MOVE 在嵌套窗口创建时的正确发送
CreateWindowEx 消息顺序修复 —— WM_SIZE/WM_MOVE 在嵌套窗口创建时的正确发送问题描述ReactOS 的CreateWindowEx在创建子窗口时WM_SIZE/WM_MOVE 的消息顺序与 Windows 不一致导致user32 winetest CreateWindowEx 测试 58 个失败后扩展到 130 个因错误修复。根因ReactOS 通过WNDS_SENDSIZEMOVEMSGS标志来控制 WM_SIZE/WM_MOVE 的发送时机顶层窗口WNDS_SENDSIZEMOVEMSGS 1 → CreateWindowEx 中不发送ShowWindow 中发送子窗口WNDS_SENDSIZEMOVEMSGS 0 → CreateWindowEx 中发送ShowWindow 中不发送这个机制本身方向正确但有三个细节问题样式修正WS_CLIPSIBLINGS/WS_CAPTION/WINDOWEDGE在 CBT hook 之前执行导致 CBT hook 看到的不是原始样式co_WinPosSendSizeMove对 WS_MAXIMIZE 窗口发送SIZE_MAXIMIZED但 CreateWindowEx 阶段窗口还未被显示/最大化应该始终发送SIZE_RESTOREDMDI 子窗口的 ShowWindow/SetWindowPos 叠加了SWP_NOZORDER但 MDI 的 Z 序由 MDICLIENT 管理不应由 win32k 自动添加修改的文件1.win32ss/user/ntuser/window.c— win32k 窗口创建核心修改 A样式修正块移到 CBT hook 之后原因CBT hook 需要看到未修正的原始样式。原来的位置IntCreateWindow 函数中大约第2069行/* Correct the window style. */if((pWnd-style(WS_CHILD|WS_POPUP))!WS_CHILD){pWnd-style|WS_CLIPSIBLINGS;...}if(!(pWnd-style(WS_CHILD|WS_POPUP)))pWnd-state|WNDS_SENDSIZEMOVEMSGS;移至co_UserCreateWindowEx函数中在 CBT hook 调用之后大约第2326行// 在 hwndInsertAfter pCbtCreate-hwndInsertAfter; 之后/* Correct the window style. */if((Window-style(WS_CHILD|WS_POPUP))!WS_CHILD){Window-style|WS_CLIPSIBLINGS;...}if(!(Window-style(WS_CHILD|WS_POPUP)))Window-state|WNDS_SENDSIZEMOVEMSGS;注意变量从pWnd改为Windowco_UserCreateWindowEx 中的局部变量名。修改 B子窗口 CreateWindowEx 中 inline 发送 WM_SIZE/WM_MOVE原因旧的co_WinPosSendSizeMove根据 WS_MAXIMIZE 发送SIZE_MAXIMIZED但 CreateWindowEx 阶段窗口还未被显示应该始终发送SIZE_RESTORED。原来的代码/* Send the WM_SIZE and WM_MOVE messages. */if(!(Window-stateWNDS_SENDSIZEMOVEMSGS)){co_WinPosSendSizeMove(Window);}改为 inline 实现/* Send the WM_SIZE and WM_MOVE messages. During initial creation, always send SIZE_RESTORED even if WS_MAXIMIZE is set, because the window hasnt been shown/maximized yet. The WS_MAXIMIZE/WS_MINIMIZE check in co_WinPosSendSizeMove is only for ShowWindow. */if(!(Window-stateWNDS_SENDSIZEMOVEMSGS)){RECTL Rect;LPARAM lParam;IntGetClientRect(Window,Rect);lParamMAKELONG(Rect.right-Rect.left,Rect.bottom-Rect.top);co_IntSendMessageNoWait(UserHMGetHandle(Window),WM_SIZE,SIZE_RESTORED,lParam);if(UserIsDesktopWindow(Window-spwndParent))lParamMAKELONG(Window-rcClient.left,Window-rcClient.top);elselParamMAKELONG(Window-rcClient.left-Window-spwndParent-rcClient.left,Window-rcClient.top-Window-spwndParent-rcClient.top);co_IntSendMessageNoWait(UserHMGetHandle(Window),WM_MOVE,0,lParam);Window-state~WNDS_SENDSIZEMOVEMSGS;}修改 CMDI 子窗口最大化时不添加 SWP_NOZORDER原因MDI 子窗口的 Z 序由 MDICLIENT 窗口管理win32k 不应自动添加 SWP_NOZORDER。原来的代码SwFlagco_WinPosMinMaximize(Window,SwFlag,NewPos);SwFlag|SWP_NOZORDER|SWP_FRAMECHANGED;/* Frame always gets changed */改为SwFlagco_WinPosMinMaximize(Window,SwFlag,NewPos);/* Frame always gets changed. MDI child Z-order is managed by MDICLIENT. */if(!(Window-ExStyleWS_EX_MDICHILD))SwFlag|SWP_NOZORDER;SwFlag|SWP_FRAMECHANGED;2.win32ss/user/user32/windows/mdi.c— user32 MDI 客户端修改 DMDI 子窗口创建后刷新框架窗口 NC 区域原因Windows 在 MDI 子窗口通过WM_MDICREATE创建后会刷新框架窗口的非客户区更新标题栏中的 MDI 子窗口标题等。在WM_MDICREATE的处理中CreateWindowEx成功后添加/* Update the frames non-client area to reflect new active child */if(child)SetWindowPos(GetParent(hwnd),NULL,0,0,0,0,SWP_NOSIZE|SWP_NOMOVE|SWP_NOZORDER|SWP_NOACTIVATE|SWP_FRAMECHANGED);这段代码放在return (LRESULT)child;之前。Window 消息顺序对照Windows vs ReactOS 修复前后顶层窗口创建无子窗口消息WindowsReactOS 旧ReactOS 新WM_GETMINMAXINFO✅✅✅WM_NCCREATE✅✅✅WM_NCCALCSIZE✅✅✅WM_CREATE✅✅✅WM_SIZEShowWindow 中CreateWindowEx 中ShowWindow 中WM_MOVEShowWindow 中CreateWindowEx 中ShowWindow 中父窗口 WM_NCCREATE 中创建子窗口Windows 顺序hwnd1 WM_NCCREATE hwnd2 WM_NCCREATE → WM_NCCALCSIZE → WM_CREATE → WM_SIZE → WM_MOVE hwnd1 WM_PARENTNOTIFY hwnd1 WM_NCCREATE_RET → WM_NCCALCSIZE → WM_CREATE → WM_SIZE → WM_MOVE (ShowWindow)ReactOS 修复前缺少 hwnd2 的 WM_SIZE/WM_MOVEhwnd1 WM_NCCREATE hwnd2 WM_NCCREATE → WM_NCCALCSIZE → WM_CREATE hwnd1 WM_PARENTNOTIFY → WM_NCCALCSIZE → WM_CREATE → ... [hwnd2 WM_SIZE/WM_MOVE 缺失]ReactOS 修复后hwnd1 WM_NCCREATE hwnd2 WM_NCCREATE → WM_NCCALCSIZE → WM_CREATE → WM_SIZE → WM_MOVE ← 修复 hwnd1 WM_PARENTNOTIFY hwnd1 WM_NCCREATE_RET → WM_NCCALCSIZE → WM_CREATEMDI 子窗口创建通过 WM_MDICREATEWindows 完整顺序hwnd1 WM_GETMINMAXINFO → WM_NCCREATE → WM_NCCALCSIZE → WM_CREATE hwnd1 WM_PARENTNOTIFY → hwnd2(CreateWindowEx) hwnd2 WM_GETMINMAXINFO → WM_NCCREATE → WM_NCCALCSIZE → WM_CREATE → WM_SIZE → WM_MOVE hwnd2 WM_GETMINMAXINFO → WM_WINDOWPOSCHANGING → WM_WINDOWPOSCHANGED → ... hwnd2 WM_SHOWWINDOW → WM_WINDOWPOSCHANGING → WM_CHILDACTIVATE → WM_WINDOWPOSCHANGING hwnd2 WM_MDIACTIVATE hwnd1 WM_WINDOWPOSCHANGING → WM_NCCALCSIZE → WM_WINDOWPOSCHANGED (frame NC refresh) hwnd1 WM_CREATE_RET恢复ShowWindow的原始保护winpos.c中的co_WinPosShowWindow不需要修改。WNDS_SENDSIZEMOVEMSGS保护保留if((Wnd-stateWNDS_SENDSIZEMOVEMSGS)!(Wnd-state2WNDS2_INDESTROY)){co_WinPosSendSizeMove(Wnd);}这样顶层窗口在 ShowWindow 中发送 WM_SIZE/WM_MOVE子窗口不会重复发送已在 CreateWindowEx 中发送过且 flag 已被清除。测试结果版本CreateWindowEx 失败数原始基线58错误修复删 CreateWindowEx SendSizeMove130最终修复0其他测试win.c wine 测试的 460 个失败是预存在的主要是最大化 MDI 子窗口尺寸计算差异600 ! 618不在本次修复范围内。