Browse Source

触摸没问题了。

张洋 3 weeks ago
parent
commit
9ed6f601e1

+ 73 - 8
DuiLib/Control/UICombo.cpp

@@ -378,9 +378,14 @@ namespace DuiLib {
 		DuiLibDiag::Log(_T("COMBO-FIN hwnd=%p alive=%ums"),
 			hWnd, (unsigned)(s_dwComboOpenTick != 0 ? (::GetTickCount() - s_dwComboOpenTick) : 0));
 		s_dwComboOpenTick = 0;
-		m_pOwner->m_pWindow = NULL;
-		m_pOwner->m_uButtonState &= ~ UISTATE_PUSHED;
-		m_pOwner->Invalidate();
+		// 【第 42 轮】owner 可能已经不在了(控件被移除/销毁后才轮到我们收尾)。
+		// 判空只防"指针为 NULL",防不了"内存已释放" —— 另一半由 CComboUI::~CComboUI()
+		// 同步收掉本窗口来保证(见那里的长注释)。两者互补,缺一不可。
+		if( m_pOwner != NULL ) {
+			m_pOwner->m_pWindow = NULL;
+			m_pOwner->m_uButtonState &= ~ UISTATE_PUSHED;
+			m_pOwner->Invalidate();
+		}
 		delete this;
 	}
 
