#include "StdAfx.h" #include "UIBase.h" #include "UIManager.h" #ifdef _DEBUG #include #pragma comment(lib, "shlwapi.lib") #endif namespace DuiLib { // WM_TOUCH 消息 ID 在 winuser.h 里受 `#if _WIN32_WINNT >= 0x0601` 保护, // 而 DuiLib 工程的 _WIN32_WINNT 兜底为 XP(0x0501),直接引用会编译失败,这里自行声明。 #ifndef WM_TOUCH #define WM_TOUCH 0x0240 #endif // 注:WM_POINTER* 常量统一放在 UIManager.cpp 的 PointerMsg 命名空间里(那边才是处理方), // 本文件不再重复定义 —— 原先为"模态期间屏蔽 owner 输入"定义的 DUI_WM_POINTER_FIRST/LAST // 已随 IsModalBlockedInputMessage 一并删除。 namespace { // ------------------------------------------------------------------------- // [模态提示音] Win7 上"点控件触发弹框"时的一声"咚"。 (已定位成因,2026-09-21) // // ── 现象(用户描述)───────────────────────────────────────────────────── // Win7:手指点主窗口某个控件 → 该控件的回调里 ShowModal() 弹出模态框 → // **咚** 一声。手指若点在弹框自己的显示区域内,则没有这声。 // 用户明确边界: // · 弹框**已经出现之后**再点弹框外的空白区发出咚声 —— **正常,不要动**; // · 要消除的只是"点击动作发生在弹框出现之前"却发出咚声的那一声。 // // ── 成因(真机日志实证,非本项目 bug)────────────────────────────────── // 真模态由 DialogBoxIndirectParam 创建,它会把 owner 置为 IsWindowEnabled(FALSE) // 以阻挡输入。而 Win32 的默认处理规定:**用户点击一个被禁用的窗口时发出 // MessageBeep(0)** —— 这是 Windows 有意给的"操作无效"提示音。 // // 本机触摸走的是指针栈(WM_TOUCH 实测 0 条),而**触摸的手势归属是按键序列 // 一次性决定的**:整个"按下 → 抬起"序列的指针消息全部投给**按下那一刻命中的 // 窗口**。真机日志里可以直接看到这条规则被违反后的结果(行 1611 - 1690): // // 1611 POINTER-MSG UP hwnd=00D4055C ... ← owner(主窗口) 收到抬起 // 1614 POINTER-MSG UP click-candidate diandan_fooditem // 1621 MODAL-DLG enter old=... owner=00D4055C ← 回调里 ShowModal,owner 被禁用 // 1638 INPUT-TRACE target=00D4055C msg=0x0006 WM_ACTIVATE ... enable=0 // ↑ 这一刻 owner 才变成禁用 // 1643 INPUT-TRACE target=00D4055C msg=0x024A WM_POINTER* ... enable=0 // 1666 POINTER-MSG UP click-candidate diandan_nature_save ← 弹框内点击,关闭弹框 // 1684 INPUT-TRACE target=00D4055C msg=0x0201 WM_LBUTTONDOWN ... extra=0xFF51578B // ↑ 系统**补发**的、属于本次触摸的 // 提升鼠标按下,落在已启用的 owner 上 // // 关键在最后一行:手势结束时(owner 已重新启用),系统才把这条**早在手势开始时就 // 排定好归属**的提升鼠标消息投递出来。可 owner 在手势中途反复被 EnableWindow(FALSE) // / TRUE 切换 —— 只要这条消息落在"owner 正处于禁用"的那一瞬间,系统就走 // "点击了禁用窗口"的默认处理 ⇒ 咚。 // Win10 无此现象,是因为它在指针层就把落点判掉,根本不会把消息投给被禁用的窗口。 // // ── 为什么不用"屏蔽消息"的解法(曾误判,已弃)────────────────────────── // 我一度写过一个基于 WH_GETMESSAGE 的钩子,无条件丢弃投给 owner 的点击类消息。 // 它错在:**弹框已出现后点空白区的那一声,用户明确说要保留**。而无条件丢弃会把它 // 一起消掉 —— 连正常的"无效操作"反馈都没了,方向反了。整段删除。 // // ── 采用的解法 ──────────────────────────────────────────────────────── // 不动消息流、不挂钩子,只在**库内**把"同一次手势的第二遍处理"精确拦掉。 // // 核心认识:一次触摸手势会被处理**两遍** —— // 第 1 遍:DuiLib 自己的触摸状态机(WM_POINTER* / WM_TOUCH)合成点击, // 同步派发到控件、触发业务回调; // 第 2 遍:系统把这次触摸"提升"成的鼠标消息(带 MOUSEEVENTF_FROMTOUCH, // 即 GetMessageExtraInfo() 高位的 0xFF5157xx),排在消息队列里稍后投出。 // 第 2 遍是多余的,必须丢——它正是"咚"和"弹框被自动点掉"的元凶。 // // 判定与执行(见 CPaintManagerUI::MessageHandler 里 WM_LBUTTONDOWN/UP 分支): // · m_bSuppressPromotedMouseForModal —— 由 ShowModal() 在进入模态循环**之前**置位 // · IsPromotedTouchMouseMessage() —— 只丢触摸提升的,真实鼠标点击天然豁免 // · IsModalInputTarget(m_hWndPaint) —— 目标是"弹框本身"或"弹框的 owner" // 生命周期按**手势配对**(DOWN 记账 / UP 销账,归零即解除),另有 10 秒僵尸兜底。 // 因此"弹框已出现后点空白区"那一声(要求保留的)照旧会响。 // // 撤防点(确定性,不依赖超时): // a) CWindowWnd::Close() 的 m_bDoModal 分支,在 EndDialog 之前; // b) __DialogWndProc 的 WM_NCDESTROY 收尾,任何销毁路径都会走到; // c) ShowModal() 返回后的兜底。 // // ⚠️ 本文件此处仅保留文字说明,不再有任何钩子/屏蔽类实现。 // // ── 第十一轮补充:上面这套"三条件拦截"**单独并不足以**修掉那四个 bug ────── // 真机验证:第九、十两轮的改动全部无效。第十一轮找到真正的两个根因 // (详见 UIManager.cpp 的 AttachPaintWindow 与 TOUCH-STATE timeout-reset): // // (A) **AttachPaintWindow 漏清输入状态**。真模态把**同一个** manager 从旧窗口 // 搬到弹框,控件树 m_pRoot 是同一棵。而 AttachDialog(非模态路径)会清 // m_pEventKey/Hover/Click/RClick,AttachPaintWindow 当初漏了同一组 // ⇒ m_pEventHover 仍指着主窗口那个商品控件,弹框期间没人去清它 // ⇒ **悬浮态一直亮到弹框关闭**(用户报的第 4 个 bug)。 // 同时 m_pEventClick / m_bMouseCapture 等旧手势中间态被带进弹框 // ⇒ 弹框第一条输入就撞上"进行中的手势",为本文件上面那套拦截**制造了输入**。 // 已在 AttachPaintWindow 开头补齐清理。 // // (B) **TOUCH-STATE timeout-reset 只看时间、不看模态是否存活**。 // 模态期间这些手势状态本来就是"故意冻结"的(在等嵌套模态循环返回), // 超时却会在 3 秒后把它们清掉 ⇒ 抑制位失效 ⇒ 第 2 遍消息漏网。 // **这正是用户报的"超过 3 秒就透点"**。 // ⚠️ 最终处理(用户明确要求"不要时间戳,完全根据状态来判断"): // 该 3000ms 超时已**整段删除**,连同指针通道那条同类超时一并删除。 // 抑制位现在只由"配对的那条提升 UP 到达"解除(纯状态)。 // 论证见 UIManager.cpp 里被删处的长注释(逐条证明这些状态不会泄漏)。 // // 结论:本文件的三条件拦截是**必要条件**(拦第 2 遍消息), // 但必须配合上面 (A)(B) 两条才有可能真正成立 —— // 之前只做拦截、不做 (A)(B),等于"防线摆好了,但门在别处开着"。 // ------------------------------------------------------------------------- typedef BOOL (WINAPI* PFN_REGISTER_TOUCH_WINDOW)(HWND, ULONG); // 与 SDK tagTOUCHINPUT 布局一致的私有副本(同样受 _WIN32_WINNT 保护,不能直接用) struct TRACE_TOUCHINPUT { LONG x; LONG y; HANDLE hSource; DWORD dwID; DWORD dwFlags; DWORD dwMask; DWORD dwTime; ULONG_PTR dwExtraInfo; DWORD cxContact; DWORD cyContact; }; typedef BOOL (WINAPI* PFN_GET_TOUCH_INPUT_INFO)(HANDLE, UINT, TRACE_TOUCHINPUT*, int); void RegisterTouchWindowIfAvailable(HWND hWnd) { if (hWnd == NULL) return; HMODULE hUser32 = ::GetModuleHandle(_T("user32.dll")); if (hUser32 == NULL) return; PFN_REGISTER_TOUCH_WINDOW pfnRegister = reinterpret_cast(::GetProcAddress(hUser32, "RegisterTouchWindow")); if (pfnRegister != NULL) pfnRegister(hWnd, 0); } // 【已移除】RefreshTouchRegistration(Unregister + Register 强制重算触摸归属)。 // // 它曾是"假模态 + WM_TOUCH 贪婪归属权"时代的补丁:试图用注销/重注册把触摸归属权 // 从 owner 重新结算到弹框头上。实测无效(弹框依然零 WM_TOUCH),且真模态方案下 // 完全没必要 —— 对话框窗口是新 HWND,创建时 RegisterTouchWindowIfAvailable 注册一次即可, // 系统自己就把触摸投给它。留着只会带来 C4505(未引用函数)警告,故整段删除。 // 诊断:WM_TOUCH 究竟落在哪个窗口上(弹框自己?还是被禁用/被挡住的 owner?)。 // ⚠️ 只 peek,绝不调用 CloseTouchInputHandle:句柄必须交给 CPaintManagerUI::OnTouchInput 关闭。 void TraceTouchArrival(HWND hWnd, WPARAM wParam, LPARAM lParam) { HMODULE hUser32 = ::GetModuleHandle(_T("user32.dll")); if (hUser32 == NULL) return; PFN_GET_TOUCH_INPUT_INFO pfnGet = reinterpret_cast(::GetProcAddress(hUser32, "GetTouchInputInfo")); int x = 0, y = 0; DWORD flags = 0; UINT cInputs = LOWORD(wParam); TRACE_TOUCHINPUT ti; ::ZeroMemory(&ti, sizeof(ti)); if (pfnGet != NULL && cInputs > 0 && pfnGet((HANDLE)lParam, 1, &ti, (int)sizeof(ti))) { x = (int)(ti.x / 100); y = (int)(ti.y / 100); flags = ti.dwFlags; } DuiLibDiag::Log(_T("WNDPROC-TOUCH hwnd=%p owner=%p parent=%p enabled=%d visible=%d pt=%d,%d flags=0x%X inputs=%u modal=%p fg=%p"), (void*)hWnd, (void*)::GetWindow(hWnd, GW_OWNER), (void*)::GetParent(hWnd), (int)::IsWindowEnabled(hWnd), (int)::IsWindowVisible(hWnd), x, y, (unsigned)flags, cInputs, (void*)DuiLib::CPaintManagerUI::GetModalWindow(), (void*)::GetForegroundWindow()); } // --------------------------------------------------------------------------- // 【临时全量诊断】记录系统到底把哪些"输入类消息"发给了哪个窗口。 // // 背景:弹框打开后,日志里既没有 WM_TOUCH(任何窗口都没有),也没有 WM_LBUTTONDOWN/UP, // 却能看到弹框收到 WM_MOUSEMOVE(hover)。这说明触摸可能被系统降级成了鼠标模拟, // 或者走了 WM_GESTURE / WM_POINTER* 另外两条通道。这三个通道目前都没被记录到, // 因此需要一段覆盖面更广的诊断来确定"消息到底有没有发出来、发给了谁"。 // // 覆盖:WM_TOUCH(0x0240) / WM_GESTURE(0x0119) / WM_POINTER*(0x0245~0x025F 及 0x0246/0x0247 变体) // WM_MOUSEMOVE(0x0200) / WM_LBUTTONDOWN(0x0201) / WM_LBUTTONUP(0x0202) // WM_NCMOUSEMOVE(0x00A0) / WM_MOUSEACTIVATE(0x0021) / WM_SETCURSOR(0x0020) // // ⚠️ 纯只读诊断:不改消息流向、不返回值、不消费句柄。只为定位问题,验证完请删除。 // --------------------------------------------------------------------------- // MSG-ID 常量(不引用任何 WM_* 宏,避免 _WIN32_WINNT 版本段差异导致编译不过) const UINT DUI_MSG_WM_TOUCH = 0x0240; const UINT DUI_MSG_WM_GESTURE = 0x0119; const UINT DUI_MSG_WM_GESTURENOTIFY = 0x011A; const UINT DUI_MSG_WM_MOUSEMOVE = 0x0200; const UINT DUI_MSG_WM_LBUTTONDOWN = 0x0201; const UINT DUI_MSG_WM_LBUTTONUP = 0x0202; const UINT DUI_MSG_WM_LBUTTONDBLCLK = 0x0203; const UINT DUI_MSG_WM_RBUTTONDOWN = 0x0204; const UINT DUI_MSG_WM_RBUTTONUP = 0x0205; const UINT DUI_MSG_WM_NCMOUSEMOVE = 0x00A0; const UINT DUI_MSG_WM_NCLBUTTONDOWN = 0x00A1; const UINT DUI_MSG_WM_MOUSEACTIVATE = 0x0021; const UINT DUI_MSG_WM_SETCURSOR = 0x0020; const UINT DUI_MSG_WM_NCHITTEST = 0x0084; const UINT DUI_MSG_WM_ACTIVATE = 0x0006; const UINT DUI_MSG_WM_ACTIVATEAPP = 0x001C; const UINT DUI_MSG_WM_SETFOCUS = 0x0007; const UINT DUI_MSG_WM_KILLFOCUS = 0x0008; void TraceInputMessage(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { LPCTSTR pszName = NULL; switch (uMsg) { // ⚠️ case 必须是"编译期常量表达式"。这些常量在匿名 namespace 里是 const UINT // 且已初始化,属于 ICE,可以安全用作 case 标签。 case DUI_MSG_WM_TOUCH: pszName = _T("WM_TOUCH"); break; case DUI_MSG_WM_GESTURE: pszName = _T("WM_GESTURE"); break; case DUI_MSG_WM_GESTURENOTIFY: pszName = _T("WM_GESTURENOTIFY"); break; case DUI_MSG_WM_MOUSEMOVE: pszName = _T("WM_MOUSEMOVE"); break; case DUI_MSG_WM_LBUTTONDOWN: pszName = _T("WM_LBUTTONDOWN"); break; case DUI_MSG_WM_LBUTTONUP: pszName = _T("WM_LBUTTONUP"); break; case DUI_MSG_WM_LBUTTONDBLCLK: pszName = _T("WM_LBUTTONDBLCLK"); break; case DUI_MSG_WM_RBUTTONDOWN: pszName = _T("WM_RBUTTONDOWN"); break; case DUI_MSG_WM_RBUTTONUP: pszName = _T("WM_RBUTTONUP"); break; case DUI_MSG_WM_NCMOUSEMOVE: pszName = _T("WM_NCMOUSEMOVE"); break; case DUI_MSG_WM_NCLBUTTONDOWN: pszName = _T("WM_NCLBUTTONDOWN"); break; case DUI_MSG_WM_MOUSEACTIVATE: pszName = _T("WM_MOUSEACTIVATE"); break; case DUI_MSG_WM_SETCURSOR: pszName = _T("WM_SETCURSOR"); break; case DUI_MSG_WM_NCHITTEST: pszName = _T("WM_NCHITTEST"); break; case DUI_MSG_WM_ACTIVATE: pszName = _T("WM_ACTIVATE"); break; case DUI_MSG_WM_ACTIVATEAPP: pszName = _T("WM_ACTIVATEAPP"); break; case DUI_MSG_WM_SETFOCUS: pszName = _T("WM_SETFOCUS"); break; case DUI_MSG_WM_KILLFOCUS: pszName = _T("WM_KILLFOCUS"); break; default: break; } // 覆盖 WM_POINTER* 一段(0x0245~0x0260,不同 SDK 版本消息号有差异,统一按范围判断) const bool bPointerMsg = (uMsg >= 0x0245 && uMsg <= 0x0260); if (pszName == NULL && !bPointerMsg) return; // WM_MOUSEMOVE / WM_NCMOUSEMOVE / WM_NCHITTEST / WM_SETCURSOR 极其频繁, // 全量记录会瞬间刷爆日志,这里每类限流 300ms。 const bool bNoisy = (uMsg == DUI_MSG_WM_MOUSEMOVE || uMsg == DUI_MSG_WM_NCMOUSEMOVE || uMsg == DUI_MSG_WM_NCHITTEST || uMsg == DUI_MSG_WM_SETCURSOR); if (bNoisy) { static DWORD s_dwLastNoisyTick = 0; static UINT s_uLastNoisyMsg = 0; const DWORD dwNow = ::GetTickCount(); if (s_uLastNoisyMsg == uMsg && (LONG)(dwNow - s_dwLastNoisyTick) < 300) return; s_dwLastNoisyTick = dwNow; s_uLastNoisyMsg = uMsg; } DWORD dwExtra = (DWORD)(DWORD_PTR)::GetMessageExtraInfo(); DuiLibDiag::Log(_T("INPUT-TRACE target=%p msg=0x%04X %s w=0x%08X l=0x%08X extra=0x%08X fromtouch=%d modal=%p fg=%p enable=%d"), (void*)hWnd, uMsg, pszName != NULL ? pszName : _T("WM_POINTER*"), (unsigned)wParam, (unsigned)lParam, (unsigned)dwExtra, (int)((dwExtra & 0xFFFFFF80) == 0xFF515700), (void*)DuiLib::CPaintManagerUI::GetModalWindow(), (void*)::GetForegroundWindow(), (int)::IsWindowEnabled(hWnd)); } // 【已移除】LogProcessWindowList / EnumProc_LogWnd(本进程顶层窗口清单,MODAL-WNDLIST)。 // 它们是排查"WM_TOUCH 究竟投给了哪个陌生 HWND"的临时诊断,只被已删除的假模态消息循环调用。 // 真模态方案下弹框自己就是触摸目标,这份清单不再需要,删除以免 C4505。 // 【已搬移】FindManagerByPaintWindow 原本定义在这里(匿名 namespace 内)。 // // 为什么必须搬走:UIBase.h 里为了让 UIManager.cpp 也能调用它,已经加了一份 // 声明 `DuiLib::CPaintManagerUI* FindManagerByPaintWindow(HWND);`。 // 于是同一个翻译单元里同时存在 // · 匿名 namespace 内的 DuiLib::`anonymous-namespace'::FindManagerByPaintWindow // · 头文件声明的 DuiLib::FindManagerByPaintWindow // 两个候选,调用点(UIBase.cpp 里 793/1076/1124/1207/1367/1395 等)无法抉择, // MSVC 直接报 C2668「对重载函数的调用不明确」。 // ⇒ 现在把它挪到匿名 namespace **之外**,作为 DuiLib 的正式成员, // 与头文件那份声明严格一一对应(定义见本文件下方 namespace 收尾之后)。 // --------------------------------------------------------------------------- // [WM_TOUCH 归属权] "补调原窗口过程"的中转函数。 // // 为什么需要它: // WM_TOUCH 不是被系统直接投递的。系统先发一条"特殊标记的 WM_MOUSEMOVE" // (开了 raw input 时是 WM_INPUT),只有这条消息走到 DefWindowProc 里, // DefWindowProc 才会把驻留的 WM_TOUCH "翻译"并派发出来 // (来源:The Witness 作者的逆向结论;MSDN《Architectural Overview》的 // greedy 段落也印证了这套投递模型)。 // 本库的 CPaintManagerUI::MessageHandler 为了不让"触摸提升出来的鼠标移动" // 破坏拖拽/悬浮状态机,会吞掉它(return true),从而跳过了 DefWindowProc // ⇒ 窗口(尤其 #32770 真模态弹框,它依赖 DefDlgProc→DefWindowProc 这条链) // 后续永远收不到 WM_TOUCH。 // // 这个中转函数由 CPaintManagerUI::RelayDefWindowProc 回调, // 作用就是"把消息喂给原窗口过程一次",补上被跳过的那次副作用。 // 用静态变量保存"当前转发目标":因为回调签名不带 this, // 而每个窗口过程在同一时刻只有一个绑定关系(主窗口 or 某个弹框)。 // 真模态切换 HWND 时通过 SetDefProcRelay 重新绑定,不会串。 // // ⚠️ 位置要求:必须定义在 __DialogWndProc 之前(它比 __WndProc 先出现)。 // --------------------------------------------------------------------------- WNDPROC g_pfnRelayTarget = NULL; HWND g_hWndRelayBound = NULL; LRESULT CALLBACK RelayWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { // 只有"当前正在转发的窗口"才有效:避免悬垂到已销毁窗口的过程上。 if (g_pfnRelayTarget == NULL || g_hWndRelayBound != hWnd) return 0; return ::CallWindowProc(g_pfnRelayTarget, hWnd, uMsg, wParam, lParam); } // 把"原窗口过程"登记为转发目标(绑定到指定 HWND)。 void BindRelayTarget(WNDPROC pfnOldProc, HWND hWnd) { g_pfnRelayTarget = pfnOldProc; g_hWndRelayBound = hWnd; } // DetachPaintWindow 之后、AttachPaintWindow 之前,用这个取出"待挂载的 manager"。 // 因为此时 manager 已从 m_aPreMessages 摘除,FindManagerByPaintWindow 找不到它了, // 所以用一个文件级静态变量中转(只在 ShowModal 的单线程窗口过程里使用,无需同步)。 static DuiLib::CPaintManagerUI* g_pDetachedManager = NULL; void SetDetachedManager(DuiLib::CPaintManagerUI* pManager) { g_pDetachedManager = pManager; } DuiLib::CPaintManagerUI* GetDetachedManager() { return g_pDetachedManager; } // 【已移除】IsModalBlockedInputMessage —— 假模态时代的"在消息循环里丢弃发给 owner 的输入消息"。 // 现在 ShowModal 走真模态(DialogBoxIndirectParam),系统对话框管理器自带 owner 禁用, // 这条兜底不但多余,还会误伤:真模态期间 owner 本就需要收到 WM_ACTIVATE/WM_KILLFOCUS 之类的 // 通知消息,无差别丢弃会让 owner 的状态机错乱。故整段删除,不再保留。 } // --------------------------------------------------------------------------- // 真模态(DialogBox)辅助:按 paint window 找到对应的 CPaintManagerUI。 // // ⚠️ 必须定义在**匿名 namespace 之外**(即 DuiLib 命名空间直接成员), // 因为 UIBase.h 里有一份同名声明供 UIManager.cpp 使用。若同时存在匿名 // namespace 内的一份,调用点会因两个候选而报 C2668「调用不明确」。 // // 用途: // · ShowModal 里要把 manager 从旧 HWND 迁到对话框新 HWND 时需要先找到它; // · CPaintManagerUI::IsModalBackdropWindow 判定"本句柄是否就是弹框自己的 // manager 窗口"时也依赖它(透传闸门的一条关键判据)。 // 实现:遍历 CPaintManagerUI::GetPaintManagers(),比对 GetPaintWindow()。 // --------------------------------------------------------------------------- DuiLib::CPaintManagerUI* FindManagerByPaintWindow(HWND hWnd) { if (hWnd == NULL) return NULL; CStdPtrArray* pManagers = DuiLib::CPaintManagerUI::GetPaintManagers(); if (pManagers == NULL) return NULL; for (int i = 0; i < pManagers->GetSize(); ++i) { DuiLib::CPaintManagerUI* pManager = static_cast(pManagers->GetAt(i)); if (pManager != NULL && pManager->GetPaintWindow() == hWnd) return pManager; } return NULL; } ///////////////////////////////////////////////////////////////////////////////////// // // void UILIB_API DUI__Trace(LPCTSTR pstrFormat, ...) { #ifdef _DEBUG TCHAR szBuffer[2048] = {0}; va_list args; va_start(args, pstrFormat); _vsntprintf(szBuffer, 2048, pstrFormat, args); va_end(args); CDuiString strMsg = szBuffer; strMsg += _T("\n"); OutputDebugString(strMsg.GetData()); #endif } LPCTSTR DUI__TraceMsg(UINT uMsg) { #define MSGDEF(x) if(uMsg==x) return _T(#x) MSGDEF(WM_SETCURSOR); MSGDEF(WM_NCHITTEST); MSGDEF(WM_NCPAINT); MSGDEF(WM_PAINT); MSGDEF(WM_ERASEBKGND); MSGDEF(WM_NCMOUSEMOVE); MSGDEF(WM_MOUSEMOVE); MSGDEF(WM_MOUSELEAVE); MSGDEF(WM_MOUSEHOVER); MSGDEF(WM_NOTIFY); MSGDEF(WM_COMMAND); MSGDEF(WM_MEASUREITEM); MSGDEF(WM_DRAWITEM); MSGDEF(WM_LBUTTONDOWN); MSGDEF(WM_LBUTTONUP); MSGDEF(WM_LBUTTONDBLCLK); MSGDEF(WM_RBUTTONDOWN); MSGDEF(WM_RBUTTONUP); MSGDEF(WM_RBUTTONDBLCLK); MSGDEF(WM_SETFOCUS); MSGDEF(WM_KILLFOCUS); MSGDEF(WM_MOVE); MSGDEF(WM_SIZE); MSGDEF(WM_SIZING); MSGDEF(WM_MOVING); MSGDEF(WM_GETMINMAXINFO); MSGDEF(WM_CAPTURECHANGED); MSGDEF(WM_WINDOWPOSCHANGED); MSGDEF(WM_WINDOWPOSCHANGING); MSGDEF(WM_NCCALCSIZE); MSGDEF(WM_NCCREATE); MSGDEF(WM_NCDESTROY); MSGDEF(WM_TIMER); MSGDEF(WM_KEYDOWN); MSGDEF(WM_KEYUP); MSGDEF(WM_CHAR); MSGDEF(WM_SYSKEYDOWN); MSGDEF(WM_SYSKEYUP); MSGDEF(WM_SYSCOMMAND); MSGDEF(WM_SYSCHAR); MSGDEF(WM_VSCROLL); MSGDEF(WM_HSCROLL); MSGDEF(WM_CHAR); MSGDEF(WM_SHOWWINDOW); MSGDEF(WM_PARENTNOTIFY); MSGDEF(WM_CREATE); MSGDEF(WM_NCACTIVATE); MSGDEF(WM_ACTIVATE); MSGDEF(WM_ACTIVATEAPP); MSGDEF(WM_CLOSE); MSGDEF(WM_DESTROY); MSGDEF(WM_GETICON); MSGDEF(WM_GETTEXT); MSGDEF(WM_GETTEXTLENGTH); static TCHAR szMsg[10]; ::wsprintf(szMsg, _T("0x%04X"), uMsg); return szMsg; } ///////////////////////////////////////////////////////////////////////////////////// // // ////////////////////////////////////////////////////////////////////////// // DUI_BASE_BEGIN_MESSAGE_MAP(CNotifyPump) DUI_END_MESSAGE_MAP() static const DUI_MSGMAP_ENTRY* DuiFindMessageEntry(const DUI_MSGMAP_ENTRY* lpEntry,TNotifyUI& msg ) { CDuiString sMsgType = msg.sType; CDuiString sCtrlName = msg.pSender->GetName(); const DUI_MSGMAP_ENTRY* pMsgTypeEntry = NULL; while (lpEntry->nSig != DuiSig_end) { if(lpEntry->sMsgType==sMsgType) { if(!lpEntry->sCtrlName.IsEmpty()) { if(lpEntry->sCtrlName==sCtrlName) { return lpEntry; } } else { pMsgTypeEntry = lpEntry; } } lpEntry++; } return pMsgTypeEntry; } bool CNotifyPump::AddVirtualWnd(CDuiString strName,CNotifyPump* pObject) { if( m_VirtualWndMap.Find(strName) == NULL ) { m_VirtualWndMap.Insert(strName.GetData(),(LPVOID)pObject); return true; } return false; } bool CNotifyPump::RemoveVirtualWnd(CDuiString strName) { if( m_VirtualWndMap.Find(strName) != NULL ) { m_VirtualWndMap.Remove(strName); return true; } return false; } bool CNotifyPump::LoopDispatch(TNotifyUI& msg) { const DUI_MSGMAP_ENTRY* lpEntry = NULL; const DUI_MSGMAP* pMessageMap = NULL; #ifndef UILIB_STATIC for(pMessageMap = GetMessageMap(); pMessageMap!=NULL; pMessageMap = (*pMessageMap->pfnGetBaseMap)()) #else for(pMessageMap = GetMessageMap(); pMessageMap!=NULL; pMessageMap = pMessageMap->pBaseMap) #endif { #ifndef UILIB_STATIC ASSERT(pMessageMap != (*pMessageMap->pfnGetBaseMap)()); #else ASSERT(pMessageMap != pMessageMap->pBaseMap); #endif if ((lpEntry = DuiFindMessageEntry(pMessageMap->lpEntries,msg)) != NULL) { goto LDispatch; } } return false; LDispatch: union DuiMessageMapFunctions mmf; mmf.pfn = lpEntry->pfn; bool bRet = false; int nSig; nSig = lpEntry->nSig; switch (nSig) { default: ASSERT(FALSE); break; case DuiSig_lwl: (this->*mmf.pfn_Notify_lwl)(msg.wParam,msg.lParam); bRet = true; break; case DuiSig_vn: (this->*mmf.pfn_Notify_vn)(msg); bRet = true; break; } return bRet; } void CNotifyPump::NotifyPump(TNotifyUI& msg) { ///遍历虚拟窗口 if( !msg.sVirtualWnd.IsEmpty() ){ for( int i = 0; i< m_VirtualWndMap.GetSize(); i++ ) { if( LPCTSTR key = m_VirtualWndMap.GetAt(i) ) { if( _tcsicmp(key, msg.sVirtualWnd.GetData()) == 0 ){ CNotifyPump* pObject = static_cast(m_VirtualWndMap.Find(key, false)); if( pObject && pObject->LoopDispatch(msg) ) return; } } } } /// //遍历主窗口 LoopDispatch( msg ); } ////////////////////////////////////////////////////////////////////////// /// CWindowWnd::CWindowWnd() : m_hWnd(NULL), m_OldWndProc(::DefWindowProc), m_bSubclassed(false), m_bUnicode(false), m_bFakeModal(false), m_bDoModal(false), m_nModalRet(0), m_pfnOldDlgWndProc(NULL), m_rcModalTarget() { } HWND CWindowWnd::GetHWND() const { return m_hWnd; } UINT CWindowWnd::GetClassStyle() const { return 0; } LPCTSTR CWindowWnd::GetSuperClassName() const { return NULL; } CWindowWnd::operator HWND() const { return m_hWnd; } void CWindowWnd::EnableUnicode() { m_bUnicode = true; } HWND CWindowWnd::CreateDuiWindow( HWND hwndParent, LPCTSTR pstrWindowName,DWORD dwStyle /*=0*/, DWORD dwExStyle /*=0*/ ) { return Create(hwndParent,pstrWindowName,dwStyle,dwExStyle,0,0,0,0,NULL); } HWND CWindowWnd::Create(HWND hwndParent, LPCTSTR pstrName, DWORD dwStyle, DWORD dwExStyle, const RECT rc, HMENU hMenu) { return Create(hwndParent, pstrName, dwStyle, dwExStyle, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, hMenu); } HWND CWindowWnd::Create(HWND hwndParent, LPCTSTR pstrName, DWORD dwStyle, DWORD dwExStyle, int x, int y, int cx, int cy, HMENU hMenu) { if( GetSuperClassName() != NULL && !RegisterSuperclass() ) return NULL; if( GetSuperClassName() == NULL && !RegisterWindowClass() ) return NULL; if(m_bUnicode) { #ifndef UNICODE LPWSTR lpClassName = a2w((char*)GetWindowClassName()); LPWSTR lpName = a2w((char*)pstrName); #else LPWSTR lpClassName = (LPWSTR)GetWindowClassName(); LPWSTR lpName = (LPWSTR)pstrName; #endif m_hWnd = ::CreateWindowExW(dwExStyle, lpClassName, lpName, dwStyle, x, y, cx, cy, hwndParent, hMenu, CPaintManagerUI::GetInstance(), this); #ifndef UNICODE delete []lpClassName; delete []lpName; #endif } else { m_hWnd = ::CreateWindowEx(dwExStyle, GetWindowClassName(), pstrName, dwStyle, x, y, cx, cy, hwndParent, hMenu, CPaintManagerUI::GetInstance(), this); } ASSERT(m_hWnd!=NULL); RegisterTouchWindowIfAvailable(m_hWnd); return m_hWnd; } HWND CWindowWnd::Subclass(HWND hWnd) { ASSERT(::IsWindow(hWnd)); ASSERT(m_hWnd==NULL); m_OldWndProc = SubclassWindow(hWnd, __WndProc); if( m_OldWndProc == NULL ) return NULL; m_bSubclassed = true; m_hWnd = hWnd; ::SetWindowLongPtr(hWnd, GWLP_USERDATA, reinterpret_cast(this)); RegisterTouchWindowIfAvailable(m_hWnd); return m_hWnd; } void CWindowWnd::Unsubclass() { ASSERT(::IsWindow(m_hWnd)); if( !::IsWindow(m_hWnd) ) return; if( !m_bSubclassed ) return; SubclassWindow(m_hWnd, m_OldWndProc); m_OldWndProc = ::DefWindowProc; m_bSubclassed = false; } void CWindowWnd::ShowWindow(bool bShow /*= true*/, bool bTakeFocus /*= false*/) { ASSERT(::IsWindow(m_hWnd)); if( !::IsWindow(m_hWnd) ) return; ::ShowWindow(m_hWnd, bShow ? (bTakeFocus ? SW_SHOWNORMAL : SW_SHOWNOACTIVATE) : SW_HIDE); } UINT CWindowWnd::ShowModal() { // ========================================================================= // 【真模态】改用 Windows 的对话框机制(DialogBoxIndirectParam)驱动模态。 // // 参考:网易 nim_duilib 的 NativeWindow_Windows::DoModal() // (duilib/Core/NativeWindow_Windows.cpp) // // ── 为什么必须换掉旧写法 ────────────────────────────────────────────── // 旧实现是"假模态":自己 ShowWindow + EnableWindow(owner,FALSE) + TOPMOST + // 抢前台 + 自写 while(GetMessage) 嵌套循环。在 Win10 触摸屏上必然失败,根因是 // 触摸消息的投递目标规则(MSDN, Getting Started with Windows Touch Messages): // // "WM_TOUCH messages are currently 'greedy.' After the first touch message is // received on a window, all touch messages are sent to that window until // another window receives focus." // // 主窗口早就 RegisterTouchWindow 过、也早就收到过 WM_TOUCH,所以触摸归属权长期 // 在它手里;弹框是自己 Create 出来的普通窗口,**不是**系统的触摸目标。 // 我们为此打了一堆补丁(手动焦点切换、刷新注册、消息循环转发、__WndProc 转发兜底), // 实测全部无效:日志里弹框始终零 WM_TOUCH、零 WM_LBUTTONDOWN/UP。 // // ── 对话框为什么能解决 ──────────────────────────────────────────────── // DialogBox 是内核实现的模态机制,它: // · 由系统跑消息循环(不需要我们自写嵌套循环); // · 由系统按正确时机禁用 owner(不需要我们 EnableWindow); // · 由系统维持前台/激活(不需要 TOPMOST / SetForegroundWindow / 焦点切换); // · **把对话框窗口本身变成输入目标**,触摸自然投给它、提升的鼠标消息也落在它身上。 // // 于是"贪婪归属权""提升鼠标被吞""owner 抢前台""禁用窗口收不到消息"这几类问题 // 全部不存在 —— 不是被绕过,而是压根不成立。 // // ── 如何与既有代码兼容 ──────────────────────────────────────────────── // 调用方约定是:new → Create(hwnd, ..., UI_WNDSTYLE_DIALOG, ...) → SetIcon → // CenterWindow() → ShowModal()。也就是说**窗口在我们被调用之前就已经 Create 好了**, // CPaintManagerUI 也已 Init 到那个 HWND 上。所以这里不能另起一个对话框窗口, // 而是:用当前窗口的几何/样式造一个 DLGTEMPLATE,让 DialogBoxIndirectParam 去创建 // 对话框窗口,再把旧的 HWND 销毁、把 DuiLib 的 manager 迁移到新 HWND —— 这样 // 调用方完全无感知,`m_hWnd` 语义不变,30+ 个调用点一行都不用改。 // ========================================================================= ASSERT(::IsWindow(m_hWnd)); if( !::IsWindow(m_hWnd) ) return 0; HWND hWndOwner = ::GetWindow(m_hWnd, GW_OWNER); if( hWndOwner == NULL ) hWndOwner = ::GetActiveWindow(); // ---- 1) 采集旧窗口的几何与样式,用于构造对话框模板 ---- RECT rcWnd = { 0 }; ::GetWindowRect(m_hWnd, &rcWnd); const int nW = rcWnd.right - rcWnd.left; const int nH = rcWnd.bottom - rcWnd.top; // 【尺寸精确性】旧窗口的客户区尺寸 —— 这是迁移后必须保持不变的量。 // // 为什么不能直接用窗口矩形(nW/nH)去填 DLGTEMPLATE.cx/cy: // 1. DLGTEMPLATE 的 cx/cy 描述的是**客户区**尺寸,不是窗口外框尺寸; // 2. 我们给模板设的样式(WS_CAPTION/WS_DLGFRAME/DS_MODALFRAME…)会让系统 // 在此基础上再往外加一圈非客户区边框。 // 若直接填 nW/nH,新窗口 = 旧窗口 + 边框 ⇒ 弹框整体变大、位置也偏。 // // 因此这里取"旧窗口客户区尺寸"作为目标客户区尺寸;创建完成后再在 __DialogProc 里 // 用 AdjustWindowRectEx 反算目标外框并 SetWindowPos 精确校正(见 __DialogProc)。 RECT rcClient = { 0 }; ::GetClientRect(m_hWnd, &rcClient); const int nClientW = rcClient.right - rcClient.left; const int nClientH = rcClient.bottom - rcClient.top; DWORD dwStyle = (DWORD)::GetWindowLong(m_hWnd, GWL_STYLE); DWORD dwExStyle = (DWORD)::GetWindowLong(m_hWnd, GWL_EXSTYLE); // 对话框专用的样式:把我们自己那套 WS_POPUPWINDOW/WS_CAPTION/WS_DLGFRAME 换成 // 对话框该有的组合。WS_POPUP 必须保留(否则会变成带系统标题栏的普通窗口)。 // WS_VISIBLE 让 DialogBox 创建后直接显示。 // // ⚠️ 关于 WS_CAPTION / DS_MODALFRAME: // 本项目的弹框(UI_WNDSTYLE_DIALOG)是**无系统标题栏的自绘弹框**,且调用方 // 均未传 WS_THICKFRAME(固定尺寸)。所以这里**不能**自作主张加 DS_MODALFRAME 或 // WS_THICKFRAME —— 那会给弹框加一圈可调整大小的边框,既改变外观又改变尺寸。 // 保持 WS_POPUP + 原有的 WS_CAPTION/WS_SYSMENU 即可(自绘弹框本来就不显示系统标题栏)。 DWORD dwDlgStyle = WS_POPUP | WS_VISIBLE | WS_CLIPSIBLINGS | WS_CLIPCHILDREN; if( dwStyle & WS_CAPTION ) dwDlgStyle |= WS_CAPTION | WS_SYSMENU; if( dwStyle & WS_THICKFRAME ) dwDlgStyle |= WS_THICKFRAME; if( dwStyle & WS_MINIMIZEBOX ) dwDlgStyle |= WS_MINIMIZEBOX; if( dwStyle & WS_MAXIMIZEBOX ) dwDlgStyle |= WS_MAXIMIZEBOX; // ex-style 基本原样透传(WS_EX_TOOLWINDOW / WS_EX_WINDOWEDGE 等),但清掉两类: // · WS_EX_APPWINDOW —— 对话框不该出现在任务栏; // · WS_EX_DLGMODALFRAME —— 会给弹框加一圈双线边框,固定尺寸自绘弹框不需要(会改变外框尺寸)。 dwExStyle &= ~(WS_EX_APPWINDOW | WS_EX_DLGMODALFRAME); // ---- 2) 构造 DLGTEMPLATE 资源 ---- // 只用 DLGTEMPLATE 本体(不含菜单/类名/标题/字体),微软文档允许这样, // 系统会套用默认的对话框字体与类(#32770)。 // 参考 nim_duilib:sizeof(DLGTEMPLATE) + 32 字节余量。 const size_t nDataSize = sizeof(DLGTEMPLATE) + 32; HGLOBAL hResource = ::GlobalAlloc(GMEM_MOVEABLE, nDataSize); if( hResource == NULL ) { DuiLibDiag::Log(_T("MODAL-DLG GlobalAlloc failed err=%u"), ::GetLastError()); return 0; } LPDLGTEMPLATE pTemplate = (LPDLGTEMPLATE)::GlobalLock(hResource); if( pTemplate == NULL ) { ::GlobalFree(hResource); return 0; } ::ZeroMemory(pTemplate, nDataSize); pTemplate->style = dwDlgStyle; pTemplate->dwExtendedStyle = dwExStyle; pTemplate->cdit = 0; // 不使用模板内控件(控件由 DuiLib 自绘) // DLGTEMPLATE 的 cx/cy 单位是对话框单位(DLU),且语义为**客户区**尺寸。 // 这里只是给系统一个初始值(避免 0 尺寸导致奇怪的默认值),真正的精确尺寸 // 由 __DialogProc 里创建完成后按旧客户区尺寸 + AdjustWindowRectEx 反算并 SetWindowPos 校正。 pTemplate->x = 0; pTemplate->y = 0; pTemplate->cx = (short)(nClientW > 0 ? nClientW : nW); pTemplate->cy = (short)(nClientH > 0 ? nClientH : nH); // ---- 3) 迁移前先把旧的 HWND 记录下来,并让 manager 做好切换准备 ---- HWND hWndOld = m_hWnd; m_bDoModal = true; m_nModalRet = 0; // 把"目标位置 + 目标客户区尺寸"暂存到成员,供 __DialogProc 在创建完成后精确校正。 // 之所以不在模板里一次算准:对话框的非客户区厚度取决于实际样式(WS_CAPTION/WS_DLGFRAME/ // WS_EX_* 组合),预估值容易差几个像素;创建后用 AdjustWindowRectEx 反算再 SetWindowPos // 是唯一能保证"客户区尺寸与旧窗口逐像素一致"的做法。 m_rcModalTarget.left = rcWnd.left; m_rcModalTarget.top = rcWnd.top; m_rcModalTarget.right = rcWnd.left + nClientW; m_rcModalTarget.bottom = rcWnd.top + nClientH; DuiLibDiag::Log(_T("MODAL-DLG enter old=%p owner=%p wndrect=%d,%d,%dx%d client=%dx%d dlgstyle=0x%08X ex=0x%08X"), (void*)hWndOld, (void*)hWndOwner, rcWnd.left, rcWnd.top, nW, nH, nClientW, nClientH, (unsigned)dwDlgStyle, (unsigned)dwExStyle); // 记录"当前模态窗口",供库内其它地方判断(如 OnTouchInput 的老兜底逻辑)。 // 注意:此时还不知道新 HWND,__DialogProc 里会再次 SetModalWindow。 HWND hPrevModal = CPaintManagerUI::GetModalWindow(); // 关键:先把旧窗口销毁、把 manager 解绑,再让 DialogBox 创建新的对话框窗口, // 这样 manager 不会出现"两个 HWND 同时活着"的错乱状态。 // 说明:调用方是在 ShowModal() 之前 Create 的,所以这里必须主动清理旧窗口。 HWND hWndOldCaptured = m_hWnd; m_hWnd = NULL; // 先置空:__DialogProc 会在 WM_INITDIALOG 里填入新句柄 // 把 manager 从旧窗口上摘下来(保留它的控件树/资源,只换 paint window) CPaintManagerUI* pManager = FindManagerByPaintWindow(hWndOldCaptured); SetDetachedManager(pManager); // ---- [模态提示音] 在进入模态循环【之前】布好消音窗口 ---- // // 时序(真机日志实证,见本文件头那段说明): // 调用方的触摸 UP 已经消费完(点击回调正在执行),但**这次手势的提升鼠标消息 // 还排在消息队列里没投出来**;紧接着我们在这里把 owner 交给 // DialogBoxIndirectParam 禁用,之后那条消息才被投递 ⇒ 撞上"点击禁用窗口" // ⇒ MessageBeep(0) 咚。 // // 所以必须**现在就**置位:等 DialogBoxIndirectParam 返回就太晚了。 // // 抑制位的生命周期是【手势级】而不是时间级:MessageHandler 收到被抑制窗口的 // 提升鼠标 DOWN 就"记账"、收到配对的 UP 就"销账",账目归零即自动解除抑制; // 另有一条 10 秒僵尸兜底,防止配对消息永久丢失导致抑制位卡死。 // 因此"弹框已出现后用户再点空白区"那一声(用户要求保留的)不受影响 —— // 那时本次手势的账早已结清,而且那一下的归属窗口本来就是弹框。 // // 关闭弹框时还会【主动】撤防(见 CWindowWnd::Close 与 __DialogWndProc 的 // WM_NCDESTROY),不依赖超时 —— 这是 Win10 "3 秒透点"的修复点。 // // 位置:必须在 pManager 取到之后(否则未声明标识符),且早于下方 // DialogBoxIndirectParam 调用即可 —— 中间那些 Detach / DestroyWindow 不影响它, // 因为抑制位是 manager 的成员,会随对象一起被 Attach 到新弹框。 if( pManager != NULL ) { pManager->SetSuppressPromotedMouseForModal(true); // ★★【透传根治 · 代数推进点】★★ // // 这是唯一**可靠**的"模态边界"信号,必须在这里推进代数。 // // 为什么不能靠在 MessageHandler 里比较"当前是否有模态 vs 上次是否有模态": // 真机时序是 // WM_LBUTTONUP(主窗口)→ 回调里同步 ShowModal() → 模态循环 → // EndDialog → GetModalWindow() 归零 → **然后**那条被卡住的 // WM_LBUTTONDOWN 才被投递。 // 主窗口的 MessageHandler 在**整个模态期间收不到任何消息** // (owner 被 EnableWindow(FALSE),消息抽不出来), // 所以"边沿检测"两边看到的都是 false ⇒ 永远不会触发 ⇒ 拦不住。 // ⇒ 边界必须在这里**主动**记录。 // // ⚠️ 为什么推进的是**全局**代数、而不是 pManager 的成员: // 本项目属性弹框(CDiandanNatureWnd)自带独立的 `CPaintManagerUI m_pm`, // 与主窗口的 manager **不是同一个对象**。真机日志铁证: // MODAL-BEEP arm manager=0DCE0960 <- 弹框的 manager // MANAGER-MSG hwnd=00D20604 manager=05796840 <- 泄漏消息落在这里 // 而"泄漏"是**线程消息队列**现象,与 manager 归属无关 // ⇒ 代数必须是线程级的(见 DuiLib::BumpPromotedMouseGenerationGlobal)。 DuiLib::BumpPromotedMouseGenerationGlobal(); // ★ V2 标记:真机自证用。看到 `gen=` 且 `v=2` 才说明跑的是**这一版**二进制。 // 上一版这行没有 gen=/v= 字段,日志里出现的是 `MODAL-BEEP arm manager=... owner=...` // ⇒ 如果新日志仍然没有 gen=/v=,那就是**没重新编译**,任何分析都不必做。 DuiLibDiag::Log(_T("MODAL-BEEP arm v=2 manager=%p owner=%p gen=%u"), (void*)pManager, (void*)hWndOwner, (unsigned)DuiLib::PeekPromotedMouseGeneration()); } // [第十一轮诊断] 模态开启瞬间的悬停/点击/捕获状态快照 —— **搬家前**的现场。 // // 真模态会把**同一个** manager 从旧窗口搬到弹框上。此前 `AttachPaintWindow` 只换 // paint window、**不清理** m_pEventHover / m_aNeedMouseLeaveNeeded / m_bMouseCapture, // 这些状态就跟着 manager 一起"住进"弹框,表现为"主窗口那个商品控件一直保持悬浮态, // 直到弹框关闭才恢复"(用户报的第 4 个 bug)。 // // ⚠️ 该缺陷**已在第十一轮修复**:AttachPaintWindow 现在会清掉这一组输入态 // (见 UIManager.cpp 该函数开头的长注释)。 // 本日志保留用于**对照验证** —— 期望看到 hover 有值(旧窗口确实残留着), // 而修复后弹框上不应再出现"悬浮态不消失"。若修复后本行仍打印 hover=null, // 说明残留另有来源,需要继续查。 if( pManager != NULL ) { DuiLibDiag::Log(_T("MODAL-STATE enter manager=%p hover=%s captured=%d dragmode=%d leaveArr=%d rclick=%s"), (void*)pManager, pManager->DbgHoverName(), (int)pManager->IsCaptured(), (int)pManager->DbgIsDragMode(), pManager->DbgNeedMouseLeaveCount(), pManager->DbgRClickName()); } if( pManager != NULL ) { pManager->DetachPaintWindow(); DuiLibDiag::Log(_T("MODAL-DLG detach manager=%p from old=%p"), (void*)pManager, (void*)hWndOldCaptured); } // 销毁旧窗口(旧窗口上的窗口过程会把 m_hWnd 清空,但我们已经在上面置 NULL 了, // 且 manager 已摘除,不会再触发 DuiLib 的绘制路径)。 if( ::IsWindow(hWndOldCaptured) ) ::DestroyWindow(hWndOldCaptured); // ---- 4) 交给系统对话框管理器跑模态循环 ---- // 这是阻塞调用,返回时模态已结束。 INT_PTR nRet = ::DialogBoxIndirectParam(CPaintManagerUI::GetInstance(), pTemplate, hWndOwner, CWindowWnd::__DialogProc, (LPARAM)this); // 资源可以释放了(DialogBoxIndirectParam 内部已把模板复制走)—— // 但谨慎起见放在 EndDialog 之后,与 nim_duilib 一致。 ::GlobalUnlock(hResource); ::GlobalFree(hResource); // 兜底:若 __DialogProc 因故没跑(创建失败 / WM_INITDIALOG 里 early-return), // 这里把中转变量清干净,避免悬垂指针影响后续 ShowModal。 SetDetachedManager(NULL); // [模态提示音] 模态循环已结束 ⇒ 消音窗口撤销。 // 注意句柄:真模态把同一个 manager Detach 到旧窗口、再 Attach 到弹框,所以 // 此刻 pManager 可能已随弹框一起销毁 —— 用 FindManagerByPaintWindow 取不到, // 因此直接用进入前缓存的指针。 // 这里只是"尽早撤销"的一层;正确性由下面两处确定性撤防保证: // a) CWindowWnd::Close() 的 m_bDoModal 分支,在 EndDialog 之前撤防; // b) __DialogWndProc 的 WM_NCDESTROY 收尾,任何销毁路径都会撤防。 // 若以上都因故没跑到,MessageHandler 里还有 10 秒僵尸兜底。 if( pManager != NULL ) { pManager->SetSuppressPromotedMouseForModal(false); DuiLibDiag::Log(_T("MODAL-BEEP disarm manager=%p"), (void*)pManager); } m_bDoModal = false; CPaintManagerUI::SetModalWindow(hPrevModal); DuiLibDiag::Log(_T("MODAL-DLG exit hwnd=%p ret=%d m_nModalRet=%u"), (void*)m_hWnd, (int)nRet, m_nModalRet); // DialogBoxIndirectParam 返回值:-1 表示创建失败 if( nRet == -1 ) { DuiLibDiag::Log(_T("MODAL-DLG failed err=%u"), ::GetLastError()); return 0; } return m_nModalRet; } // ----------------------------------------------------------------------------- // 真模态:对话框过程。 // // 生命周期: // DialogBoxIndirectParam → 创建 #32770 窗口 → WM_INITDIALOG(此时才有真 HWND) // → 把 manager 挂到新 HWND、换掉 GWLP_WNDPROC // → return TRUE(表示我们已经处理了焦点) // ----------------------------------------------------------------------------- INT_PTR CALLBACK CWindowWnd::__DialogProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam) { if( uMsg == WM_INITDIALOG ) { // 这是对话框的第一个消息;lParam 是我们传进来的 this CWindowWnd* pThis = reinterpret_cast(lParam); if( pThis == NULL ) return TRUE; ASSERT(pThis->m_hWnd == NULL); pThis->m_hWnd = hDlg; // ---- 换掉对话框默认的窗口过程 ---- // 这样后续所有消息(含 WM_PAINT / WM_TOUCH / 鼠标)都会走到 // CWindowWnd::HandleMessage → CPaintManagerUI,DuiLib 的自绘与路由完全保留。 ASSERT(pThis->m_pfnOldDlgWndProc == NULL); pThis->m_pfnOldDlgWndProc = (WNDPROC)::GetWindowLongPtr(hDlg, GWLP_WNDPROC); ::SetWindowLongPtr(hDlg, GWLP_WNDPROC, (LONG_PTR)(WNDPROC)CWindowWnd::__DialogWndProc); ::SetWindowLongPtr(hDlg, GWLP_USERDATA, (LONG_PTR)pThis); // ---- 登记为当前模态窗口 ---- CPaintManagerUI::SetModalWindow(hDlg); // ---- 触摸注册:对话框是全新的 HWND,必须注册,否则收不到 WM_TOUCH ---- RegisterTouchWindowIfAvailable(hDlg); DuiLibDiag::Log(_T("MODAL-DLG init dlg=%p oldwndproc=%p focus=%p"), (void*)hDlg, (void*)pThis->m_pfnOldDlgWndProc, (void*)::GetFocus()); // ---- 把旧的 CPaintManagerUI 挂到对话框窗口上 ---- // 调用方在 ShowModal() 之前已 Create + manager.Init 到旧 HWND;这里把 manager 迁移过来, // 让 DuiLib 的全部既有逻辑(绘制 / 消息 / 控件树)在新窗口上继续工作。 CPaintManagerUI* pManager = GetDetachedManager(); if( pManager != NULL ) { pManager->AttachPaintWindow(hDlg); DuiLibDiag::Log(_T("MODAL-DLG attach manager=%p to dlg=%p"), (void*)pManager, (void*)hDlg); // 用掉即清:避免这个文件级静态变量残留一个已失效的指针, // 影响下一次 ShowModal(尤其嵌套弹框:A 弹框里再开 B 弹框)。 SetDetachedManager(NULL); } else { DuiLibDiag::Log(_T("MODAL-DLG no detached manager (dialog will Init itself)")); } // ---- [WM_TOUCH 归属权] 立刻把"补调原窗口过程"的钩子绑到本弹框 ---- // 为什么必须在这里(而不是等 __DialogWndProc 首次进入时再绑): // __DialogProc 在 WM_INITDIALOG 里直接调 pThis->HandleMessage(见下方), // 这条路径**不经过** __DialogWndProc,所以"首次进入窗口过程时绑定" // 在这条路上不会触发。而 AttachPaintWindow 之后 manager 已经指向 hDlg, // 一旦有 WM_MOUSEMOVE 到达就可能需要补调 ⇒ 必须现在就绑好。 // 绑定对象是 m_pfnOldDlgWndProc(即 #32770 原过程 / DefDlgProc)。 if( pManager != NULL ) { // m_pfnOldDlgWndProc 本身就是 WNDPROC;只有它意外为空时才回落到 DefDlgProc // (签名是 INT_PTR 返回,需显式转换,不能与 WNDPROC 混在三目里)。 WNDPROC pfnDlgTarget = pThis->m_pfnOldDlgWndProc; if( pfnDlgTarget == NULL ) pfnDlgTarget = reinterpret_cast(::DefDlgProc); BindRelayTarget(pfnDlgTarget, hDlg); pManager->SetDefProcRelay(&RelayWndProc, hDlg); } // ---- 尺寸校正(关键)---- // DLGTEMPLATE 里给的 cx/cy 是对话框单位(DLU)、且语义为「客户区」,系统创建时还会 // 按实际样式额外加一圈非客户区边框。所以不能指望"模板尺寸 == 目标尺寸"。 // 这里用 AdjustWindowRectEx 按对话框**真实**的样式反算目标外框,再 SetWindowPos 精确校正, // 保证客户区尺寸与原窗口逐像素一致 —— 这正是"弹框变大了"的修法。 // // 放在 AttachPaintWindow 之后、HandleMessage(WM_INITDIALOG) 之前: // 此时 manager 已绑定新 HWND,尺寸变化会走正常的布局流程;且业务层 OnCreate 之前 // 窗口就已经是最终尺寸,避免"先小后大"的闪动与一次性布局错位。 { const DWORD dwRealStyle = (DWORD)::GetWindowLong(hDlg, GWL_STYLE); const DWORD dwRealExStyle = (DWORD)::GetWindowLong(hDlg, GWL_EXSTYLE); RECT rcAdjust = { 0, 0, pThis->m_rcModalTarget.right - pThis->m_rcModalTarget.left, pThis->m_rcModalTarget.bottom - pThis->m_rcModalTarget.top }; const int nClientW = rcAdjust.right; const int nClientH = rcAdjust.bottom; // 反算:以"目标客户区尺寸"为输入,得到需要的外框尺寸 ::AdjustWindowRectEx(&rcAdjust, dwRealStyle, FALSE, dwRealExStyle); const int nWndW = rcAdjust.right - rcAdjust.left; const int nWndH = rcAdjust.bottom - rcAdjust.top; RECT rcCur = { 0 }; ::GetWindowRect(hDlg, &rcCur); DuiLibDiag::Log(_T("MODAL-DLG resize before cur=%dx%d clientTarget=%dx%d -> wnd=%dx%d hide=%d"), rcCur.right - rcCur.left, rcCur.bottom - rcCur.top, nClientW, nClientH, nWndW, nWndH, (int)((dwRealStyle & WS_VISIBLE) == 0)); // SWP_FRAMECHANGED 让系统重新计算非客户区(样式刚被 SetWindowLongPtr 换过) ::SetWindowPos(hDlg, NULL, pThis->m_rcModalTarget.left, pThis->m_rcModalTarget.top, nWndW, nWndH, SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED); RECT rcAfter = { 0 }; ::GetClientRect(hDlg, &rcAfter); DuiLibDiag::Log(_T("MODAL-DLG resize after client=%dx%d (target=%dx%d)"), rcAfter.right - rcAfter.left, rcAfter.bottom - rcAfter.top, nClientW, nClientH); } // 交给 CWindowWnd::HandleMessage 处理 WM_INITDIALOG,业务层的 OnCreate 等由此触发。 pThis->HandleMessage(uMsg, wParam, lParam); ::SetFocus(hDlg); return TRUE; // TRUE = 我们自己设置了焦点 } return FALSE; } // ----------------------------------------------------------------------------- // 真模态:对话框窗口的窗口过程。 // 几乎就是 __WndProc 的对话框版本。 // ----------------------------------------------------------------------------- LRESULT CALLBACK CWindowWnd::__DialogWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { CWindowWnd* pThis = reinterpret_cast(::GetWindowLongPtr(hWnd, GWLP_USERDATA)); if( uMsg == WM_NCDESTROY && pThis != NULL ) { // 顺序很关键: // 1) 先把窗口过程还原(此后系统调用会回到对话框默认过程); // 2) 再让 DefWindowProc 做标准清理; // 3) 清 GWLP_USERDATA —— 必须在 HandleMessage 之前,防止重入时又拿到 this; // 4) 清模态登记; // 5) m_hWnd 置空【要在 Unsubclass 之前】—— 注意本项目弹框并不 Subclass, // m_bSubclassed 恒为 false,这里只是保持与 __WndProc 一致的写法, // 并让 OnFinalMessage 里看到的 m_hWnd 为空(与旧行为一致)。 if( pThis->m_pfnOldDlgWndProc != NULL ) { ::SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)pThis->m_pfnOldDlgWndProc); pThis->m_pfnOldDlgWndProc = NULL; } LRESULT lRes = ::DefWindowProc(hWnd, uMsg, wParam, lParam); // [WM_TOUCH 归属权] 解绑转发目标,避免悬垂到已销毁的对话框窗口过程。 { DuiLib::CPaintManagerUI* pMgrNc = FindManagerByPaintWindow(hWnd); if( pMgrNc != NULL ) { pMgrNc->ClearDefProcRelay(hWnd); // [模态提示音 / 透点] 第二道撤防:不管弹框以哪条路径死掉 // (Close / EndDialog / 外部 DestroyWindow / 创建失败收尾), // 只要它的窗口在销毁,就不允许消音窗口继续存在。 // 抑制位跟着 manager 对象走,而 manager 可能被回收再复用, // 若不清就可能在下一个不相关的窗口上"诈尸",吞掉用户的真实点击。 pMgrNc->SetSuppressPromotedMouseForModal(false); } BindRelayTarget(NULL, NULL); } ::SetWindowLongPtr(hWnd, GWLP_USERDATA, 0L); CPaintManagerUI::SetModalWindow(NULL); pThis->m_hWnd = NULL; if( pThis->m_bSubclassed ) pThis->Unsubclass(); pThis->OnFinalMessage(hWnd); return lRes; } // WM_INITDIALOG 已经由 __DialogProc 处理并 return TRUE, // 一般不会再走到这里;若系统仍投递(例如 EndDialog 之后的收尾路径), // 直接交回对话框默认过程,避免被 HandleMessage 当成普通消息误处理。 if( uMsg == WM_INITDIALOG ) return ::DefDlgProc(hWnd, uMsg, wParam, lParam); // 诊断:确认 WM_TOUCH 到底投递给了哪个窗口。 if( uMsg == WM_TOUCH ) TraceTouchArrival(hWnd, wParam, lParam); if( pThis != NULL ) TraceInputMessage(hWnd, uMsg, wParam, lParam); if( pThis != NULL ) { // --------------------------------------------------------------- // [WM_TOUCH 归属权] 注册"补调原窗口过程"的钩子。 // // 真模态弹框的窗口类是系统对话框类 #32770,它把"把特殊标记的 // WM_MOUSEMOVE 翻译成 WM_TOUCH"这件事放在 DefDlgProc → DefWindowProc 里做。 // 而 __DialogWndProc 一旦 pThis != NULL 就全部交给 HandleMessage, // 再也不回落 DefDlgProc ⇒ 弹框永远收不到 WM_TOUCH(与实测吻合)。 // // 这里登记的原过程是 m_pfnOldDlgWndProc(也就是 DefDlgProc), // 使 manager 在吞掉触摸提升鼠标消息时能补调它一次,把翻译链接上。 // --------------------------------------------------------------- // ⚠️ 判据必须是"当前绑定的窗口不是本窗口",不能写 `GetDefProcRelay() == NULL`: // 真模态弹框复用的是**同一个** manager 对象(从旧窗口 Detach、再 Attach 到弹框), // 主窗口早已注册过钩子 ⇒ 按 NULL 判断则弹框永远不重新绑定, // 转发目标会一直指向主窗口的原过程,补调无效甚至喂错窗口。 DuiLib::CPaintManagerUI* pMgr = FindManagerByPaintWindow(hWnd); if( pMgr != NULL && pMgr->GetDefProcRelayBoundWindow() != hWnd ) { // ⚠️ DefDlgProc 的签名是 `INT_PTR (CALLBACK*)(...)`,与 WNDPROC // (`LRESULT (CALLBACK*)(...)`)返回类型不同,三目运算符会类型不匹配。 // 用一个显式分支赋值,避免 C2446。 WNDPROC pfnTarget = NULL; if( pThis->m_pfnOldDlgWndProc != NULL ) pfnTarget = pThis->m_pfnOldDlgWndProc; else pfnTarget = reinterpret_cast(::DefDlgProc); BindRelayTarget(pfnTarget, hWnd); pMgr->SetDefProcRelay(&RelayWndProc, hWnd); } return pThis->HandleMessage(uMsg, wParam, lParam); } return ::DefDlgProc(hWnd, uMsg, wParam, lParam); } void CWindowWnd::ShowModalFake() { ASSERT(::IsWindow(m_hWnd)); auto p_hwnd = GetWindowOwner(m_hWnd); ASSERT(::IsWindow(p_hwnd)); ASSERT(p_hwnd); ::EnableWindow(p_hwnd, FALSE); ShowWindow(); m_bFakeModal = true; } void CWindowWnd::Close(UINT nRet) { if (m_bFakeModal) { auto parent_hwnd = GetWindowOwner(m_hWnd); ASSERT(::IsWindow(parent_hwnd)); ::EnableWindow(parent_hwnd, TRUE); ::SetFocus(parent_hwnd); m_bFakeModal = false; } ASSERT(::IsWindow(m_hWnd)); if (!::IsWindow(m_hWnd)) return; // ------------------------------------------------------------------------- // 真模态:走 EndDialog 结束系统对话框循环。 // // 旧的假模态是 PostMessage(WM_CLOSE) 让自写循环退出;真模态下必须用 EndDialog, // 否则 DialogBoxIndirectParam 不会返回。返回值写入 m_nModalRet,由 ShowModal 读走。 // // 注意:EndDialog 会销毁对话框窗口 → 触发 WM_NCDESTROY → __DialogWndProc 收尾 // (还原窗口过程、清 USERDATA、调 OnFinalMessage)。所以这里不要再 DestroyWindow。 // ------------------------------------------------------------------------- if (m_bDoModal) { m_nModalRet = nRet; // --------------------------------------------------------------------- // [模态提示音 / 透点] 关弹框时【立即】撤掉消音窗口 —— 这是 Win10 "3 秒透点" // 的关键修复点。 // // 现象(Win10):点商品控件弹属性框;3 秒内点"选好了"一切正常;一旦超过 3 秒 // 再点"选好了",弹框关掉的同时还会"透"到底下、又点到"选好了"背后的商品控件。 // // 成因:消音窗口原先只靠 MessageHandler 里的超时自愈撤销,而那个超时是 3000ms // (UIManager.cpp 的 TOUCH-STATE timeout-reset)。用户观察到的"3 秒"就是它。 // 超过 3 秒后,抑制位已被超时清掉,可**这次手势的提升鼠标消息还排在队列里没 // 投出来**(触摸 UP 的点击回调此刻正在执行)⇒ 抑制失效 ⇒ 那条残留消息被放行 // ⇒ 命中弹框背后的窗口 ⇒ 透点。 // 实质是"靠一个超时 bug 去挡另一个时序 bug",只是恰好 3 秒内不出问题。 // // 修复:弹框关闭是**确定性时点**,比任何超时都可靠 —— 在 EndDialog 之前主动把 // 同一 manager 上的抑制状态清干净(含未配对的 pending 计数,见 // SetSuppressPromotedMouseForModal(false) 的实现)。 // // 为什么用 FindManagerByPaintWindow 而不是 ShowModal 里缓存的 pManager: // 真模态把同一个 manager 从旧窗口 Detach、再 Attach 到本弹框,所以此刻 // m_hWnd 就是弹框句柄,能直接查到它。查不到也无妨 —— 下面 EndDialog 会让 // __DialogWndProc 的 WM_NCDESTROY 收尾,那里还有第二道撤防。 // --------------------------------------------------------------------- { DuiLib::CPaintManagerUI* pMgrClosing = FindManagerByPaintWindow(m_hWnd); if (pMgrClosing != NULL) { pMgrClosing->SetSuppressPromotedMouseForModal(false); DuiLibDiag::Log(_T("MODAL-BEEP disarm-on-close manager=%p hwnd=%p"), (void*)pMgrClosing, (void*)m_hWnd); } } DuiLibDiag::Log(_T("MODAL-DLG EndDialog hwnd=%p ret=%u"), (void*)m_hWnd, nRet); ::EndDialog(m_hWnd, (INT_PTR)nRet); return; } PostMessage(WM_CLOSE, (WPARAM)nRet, 0L); } void CWindowWnd::CenterWindow() { ASSERT(::IsWindow(m_hWnd)); ASSERT((GetWindowStyle(m_hWnd)&WS_CHILD)==0); RECT rcDlg = { 0 }; ::GetWindowRect(m_hWnd, &rcDlg); RECT rcArea = { 0 }; RECT rcCenter = { 0 }; HWND hWnd=*this; HWND hWndParent = ::GetParent(m_hWnd); HWND hWndCenter = ::GetWindowOwner(m_hWnd); if (hWndCenter!=NULL) hWnd=hWndCenter; // 处理多显示器模式下屏幕居中 MONITORINFO oMonitor = {}; oMonitor.cbSize = sizeof(oMonitor); ::GetMonitorInfo(::MonitorFromWindow(hWnd, MONITOR_DEFAULTTONEAREST), &oMonitor); rcArea = oMonitor.rcWork; if( hWndCenter == NULL ) rcCenter = rcArea; else ::GetWindowRect(hWndCenter, &rcCenter); int DlgWidth = rcDlg.right - rcDlg.left; int DlgHeight = rcDlg.bottom - rcDlg.top; // Find dialog's upper left based on rcCenter int xLeft = (rcCenter.left + rcCenter.right) / 2 - DlgWidth / 2; int yTop = (rcCenter.top + rcCenter.bottom) / 2 - DlgHeight / 2; // The dialog is outside the screen, move it inside if( xLeft < rcArea.left ) xLeft = rcArea.left; else if( xLeft + DlgWidth > rcArea.right ) xLeft = rcArea.right - DlgWidth; if( yTop < rcArea.top ) yTop = rcArea.top; else if( yTop + DlgHeight > rcArea.bottom ) yTop = rcArea.bottom - DlgHeight; ::SetWindowPos(m_hWnd, NULL, xLeft, yTop, -1, -1, SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE); } void CWindowWnd::SetIcon(UINT nRes) { HICON hIcon = (HICON)::LoadImage(CPaintManagerUI::GetInstance(), MAKEINTRESOURCE(nRes), IMAGE_ICON, (::GetSystemMetrics(SM_CXICON) + 15) & ~15, (::GetSystemMetrics(SM_CYICON) + 15) & ~15, // 防止高DPI下图标模糊 LR_DEFAULTCOLOR); ASSERT(hIcon); ::SendMessage(m_hWnd, WM_SETICON, (WPARAM) TRUE, (LPARAM) hIcon); hIcon = (HICON)::LoadImage(CPaintManagerUI::GetInstance(), MAKEINTRESOURCE(nRes), IMAGE_ICON, (::GetSystemMetrics(SM_CXICON) + 15) & ~15, (::GetSystemMetrics(SM_CYICON) + 15) & ~15, // 防止高DPI下图标模糊 LR_DEFAULTCOLOR); ASSERT(hIcon); ::SendMessage(m_hWnd, WM_SETICON, (WPARAM) FALSE, (LPARAM) hIcon); } bool CWindowWnd::RegisterWindowClass() { WNDCLASS wc = { 0 }; wc.style = GetClassStyle(); wc.cbClsExtra = 0; wc.cbWndExtra = 0; wc.hIcon = NULL; wc.lpfnWndProc = CWindowWnd::__WndProc; wc.hInstance = CPaintManagerUI::GetInstance(); wc.hCursor = ::LoadCursor(NULL, IDC_ARROW); wc.hbrBackground = NULL; wc.lpszMenuName = NULL; wc.lpszClassName = GetWindowClassName(); ATOM ret = ::RegisterClass(&wc); ASSERT(ret!=NULL || ::GetLastError()==ERROR_CLASS_ALREADY_EXISTS); return ret != NULL || ::GetLastError() == ERROR_CLASS_ALREADY_EXISTS; } bool CWindowWnd::RegisterSuperclass() { if(m_bUnicode) { #ifndef UNICODE LPWSTR lpSuperClassName = a2w((char*)GetSuperClassName()); LPWSTR lpClassName = a2w((char*)GetWindowClassName()); #else LPWSTR lpSuperClassName = (LPWSTR)GetSuperClassName(); LPWSTR lpClassName = (LPWSTR)GetWindowClassName(); #endif // Get the class information from an existing // window so we can subclass it later on... WNDCLASSEXW wc = { 0 }; wc.cbSize = sizeof(WNDCLASSEXW); if( !::GetClassInfoExW(NULL, lpSuperClassName, &wc) ) { if( !::GetClassInfoExW(CPaintManagerUI::GetInstance(), lpSuperClassName, &wc) ) { ASSERT(!"Unable to locate window class"); return NULL; } } m_OldWndProc = wc.lpfnWndProc; wc.lpfnWndProc = CWindowWnd::__ControlProc; wc.hInstance = CPaintManagerUI::GetInstance(); wc.lpszClassName = lpClassName; ATOM ret = ::RegisterClassExW(&wc); #ifndef UNICODE delete []lpClassName; delete []lpSuperClassName; #endif ASSERT(ret!=NULL || ::GetLastError()==ERROR_CLASS_ALREADY_EXISTS); return ret != NULL || ::GetLastError() == ERROR_CLASS_ALREADY_EXISTS; } else { // Get the class information from an existing // window so we can subclass it later on... WNDCLASSEX wc = { 0 }; wc.cbSize = sizeof(WNDCLASSEX); if( !::GetClassInfoEx(NULL, GetSuperClassName(), &wc) ) { if( !::GetClassInfoEx(CPaintManagerUI::GetInstance(), GetSuperClassName(), &wc) ) { ASSERT(!"Unable to locate window class"); return NULL; } } m_OldWndProc = wc.lpfnWndProc; wc.lpfnWndProc = CWindowWnd::__ControlProc; wc.hInstance = CPaintManagerUI::GetInstance(); wc.lpszClassName = GetWindowClassName(); ATOM ret = ::RegisterClassEx(&wc); ASSERT(ret!=NULL || ::GetLastError()==ERROR_CLASS_ALREADY_EXISTS); return ret != NULL || ::GetLastError() == ERROR_CLASS_ALREADY_EXISTS; } } // 注:RelayWndProc / BindRelayTarget 已上移到文件前部的**匿名 namespace 内** // (约 308 / 317 行),因为 __DialogWndProc 比本函数先出现、也要用到它们。 // 另注:FindManagerByPaintWindow 也已移到匿名 namespace **之外**(约 350 行), // 以便与 UIBase.h 里的同名声明一一对应(否则调用点会报 C2668 二义)。 LRESULT CALLBACK CWindowWnd::__WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { CWindowWnd* pThis = NULL; if( uMsg == WM_NCCREATE ) { LPCREATESTRUCT lpcs = reinterpret_cast(lParam); pThis = static_cast(lpcs->lpCreateParams); pThis->m_hWnd = hWnd; ::SetWindowLongPtr(hWnd, GWLP_USERDATA, reinterpret_cast(pThis)); } else { pThis = reinterpret_cast(::GetWindowLongPtr(hWnd, GWLP_USERDATA)); if( uMsg == WM_NCDESTROY && pThis != NULL ) { LRESULT lRes = ::CallWindowProc(pThis->m_OldWndProc, hWnd, uMsg, wParam, lParam); // [WM_TOUCH 归属权] 解绑转发目标,避免悬垂到已销毁窗口的窗口过程。 DuiLib::CPaintManagerUI* pMgrNc = FindManagerByPaintWindow(hWnd); if( pMgrNc != NULL ) pMgrNc->ClearDefProcRelay(hWnd); BindRelayTarget(NULL, NULL); ::SetWindowLongPtr(pThis->m_hWnd, GWLP_USERDATA, 0L); if( pThis->m_bSubclassed ) pThis->Unsubclass(); pThis->m_hWnd = NULL; pThis->OnFinalMessage(hWnd); return lRes; } } // 诊断:确认 WM_TOUCH 到底投递给了哪个窗口(弹框 / owner / 其它)。 if( uMsg == WM_TOUCH ) TraceTouchArrival(hWnd, wParam, lParam); // 【临时全量诊断】所有输入类消息(含 WM_TOUCH / WM_GESTURE / WM_POINTER* / 鼠标 / 激活) // 都记一条,用来判断系统到底把消息发给了谁。 if( pThis != NULL ) TraceInputMessage(hWnd, uMsg, wParam, lParam); if( pThis != NULL ) { // --------------------------------------------------------------- // [WM_TOUCH 归属权] 注册"补调原窗口过程"的钩子。 // 把 m_OldWndProc(本窗口类的原过程)登记为转发目标,并交给 manager。 // manager 在吞掉"触摸提升 WM_MOUSEMOVE"时会回调它一次, // 让系统的 WM_TOUCH 翻译链不断掉。 // // 判据用"绑定窗口 != 本窗口"而非"钩子为空":真模态弹框复用同一个 // manager,换 HWND 时必须能重新绑定到对话框架自己的原过程。 // 开销:仅在窗口首个消息时走一遍查找,之后靠 manager 内的指针直连。 // --------------------------------------------------------------- DuiLib::CPaintManagerUI* pMgr = FindManagerByPaintWindow(hWnd); if( pMgr != NULL && pMgr->GetDefProcRelayBoundWindow() != hWnd ) { BindRelayTarget(pThis->m_OldWndProc, hWnd); pMgr->SetDefProcRelay(&RelayWndProc, hWnd); } return pThis->HandleMessage(uMsg, wParam, lParam); } else { return ::DefWindowProc(hWnd, uMsg, wParam, lParam); } } LRESULT CALLBACK CWindowWnd::__ControlProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { CWindowWnd* pThis = NULL; if( uMsg == WM_NCCREATE ) { LPCREATESTRUCT lpcs = reinterpret_cast(lParam); pThis = static_cast(lpcs->lpCreateParams); ::SetProp(hWnd, _T("WndX"), (HANDLE) pThis); pThis->m_hWnd = hWnd; } else { pThis = reinterpret_cast(::GetProp(hWnd, _T("WndX"))); if( uMsg == WM_NCDESTROY && pThis != NULL ) { LRESULT lRes = ::CallWindowProc(pThis->m_OldWndProc, hWnd, uMsg, wParam, lParam); if( pThis->m_bSubclassed ) pThis->Unsubclass(); ::SetProp(hWnd, _T("WndX"), NULL); pThis->m_hWnd = NULL; pThis->OnFinalMessage(hWnd); return lRes; } } if( pThis != NULL ) { return pThis->HandleMessage(uMsg, wParam, lParam); } else { return ::DefWindowProc(hWnd, uMsg, wParam, lParam); } } LRESULT CWindowWnd::SendMessage(UINT uMsg, WPARAM wParam /*= 0*/, LPARAM lParam /*= 0*/) { ASSERT(::IsWindow(m_hWnd)); return ::SendMessage(m_hWnd, uMsg, wParam, lParam); } LRESULT CWindowWnd::PostMessage(UINT uMsg, WPARAM wParam /*= 0*/, LPARAM lParam /*= 0*/) { ASSERT(::IsWindow(m_hWnd)); return ::PostMessage(m_hWnd, uMsg, wParam, lParam); } void CWindowWnd::ResizeClient(int cx /*= -1*/, int cy /*= -1*/) { ASSERT(::IsWindow(m_hWnd)); RECT rc = { 0 }; if( !::GetClientRect(m_hWnd, &rc) ) return; if( cx != -1 ) rc.right = cx; if( cy != -1 ) rc.bottom = cy; if( !::AdjustWindowRectEx(&rc, GetWindowStyle(m_hWnd), (!(GetWindowStyle(m_hWnd) & WS_CHILD) && (::GetMenu(m_hWnd) != NULL)), GetWindowExStyle(m_hWnd)) ) return; ::SetWindowPos(m_hWnd, NULL, 0, 0, rc.right - rc.left, rc.bottom - rc.top, SWP_NOZORDER | SWP_NOMOVE | SWP_NOACTIVATE); } LRESULT CWindowWnd::HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam) { return ::CallWindowProc(m_OldWndProc, m_hWnd, uMsg, wParam, lParam); } void CWindowWnd::OnFinalMessage(HWND /*hWnd*/) { } } // namespace DuiLib