UIBase.cpp 72 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489
  1. #include "StdAfx.h"
  2. #include "UIBase.h"
  3. #include "UIManager.h"
  4. #ifdef _DEBUG
  5. #include <shlwapi.h>
  6. #pragma comment(lib, "shlwapi.lib")
  7. #endif
  8. namespace DuiLib {
  9. // WM_TOUCH 消息 ID 在 winuser.h 里受 `#if _WIN32_WINNT >= 0x0601` 保护,
  10. // 而 DuiLib 工程的 _WIN32_WINNT 兜底为 XP(0x0501),直接引用会编译失败,这里自行声明。
  11. #ifndef WM_TOUCH
  12. #define WM_TOUCH 0x0240
  13. #endif
  14. // 注:WM_POINTER* 常量统一放在 UIManager.cpp 的 PointerMsg 命名空间里(那边才是处理方),
  15. // 本文件不再重复定义 —— 原先为"模态期间屏蔽 owner 输入"定义的 DUI_WM_POINTER_FIRST/LAST
  16. // 已随 IsModalBlockedInputMessage 一并删除。
  17. namespace {
  18. // -------------------------------------------------------------------------
  19. // [模态提示音] Win7 上"点控件触发弹框"时的一声"咚"。 (已定位成因,2026-09-21)
  20. //
  21. // ── 现象(用户描述)─────────────────────────────────────────────────────
  22. // Win7:手指点主窗口某个控件 → 该控件的回调里 ShowModal() 弹出模态框 →
  23. // **咚** 一声。手指若点在弹框自己的显示区域内,则没有这声。
  24. // 用户明确边界:
  25. // · 弹框**已经出现之后**再点弹框外的空白区发出咚声 —— **正常,不要动**;
  26. // · 要消除的只是"点击动作发生在弹框出现之前"却发出咚声的那一声。
  27. //
  28. // ── 成因(真机日志实证,非本项目 bug)──────────────────────────────────
  29. // 真模态由 DialogBoxIndirectParam 创建,它会把 owner 置为 IsWindowEnabled(FALSE)
  30. // 以阻挡输入。而 Win32 的默认处理规定:**用户点击一个被禁用的窗口时发出
  31. // MessageBeep(0)** —— 这是 Windows 有意给的"操作无效"提示音。
  32. //
  33. // 本机触摸走的是指针栈(WM_TOUCH 实测 0 条),而**触摸的手势归属是按键序列
  34. // 一次性决定的**:整个"按下 → 抬起"序列的指针消息全部投给**按下那一刻命中的
  35. // 窗口**。真机日志里可以直接看到这条规则被违反后的结果(行 1611 - 1690):
  36. //
  37. // 1611 POINTER-MSG UP hwnd=00D4055C ... ← owner(主窗口) 收到抬起
  38. // 1614 POINTER-MSG UP click-candidate diandan_fooditem
  39. // 1621 MODAL-DLG enter old=... owner=00D4055C ← 回调里 ShowModal,owner 被禁用
  40. // 1638 INPUT-TRACE target=00D4055C msg=0x0006 WM_ACTIVATE ... enable=0
  41. // ↑ 这一刻 owner 才变成禁用
  42. // 1643 INPUT-TRACE target=00D4055C msg=0x024A WM_POINTER* ... enable=0
  43. // 1666 POINTER-MSG UP click-candidate diandan_nature_save ← 弹框内点击,关闭弹框
  44. // 1684 INPUT-TRACE target=00D4055C msg=0x0201 WM_LBUTTONDOWN ... extra=0xFF51578B
  45. // ↑ 系统**补发**的、属于本次触摸的
  46. // 提升鼠标按下,落在已启用的 owner 上
  47. //
  48. // 关键在最后一行:手势结束时(owner 已重新启用),系统才把这条**早在手势开始时就
  49. // 排定好归属**的提升鼠标消息投递出来。可 owner 在手势中途反复被 EnableWindow(FALSE)
  50. // / TRUE 切换 —— 只要这条消息落在"owner 正处于禁用"的那一瞬间,系统就走
  51. // "点击了禁用窗口"的默认处理 ⇒ 咚。
  52. // Win10 无此现象,是因为它在指针层就把落点判掉,根本不会把消息投给被禁用的窗口。
  53. //
  54. // ── 为什么不用"屏蔽消息"的解法(曾误判,已弃)──────────────────────────
  55. // 我一度写过一个基于 WH_GETMESSAGE 的钩子,无条件丢弃投给 owner 的点击类消息。
  56. // 它错在:**弹框已出现后点空白区的那一声,用户明确说要保留**。而无条件丢弃会把它
  57. // 一起消掉 —— 连正常的"无效操作"反馈都没了,方向反了。整段删除。
  58. //
  59. // ── 采用的解法 ────────────────────────────────────────────────────────
  60. // 不动消息流、不挂钩子,只在**库内**把"同一次手势的第二遍处理"精确拦掉。
  61. //
  62. // 核心认识:一次触摸手势会被处理**两遍** ——
  63. // 第 1 遍:DuiLib 自己的触摸状态机(WM_POINTER* / WM_TOUCH)合成点击,
  64. // 同步派发到控件、触发业务回调;
  65. // 第 2 遍:系统把这次触摸"提升"成的鼠标消息(带 MOUSEEVENTF_FROMTOUCH,
  66. // 即 GetMessageExtraInfo() 高位的 0xFF5157xx),排在消息队列里稍后投出。
  67. // 第 2 遍是多余的,必须丢——它正是"咚"和"弹框被自动点掉"的元凶。
  68. //
  69. // 判定与执行(见 CPaintManagerUI::MessageHandler 里 WM_LBUTTONDOWN/UP 分支):
  70. // · m_bSuppressPromotedMouseForModal —— 由 ShowModal() 在进入模态循环**之前**置位
  71. // · IsPromotedTouchMouseMessage() —— 只丢触摸提升的,真实鼠标点击天然豁免
  72. // · IsModalInputTarget(m_hWndPaint) —— 目标是"弹框本身"或"弹框的 owner"
  73. // 生命周期按**手势配对**(DOWN 记账 / UP 销账,归零即解除),另有 10 秒僵尸兜底。
  74. // 因此"弹框已出现后点空白区"那一声(要求保留的)照旧会响。
  75. //
  76. // 撤防点(确定性,不依赖超时):
  77. // a) CWindowWnd::Close() 的 m_bDoModal 分支,在 EndDialog 之前;
  78. // b) __DialogWndProc 的 WM_NCDESTROY 收尾,任何销毁路径都会走到;
  79. // c) ShowModal() 返回后的兜底。
  80. //
  81. // ⚠️ 本文件此处仅保留文字说明,不再有任何钩子/屏蔽类实现。
  82. //
  83. // ── 第十一轮补充:上面这套"三条件拦截"**单独并不足以**修掉那四个 bug ──────
  84. // 真机验证:第九、十两轮的改动全部无效。第十一轮找到真正的两个根因
  85. // (详见 UIManager.cpp 的 AttachPaintWindow 与 TOUCH-STATE timeout-reset):
  86. //
  87. // (A) **AttachPaintWindow 漏清输入状态**。真模态把**同一个** manager 从旧窗口
  88. // 搬到弹框,控件树 m_pRoot 是同一棵。而 AttachDialog(非模态路径)会清
  89. // m_pEventKey/Hover/Click/RClick,AttachPaintWindow 当初漏了同一组
  90. // ⇒ m_pEventHover 仍指着主窗口那个商品控件,弹框期间没人去清它
  91. // ⇒ **悬浮态一直亮到弹框关闭**(用户报的第 4 个 bug)。
  92. // 同时 m_pEventClick / m_bMouseCapture 等旧手势中间态被带进弹框
  93. // ⇒ 弹框第一条输入就撞上"进行中的手势",为本文件上面那套拦截**制造了输入**。
  94. // 已在 AttachPaintWindow 开头补齐清理。
  95. //
  96. // (B) **TOUCH-STATE timeout-reset 只看时间、不看模态是否存活**。
  97. // 模态期间这些手势状态本来就是"故意冻结"的(在等嵌套模态循环返回),
  98. // 超时却会在 3 秒后把它们清掉 ⇒ 抑制位失效 ⇒ 第 2 遍消息漏网。
  99. // **这正是用户报的"超过 3 秒就透点"**。
  100. // ⚠️ 最终处理(用户明确要求"不要时间戳,完全根据状态来判断"):
  101. // 该 3000ms 超时已**整段删除**,连同指针通道那条同类超时一并删除。
  102. // 抑制位现在只由"配对的那条提升 UP 到达"解除(纯状态)。
  103. // 论证见 UIManager.cpp 里被删处的长注释(逐条证明这些状态不会泄漏)。
  104. //
  105. // 结论:本文件的三条件拦截是**必要条件**(拦第 2 遍消息),
  106. // 但必须配合上面 (A)(B) 两条才有可能真正成立 ——
  107. // 之前只做拦截、不做 (A)(B),等于"防线摆好了,但门在别处开着"。
  108. // -------------------------------------------------------------------------
  109. typedef BOOL (WINAPI* PFN_REGISTER_TOUCH_WINDOW)(HWND, ULONG);
  110. // 与 SDK tagTOUCHINPUT 布局一致的私有副本(同样受 _WIN32_WINNT 保护,不能直接用)
  111. struct TRACE_TOUCHINPUT
  112. {
  113. LONG x;
  114. LONG y;
  115. HANDLE hSource;
  116. DWORD dwID;
  117. DWORD dwFlags;
  118. DWORD dwMask;
  119. DWORD dwTime;
  120. ULONG_PTR dwExtraInfo;
  121. DWORD cxContact;
  122. DWORD cyContact;
  123. };
  124. typedef BOOL (WINAPI* PFN_GET_TOUCH_INPUT_INFO)(HANDLE, UINT, TRACE_TOUCHINPUT*, int);
  125. void RegisterTouchWindowIfAvailable(HWND hWnd)
  126. {
  127. if (hWnd == NULL) return;
  128. HMODULE hUser32 = ::GetModuleHandle(_T("user32.dll"));
  129. if (hUser32 == NULL) return;
  130. PFN_REGISTER_TOUCH_WINDOW pfnRegister =
  131. reinterpret_cast<PFN_REGISTER_TOUCH_WINDOW>(::GetProcAddress(hUser32, "RegisterTouchWindow"));
  132. if (pfnRegister != NULL)
  133. pfnRegister(hWnd, 0);
  134. }
  135. // 【已移除】RefreshTouchRegistration(Unregister + Register 强制重算触摸归属)。
  136. //
  137. // 它曾是"假模态 + WM_TOUCH 贪婪归属权"时代的补丁:试图用注销/重注册把触摸归属权
  138. // 从 owner 重新结算到弹框头上。实测无效(弹框依然零 WM_TOUCH),且真模态方案下
  139. // 完全没必要 —— 对话框窗口是新 HWND,创建时 RegisterTouchWindowIfAvailable 注册一次即可,
  140. // 系统自己就把触摸投给它。留着只会带来 C4505(未引用函数)警告,故整段删除。
  141. // 诊断:WM_TOUCH 究竟落在哪个窗口上(弹框自己?还是被禁用/被挡住的 owner?)。
  142. // ⚠️ 只 peek,绝不调用 CloseTouchInputHandle:句柄必须交给 CPaintManagerUI::OnTouchInput 关闭。
  143. void TraceTouchArrival(HWND hWnd, WPARAM wParam, LPARAM lParam)
  144. {
  145. HMODULE hUser32 = ::GetModuleHandle(_T("user32.dll"));
  146. if (hUser32 == NULL) return;
  147. PFN_GET_TOUCH_INPUT_INFO pfnGet =
  148. reinterpret_cast<PFN_GET_TOUCH_INPUT_INFO>(::GetProcAddress(hUser32, "GetTouchInputInfo"));
  149. int x = 0, y = 0;
  150. DWORD flags = 0;
  151. UINT cInputs = LOWORD(wParam);
  152. TRACE_TOUCHINPUT ti;
  153. ::ZeroMemory(&ti, sizeof(ti));
  154. if (pfnGet != NULL && cInputs > 0 && pfnGet((HANDLE)lParam, 1, &ti, (int)sizeof(ti)))
  155. {
  156. x = (int)(ti.x / 100);
  157. y = (int)(ti.y / 100);
  158. flags = ti.dwFlags;
  159. }
  160. 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"),
  161. (void*)hWnd, (void*)::GetWindow(hWnd, GW_OWNER), (void*)::GetParent(hWnd),
  162. (int)::IsWindowEnabled(hWnd), (int)::IsWindowVisible(hWnd),
  163. x, y, (unsigned)flags, cInputs,
  164. (void*)DuiLib::CPaintManagerUI::GetModalWindow(), (void*)::GetForegroundWindow());
  165. }
  166. // ---------------------------------------------------------------------------
  167. // 【临时全量诊断】记录系统到底把哪些"输入类消息"发给了哪个窗口。
  168. //
  169. // 背景:弹框打开后,日志里既没有 WM_TOUCH(任何窗口都没有),也没有 WM_LBUTTONDOWN/UP,
  170. // 却能看到弹框收到 WM_MOUSEMOVE(hover)。这说明触摸可能被系统降级成了鼠标模拟,
  171. // 或者走了 WM_GESTURE / WM_POINTER* 另外两条通道。这三个通道目前都没被记录到,
  172. // 因此需要一段覆盖面更广的诊断来确定"消息到底有没有发出来、发给了谁"。
  173. //
  174. // 覆盖:WM_TOUCH(0x0240) / WM_GESTURE(0x0119) / WM_POINTER*(0x0245~0x025F 及 0x0246/0x0247 变体)
  175. // WM_MOUSEMOVE(0x0200) / WM_LBUTTONDOWN(0x0201) / WM_LBUTTONUP(0x0202)
  176. // WM_NCMOUSEMOVE(0x00A0) / WM_MOUSEACTIVATE(0x0021) / WM_SETCURSOR(0x0020)
  177. //
  178. // ⚠️ 纯只读诊断:不改消息流向、不返回值、不消费句柄。只为定位问题,验证完请删除。
  179. // ---------------------------------------------------------------------------
  180. // MSG-ID 常量(不引用任何 WM_* 宏,避免 _WIN32_WINNT 版本段差异导致编译不过)
  181. const UINT DUI_MSG_WM_TOUCH = 0x0240;
  182. const UINT DUI_MSG_WM_GESTURE = 0x0119;
  183. const UINT DUI_MSG_WM_GESTURENOTIFY = 0x011A;
  184. const UINT DUI_MSG_WM_MOUSEMOVE = 0x0200;
  185. const UINT DUI_MSG_WM_LBUTTONDOWN = 0x0201;
  186. const UINT DUI_MSG_WM_LBUTTONUP = 0x0202;
  187. const UINT DUI_MSG_WM_LBUTTONDBLCLK = 0x0203;
  188. const UINT DUI_MSG_WM_RBUTTONDOWN = 0x0204;
  189. const UINT DUI_MSG_WM_RBUTTONUP = 0x0205;
  190. const UINT DUI_MSG_WM_NCMOUSEMOVE = 0x00A0;
  191. const UINT DUI_MSG_WM_NCLBUTTONDOWN = 0x00A1;
  192. const UINT DUI_MSG_WM_MOUSEACTIVATE = 0x0021;
  193. const UINT DUI_MSG_WM_SETCURSOR = 0x0020;
  194. const UINT DUI_MSG_WM_NCHITTEST = 0x0084;
  195. const UINT DUI_MSG_WM_ACTIVATE = 0x0006;
  196. const UINT DUI_MSG_WM_ACTIVATEAPP = 0x001C;
  197. const UINT DUI_MSG_WM_SETFOCUS = 0x0007;
  198. const UINT DUI_MSG_WM_KILLFOCUS = 0x0008;
  199. void TraceInputMessage(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
  200. {
  201. LPCTSTR pszName = NULL;
  202. switch (uMsg)
  203. {
  204. // ⚠️ case 必须是"编译期常量表达式"。这些常量在匿名 namespace 里是 const UINT
  205. // 且已初始化,属于 ICE,可以安全用作 case 标签。
  206. case DUI_MSG_WM_TOUCH: pszName = _T("WM_TOUCH"); break;
  207. case DUI_MSG_WM_GESTURE: pszName = _T("WM_GESTURE"); break;
  208. case DUI_MSG_WM_GESTURENOTIFY: pszName = _T("WM_GESTURENOTIFY"); break;
  209. case DUI_MSG_WM_MOUSEMOVE: pszName = _T("WM_MOUSEMOVE"); break;
  210. case DUI_MSG_WM_LBUTTONDOWN: pszName = _T("WM_LBUTTONDOWN"); break;
  211. case DUI_MSG_WM_LBUTTONUP: pszName = _T("WM_LBUTTONUP"); break;
  212. case DUI_MSG_WM_LBUTTONDBLCLK: pszName = _T("WM_LBUTTONDBLCLK"); break;
  213. case DUI_MSG_WM_RBUTTONDOWN: pszName = _T("WM_RBUTTONDOWN"); break;
  214. case DUI_MSG_WM_RBUTTONUP: pszName = _T("WM_RBUTTONUP"); break;
  215. case DUI_MSG_WM_NCMOUSEMOVE: pszName = _T("WM_NCMOUSEMOVE"); break;
  216. case DUI_MSG_WM_NCLBUTTONDOWN: pszName = _T("WM_NCLBUTTONDOWN"); break;
  217. case DUI_MSG_WM_MOUSEACTIVATE: pszName = _T("WM_MOUSEACTIVATE"); break;
  218. case DUI_MSG_WM_SETCURSOR: pszName = _T("WM_SETCURSOR"); break;
  219. case DUI_MSG_WM_NCHITTEST: pszName = _T("WM_NCHITTEST"); break;
  220. case DUI_MSG_WM_ACTIVATE: pszName = _T("WM_ACTIVATE"); break;
  221. case DUI_MSG_WM_ACTIVATEAPP: pszName = _T("WM_ACTIVATEAPP"); break;
  222. case DUI_MSG_WM_SETFOCUS: pszName = _T("WM_SETFOCUS"); break;
  223. case DUI_MSG_WM_KILLFOCUS: pszName = _T("WM_KILLFOCUS"); break;
  224. default: break;
  225. }
  226. // 覆盖 WM_POINTER* 一段(0x0245~0x0260,不同 SDK 版本消息号有差异,统一按范围判断)
  227. const bool bPointerMsg = (uMsg >= 0x0245 && uMsg <= 0x0260);
  228. if (pszName == NULL && !bPointerMsg) return;
  229. // WM_MOUSEMOVE / WM_NCMOUSEMOVE / WM_NCHITTEST / WM_SETCURSOR 极其频繁,
  230. // 全量记录会瞬间刷爆日志,这里每类限流 300ms。
  231. const bool bNoisy = (uMsg == DUI_MSG_WM_MOUSEMOVE || uMsg == DUI_MSG_WM_NCMOUSEMOVE
  232. || uMsg == DUI_MSG_WM_NCHITTEST || uMsg == DUI_MSG_WM_SETCURSOR);
  233. if (bNoisy)
  234. {
  235. static DWORD s_dwLastNoisyTick = 0;
  236. static UINT s_uLastNoisyMsg = 0;
  237. const DWORD dwNow = ::GetTickCount();
  238. if (s_uLastNoisyMsg == uMsg && (LONG)(dwNow - s_dwLastNoisyTick) < 300) return;
  239. s_dwLastNoisyTick = dwNow;
  240. s_uLastNoisyMsg = uMsg;
  241. }
  242. DWORD dwExtra = (DWORD)(DWORD_PTR)::GetMessageExtraInfo();
  243. 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"),
  244. (void*)hWnd, uMsg, pszName != NULL ? pszName : _T("WM_POINTER*"),
  245. (unsigned)wParam, (unsigned)lParam, (unsigned)dwExtra,
  246. (int)((dwExtra & 0xFFFFFF80) == 0xFF515700),
  247. (void*)DuiLib::CPaintManagerUI::GetModalWindow(),
  248. (void*)::GetForegroundWindow(),
  249. (int)::IsWindowEnabled(hWnd));
  250. }
  251. // 【已移除】LogProcessWindowList / EnumProc_LogWnd(本进程顶层窗口清单,MODAL-WNDLIST)。
  252. // 它们是排查"WM_TOUCH 究竟投给了哪个陌生 HWND"的临时诊断,只被已删除的假模态消息循环调用。
  253. // 真模态方案下弹框自己就是触摸目标,这份清单不再需要,删除以免 C4505。
  254. // 【已搬移】FindManagerByPaintWindow 原本定义在这里(匿名 namespace 内)。
  255. //
  256. // 为什么必须搬走:UIBase.h 里为了让 UIManager.cpp 也能调用它,已经加了一份
  257. // 声明 `DuiLib::CPaintManagerUI* FindManagerByPaintWindow(HWND);`。
  258. // 于是同一个翻译单元里同时存在
  259. // · 匿名 namespace 内的 DuiLib::`anonymous-namespace'::FindManagerByPaintWindow
  260. // · 头文件声明的 DuiLib::FindManagerByPaintWindow
  261. // 两个候选,调用点(UIBase.cpp 里 793/1076/1124/1207/1367/1395 等)无法抉择,
  262. // MSVC 直接报 C2668「对重载函数的调用不明确」。
  263. // ⇒ 现在把它挪到匿名 namespace **之外**,作为 DuiLib 的正式成员,
  264. // 与头文件那份声明严格一一对应(定义见本文件下方 namespace 收尾之后)。
  265. // ---------------------------------------------------------------------------
  266. // [WM_TOUCH 归属权] "补调原窗口过程"的中转函数。
  267. //
  268. // 为什么需要它:
  269. // WM_TOUCH 不是被系统直接投递的。系统先发一条"特殊标记的 WM_MOUSEMOVE"
  270. // (开了 raw input 时是 WM_INPUT),只有这条消息走到 DefWindowProc 里,
  271. // DefWindowProc 才会把驻留的 WM_TOUCH "翻译"并派发出来
  272. // (来源:The Witness 作者的逆向结论;MSDN《Architectural Overview》的
  273. // greedy 段落也印证了这套投递模型)。
  274. // 本库的 CPaintManagerUI::MessageHandler 为了不让"触摸提升出来的鼠标移动"
  275. // 破坏拖拽/悬浮状态机,会吞掉它(return true),从而跳过了 DefWindowProc
  276. // ⇒ 窗口(尤其 #32770 真模态弹框,它依赖 DefDlgProc→DefWindowProc 这条链)
  277. // 后续永远收不到 WM_TOUCH。
  278. //
  279. // 这个中转函数由 CPaintManagerUI::RelayDefWindowProc 回调,
  280. // 作用就是"把消息喂给原窗口过程一次",补上被跳过的那次副作用。
  281. // 用静态变量保存"当前转发目标":因为回调签名不带 this,
  282. // 而每个窗口过程在同一时刻只有一个绑定关系(主窗口 or 某个弹框)。
  283. // 真模态切换 HWND 时通过 SetDefProcRelay 重新绑定,不会串。
  284. //
  285. // ⚠️ 位置要求:必须定义在 __DialogWndProc 之前(它比 __WndProc 先出现)。
  286. // ---------------------------------------------------------------------------
  287. WNDPROC g_pfnRelayTarget = NULL;
  288. HWND g_hWndRelayBound = NULL;
  289. LRESULT CALLBACK RelayWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
  290. {
  291. // 只有"当前正在转发的窗口"才有效:避免悬垂到已销毁窗口的过程上。
  292. if (g_pfnRelayTarget == NULL || g_hWndRelayBound != hWnd)
  293. return 0;
  294. return ::CallWindowProc(g_pfnRelayTarget, hWnd, uMsg, wParam, lParam);
  295. }
  296. // 把"原窗口过程"登记为转发目标(绑定到指定 HWND)。
  297. void BindRelayTarget(WNDPROC pfnOldProc, HWND hWnd)
  298. {
  299. g_pfnRelayTarget = pfnOldProc;
  300. g_hWndRelayBound = hWnd;
  301. }
  302. // DetachPaintWindow 之后、AttachPaintWindow 之前,用这个取出"待挂载的 manager"。
  303. // 因为此时 manager 已从 m_aPreMessages 摘除,FindManagerByPaintWindow 找不到它了,
  304. // 所以用一个文件级静态变量中转(只在 ShowModal 的单线程窗口过程里使用,无需同步)。
  305. static DuiLib::CPaintManagerUI* g_pDetachedManager = NULL;
  306. void SetDetachedManager(DuiLib::CPaintManagerUI* pManager) { g_pDetachedManager = pManager; }
  307. DuiLib::CPaintManagerUI* GetDetachedManager() { return g_pDetachedManager; }
  308. // 【已移除】IsModalBlockedInputMessage —— 假模态时代的"在消息循环里丢弃发给 owner 的输入消息"。
  309. // 现在 ShowModal 走真模态(DialogBoxIndirectParam),系统对话框管理器自带 owner 禁用,
  310. // 这条兜底不但多余,还会误伤:真模态期间 owner 本就需要收到 WM_ACTIVATE/WM_KILLFOCUS 之类的
  311. // 通知消息,无差别丢弃会让 owner 的状态机错乱。故整段删除,不再保留。
  312. }
  313. // ---------------------------------------------------------------------------
  314. // 真模态(DialogBox)辅助:按 paint window 找到对应的 CPaintManagerUI。
  315. //
  316. // ⚠️ 必须定义在**匿名 namespace 之外**(即 DuiLib 命名空间直接成员),
  317. // 因为 UIBase.h 里有一份同名声明供 UIManager.cpp 使用。若同时存在匿名
  318. // namespace 内的一份,调用点会因两个候选而报 C2668「调用不明确」。
  319. //
  320. // 用途:
  321. // · ShowModal 里要把 manager 从旧 HWND 迁到对话框新 HWND 时需要先找到它;
  322. // · CPaintManagerUI::IsModalBackdropWindow 判定"本句柄是否就是弹框自己的
  323. // manager 窗口"时也依赖它(透传闸门的一条关键判据)。
  324. // 实现:遍历 CPaintManagerUI::GetPaintManagers(),比对 GetPaintWindow()。
  325. // ---------------------------------------------------------------------------
  326. DuiLib::CPaintManagerUI* FindManagerByPaintWindow(HWND hWnd)
  327. {
  328. if (hWnd == NULL) return NULL;
  329. CStdPtrArray* pManagers = DuiLib::CPaintManagerUI::GetPaintManagers();
  330. if (pManagers == NULL) return NULL;
  331. for (int i = 0; i < pManagers->GetSize(); ++i)
  332. {
  333. DuiLib::CPaintManagerUI* pManager = static_cast<DuiLib::CPaintManagerUI*>(pManagers->GetAt(i));
  334. if (pManager != NULL && pManager->GetPaintWindow() == hWnd) return pManager;
  335. }
  336. return NULL;
  337. }
  338. /////////////////////////////////////////////////////////////////////////////////////
  339. //
  340. //
  341. void UILIB_API DUI__Trace(LPCTSTR pstrFormat, ...)
  342. {
  343. #ifdef _DEBUG
  344. TCHAR szBuffer[2048] = {0};
  345. va_list args;
  346. va_start(args, pstrFormat);
  347. _vsntprintf(szBuffer, 2048, pstrFormat, args);
  348. va_end(args);
  349. CDuiString strMsg = szBuffer;
  350. strMsg += _T("\n");
  351. OutputDebugString(strMsg.GetData());
  352. #endif
  353. }
  354. LPCTSTR DUI__TraceMsg(UINT uMsg)
  355. {
  356. #define MSGDEF(x) if(uMsg==x) return _T(#x)
  357. MSGDEF(WM_SETCURSOR);
  358. MSGDEF(WM_NCHITTEST);
  359. MSGDEF(WM_NCPAINT);
  360. MSGDEF(WM_PAINT);
  361. MSGDEF(WM_ERASEBKGND);
  362. MSGDEF(WM_NCMOUSEMOVE);
  363. MSGDEF(WM_MOUSEMOVE);
  364. MSGDEF(WM_MOUSELEAVE);
  365. MSGDEF(WM_MOUSEHOVER);
  366. MSGDEF(WM_NOTIFY);
  367. MSGDEF(WM_COMMAND);
  368. MSGDEF(WM_MEASUREITEM);
  369. MSGDEF(WM_DRAWITEM);
  370. MSGDEF(WM_LBUTTONDOWN);
  371. MSGDEF(WM_LBUTTONUP);
  372. MSGDEF(WM_LBUTTONDBLCLK);
  373. MSGDEF(WM_RBUTTONDOWN);
  374. MSGDEF(WM_RBUTTONUP);
  375. MSGDEF(WM_RBUTTONDBLCLK);
  376. MSGDEF(WM_SETFOCUS);
  377. MSGDEF(WM_KILLFOCUS);
  378. MSGDEF(WM_MOVE);
  379. MSGDEF(WM_SIZE);
  380. MSGDEF(WM_SIZING);
  381. MSGDEF(WM_MOVING);
  382. MSGDEF(WM_GETMINMAXINFO);
  383. MSGDEF(WM_CAPTURECHANGED);
  384. MSGDEF(WM_WINDOWPOSCHANGED);
  385. MSGDEF(WM_WINDOWPOSCHANGING);
  386. MSGDEF(WM_NCCALCSIZE);
  387. MSGDEF(WM_NCCREATE);
  388. MSGDEF(WM_NCDESTROY);
  389. MSGDEF(WM_TIMER);
  390. MSGDEF(WM_KEYDOWN);
  391. MSGDEF(WM_KEYUP);
  392. MSGDEF(WM_CHAR);
  393. MSGDEF(WM_SYSKEYDOWN);
  394. MSGDEF(WM_SYSKEYUP);
  395. MSGDEF(WM_SYSCOMMAND);
  396. MSGDEF(WM_SYSCHAR);
  397. MSGDEF(WM_VSCROLL);
  398. MSGDEF(WM_HSCROLL);
  399. MSGDEF(WM_CHAR);
  400. MSGDEF(WM_SHOWWINDOW);
  401. MSGDEF(WM_PARENTNOTIFY);
  402. MSGDEF(WM_CREATE);
  403. MSGDEF(WM_NCACTIVATE);
  404. MSGDEF(WM_ACTIVATE);
  405. MSGDEF(WM_ACTIVATEAPP);
  406. MSGDEF(WM_CLOSE);
  407. MSGDEF(WM_DESTROY);
  408. MSGDEF(WM_GETICON);
  409. MSGDEF(WM_GETTEXT);
  410. MSGDEF(WM_GETTEXTLENGTH);
  411. static TCHAR szMsg[10];
  412. ::wsprintf(szMsg, _T("0x%04X"), uMsg);
  413. return szMsg;
  414. }
  415. /////////////////////////////////////////////////////////////////////////////////////
  416. //
  417. //
  418. //////////////////////////////////////////////////////////////////////////
  419. //
  420. DUI_BASE_BEGIN_MESSAGE_MAP(CNotifyPump)
  421. DUI_END_MESSAGE_MAP()
  422. static const DUI_MSGMAP_ENTRY* DuiFindMessageEntry(const DUI_MSGMAP_ENTRY* lpEntry,TNotifyUI& msg )
  423. {
  424. CDuiString sMsgType = msg.sType;
  425. CDuiString sCtrlName = msg.pSender->GetName();
  426. const DUI_MSGMAP_ENTRY* pMsgTypeEntry = NULL;
  427. while (lpEntry->nSig != DuiSig_end)
  428. {
  429. if(lpEntry->sMsgType==sMsgType)
  430. {
  431. if(!lpEntry->sCtrlName.IsEmpty())
  432. {
  433. if(lpEntry->sCtrlName==sCtrlName)
  434. {
  435. return lpEntry;
  436. }
  437. }
  438. else
  439. {
  440. pMsgTypeEntry = lpEntry;
  441. }
  442. }
  443. lpEntry++;
  444. }
  445. return pMsgTypeEntry;
  446. }
  447. bool CNotifyPump::AddVirtualWnd(CDuiString strName,CNotifyPump* pObject)
  448. {
  449. if( m_VirtualWndMap.Find(strName) == NULL )
  450. {
  451. m_VirtualWndMap.Insert(strName.GetData(),(LPVOID)pObject);
  452. return true;
  453. }
  454. return false;
  455. }
  456. bool CNotifyPump::RemoveVirtualWnd(CDuiString strName)
  457. {
  458. if( m_VirtualWndMap.Find(strName) != NULL )
  459. {
  460. m_VirtualWndMap.Remove(strName);
  461. return true;
  462. }
  463. return false;
  464. }
  465. bool CNotifyPump::LoopDispatch(TNotifyUI& msg)
  466. {
  467. const DUI_MSGMAP_ENTRY* lpEntry = NULL;
  468. const DUI_MSGMAP* pMessageMap = NULL;
  469. #ifndef UILIB_STATIC
  470. for(pMessageMap = GetMessageMap(); pMessageMap!=NULL; pMessageMap = (*pMessageMap->pfnGetBaseMap)())
  471. #else
  472. for(pMessageMap = GetMessageMap(); pMessageMap!=NULL; pMessageMap = pMessageMap->pBaseMap)
  473. #endif
  474. {
  475. #ifndef UILIB_STATIC
  476. ASSERT(pMessageMap != (*pMessageMap->pfnGetBaseMap)());
  477. #else
  478. ASSERT(pMessageMap != pMessageMap->pBaseMap);
  479. #endif
  480. if ((lpEntry = DuiFindMessageEntry(pMessageMap->lpEntries,msg)) != NULL)
  481. {
  482. goto LDispatch;
  483. }
  484. }
  485. return false;
  486. LDispatch:
  487. union DuiMessageMapFunctions mmf;
  488. mmf.pfn = lpEntry->pfn;
  489. bool bRet = false;
  490. int nSig;
  491. nSig = lpEntry->nSig;
  492. switch (nSig)
  493. {
  494. default:
  495. ASSERT(FALSE);
  496. break;
  497. case DuiSig_lwl:
  498. (this->*mmf.pfn_Notify_lwl)(msg.wParam,msg.lParam);
  499. bRet = true;
  500. break;
  501. case DuiSig_vn:
  502. (this->*mmf.pfn_Notify_vn)(msg);
  503. bRet = true;
  504. break;
  505. }
  506. return bRet;
  507. }
  508. void CNotifyPump::NotifyPump(TNotifyUI& msg)
  509. {
  510. ///遍历虚拟窗口
  511. if( !msg.sVirtualWnd.IsEmpty() ){
  512. for( int i = 0; i< m_VirtualWndMap.GetSize(); i++ ) {
  513. if( LPCTSTR key = m_VirtualWndMap.GetAt(i) ) {
  514. if( _tcsicmp(key, msg.sVirtualWnd.GetData()) == 0 ){
  515. CNotifyPump* pObject = static_cast<CNotifyPump*>(m_VirtualWndMap.Find(key, false));
  516. if( pObject && pObject->LoopDispatch(msg) )
  517. return;
  518. }
  519. }
  520. }
  521. }
  522. ///
  523. //遍历主窗口
  524. LoopDispatch( msg );
  525. }
  526. //////////////////////////////////////////////////////////////////////////
  527. ///
  528. 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()
  529. {
  530. }
  531. HWND CWindowWnd::GetHWND() const
  532. {
  533. return m_hWnd;
  534. }
  535. UINT CWindowWnd::GetClassStyle() const
  536. {
  537. return 0;
  538. }
  539. LPCTSTR CWindowWnd::GetSuperClassName() const
  540. {
  541. return NULL;
  542. }
  543. CWindowWnd::operator HWND() const
  544. {
  545. return m_hWnd;
  546. }
  547. void CWindowWnd::EnableUnicode()
  548. {
  549. m_bUnicode = true;
  550. }
  551. HWND CWindowWnd::CreateDuiWindow( HWND hwndParent, LPCTSTR pstrWindowName,DWORD dwStyle /*=0*/, DWORD dwExStyle /*=0*/ )
  552. {
  553. return Create(hwndParent,pstrWindowName,dwStyle,dwExStyle,0,0,0,0,NULL);
  554. }
  555. HWND CWindowWnd::Create(HWND hwndParent, LPCTSTR pstrName, DWORD dwStyle, DWORD dwExStyle, const RECT rc, HMENU hMenu)
  556. {
  557. return Create(hwndParent, pstrName, dwStyle, dwExStyle, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, hMenu);
  558. }
  559. HWND CWindowWnd::Create(HWND hwndParent, LPCTSTR pstrName, DWORD dwStyle, DWORD dwExStyle, int x, int y, int cx, int cy, HMENU hMenu)
  560. {
  561. if( GetSuperClassName() != NULL && !RegisterSuperclass() ) return NULL;
  562. if( GetSuperClassName() == NULL && !RegisterWindowClass() ) return NULL;
  563. if(m_bUnicode) {
  564. #ifndef UNICODE
  565. LPWSTR lpClassName = a2w((char*)GetWindowClassName());
  566. LPWSTR lpName = a2w((char*)pstrName);
  567. #else
  568. LPWSTR lpClassName = (LPWSTR)GetWindowClassName();
  569. LPWSTR lpName = (LPWSTR)pstrName;
  570. #endif
  571. m_hWnd = ::CreateWindowExW(dwExStyle, lpClassName, lpName, dwStyle, x, y, cx, cy, hwndParent, hMenu, CPaintManagerUI::GetInstance(), this);
  572. #ifndef UNICODE
  573. delete []lpClassName;
  574. delete []lpName;
  575. #endif
  576. }
  577. else {
  578. m_hWnd = ::CreateWindowEx(dwExStyle, GetWindowClassName(), pstrName, dwStyle, x, y, cx, cy, hwndParent, hMenu, CPaintManagerUI::GetInstance(), this);
  579. }
  580. ASSERT(m_hWnd!=NULL);
  581. RegisterTouchWindowIfAvailable(m_hWnd);
  582. return m_hWnd;
  583. }
  584. HWND CWindowWnd::Subclass(HWND hWnd)
  585. {
  586. ASSERT(::IsWindow(hWnd));
  587. ASSERT(m_hWnd==NULL);
  588. m_OldWndProc = SubclassWindow(hWnd, __WndProc);
  589. if( m_OldWndProc == NULL ) return NULL;
  590. m_bSubclassed = true;
  591. m_hWnd = hWnd;
  592. ::SetWindowLongPtr(hWnd, GWLP_USERDATA, reinterpret_cast<LPARAM>(this));
  593. RegisterTouchWindowIfAvailable(m_hWnd);
  594. return m_hWnd;
  595. }
  596. void CWindowWnd::Unsubclass()
  597. {
  598. ASSERT(::IsWindow(m_hWnd));
  599. if( !::IsWindow(m_hWnd) ) return;
  600. if( !m_bSubclassed ) return;
  601. SubclassWindow(m_hWnd, m_OldWndProc);
  602. m_OldWndProc = ::DefWindowProc;
  603. m_bSubclassed = false;
  604. }
  605. void CWindowWnd::ShowWindow(bool bShow /*= true*/, bool bTakeFocus /*= false*/)
  606. {
  607. ASSERT(::IsWindow(m_hWnd));
  608. if( !::IsWindow(m_hWnd) ) return;
  609. ::ShowWindow(m_hWnd, bShow ? (bTakeFocus ? SW_SHOWNORMAL : SW_SHOWNOACTIVATE) : SW_HIDE);
  610. }
  611. UINT CWindowWnd::ShowModal()
  612. {
  613. // =========================================================================
  614. // 【真模态】改用 Windows 的对话框机制(DialogBoxIndirectParam)驱动模态。
  615. //
  616. // 参考:网易 nim_duilib 的 NativeWindow_Windows::DoModal()
  617. // (duilib/Core/NativeWindow_Windows.cpp)
  618. //
  619. // ── 为什么必须换掉旧写法 ──────────────────────────────────────────────
  620. // 旧实现是"假模态":自己 ShowWindow + EnableWindow(owner,FALSE) + TOPMOST +
  621. // 抢前台 + 自写 while(GetMessage) 嵌套循环。在 Win10 触摸屏上必然失败,根因是
  622. // 触摸消息的投递目标规则(MSDN, Getting Started with Windows Touch Messages):
  623. //
  624. // "WM_TOUCH messages are currently 'greedy.' After the first touch message is
  625. // received on a window, all touch messages are sent to that window until
  626. // another window receives focus."
  627. //
  628. // 主窗口早就 RegisterTouchWindow 过、也早就收到过 WM_TOUCH,所以触摸归属权长期
  629. // 在它手里;弹框是自己 Create 出来的普通窗口,**不是**系统的触摸目标。
  630. // 我们为此打了一堆补丁(手动焦点切换、刷新注册、消息循环转发、__WndProc 转发兜底),
  631. // 实测全部无效:日志里弹框始终零 WM_TOUCH、零 WM_LBUTTONDOWN/UP。
  632. //
  633. // ── 对话框为什么能解决 ────────────────────────────────────────────────
  634. // DialogBox 是内核实现的模态机制,它:
  635. // · 由系统跑消息循环(不需要我们自写嵌套循环);
  636. // · 由系统按正确时机禁用 owner(不需要我们 EnableWindow);
  637. // · 由系统维持前台/激活(不需要 TOPMOST / SetForegroundWindow / 焦点切换);
  638. // · **把对话框窗口本身变成输入目标**,触摸自然投给它、提升的鼠标消息也落在它身上。
  639. //
  640. // 于是"贪婪归属权""提升鼠标被吞""owner 抢前台""禁用窗口收不到消息"这几类问题
  641. // 全部不存在 —— 不是被绕过,而是压根不成立。
  642. //
  643. // ── 如何与既有代码兼容 ────────────────────────────────────────────────
  644. // 调用方约定是:new → Create(hwnd, ..., UI_WNDSTYLE_DIALOG, ...) → SetIcon →
  645. // CenterWindow() → ShowModal()。也就是说**窗口在我们被调用之前就已经 Create 好了**,
  646. // CPaintManagerUI 也已 Init 到那个 HWND 上。所以这里不能另起一个对话框窗口,
  647. // 而是:用当前窗口的几何/样式造一个 DLGTEMPLATE,让 DialogBoxIndirectParam 去创建
  648. // 对话框窗口,再把旧的 HWND 销毁、把 DuiLib 的 manager 迁移到新 HWND —— 这样
  649. // 调用方完全无感知,`m_hWnd` 语义不变,30+ 个调用点一行都不用改。
  650. // =========================================================================
  651. ASSERT(::IsWindow(m_hWnd));
  652. if( !::IsWindow(m_hWnd) ) return 0;
  653. HWND hWndOwner = ::GetWindow(m_hWnd, GW_OWNER);
  654. if( hWndOwner == NULL ) hWndOwner = ::GetActiveWindow();
  655. // ---- 1) 采集旧窗口的几何与样式,用于构造对话框模板 ----
  656. RECT rcWnd = { 0 };
  657. ::GetWindowRect(m_hWnd, &rcWnd);
  658. const int nW = rcWnd.right - rcWnd.left;
  659. const int nH = rcWnd.bottom - rcWnd.top;
  660. // 【尺寸精确性】旧窗口的客户区尺寸 —— 这是迁移后必须保持不变的量。
  661. //
  662. // 为什么不能直接用窗口矩形(nW/nH)去填 DLGTEMPLATE.cx/cy:
  663. // 1. DLGTEMPLATE 的 cx/cy 描述的是**客户区**尺寸,不是窗口外框尺寸;
  664. // 2. 我们给模板设的样式(WS_CAPTION/WS_DLGFRAME/DS_MODALFRAME…)会让系统
  665. // 在此基础上再往外加一圈非客户区边框。
  666. // 若直接填 nW/nH,新窗口 = 旧窗口 + 边框 ⇒ 弹框整体变大、位置也偏。
  667. //
  668. // 因此这里取"旧窗口客户区尺寸"作为目标客户区尺寸;创建完成后再在 __DialogProc 里
  669. // 用 AdjustWindowRectEx 反算目标外框并 SetWindowPos 精确校正(见 __DialogProc)。
  670. RECT rcClient = { 0 };
  671. ::GetClientRect(m_hWnd, &rcClient);
  672. const int nClientW = rcClient.right - rcClient.left;
  673. const int nClientH = rcClient.bottom - rcClient.top;
  674. DWORD dwStyle = (DWORD)::GetWindowLong(m_hWnd, GWL_STYLE);
  675. DWORD dwExStyle = (DWORD)::GetWindowLong(m_hWnd, GWL_EXSTYLE);
  676. // 对话框专用的样式:把我们自己那套 WS_POPUPWINDOW/WS_CAPTION/WS_DLGFRAME 换成
  677. // 对话框该有的组合。WS_POPUP 必须保留(否则会变成带系统标题栏的普通窗口)。
  678. // WS_VISIBLE 让 DialogBox 创建后直接显示。
  679. //
  680. // ⚠️ 关于 WS_CAPTION / DS_MODALFRAME:
  681. // 本项目的弹框(UI_WNDSTYLE_DIALOG)是**无系统标题栏的自绘弹框**,且调用方
  682. // 均未传 WS_THICKFRAME(固定尺寸)。所以这里**不能**自作主张加 DS_MODALFRAME 或
  683. // WS_THICKFRAME —— 那会给弹框加一圈可调整大小的边框,既改变外观又改变尺寸。
  684. // 保持 WS_POPUP + 原有的 WS_CAPTION/WS_SYSMENU 即可(自绘弹框本来就不显示系统标题栏)。
  685. DWORD dwDlgStyle = WS_POPUP | WS_VISIBLE | WS_CLIPSIBLINGS | WS_CLIPCHILDREN;
  686. if( dwStyle & WS_CAPTION ) dwDlgStyle |= WS_CAPTION | WS_SYSMENU;
  687. if( dwStyle & WS_THICKFRAME ) dwDlgStyle |= WS_THICKFRAME;
  688. if( dwStyle & WS_MINIMIZEBOX ) dwDlgStyle |= WS_MINIMIZEBOX;
  689. if( dwStyle & WS_MAXIMIZEBOX ) dwDlgStyle |= WS_MAXIMIZEBOX;
  690. // ex-style 基本原样透传(WS_EX_TOOLWINDOW / WS_EX_WINDOWEDGE 等),但清掉两类:
  691. // · WS_EX_APPWINDOW —— 对话框不该出现在任务栏;
  692. // · WS_EX_DLGMODALFRAME —— 会给弹框加一圈双线边框,固定尺寸自绘弹框不需要(会改变外框尺寸)。
  693. dwExStyle &= ~(WS_EX_APPWINDOW | WS_EX_DLGMODALFRAME);
  694. // ---- 2) 构造 DLGTEMPLATE 资源 ----
  695. // 只用 DLGTEMPLATE 本体(不含菜单/类名/标题/字体),微软文档允许这样,
  696. // 系统会套用默认的对话框字体与类(#32770)。
  697. // 参考 nim_duilib:sizeof(DLGTEMPLATE) + 32 字节余量。
  698. const size_t nDataSize = sizeof(DLGTEMPLATE) + 32;
  699. HGLOBAL hResource = ::GlobalAlloc(GMEM_MOVEABLE, nDataSize);
  700. if( hResource == NULL )
  701. {
  702. DuiLibDiag::Log(_T("MODAL-DLG GlobalAlloc failed err=%u"), ::GetLastError());
  703. return 0;
  704. }
  705. LPDLGTEMPLATE pTemplate = (LPDLGTEMPLATE)::GlobalLock(hResource);
  706. if( pTemplate == NULL )
  707. {
  708. ::GlobalFree(hResource);
  709. return 0;
  710. }
  711. ::ZeroMemory(pTemplate, nDataSize);
  712. pTemplate->style = dwDlgStyle;
  713. pTemplate->dwExtendedStyle = dwExStyle;
  714. pTemplate->cdit = 0; // 不使用模板内控件(控件由 DuiLib 自绘)
  715. // DLGTEMPLATE 的 cx/cy 单位是对话框单位(DLU),且语义为**客户区**尺寸。
  716. // 这里只是给系统一个初始值(避免 0 尺寸导致奇怪的默认值),真正的精确尺寸
  717. // 由 __DialogProc 里创建完成后按旧客户区尺寸 + AdjustWindowRectEx 反算并 SetWindowPos 校正。
  718. pTemplate->x = 0;
  719. pTemplate->y = 0;
  720. pTemplate->cx = (short)(nClientW > 0 ? nClientW : nW);
  721. pTemplate->cy = (short)(nClientH > 0 ? nClientH : nH);
  722. // ---- 3) 迁移前先把旧的 HWND 记录下来,并让 manager 做好切换准备 ----
  723. HWND hWndOld = m_hWnd;
  724. m_bDoModal = true;
  725. m_nModalRet = 0;
  726. // 把"目标位置 + 目标客户区尺寸"暂存到成员,供 __DialogProc 在创建完成后精确校正。
  727. // 之所以不在模板里一次算准:对话框的非客户区厚度取决于实际样式(WS_CAPTION/WS_DLGFRAME/
  728. // WS_EX_* 组合),预估值容易差几个像素;创建后用 AdjustWindowRectEx 反算再 SetWindowPos
  729. // 是唯一能保证"客户区尺寸与旧窗口逐像素一致"的做法。
  730. m_rcModalTarget.left = rcWnd.left;
  731. m_rcModalTarget.top = rcWnd.top;
  732. m_rcModalTarget.right = rcWnd.left + nClientW;
  733. m_rcModalTarget.bottom = rcWnd.top + nClientH;
  734. DuiLibDiag::Log(_T("MODAL-DLG enter old=%p owner=%p wndrect=%d,%d,%dx%d client=%dx%d dlgstyle=0x%08X ex=0x%08X"),
  735. (void*)hWndOld, (void*)hWndOwner, rcWnd.left, rcWnd.top, nW, nH, nClientW, nClientH,
  736. (unsigned)dwDlgStyle, (unsigned)dwExStyle);
  737. // 记录"当前模态窗口",供库内其它地方判断(如 OnTouchInput 的老兜底逻辑)。
  738. // 注意:此时还不知道新 HWND,__DialogProc 里会再次 SetModalWindow。
  739. HWND hPrevModal = CPaintManagerUI::GetModalWindow();
  740. // 关键:先把旧窗口销毁、把 manager 解绑,再让 DialogBox 创建新的对话框窗口,
  741. // 这样 manager 不会出现"两个 HWND 同时活着"的错乱状态。
  742. // 说明:调用方是在 ShowModal() 之前 Create 的,所以这里必须主动清理旧窗口。
  743. HWND hWndOldCaptured = m_hWnd;
  744. m_hWnd = NULL; // 先置空:__DialogProc 会在 WM_INITDIALOG 里填入新句柄
  745. // 把 manager 从旧窗口上摘下来(保留它的控件树/资源,只换 paint window)
  746. CPaintManagerUI* pManager = FindManagerByPaintWindow(hWndOldCaptured);
  747. SetDetachedManager(pManager);
  748. // ---- [模态提示音] 在进入模态循环【之前】布好消音窗口 ----
  749. //
  750. // 时序(真机日志实证,见本文件头那段说明):
  751. // 调用方的触摸 UP 已经消费完(点击回调正在执行),但**这次手势的提升鼠标消息
  752. // 还排在消息队列里没投出来**;紧接着我们在这里把 owner 交给
  753. // DialogBoxIndirectParam 禁用,之后那条消息才被投递 ⇒ 撞上"点击禁用窗口"
  754. // ⇒ MessageBeep(0) 咚。
  755. //
  756. // 所以必须**现在就**置位:等 DialogBoxIndirectParam 返回就太晚了。
  757. //
  758. // 抑制位的生命周期是【手势级】而不是时间级:MessageHandler 收到被抑制窗口的
  759. // 提升鼠标 DOWN 就"记账"、收到配对的 UP 就"销账",账目归零即自动解除抑制;
  760. // 另有一条 10 秒僵尸兜底,防止配对消息永久丢失导致抑制位卡死。
  761. // 因此"弹框已出现后用户再点空白区"那一声(用户要求保留的)不受影响 ——
  762. // 那时本次手势的账早已结清,而且那一下的归属窗口本来就是弹框。
  763. //
  764. // 关闭弹框时还会【主动】撤防(见 CWindowWnd::Close 与 __DialogWndProc 的
  765. // WM_NCDESTROY),不依赖超时 —— 这是 Win10 "3 秒透点"的修复点。
  766. //
  767. // 位置:必须在 pManager 取到之后(否则未声明标识符),且早于下方
  768. // DialogBoxIndirectParam 调用即可 —— 中间那些 Detach / DestroyWindow 不影响它,
  769. // 因为抑制位是 manager 的成员,会随对象一起被 Attach 到新弹框。
  770. if( pManager != NULL )
  771. {
  772. pManager->SetSuppressPromotedMouseForModal(true);
  773. // ★★【透传根治 · 代数推进点】★★
  774. //
  775. // 这是唯一**可靠**的"模态边界"信号,必须在这里推进代数。
  776. //
  777. // 为什么不能靠在 MessageHandler 里比较"当前是否有模态 vs 上次是否有模态":
  778. // 真机时序是
  779. // WM_LBUTTONUP(主窗口)→ 回调里同步 ShowModal() → 模态循环 →
  780. // EndDialog → GetModalWindow() 归零 → **然后**那条被卡住的
  781. // WM_LBUTTONDOWN 才被投递。
  782. // 主窗口的 MessageHandler 在**整个模态期间收不到任何消息**
  783. // (owner 被 EnableWindow(FALSE),消息抽不出来),
  784. // 所以"边沿检测"两边看到的都是 false ⇒ 永远不会触发 ⇒ 拦不住。
  785. // ⇒ 边界必须在这里**主动**记录。
  786. //
  787. // ⚠️ 为什么推进的是**全局**代数、而不是 pManager 的成员:
  788. // 本项目属性弹框(CDiandanNatureWnd)自带独立的 `CPaintManagerUI m_pm`,
  789. // 与主窗口的 manager **不是同一个对象**。真机日志铁证:
  790. // MODAL-BEEP arm manager=0DCE0960 <- 弹框的 manager
  791. // MANAGER-MSG hwnd=00D20604 manager=05796840 <- 泄漏消息落在这里
  792. // 而"泄漏"是**线程消息队列**现象,与 manager 归属无关
  793. // ⇒ 代数必须是线程级的(见 DuiLib::BumpPromotedMouseGenerationGlobal)。
  794. DuiLib::BumpPromotedMouseGenerationGlobal();
  795. // ★ V2 标记:真机自证用。看到 `gen=` 且 `v=2` 才说明跑的是**这一版**二进制。
  796. // 上一版这行没有 gen=/v= 字段,日志里出现的是 `MODAL-BEEP arm manager=... owner=...`
  797. // ⇒ 如果新日志仍然没有 gen=/v=,那就是**没重新编译**,任何分析都不必做。
  798. DuiLibDiag::Log(_T("MODAL-BEEP arm v=2 manager=%p owner=%p gen=%u"),
  799. (void*)pManager, (void*)hWndOwner, (unsigned)DuiLib::PeekPromotedMouseGeneration());
  800. }
  801. // [第十一轮诊断] 模态开启瞬间的悬停/点击/捕获状态快照 —— **搬家前**的现场。
  802. //
  803. // 真模态会把**同一个** manager 从旧窗口搬到弹框上。此前 `AttachPaintWindow` 只换
  804. // paint window、**不清理** m_pEventHover / m_aNeedMouseLeaveNeeded / m_bMouseCapture,
  805. // 这些状态就跟着 manager 一起"住进"弹框,表现为"主窗口那个商品控件一直保持悬浮态,
  806. // 直到弹框关闭才恢复"(用户报的第 4 个 bug)。
  807. //
  808. // ⚠️ 该缺陷**已在第十一轮修复**:AttachPaintWindow 现在会清掉这一组输入态
  809. // (见 UIManager.cpp 该函数开头的长注释)。
  810. // 本日志保留用于**对照验证** —— 期望看到 hover 有值(旧窗口确实残留着),
  811. // 而修复后弹框上不应再出现"悬浮态不消失"。若修复后本行仍打印 hover=null,
  812. // 说明残留另有来源,需要继续查。
  813. if( pManager != NULL )
  814. {
  815. DuiLibDiag::Log(_T("MODAL-STATE enter manager=%p hover=%s captured=%d dragmode=%d leaveArr=%d rclick=%s"),
  816. (void*)pManager,
  817. pManager->DbgHoverName(), (int)pManager->IsCaptured(), (int)pManager->DbgIsDragMode(),
  818. pManager->DbgNeedMouseLeaveCount(), pManager->DbgRClickName());
  819. }
  820. if( pManager != NULL )
  821. {
  822. pManager->DetachPaintWindow();
  823. DuiLibDiag::Log(_T("MODAL-DLG detach manager=%p from old=%p"),
  824. (void*)pManager, (void*)hWndOldCaptured);
  825. }
  826. // 销毁旧窗口(旧窗口上的窗口过程会把 m_hWnd 清空,但我们已经在上面置 NULL 了,
  827. // 且 manager 已摘除,不会再触发 DuiLib 的绘制路径)。
  828. if( ::IsWindow(hWndOldCaptured) ) ::DestroyWindow(hWndOldCaptured);
  829. // ---- 4) 交给系统对话框管理器跑模态循环 ----
  830. // 这是阻塞调用,返回时模态已结束。
  831. INT_PTR nRet = ::DialogBoxIndirectParam(CPaintManagerUI::GetInstance(),
  832. pTemplate,
  833. hWndOwner,
  834. CWindowWnd::__DialogProc,
  835. (LPARAM)this);
  836. // 资源可以释放了(DialogBoxIndirectParam 内部已把模板复制走)——
  837. // 但谨慎起见放在 EndDialog 之后,与 nim_duilib 一致。
  838. ::GlobalUnlock(hResource);
  839. ::GlobalFree(hResource);
  840. // 兜底:若 __DialogProc 因故没跑(创建失败 / WM_INITDIALOG 里 early-return),
  841. // 这里把中转变量清干净,避免悬垂指针影响后续 ShowModal。
  842. SetDetachedManager(NULL);
  843. // [模态提示音] 模态循环已结束 ⇒ 消音窗口撤销。
  844. // 注意句柄:真模态把同一个 manager Detach 到旧窗口、再 Attach 到弹框,所以
  845. // 此刻 pManager 可能已随弹框一起销毁 —— 用 FindManagerByPaintWindow 取不到,
  846. // 因此直接用进入前缓存的指针。
  847. // 这里只是"尽早撤销"的一层;正确性由下面两处确定性撤防保证:
  848. // a) CWindowWnd::Close() 的 m_bDoModal 分支,在 EndDialog 之前撤防;
  849. // b) __DialogWndProc 的 WM_NCDESTROY 收尾,任何销毁路径都会撤防。
  850. // 若以上都因故没跑到,MessageHandler 里还有 10 秒僵尸兜底。
  851. if( pManager != NULL )
  852. {
  853. pManager->SetSuppressPromotedMouseForModal(false);
  854. DuiLibDiag::Log(_T("MODAL-BEEP disarm manager=%p"), (void*)pManager);
  855. }
  856. m_bDoModal = false;
  857. CPaintManagerUI::SetModalWindow(hPrevModal);
  858. DuiLibDiag::Log(_T("MODAL-DLG exit hwnd=%p ret=%d m_nModalRet=%u"),
  859. (void*)m_hWnd, (int)nRet, m_nModalRet);
  860. // DialogBoxIndirectParam 返回值:-1 表示创建失败
  861. if( nRet == -1 )
  862. {
  863. DuiLibDiag::Log(_T("MODAL-DLG failed err=%u"), ::GetLastError());
  864. return 0;
  865. }
  866. return m_nModalRet;
  867. }
  868. // -----------------------------------------------------------------------------
  869. // 真模态:对话框过程。
  870. //
  871. // 生命周期:
  872. // DialogBoxIndirectParam → 创建 #32770 窗口 → WM_INITDIALOG(此时才有真 HWND)
  873. // → 把 manager 挂到新 HWND、换掉 GWLP_WNDPROC
  874. // → return TRUE(表示我们已经处理了焦点)
  875. // -----------------------------------------------------------------------------
  876. INT_PTR CALLBACK CWindowWnd::__DialogProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
  877. {
  878. if( uMsg == WM_INITDIALOG )
  879. {
  880. // 这是对话框的第一个消息;lParam 是我们传进来的 this
  881. CWindowWnd* pThis = reinterpret_cast<CWindowWnd*>(lParam);
  882. if( pThis == NULL ) return TRUE;
  883. ASSERT(pThis->m_hWnd == NULL);
  884. pThis->m_hWnd = hDlg;
  885. // ---- 换掉对话框默认的窗口过程 ----
  886. // 这样后续所有消息(含 WM_PAINT / WM_TOUCH / 鼠标)都会走到
  887. // CWindowWnd::HandleMessage → CPaintManagerUI,DuiLib 的自绘与路由完全保留。
  888. ASSERT(pThis->m_pfnOldDlgWndProc == NULL);
  889. pThis->m_pfnOldDlgWndProc = (WNDPROC)::GetWindowLongPtr(hDlg, GWLP_WNDPROC);
  890. ::SetWindowLongPtr(hDlg, GWLP_WNDPROC, (LONG_PTR)(WNDPROC)CWindowWnd::__DialogWndProc);
  891. ::SetWindowLongPtr(hDlg, GWLP_USERDATA, (LONG_PTR)pThis);
  892. // ---- 登记为当前模态窗口 ----
  893. CPaintManagerUI::SetModalWindow(hDlg);
  894. // ---- 触摸注册:对话框是全新的 HWND,必须注册,否则收不到 WM_TOUCH ----
  895. RegisterTouchWindowIfAvailable(hDlg);
  896. DuiLibDiag::Log(_T("MODAL-DLG init dlg=%p oldwndproc=%p focus=%p"),
  897. (void*)hDlg, (void*)pThis->m_pfnOldDlgWndProc, (void*)::GetFocus());
  898. // ---- 把旧的 CPaintManagerUI 挂到对话框窗口上 ----
  899. // 调用方在 ShowModal() 之前已 Create + manager.Init 到旧 HWND;这里把 manager 迁移过来,
  900. // 让 DuiLib 的全部既有逻辑(绘制 / 消息 / 控件树)在新窗口上继续工作。
  901. CPaintManagerUI* pManager = GetDetachedManager();
  902. if( pManager != NULL )
  903. {
  904. pManager->AttachPaintWindow(hDlg);
  905. DuiLibDiag::Log(_T("MODAL-DLG attach manager=%p to dlg=%p"), (void*)pManager, (void*)hDlg);
  906. // 用掉即清:避免这个文件级静态变量残留一个已失效的指针,
  907. // 影响下一次 ShowModal(尤其嵌套弹框:A 弹框里再开 B 弹框)。
  908. SetDetachedManager(NULL);
  909. }
  910. else
  911. {
  912. DuiLibDiag::Log(_T("MODAL-DLG no detached manager (dialog will Init itself)"));
  913. }
  914. // ---- [WM_TOUCH 归属权] 立刻把"补调原窗口过程"的钩子绑到本弹框 ----
  915. // 为什么必须在这里(而不是等 __DialogWndProc 首次进入时再绑):
  916. // __DialogProc 在 WM_INITDIALOG 里直接调 pThis->HandleMessage(见下方),
  917. // 这条路径**不经过** __DialogWndProc,所以"首次进入窗口过程时绑定"
  918. // 在这条路上不会触发。而 AttachPaintWindow 之后 manager 已经指向 hDlg,
  919. // 一旦有 WM_MOUSEMOVE 到达就可能需要补调 ⇒ 必须现在就绑好。
  920. // 绑定对象是 m_pfnOldDlgWndProc(即 #32770 原过程 / DefDlgProc)。
  921. if( pManager != NULL )
  922. {
  923. // m_pfnOldDlgWndProc 本身就是 WNDPROC;只有它意外为空时才回落到 DefDlgProc
  924. // (签名是 INT_PTR 返回,需显式转换,不能与 WNDPROC 混在三目里)。
  925. WNDPROC pfnDlgTarget = pThis->m_pfnOldDlgWndProc;
  926. if( pfnDlgTarget == NULL )
  927. pfnDlgTarget = reinterpret_cast<WNDPROC>(::DefDlgProc);
  928. BindRelayTarget(pfnDlgTarget, hDlg);
  929. pManager->SetDefProcRelay(&RelayWndProc, hDlg);
  930. }
  931. // ---- 尺寸校正(关键)----
  932. // DLGTEMPLATE 里给的 cx/cy 是对话框单位(DLU)、且语义为「客户区」,系统创建时还会
  933. // 按实际样式额外加一圈非客户区边框。所以不能指望"模板尺寸 == 目标尺寸"。
  934. // 这里用 AdjustWindowRectEx 按对话框**真实**的样式反算目标外框,再 SetWindowPos 精确校正,
  935. // 保证客户区尺寸与原窗口逐像素一致 —— 这正是"弹框变大了"的修法。
  936. //
  937. // 放在 AttachPaintWindow 之后、HandleMessage(WM_INITDIALOG) 之前:
  938. // 此时 manager 已绑定新 HWND,尺寸变化会走正常的布局流程;且业务层 OnCreate 之前
  939. // 窗口就已经是最终尺寸,避免"先小后大"的闪动与一次性布局错位。
  940. {
  941. const DWORD dwRealStyle = (DWORD)::GetWindowLong(hDlg, GWL_STYLE);
  942. const DWORD dwRealExStyle = (DWORD)::GetWindowLong(hDlg, GWL_EXSTYLE);
  943. RECT rcAdjust = { 0, 0,
  944. pThis->m_rcModalTarget.right - pThis->m_rcModalTarget.left,
  945. pThis->m_rcModalTarget.bottom - pThis->m_rcModalTarget.top };
  946. const int nClientW = rcAdjust.right;
  947. const int nClientH = rcAdjust.bottom;
  948. // 反算:以"目标客户区尺寸"为输入,得到需要的外框尺寸
  949. ::AdjustWindowRectEx(&rcAdjust, dwRealStyle, FALSE, dwRealExStyle);
  950. const int nWndW = rcAdjust.right - rcAdjust.left;
  951. const int nWndH = rcAdjust.bottom - rcAdjust.top;
  952. RECT rcCur = { 0 };
  953. ::GetWindowRect(hDlg, &rcCur);
  954. DuiLibDiag::Log(_T("MODAL-DLG resize before cur=%dx%d clientTarget=%dx%d -> wnd=%dx%d hide=%d"),
  955. rcCur.right - rcCur.left, rcCur.bottom - rcCur.top,
  956. nClientW, nClientH, nWndW, nWndH,
  957. (int)((dwRealStyle & WS_VISIBLE) == 0));
  958. // SWP_FRAMECHANGED 让系统重新计算非客户区(样式刚被 SetWindowLongPtr 换过)
  959. ::SetWindowPos(hDlg, NULL,
  960. pThis->m_rcModalTarget.left, pThis->m_rcModalTarget.top,
  961. nWndW, nWndH,
  962. SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED);
  963. RECT rcAfter = { 0 };
  964. ::GetClientRect(hDlg, &rcAfter);
  965. DuiLibDiag::Log(_T("MODAL-DLG resize after client=%dx%d (target=%dx%d)"),
  966. rcAfter.right - rcAfter.left, rcAfter.bottom - rcAfter.top, nClientW, nClientH);
  967. }
  968. // 交给 CWindowWnd::HandleMessage 处理 WM_INITDIALOG,业务层的 OnCreate 等由此触发。
  969. pThis->HandleMessage(uMsg, wParam, lParam);
  970. ::SetFocus(hDlg);
  971. return TRUE; // TRUE = 我们自己设置了焦点
  972. }
  973. return FALSE;
  974. }
  975. // -----------------------------------------------------------------------------
  976. // 真模态:对话框窗口的窗口过程。
  977. // 几乎就是 __WndProc 的对话框版本。
  978. // -----------------------------------------------------------------------------
  979. LRESULT CALLBACK CWindowWnd::__DialogWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
  980. {
  981. CWindowWnd* pThis = reinterpret_cast<CWindowWnd*>(::GetWindowLongPtr(hWnd, GWLP_USERDATA));
  982. if( uMsg == WM_NCDESTROY && pThis != NULL )
  983. {
  984. // 顺序很关键:
  985. // 1) 先把窗口过程还原(此后系统调用会回到对话框默认过程);
  986. // 2) 再让 DefWindowProc 做标准清理;
  987. // 3) 清 GWLP_USERDATA —— 必须在 HandleMessage 之前,防止重入时又拿到 this;
  988. // 4) 清模态登记;
  989. // 5) m_hWnd 置空【要在 Unsubclass 之前】—— 注意本项目弹框并不 Subclass,
  990. // m_bSubclassed 恒为 false,这里只是保持与 __WndProc 一致的写法,
  991. // 并让 OnFinalMessage 里看到的 m_hWnd 为空(与旧行为一致)。
  992. if( pThis->m_pfnOldDlgWndProc != NULL )
  993. {
  994. ::SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)pThis->m_pfnOldDlgWndProc);
  995. pThis->m_pfnOldDlgWndProc = NULL;
  996. }
  997. LRESULT lRes = ::DefWindowProc(hWnd, uMsg, wParam, lParam);
  998. // [WM_TOUCH 归属权] 解绑转发目标,避免悬垂到已销毁的对话框窗口过程。
  999. {
  1000. DuiLib::CPaintManagerUI* pMgrNc = FindManagerByPaintWindow(hWnd);
  1001. if( pMgrNc != NULL )
  1002. {
  1003. pMgrNc->ClearDefProcRelay(hWnd);
  1004. // [模态提示音 / 透点] 第二道撤防:不管弹框以哪条路径死掉
  1005. // (Close / EndDialog / 外部 DestroyWindow / 创建失败收尾),
  1006. // 只要它的窗口在销毁,就不允许消音窗口继续存在。
  1007. // 抑制位跟着 manager 对象走,而 manager 可能被回收再复用,
  1008. // 若不清就可能在下一个不相关的窗口上"诈尸",吞掉用户的真实点击。
  1009. pMgrNc->SetSuppressPromotedMouseForModal(false);
  1010. }
  1011. BindRelayTarget(NULL, NULL);
  1012. }
  1013. ::SetWindowLongPtr(hWnd, GWLP_USERDATA, 0L);
  1014. CPaintManagerUI::SetModalWindow(NULL);
  1015. pThis->m_hWnd = NULL;
  1016. if( pThis->m_bSubclassed ) pThis->Unsubclass();
  1017. pThis->OnFinalMessage(hWnd);
  1018. return lRes;
  1019. }
  1020. // WM_INITDIALOG 已经由 __DialogProc 处理并 return TRUE,
  1021. // 一般不会再走到这里;若系统仍投递(例如 EndDialog 之后的收尾路径),
  1022. // 直接交回对话框默认过程,避免被 HandleMessage 当成普通消息误处理。
  1023. if( uMsg == WM_INITDIALOG )
  1024. return ::DefDlgProc(hWnd, uMsg, wParam, lParam);
  1025. // 诊断:确认 WM_TOUCH 到底投递给了哪个窗口。
  1026. if( uMsg == WM_TOUCH ) TraceTouchArrival(hWnd, wParam, lParam);
  1027. if( pThis != NULL ) TraceInputMessage(hWnd, uMsg, wParam, lParam);
  1028. if( pThis != NULL )
  1029. {
  1030. // ---------------------------------------------------------------
  1031. // [WM_TOUCH 归属权] 注册"补调原窗口过程"的钩子。
  1032. //
  1033. // 真模态弹框的窗口类是系统对话框类 #32770,它把"把特殊标记的
  1034. // WM_MOUSEMOVE 翻译成 WM_TOUCH"这件事放在 DefDlgProc → DefWindowProc 里做。
  1035. // 而 __DialogWndProc 一旦 pThis != NULL 就全部交给 HandleMessage,
  1036. // 再也不回落 DefDlgProc ⇒ 弹框永远收不到 WM_TOUCH(与实测吻合)。
  1037. //
  1038. // 这里登记的原过程是 m_pfnOldDlgWndProc(也就是 DefDlgProc),
  1039. // 使 manager 在吞掉触摸提升鼠标消息时能补调它一次,把翻译链接上。
  1040. // ---------------------------------------------------------------
  1041. // ⚠️ 判据必须是"当前绑定的窗口不是本窗口",不能写 `GetDefProcRelay() == NULL`:
  1042. // 真模态弹框复用的是**同一个** manager 对象(从旧窗口 Detach、再 Attach 到弹框),
  1043. // 主窗口早已注册过钩子 ⇒ 按 NULL 判断则弹框永远不重新绑定,
  1044. // 转发目标会一直指向主窗口的原过程,补调无效甚至喂错窗口。
  1045. DuiLib::CPaintManagerUI* pMgr = FindManagerByPaintWindow(hWnd);
  1046. if( pMgr != NULL && pMgr->GetDefProcRelayBoundWindow() != hWnd )
  1047. {
  1048. // ⚠️ DefDlgProc 的签名是 `INT_PTR (CALLBACK*)(...)`,与 WNDPROC
  1049. // (`LRESULT (CALLBACK*)(...)`)返回类型不同,三目运算符会类型不匹配。
  1050. // 用一个显式分支赋值,避免 C2446。
  1051. WNDPROC pfnTarget = NULL;
  1052. if( pThis->m_pfnOldDlgWndProc != NULL )
  1053. pfnTarget = pThis->m_pfnOldDlgWndProc;
  1054. else
  1055. pfnTarget = reinterpret_cast<WNDPROC>(::DefDlgProc);
  1056. BindRelayTarget(pfnTarget, hWnd);
  1057. pMgr->SetDefProcRelay(&RelayWndProc, hWnd);
  1058. }
  1059. return pThis->HandleMessage(uMsg, wParam, lParam);
  1060. }
  1061. return ::DefDlgProc(hWnd, uMsg, wParam, lParam);
  1062. }
  1063. void CWindowWnd::ShowModalFake()
  1064. {
  1065. ASSERT(::IsWindow(m_hWnd));
  1066. auto p_hwnd = GetWindowOwner(m_hWnd);
  1067. ASSERT(::IsWindow(p_hwnd));
  1068. ASSERT(p_hwnd);
  1069. ::EnableWindow(p_hwnd, FALSE);
  1070. ShowWindow();
  1071. m_bFakeModal = true;
  1072. }
  1073. void CWindowWnd::Close(UINT nRet)
  1074. {
  1075. if (m_bFakeModal)
  1076. {
  1077. auto parent_hwnd = GetWindowOwner(m_hWnd);
  1078. ASSERT(::IsWindow(parent_hwnd));
  1079. ::EnableWindow(parent_hwnd, TRUE);
  1080. ::SetFocus(parent_hwnd);
  1081. m_bFakeModal = false;
  1082. }
  1083. ASSERT(::IsWindow(m_hWnd));
  1084. if (!::IsWindow(m_hWnd)) return;
  1085. // -------------------------------------------------------------------------
  1086. // 真模态:走 EndDialog 结束系统对话框循环。
  1087. //
  1088. // 旧的假模态是 PostMessage(WM_CLOSE) 让自写循环退出;真模态下必须用 EndDialog,
  1089. // 否则 DialogBoxIndirectParam 不会返回。返回值写入 m_nModalRet,由 ShowModal 读走。
  1090. //
  1091. // 注意:EndDialog 会销毁对话框窗口 → 触发 WM_NCDESTROY → __DialogWndProc 收尾
  1092. // (还原窗口过程、清 USERDATA、调 OnFinalMessage)。所以这里不要再 DestroyWindow。
  1093. // -------------------------------------------------------------------------
  1094. if (m_bDoModal)
  1095. {
  1096. m_nModalRet = nRet;
  1097. // ---------------------------------------------------------------------
  1098. // [模态提示音 / 透点] 关弹框时【立即】撤掉消音窗口 —— 这是 Win10 "3 秒透点"
  1099. // 的关键修复点。
  1100. //
  1101. // 现象(Win10):点商品控件弹属性框;3 秒内点"选好了"一切正常;一旦超过 3 秒
  1102. // 再点"选好了",弹框关掉的同时还会"透"到底下、又点到"选好了"背后的商品控件。
  1103. //
  1104. // 成因:消音窗口原先只靠 MessageHandler 里的超时自愈撤销,而那个超时是 3000ms
  1105. // (UIManager.cpp 的 TOUCH-STATE timeout-reset)。用户观察到的"3 秒"就是它。
  1106. // 超过 3 秒后,抑制位已被超时清掉,可**这次手势的提升鼠标消息还排在队列里没
  1107. // 投出来**(触摸 UP 的点击回调此刻正在执行)⇒ 抑制失效 ⇒ 那条残留消息被放行
  1108. // ⇒ 命中弹框背后的窗口 ⇒ 透点。
  1109. // 实质是"靠一个超时 bug 去挡另一个时序 bug",只是恰好 3 秒内不出问题。
  1110. //
  1111. // 修复:弹框关闭是**确定性时点**,比任何超时都可靠 —— 在 EndDialog 之前主动把
  1112. // 同一 manager 上的抑制状态清干净(含未配对的 pending 计数,见
  1113. // SetSuppressPromotedMouseForModal(false) 的实现)。
  1114. //
  1115. // 为什么用 FindManagerByPaintWindow 而不是 ShowModal 里缓存的 pManager:
  1116. // 真模态把同一个 manager 从旧窗口 Detach、再 Attach 到本弹框,所以此刻
  1117. // m_hWnd 就是弹框句柄,能直接查到它。查不到也无妨 —— 下面 EndDialog 会让
  1118. // __DialogWndProc 的 WM_NCDESTROY 收尾,那里还有第二道撤防。
  1119. // ---------------------------------------------------------------------
  1120. {
  1121. DuiLib::CPaintManagerUI* pMgrClosing = FindManagerByPaintWindow(m_hWnd);
  1122. if (pMgrClosing != NULL)
  1123. {
  1124. pMgrClosing->SetSuppressPromotedMouseForModal(false);
  1125. DuiLibDiag::Log(_T("MODAL-BEEP disarm-on-close manager=%p hwnd=%p"),
  1126. (void*)pMgrClosing, (void*)m_hWnd);
  1127. }
  1128. }
  1129. DuiLibDiag::Log(_T("MODAL-DLG EndDialog hwnd=%p ret=%u"), (void*)m_hWnd, nRet);
  1130. ::EndDialog(m_hWnd, (INT_PTR)nRet);
  1131. return;
  1132. }
  1133. PostMessage(WM_CLOSE, (WPARAM)nRet, 0L);
  1134. }
  1135. void CWindowWnd::CenterWindow()
  1136. {
  1137. ASSERT(::IsWindow(m_hWnd));
  1138. ASSERT((GetWindowStyle(m_hWnd)&WS_CHILD)==0);
  1139. RECT rcDlg = { 0 };
  1140. ::GetWindowRect(m_hWnd, &rcDlg);
  1141. RECT rcArea = { 0 };
  1142. RECT rcCenter = { 0 };
  1143. HWND hWnd=*this;
  1144. HWND hWndParent = ::GetParent(m_hWnd);
  1145. HWND hWndCenter = ::GetWindowOwner(m_hWnd);
  1146. if (hWndCenter!=NULL)
  1147. hWnd=hWndCenter;
  1148. // 处理多显示器模式下屏幕居中
  1149. MONITORINFO oMonitor = {};
  1150. oMonitor.cbSize = sizeof(oMonitor);
  1151. ::GetMonitorInfo(::MonitorFromWindow(hWnd, MONITOR_DEFAULTTONEAREST), &oMonitor);
  1152. rcArea = oMonitor.rcWork;
  1153. if( hWndCenter == NULL )
  1154. rcCenter = rcArea;
  1155. else
  1156. ::GetWindowRect(hWndCenter, &rcCenter);
  1157. int DlgWidth = rcDlg.right - rcDlg.left;
  1158. int DlgHeight = rcDlg.bottom - rcDlg.top;
  1159. // Find dialog's upper left based on rcCenter
  1160. int xLeft = (rcCenter.left + rcCenter.right) / 2 - DlgWidth / 2;
  1161. int yTop = (rcCenter.top + rcCenter.bottom) / 2 - DlgHeight / 2;
  1162. // The dialog is outside the screen, move it inside
  1163. if( xLeft < rcArea.left ) xLeft = rcArea.left;
  1164. else if( xLeft + DlgWidth > rcArea.right ) xLeft = rcArea.right - DlgWidth;
  1165. if( yTop < rcArea.top ) yTop = rcArea.top;
  1166. else if( yTop + DlgHeight > rcArea.bottom ) yTop = rcArea.bottom - DlgHeight;
  1167. ::SetWindowPos(m_hWnd, NULL, xLeft, yTop, -1, -1, SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
  1168. }
  1169. void CWindowWnd::SetIcon(UINT nRes)
  1170. {
  1171. HICON hIcon = (HICON)::LoadImage(CPaintManagerUI::GetInstance(), MAKEINTRESOURCE(nRes), IMAGE_ICON,
  1172. (::GetSystemMetrics(SM_CXICON) + 15) & ~15, (::GetSystemMetrics(SM_CYICON) + 15) & ~15, // 防止高DPI下图标模糊
  1173. LR_DEFAULTCOLOR);
  1174. ASSERT(hIcon);
  1175. ::SendMessage(m_hWnd, WM_SETICON, (WPARAM) TRUE, (LPARAM) hIcon);
  1176. hIcon = (HICON)::LoadImage(CPaintManagerUI::GetInstance(), MAKEINTRESOURCE(nRes), IMAGE_ICON,
  1177. (::GetSystemMetrics(SM_CXICON) + 15) & ~15, (::GetSystemMetrics(SM_CYICON) + 15) & ~15, // 防止高DPI下图标模糊
  1178. LR_DEFAULTCOLOR);
  1179. ASSERT(hIcon);
  1180. ::SendMessage(m_hWnd, WM_SETICON, (WPARAM) FALSE, (LPARAM) hIcon);
  1181. }
  1182. bool CWindowWnd::RegisterWindowClass()
  1183. {
  1184. WNDCLASS wc = { 0 };
  1185. wc.style = GetClassStyle();
  1186. wc.cbClsExtra = 0;
  1187. wc.cbWndExtra = 0;
  1188. wc.hIcon = NULL;
  1189. wc.lpfnWndProc = CWindowWnd::__WndProc;
  1190. wc.hInstance = CPaintManagerUI::GetInstance();
  1191. wc.hCursor = ::LoadCursor(NULL, IDC_ARROW);
  1192. wc.hbrBackground = NULL;
  1193. wc.lpszMenuName = NULL;
  1194. wc.lpszClassName = GetWindowClassName();
  1195. ATOM ret = ::RegisterClass(&wc);
  1196. ASSERT(ret!=NULL || ::GetLastError()==ERROR_CLASS_ALREADY_EXISTS);
  1197. return ret != NULL || ::GetLastError() == ERROR_CLASS_ALREADY_EXISTS;
  1198. }
  1199. bool CWindowWnd::RegisterSuperclass()
  1200. {
  1201. if(m_bUnicode) {
  1202. #ifndef UNICODE
  1203. LPWSTR lpSuperClassName = a2w((char*)GetSuperClassName());
  1204. LPWSTR lpClassName = a2w((char*)GetWindowClassName());
  1205. #else
  1206. LPWSTR lpSuperClassName = (LPWSTR)GetSuperClassName();
  1207. LPWSTR lpClassName = (LPWSTR)GetWindowClassName();
  1208. #endif
  1209. // Get the class information from an existing
  1210. // window so we can subclass it later on...
  1211. WNDCLASSEXW wc = { 0 };
  1212. wc.cbSize = sizeof(WNDCLASSEXW);
  1213. if( !::GetClassInfoExW(NULL, lpSuperClassName, &wc) ) {
  1214. if( !::GetClassInfoExW(CPaintManagerUI::GetInstance(), lpSuperClassName, &wc) ) {
  1215. ASSERT(!"Unable to locate window class");
  1216. return NULL;
  1217. }
  1218. }
  1219. m_OldWndProc = wc.lpfnWndProc;
  1220. wc.lpfnWndProc = CWindowWnd::__ControlProc;
  1221. wc.hInstance = CPaintManagerUI::GetInstance();
  1222. wc.lpszClassName = lpClassName;
  1223. ATOM ret = ::RegisterClassExW(&wc);
  1224. #ifndef UNICODE
  1225. delete []lpClassName;
  1226. delete []lpSuperClassName;
  1227. #endif
  1228. ASSERT(ret!=NULL || ::GetLastError()==ERROR_CLASS_ALREADY_EXISTS);
  1229. return ret != NULL || ::GetLastError() == ERROR_CLASS_ALREADY_EXISTS;
  1230. }
  1231. else {
  1232. // Get the class information from an existing
  1233. // window so we can subclass it later on...
  1234. WNDCLASSEX wc = { 0 };
  1235. wc.cbSize = sizeof(WNDCLASSEX);
  1236. if( !::GetClassInfoEx(NULL, GetSuperClassName(), &wc) ) {
  1237. if( !::GetClassInfoEx(CPaintManagerUI::GetInstance(), GetSuperClassName(), &wc) ) {
  1238. ASSERT(!"Unable to locate window class");
  1239. return NULL;
  1240. }
  1241. }
  1242. m_OldWndProc = wc.lpfnWndProc;
  1243. wc.lpfnWndProc = CWindowWnd::__ControlProc;
  1244. wc.hInstance = CPaintManagerUI::GetInstance();
  1245. wc.lpszClassName = GetWindowClassName();
  1246. ATOM ret = ::RegisterClassEx(&wc);
  1247. ASSERT(ret!=NULL || ::GetLastError()==ERROR_CLASS_ALREADY_EXISTS);
  1248. return ret != NULL || ::GetLastError() == ERROR_CLASS_ALREADY_EXISTS;
  1249. }
  1250. }
  1251. // 注:RelayWndProc / BindRelayTarget 已上移到文件前部的**匿名 namespace 内**
  1252. // (约 308 / 317 行),因为 __DialogWndProc 比本函数先出现、也要用到它们。
  1253. // 另注:FindManagerByPaintWindow 也已移到匿名 namespace **之外**(约 350 行),
  1254. // 以便与 UIBase.h 里的同名声明一一对应(否则调用点会报 C2668 二义)。
  1255. LRESULT CALLBACK CWindowWnd::__WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
  1256. { CWindowWnd* pThis = NULL;
  1257. if( uMsg == WM_NCCREATE ) {
  1258. LPCREATESTRUCT lpcs = reinterpret_cast<LPCREATESTRUCT>(lParam);
  1259. pThis = static_cast<CWindowWnd*>(lpcs->lpCreateParams);
  1260. pThis->m_hWnd = hWnd;
  1261. ::SetWindowLongPtr(hWnd, GWLP_USERDATA, reinterpret_cast<LPARAM>(pThis));
  1262. }
  1263. else {
  1264. pThis = reinterpret_cast<CWindowWnd*>(::GetWindowLongPtr(hWnd, GWLP_USERDATA));
  1265. if( uMsg == WM_NCDESTROY && pThis != NULL ) {
  1266. LRESULT lRes = ::CallWindowProc(pThis->m_OldWndProc, hWnd, uMsg, wParam, lParam);
  1267. // [WM_TOUCH 归属权] 解绑转发目标,避免悬垂到已销毁窗口的窗口过程。
  1268. DuiLib::CPaintManagerUI* pMgrNc = FindManagerByPaintWindow(hWnd);
  1269. if( pMgrNc != NULL ) pMgrNc->ClearDefProcRelay(hWnd);
  1270. BindRelayTarget(NULL, NULL);
  1271. ::SetWindowLongPtr(pThis->m_hWnd, GWLP_USERDATA, 0L);
  1272. if( pThis->m_bSubclassed ) pThis->Unsubclass();
  1273. pThis->m_hWnd = NULL;
  1274. pThis->OnFinalMessage(hWnd);
  1275. return lRes;
  1276. }
  1277. }
  1278. // 诊断:确认 WM_TOUCH 到底投递给了哪个窗口(弹框 / owner / 其它)。
  1279. if( uMsg == WM_TOUCH ) TraceTouchArrival(hWnd, wParam, lParam);
  1280. // 【临时全量诊断】所有输入类消息(含 WM_TOUCH / WM_GESTURE / WM_POINTER* / 鼠标 / 激活)
  1281. // 都记一条,用来判断系统到底把消息发给了谁。
  1282. if( pThis != NULL ) TraceInputMessage(hWnd, uMsg, wParam, lParam);
  1283. if( pThis != NULL ) {
  1284. // ---------------------------------------------------------------
  1285. // [WM_TOUCH 归属权] 注册"补调原窗口过程"的钩子。
  1286. // 把 m_OldWndProc(本窗口类的原过程)登记为转发目标,并交给 manager。
  1287. // manager 在吞掉"触摸提升 WM_MOUSEMOVE"时会回调它一次,
  1288. // 让系统的 WM_TOUCH 翻译链不断掉。
  1289. //
  1290. // 判据用"绑定窗口 != 本窗口"而非"钩子为空":真模态弹框复用同一个
  1291. // manager,换 HWND 时必须能重新绑定到对话框架自己的原过程。
  1292. // 开销:仅在窗口首个消息时走一遍查找,之后靠 manager 内的指针直连。
  1293. // ---------------------------------------------------------------
  1294. DuiLib::CPaintManagerUI* pMgr = FindManagerByPaintWindow(hWnd);
  1295. if( pMgr != NULL && pMgr->GetDefProcRelayBoundWindow() != hWnd )
  1296. {
  1297. BindRelayTarget(pThis->m_OldWndProc, hWnd);
  1298. pMgr->SetDefProcRelay(&RelayWndProc, hWnd);
  1299. }
  1300. return pThis->HandleMessage(uMsg, wParam, lParam);
  1301. }
  1302. else {
  1303. return ::DefWindowProc(hWnd, uMsg, wParam, lParam);
  1304. }
  1305. }
  1306. LRESULT CALLBACK CWindowWnd::__ControlProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
  1307. {
  1308. CWindowWnd* pThis = NULL;
  1309. if( uMsg == WM_NCCREATE ) {
  1310. LPCREATESTRUCT lpcs = reinterpret_cast<LPCREATESTRUCT>(lParam);
  1311. pThis = static_cast<CWindowWnd*>(lpcs->lpCreateParams);
  1312. ::SetProp(hWnd, _T("WndX"), (HANDLE) pThis);
  1313. pThis->m_hWnd = hWnd;
  1314. }
  1315. else {
  1316. pThis = reinterpret_cast<CWindowWnd*>(::GetProp(hWnd, _T("WndX")));
  1317. if( uMsg == WM_NCDESTROY && pThis != NULL ) {
  1318. LRESULT lRes = ::CallWindowProc(pThis->m_OldWndProc, hWnd, uMsg, wParam, lParam);
  1319. if( pThis->m_bSubclassed ) pThis->Unsubclass();
  1320. ::SetProp(hWnd, _T("WndX"), NULL);
  1321. pThis->m_hWnd = NULL;
  1322. pThis->OnFinalMessage(hWnd);
  1323. return lRes;
  1324. }
  1325. }
  1326. if( pThis != NULL ) {
  1327. return pThis->HandleMessage(uMsg, wParam, lParam);
  1328. }
  1329. else {
  1330. return ::DefWindowProc(hWnd, uMsg, wParam, lParam);
  1331. }
  1332. }
  1333. LRESULT CWindowWnd::SendMessage(UINT uMsg, WPARAM wParam /*= 0*/, LPARAM lParam /*= 0*/)
  1334. {
  1335. ASSERT(::IsWindow(m_hWnd));
  1336. return ::SendMessage(m_hWnd, uMsg, wParam, lParam);
  1337. }
  1338. LRESULT CWindowWnd::PostMessage(UINT uMsg, WPARAM wParam /*= 0*/, LPARAM lParam /*= 0*/)
  1339. {
  1340. ASSERT(::IsWindow(m_hWnd));
  1341. return ::PostMessage(m_hWnd, uMsg, wParam, lParam);
  1342. }
  1343. void CWindowWnd::ResizeClient(int cx /*= -1*/, int cy /*= -1*/)
  1344. {
  1345. ASSERT(::IsWindow(m_hWnd));
  1346. RECT rc = { 0 };
  1347. if( !::GetClientRect(m_hWnd, &rc) ) return;
  1348. if( cx != -1 ) rc.right = cx;
  1349. if( cy != -1 ) rc.bottom = cy;
  1350. if( !::AdjustWindowRectEx(&rc, GetWindowStyle(m_hWnd), (!(GetWindowStyle(m_hWnd) & WS_CHILD) && (::GetMenu(m_hWnd) != NULL)), GetWindowExStyle(m_hWnd)) ) return;
  1351. ::SetWindowPos(m_hWnd, NULL, 0, 0, rc.right - rc.left, rc.bottom - rc.top, SWP_NOZORDER | SWP_NOMOVE | SWP_NOACTIVATE);
  1352. }
  1353. LRESULT CWindowWnd::HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam)
  1354. {
  1355. return ::CallWindowProc(m_OldWndProc, m_hWnd, uMsg, wParam, lParam);
  1356. }
  1357. void CWindowWnd::OnFinalMessage(HWND /*hWnd*/)
  1358. {
  1359. }
  1360. } // namespace DuiLib