@@ -468,10 +473,15 @@ namespace DuiLib {
 			// 【诊断】关闭现场:走到这里说明"关下拉框"的收尾已经开始。
 			DuiLibDiag::Log(_T("COMBO-CLOSE reason=WM_CLOSE alive=%ums"),
 				(unsigned)(s_dwComboOpenTick != 0 ? (::GetTickCount() - s_dwComboOpenTick) : 0));
-			m_pOwner->SetManager(m_pOwner->GetManager(), m_pOwner->GetParent(), false);
-			RECT rcNull = { 0 };
-			for( int i = 0; i < m_pOwner->GetCount(); i++ ) static_cast<CControlUI*>(m_pOwner->GetItemAt(i))->SetPos(rcNull);
-			m_pOwner->SetFocus();
+			// 【第 42 轮】owner 可能已被移出控件树(此时它的 manager 是 NULL)或已销毁。
+			// 这段收尾是"把 items 归还给 owner 的 manager",owner 不在了就没有归还对象,
+			// 直接跳过即可 —— 但**绝不能**不判空就解引用。
+			if( m_pOwner != NULL ) {
+				m_pOwner->SetManager(m_pOwner->GetManager(), m_pOwner->GetParent(), false);
+				RECT rcNull = { 0 };
+				for( int i = 0; i < m_pOwner->GetCount(); i++ ) static_cast<CControlUI*>(m_pOwner->GetItemAt(i))->SetPos(rcNull);
+				m_pOwner->SetFocus();
+			}
 		}
 		else if( uMsg == WM_LBUTTONDOWN ) {
 			POINT pt = { 0 };
@@ -534,8 +544,18 @@ namespace DuiLib {
 			// 【诊断】谁把焦点抢走了 —— 这一行的 wParam 就是"接任焦点的窗口"。
 			// 若它与 owner 相同,说明是"主窗口被重新激活"把我们挤掉,而不是你点了别处
 			//(鼠标激活发生在**进入窗口过程之前**,所以这里拦不住)。
+			// ⚠️⚠️【第 42 轮 · "手指快速触摸点 Combo 直接崩"的修点】这一行的实参以前是
+			//     `m_pOwner->GetManager()->GetPaintWindow()` —— **裸连环解引用**。两个理由使它必崩:
+			//     ① `Log(...)` 的**实参在调用之前就求值**,与日志开关无关
+			//        (把 `Enabled()` 改成 false 也拦不住 —— `Log` 第一行 return 时表达式早算完了);
+			//     ② owner 被移出控件树 / 正在销毁时 `GetManager()` 返回 NULL
+			//        ⇒ `NULL->GetPaintWindow()` ⇒ 访问违例 ⇒ 进程直接崩。
+			//   ⇒ 铁律:**探针里的每一次指针解引用都必须先判空 —— 探针的代价不能高于它探测的东西。**
+			//     (本项目第二次栽在同一件事上:第一次是把 CDuiString 对象塞进变参。)
+			CPaintManagerUI* pOwnerMgr = (m_pOwner != NULL) ? m_pOwner->GetManager() : NULL;
+			const HWND hOwnerWnd = (pOwnerMgr != NULL) ? pOwnerMgr->GetPaintWindow() : NULL;
 			DuiLibDiag::Log(_T("COMBO-DECIDE KILLFOCUS newFocus=%p self=%p ownerHwnd=%p -> %s alive=%ums"),
-				(HWND)wParam, m_hWnd, m_pOwner->GetManager()->GetPaintWindow(),
+				(HWND)wParam, m_hWnd, hOwnerWnd,
 				(m_hWnd != (HWND)wParam) ? _T("延迟一帧判定") : _T("忽略(焦点回到自己)"),
 				(unsigned)(s_dwComboOpenTick != 0 ? (::GetTickCount() - s_dwComboOpenTick) : 0));
 			// ⚠️ 不能在这里当场 PostMessage(WM_CLOSE):
@@ -788,6 +808,51 @@ namespace DuiLib {
 	////////////////////////////////////////////////////////
 	IMPLEMENT_DUICONTROL(CComboUI)
 
+	// ======================================================================
+	// ★★【第 42 轮 · "手指快速触摸点击 Combo 程序直接崩"】owner 死了必须同步收掉下拉窗 ★★
+	//
+	// 【为什么必须有这个析构】下拉窗(CComboWnd)是一个**独立顶层窗口**,它的生命周期
+	//   跟 owner 控件**没有**任何自动绑定关系。而它收尾时(OnFinalMessage)会回头写 owner:
+	//       m_pOwner->m_pWindow = NULL;
+	//       m_pOwner->m_uButtonState &= ~UISTATE_PUSHED;
+	//       m_pOwner->Invalidate();
+	//   ⇒ 只要 owner 先被销毁(设置页重建 / 控件被 Remove+delete),这三行就是
+	//     **写已释放内存** ⇒ 访问违例 ⇒ 进程直接崩。
+	//
+	// 【为什么触摸下特别容易撞上(鼠标不犯的原因)】
+	//   鼠标按在 Combo/item 上时,系统会**先激活 owner**(激活发生在进入窗口过程之前),
+	//   下拉窗随即因失焦而关掉 —— 这个"关闭"通常发生在 owner 被销毁**之前**。
+	//   而触摸的点击是本项目触摸链**合成**的(CPaintManagerUI::TouchUp),
+	//   **不改变窗口激活** ⇒ 下拉窗的关闭只能靠显式收尾(itemclick / Activate 复用分支),
+	//   于是它**可能晚于** owner 的销毁(业务在 itemselect 回调里重建设置页时尤其如此)
+	//   ⇒ 撞上悬垂写的概率远高于鼠标。这就是"手指点崩、鼠标点不崩"的分界线。
+	//
+	// ⚠️⚠️ 为什么**不能**照抄 DuiLib 原版的 `if (m_pWindow) m_pWindow->Close();`
+	//   本项目 `CWindowWnd::Close()` 末尾是 `PostMessage(WM_CLOSE, ...)` —— **异步**!
+	//   在析构里 Post 一条消息,真正的销毁与 OnFinalMessage 会推迟到本对象
+	//   **析构完成之后**才跑 ⇒ 悬垂照旧,等于没修。
+	//
+	// ⇒ 修法:**同步**走完同一条收尾链,让 OnFinalMessage 在本对象仍然有效时跑完。
+	//   SendMessage(WM_CLOSE) 会进 CComboWnd::HandleMessage 的 WM_CLOSE 分支
+	//   (把 items 归还给 owner 的 manager、复位 owner 状态 —— 这段收尾不能跳过),
+	//   随后由基类/DefWindowProc 销毁窗口 ⇒ WM_NCDESTROY ⇒ OnFinalMessage(同步)。
+	//   仍不放心就再 DestroyWindow 兜一次;两处都用线程 ID 校验,避免句柄被系统
+	//   复用到别的窗口时误杀。
+	// ======================================================================
+	CComboUI::~CComboUI()
+	{
+		if( m_pWindow == NULL ) return;
+
+		HWND hDropWnd = m_pWindow->GetHWND();
+		if( hDropWnd != NULL && ::IsWindow(hDropWnd)
+			&& ::GetWindowThreadProcessId(hDropWnd, NULL) == ::GetCurrentThreadId() )
+		{
+			::SendMessage(hDropWnd, WM_CLOSE, 0L, 0L);
+			if( ::IsWindow(hDropWnd) ) ::DestroyWindow(hDropWnd);
+		}
+		m_pWindow = NULL;   // OnFinalMessage 里已置 NULL;这里兜底,防它没跑到
+	}
+
 	CComboUI::CComboUI() : m_uTextStyle(DT_VCENTER | DT_SINGLELINE)
 		, m_dwTextColor(0)
 		, m_dwDisabledTextColor(0)

+ 1 - 0
DuiLib/Control/UICombo.h

@@ -15,6 +15,7 @@ namespace DuiLib {
 		friend class CComboWnd;
 	public:
 		CComboUI();
+		~CComboUI();
 
 		LPCTSTR GetClass() const;
 		LPVOID GetInterface(LPCTSTR pstrName);

+ 44 - 0
DuiLib/Core/UIManager.cpp

@@ -5796,6 +5796,50 @@ namespace DuiLib
 			// 这条通道是"抢焦点"那条路径的兜底:即使某处又漏了一笔收回,
 			// 只要用户再点一次这个输入框,键盘焦点就能回来。
 			// ==============================================================
+			// ==============================================================
+			// ★★★【r43 · "手指点已展开的 Combo ⇒ 进程直接崩"的修点】★★★
+			//
+			// 本分支(早退)的语义是"焦点目标没变"。但它用 `pControl == m_pFocus`
+			// 表达这件事时,`SetFocus(NULL)` 会让 **`NULL == NULL` 也成立** ——
+			// 此刻两边都**没有控件**,再往下走 `pControl->Event(...)` 就是
+			// **空指针解引用 ⇒ 访问违例 ⇒ 进程直接崩**。
+			// ⇒ 凡走"补派"这条路,必须先确认**真的存在那个控件**。
+			//
+			// 真机触发链(这一条同时解释了"只有触摸崩、鼠标不崩、点下拉项不崩"):
+			//   ① 手指按在已展开 Combo 的 **owner** 上 ⇒ 系统先激活 owner
+			//      (指针激活,发生在进入窗口过程之前)⇒ 下拉窗收到 WM_KILLFOCUS
+			//      ⇒ 本窗口 PostMessage(延迟一帧判定);
+			//   ② 手指还按着,消息泵把那条判定取走 ⇒ 触点确实落在 owner 上
+			//      ⇒ 走"赦免"分支 ⇒ `::SetFocus(下拉窗)`
+			//      ⇒ **键盘焦点回到下拉窗自己**;
+			//   ③ 手指抬起 ⇒ 合成点击 ⇒ `Activate()` ⇒ `PostCloseOnce()` ⇒ WM_CLOSE;
+			//   ④ 下拉窗处理 WM_CLOSE ⇒ 它**自己的** manager(`m_pm`,其 `m_pFocus`
+			//      从未被任何控件占用过 = NULL)执行 `case WM_CLOSE` 里的
+			//      `SetFocus(NULL)` ⇒ 命中本分支 ⇒ `NULL->Event()` ⇒ 崩。
+			//
+			//   · 点下拉项为什么不崩:那条路 `CComboUI::SelectItem` 会
+			//     `item->SetFocus()` ⇒ 下拉窗 manager 的 `m_pFocus` 是那个 item(非空)
+			//     ⇒ 走下面**正常路径**(先给旧焦点派 KILLFOCUS,再
+			//     `if (pControl == NULL) return;`)⇒ 碰不到空指针。
+			//   · 鼠标为什么不崩:鼠标点击时系统**先激活 owner**,而 BUTTONDOWN 是在
+			//     **同一个 WndProc 调用**里派发的 ⇒ `Activate()` 抢在队列里那条
+			//     "延迟一帧判定"**之前**把 `m_bClosePosted` 置真 ⇒ 判定当场早退
+			//     ⇒ 第②步的赦免**不会发生** ⇒ WM_CLOSE 时键盘焦点还在 owner 画布窗口
+			//     ⇒ `hFocusWnd != m_hWndPaint` ⇒ 根本进不了本分支。
+			//     触摸输掉这场竞速,是因为它的点击要等到**抬手**才被合成
+			//     (按下与抬起之间消息泵已经转过了)。
+			//   · Win7/Win10 都崩:两条触摸通道的下拉窗最终都是"自己持有键盘焦点"
+			//     (Win7 靠提升副本把 owner 重新激活、Win10 靠第②步的赦免),
+			//     与走的是 WM_TOUCH 还是 WM_POINTER* 无关。
+			//
+			// 顺带修掉一颗**同源的雷**:任何"窗口自己持有键盘焦点 + m_pFocus 为空"
+			//   时收到 WM_CLOSE(例如程序退出时主窗口的 WM_CLOSE)都会踩到这里。
+			// ==============================================================
+			if (pControl == NULL)
+			{
+				DuiLibDiag::Log(_T("MGR-SETFOCUS 放弃焦点(两边都没有控件) -> 不做任何派发"));
+				return;
+			}
 			if (hFocusWnd == m_hWndPaint)
 			{
 				DuiLibDiag::Log(_T("MGR-SETFOCUS 目标未变但键盘焦点停在画布窗口 -> 补派 UIEVENT_SETFOCUS(不发业务通知)"));

+ 9 - 3
DuiLib/Utils/UIDiag.cpp

@@ -16,10 +16,16 @@
 
 namespace DuiLibDiag
 {
-	// ★ 全库唯一的日志开关:需要关闭诊断时改这里(返回 false)。
+	// ★ 全库唯一的日志开关:需要重新开启诊断时把这里改回 true 并重编。
+	//
+	// 2026-09-24(第 41 轮):Win10 触摸键盘输入问题(第 40 轮修:CEditUI 合成鼠标手势
+	// 只发 DOWN 不发 UP ⇒ 原生 EDIT 停在半截手势)已真机验收通过,收尾关闭诊断输出。
+	// 关闭后 Enabled() 是编译期常量 false,整个 Log 函数体会被当死代码消除
+	// ⇒ 运行时零开销、零文件 IO,二进制里也不再出现 touch_trace.log / DIAG-BUILD 字样。
+	// 埋点代码全部保留在源码里,随时改回 true 重编即可复现现场。
 	inline bool Enabled()
 	{
-		return true;
+		return false;
 	}
 
 	// 极简 TCHAR* → UTF-8 char* 转换(仅用于日志路径;非线程安全,够用即可)
@@ -83,7 +89,7 @@ namespace DuiLibDiag
 		{
 			if (bWriteBanner)
 			{
-				fprintf(f, "======== DIAG-BUILD 2026-09-24 r40 (pair LButtonDown/Up + edit state probe) ========\n");
+				fprintf(f, "======== DIAG-BUILD 2026-09-24 r43 (SetFocus(NULL) null-deref fix; diagnostics OFF by default) ========\n");
 			}
 			fprintf(f, "[%8u][T%5u] %s\n", (unsigned)::GetTickCount(), (unsigned)::GetCurrentThreadId(), utf8);
 			// 立刻冲盘:程序被强杀 / 挂死 / 断电时,日志尾部(往往正是关键现场)不能丢。

BIN
bin/Win32/Debug/zhipuzi_pos_windows/DuiLib_d.dll


BIN
bin/Win32/Release/zhipuzi_pos_windows/DuiLib.dll


BIN
bin/Win32/Release/zhipuzi_pos_windows/zhipuzi_pos_windows.exe


BIN
dll/debug/DuiLib_d.dll


BIN
dll/release/DuiLib.dll


BIN
lib/debug/DuiLib_d.lib


BIN
lib/release/DuiLib.lib


BIN
pdb/release/DuiLib.pdb