Browse Source

保存,主要修复win10的触摸滑动。

张洋 3 weeks ago
parent
commit
e53ad8706b

+ 1 - 0
.gitignore

@@ -11,3 +11,4 @@ zhipuzi_pos_windows.exe
 zhipuzi_pos_windows.pdb
 /bin/Win32/Debug/zhipuzi_pos_windows/DuiLib_d.pdb
 /.workbuddy
+/DuiLib/temp

+ 2 - 2
DuiLib/Control/UIButton.cpp

@@ -219,8 +219,8 @@ namespace DuiLib
 		if( !CControlUI::Activate() ) return false;
 		if( m_pManager != NULL )
 		{
-			// [第十一轮诊断] 见 UIlib.h 里 DuiLibDiag 的说明(TouchTrace 是
-			// UIManager.cpp 匿名命名空间里的,别的 .cpp 取不到,故走这个公共出口)。
+			// 诊断出口:见 Utils/UIDiag.h。
+			// (现在全库诊断都走 DuiLibDiag::Log,只有 UIDiag.cpp 真正落盘。)
 			DuiLibDiag::LogButtonActivate(GetName().GetData(),
 				m_pManager->IsCaptured(),
 				(unsigned)m_uButtonState,

+ 9 - 6
DuiLib/Control/UIButton.h

@@ -29,14 +29,17 @@ namespace DuiLib
 		//   弹框把 manager 搬走时 CAPTURED 被清掉,导致弹框关闭后那条补发的
 		//   BUTTONUP 进不了分支,PUSHED 就永远留着。
 		//
-		// CControlUI 基类里没有可用的"清按下态"虚函数,所以这里自己提供一个,
-		// 由 CPaintManagerUI 在模态搬家时按需调用(走 GetInterface)。
+		// CControlUI 基类里现在有了 `virtual void ClearPushedState()`(默认空实现),
+		// 本类把它 override 成"清 PUSHED|CAPTURED|HOT 三个显示位"。
+		// 于是 CPaintManagerUI::ClearPushedStateAt 只需按基类指针调用,
+		// 就能同时覆盖 CButtonUI 与业务自绘控件(如 CDiandanFoodItemUI)。
 		//
-		// ⚠️ 只用于"搬家"这种确定性时点。**不要**拿它去给触摸抬手打补丁 ——
-		//    触摸路径的按下态残留是**消息顺序**问题(状态收尾被夹在业务回调之前),
-		//    已在 CPaintManagerUI::TapCommitControl 里按顺序修正,不靠事后补清。
+		// ⚠️ 只用于"搬家"与"触摸滑动起手"这种确定性时点。
+		//    **不要**拿它去给触摸抬手打补丁 —— 触摸路径的按下态残留是
+		//    **消息顺序**问题(状态收尾被夹在业务回调之前),已在
+		//    EndTouchSlideGesture / TouchUp 里按顺序修正,不靠事后补清。
 		// ------------------------------------------------------------------
-		void ClearPushedState();
+		virtual void ClearPushedState();
 
 		virtual LPCTSTR GetNormalImage();
 		virtual void SetNormalImage(LPCTSTR pStrImage);

+ 17 - 17
DuiLib/Core/UIBase.cpp

@@ -167,7 +167,7 @@ namespace {
             y = (int)(ti.y / 100);
             flags = ti.dwFlags;
         }
-        DuiLib::TouchTrace::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"),
+        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,
@@ -255,7 +255,7 @@ namespace {
         }
 
         DWORD dwExtra = (DWORD)(DWORD_PTR)::GetMessageExtraInfo();
-        DuiLib::TouchTrace::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"),
+        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),
@@ -758,7 +758,7 @@ UINT CWindowWnd::ShowModal()
     HGLOBAL hResource = ::GlobalAlloc(GMEM_MOVEABLE, nDataSize);
     if( hResource == NULL )
     {
-        DuiLib::TouchTrace::Log(_T("MODAL-DLG GlobalAlloc failed err=%u"), ::GetLastError());
+        DuiLibDiag::Log(_T("MODAL-DLG GlobalAlloc failed err=%u"), ::GetLastError());
         return 0;
     }
     LPDLGTEMPLATE pTemplate = (LPDLGTEMPLATE)::GlobalLock(hResource);
@@ -794,7 +794,7 @@ UINT CWindowWnd::ShowModal()
     m_rcModalTarget.right  = rcWnd.left + nClientW;
     m_rcModalTarget.bottom = rcWnd.top  + nClientH;
 
-    DuiLib::TouchTrace::Log(_T("MODAL-DLG enter old=%p owner=%p wndrect=%d,%d,%dx%d client=%dx%d dlgstyle=0x%08X ex=0x%08X"),
+    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);
 
@@ -862,7 +862,7 @@ UINT CWindowWnd::ShowModal()
         // ★ V2 标记:真机自证用。看到 `gen=` 且 `v=2` 才说明跑的是**这一版**二进制。
         //   上一版这行没有 gen=/v= 字段,日志里出现的是 `MODAL-BEEP arm manager=... owner=...`
         //   ⇒ 如果新日志仍然没有 gen=/v=,那就是**没重新编译**,任何分析都不必做。
-        DuiLib::TouchTrace::Log(_T("MODAL-BEEP arm v=2 manager=%p owner=%p gen=%u"),
+        DuiLibDiag::Log(_T("MODAL-BEEP arm v=2 manager=%p owner=%p gen=%u"),
             (void*)pManager, (void*)hWndOwner, (unsigned)DuiLib::PeekPromotedMouseGeneration());
     }
 
@@ -880,7 +880,7 @@ UINT CWindowWnd::ShowModal()
     //    说明残留另有来源,需要继续查。
     if( pManager != NULL )
     {
-        DuiLib::TouchTrace::Log(_T("MODAL-STATE enter manager=%p hover=%s captured=%d dragmode=%d leaveArr=%d rclick=%s"),
+        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());
@@ -889,7 +889,7 @@ UINT CWindowWnd::ShowModal()
     if( pManager != NULL )
     {
         pManager->DetachPaintWindow();
-        DuiLib::TouchTrace::Log(_T("MODAL-DLG detach manager=%p from old=%p"),
+        DuiLibDiag::Log(_T("MODAL-DLG detach manager=%p from old=%p"),
             (void*)pManager, (void*)hWndOldCaptured);
     }
     // 销毁旧窗口(旧窗口上的窗口过程会把 m_hWnd 清空,但我们已经在上面置 NULL 了,
@@ -924,19 +924,19 @@ UINT CWindowWnd::ShowModal()
     if( pManager != NULL )
     {
         pManager->SetSuppressPromotedMouseForModal(false);
-        DuiLib::TouchTrace::Log(_T("MODAL-BEEP disarm manager=%p"), (void*)pManager);
+        DuiLibDiag::Log(_T("MODAL-BEEP disarm manager=%p"), (void*)pManager);
     }
 
     m_bDoModal = false;
     CPaintManagerUI::SetModalWindow(hPrevModal);
 
-    DuiLib::TouchTrace::Log(_T("MODAL-DLG exit hwnd=%p ret=%d m_nModalRet=%u"),
+    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 )
     {
-        DuiLib::TouchTrace::Log(_T("MODAL-DLG failed err=%u"), ::GetLastError());
+        DuiLibDiag::Log(_T("MODAL-DLG failed err=%u"), ::GetLastError());
         return 0;
     }
 
@@ -976,7 +976,7 @@ INT_PTR CALLBACK CWindowWnd::__DialogProc(HWND hDlg, UINT uMsg, WPARAM wParam, L
         // ---- 触摸注册:对话框是全新的 HWND,必须注册,否则收不到 WM_TOUCH ----
         RegisterTouchWindowIfAvailable(hDlg);
 
-        DuiLib::TouchTrace::Log(_T("MODAL-DLG init dlg=%p oldwndproc=%p focus=%p"),
+        DuiLibDiag::Log(_T("MODAL-DLG init dlg=%p oldwndproc=%p focus=%p"),
             (void*)hDlg, (void*)pThis->m_pfnOldDlgWndProc, (void*)::GetFocus());
 
         // ---- 把旧的 CPaintManagerUI 挂到对话框窗口上 ----
@@ -986,14 +986,14 @@ INT_PTR CALLBACK CWindowWnd::__DialogProc(HWND hDlg, UINT uMsg, WPARAM wParam, L
         if( pManager != NULL )
         {
             pManager->AttachPaintWindow(hDlg);
-            DuiLib::TouchTrace::Log(_T("MODAL-DLG attach manager=%p to dlg=%p"), (void*)pManager, (void*)hDlg);
+            DuiLibDiag::Log(_T("MODAL-DLG attach manager=%p to dlg=%p"), (void*)pManager, (void*)hDlg);
             // 用掉即清:避免这个文件级静态变量残留一个已失效的指针,
             // 影响下一次 ShowModal(尤其嵌套弹框:A 弹框里再开 B 弹框)。
             SetDetachedManager(NULL);
         }
         else
         {
-            DuiLib::TouchTrace::Log(_T("MODAL-DLG no detached manager (dialog will Init itself)"));
+            DuiLibDiag::Log(_T("MODAL-DLG no detached manager (dialog will Init itself)"));
         }
 
         // ---- [WM_TOUCH 归属权] 立刻把"补调原窗口过程"的钩子绑到本弹框 ----
@@ -1039,7 +1039,7 @@ INT_PTR CALLBACK CWindowWnd::__DialogProc(HWND hDlg, UINT uMsg, WPARAM wParam, L
 
             RECT rcCur = { 0 };
             ::GetWindowRect(hDlg, &rcCur);
-            DuiLib::TouchTrace::Log(_T("MODAL-DLG resize before cur=%dx%d clientTarget=%dx%d -> wnd=%dx%d hide=%d"),
+            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));
@@ -1052,7 +1052,7 @@ INT_PTR CALLBACK CWindowWnd::__DialogProc(HWND hDlg, UINT uMsg, WPARAM wParam, L
 
             RECT rcAfter = { 0 };
             ::GetClientRect(hDlg, &rcAfter);
-            DuiLib::TouchTrace::Log(_T("MODAL-DLG resize after client=%dx%d (target=%dx%d)"),
+            DuiLibDiag::Log(_T("MODAL-DLG resize after client=%dx%d (target=%dx%d)"),
                 rcAfter.right - rcAfter.left, rcAfter.bottom - rcAfter.top, nClientW, nClientH);
         }
 
@@ -1227,12 +1227,12 @@ void CWindowWnd::Close(UINT nRet)
             if (pMgrClosing != NULL)
             {
                 pMgrClosing->SetSuppressPromotedMouseForModal(false);
-                DuiLib::TouchTrace::Log(_T("MODAL-BEEP disarm-on-close manager=%p hwnd=%p"),
+                DuiLibDiag::Log(_T("MODAL-BEEP disarm-on-close manager=%p hwnd=%p"),
                     (void*)pMgrClosing, (void*)m_hWnd);
             }
         }
 
-        DuiLib::TouchTrace::Log(_T("MODAL-DLG EndDialog hwnd=%p ret=%u"), (void*)m_hWnd, nRet);
+        DuiLibDiag::Log(_T("MODAL-DLG EndDialog hwnd=%p ret=%u"), (void*)m_hWnd, nRet);
         ::EndDialog(m_hWnd, (INT_PTR)nRet);
         return;
     }

+ 4 - 4
DuiLib/Core/UIBase.h

@@ -43,10 +43,10 @@ namespace DuiLib {
 	void UILIB_API DUI__Trace(LPCTSTR pstrFormat, ...);
 	LPCTSTR UILIB_API DUI__TraceMsg(UINT uMsg);
 
-	namespace TouchTrace
-	{
-		void UILIB_API Log(LPCTSTR pstrFormat, ...);
-	}
+	// 诊断日志出口已**统一到 `DuiLibDiag`**(Utils/UIDiag.h)。
+	// 此处原先声明的 `namespace TouchTrace { void Log(LPCTSTR, ...); }` 已删除
+	// —— UIManager.cpp 里的那份本地实现没了,声明留着只会让漏改的调用点
+	// 变成"能编译但链接失败",反而不如让它直接在编译期报错。
 
 	/////////////////////////////////////////////////////////////////////////////////////
 	//

+ 22 - 0
DuiLib/Core/UIControl.cpp

@@ -841,6 +841,28 @@ namespace DuiLib
 		if (OnEvent(&event)) DoEvent(event);
 	}
 
+	void CControlUI::ClearPushedState()
+	{
+		// ◆【滑动起手误按压 · 基类默认实现】
+		//
+		// 语义:把"因手指/鼠标按下而进入的按压视觉"复位为普通态,
+		//       **只改视觉,不发任何通知、不 Activate、不碰业务数据**。
+		//
+		// 这里刻意留空:绝大多数控件的"按下视觉"是 CControlUI 自己的
+		// 状态机(m_uButtonState 那套,属于 CButtonUI)或根本不存在按压视觉,
+		// 对它们调用本函数必须是**安全 no-op**。
+		//
+		// 真正需要覆盖的是那些**自绘、且继承自布局类**的业务控件 ——
+		// 它们的按下态靠"换背景图/切样式"实现,与 m_uButtonState 无关。
+		// 典型:本项目的 CDiandanFoodItemUI : CVerticalLayoutUI,
+		//       它的 TOUCHDOWN 会 SetCardState("pressed"),但没有状态位可查。
+		// 这类控件在自己的头文件里 override 本函数即可被库侧按需清扫,
+		// 而库不需要认识业务的任何一个具体类。
+		//
+		// 调用方:CPaintManagerUI::ClearPushedStateAt(触摸链判定为滑动时,
+		//         对起手命中的控件补一次清扫)。
+	}
+
 	void CControlUI::DoEvent(TEventUI& event)
 	{
 		// [触屏模型] 触摸事件不做“归一化”:CPaintManagerUI::OnTouchInput 只派发原生

+ 25 - 0
DuiLib/Core/UIControl.h

@@ -175,6 +175,31 @@ namespace DuiLib {
 		virtual void Event(TEventUI& event);
 		virtual void DoEvent(TEventUI& event);
 
+		// ◆【滑动起手误按压 · 公共清接口】
+		//
+		// 语义:把本控件"因手指/鼠标按下而进入的按压视觉"复位为普通态。
+		// **只改视觉 —— 不发任何通知、不 Activate、不碰业务数据。**
+		//
+		// 为什么放在基类而不是继续用 CButtonUI::ClearPushedState:
+		//   CPaintManagerUI::ClearPushedStateAt 原先走的是
+		//   `GetInterface(DUI_CTR_BUTTON)` ⇒ 只有 CButtonUI 及少数实现了该接口的
+		//   控件能被清到。业务侧那些**自绘的、继承自布局类**的控件
+		//   (如本项目的 CDiandanFoodItemUI : CVerticalLayoutUI,按下态靠换背景图
+		//     实现,根本没有 m_uButtonState)会被那条早退路直接跳过 ⇒
+		//   触摸滑动起手时留下按压态背景(用户实测 bug)。
+		//
+		// 有了基类虚函数,库侧只要按基类指针 `pControl->ClearPushedState()`
+		// 调用即可自动虚分派,不需要库去认识业务的每一个自绘控件。
+		//
+		// 默认空实现:绝大多数控件没有"按压视觉",调用它必须是安全的 no-op。
+		//
+		// ⚠️⚠️ 这里只做**声明**,定义在 UIControl.cpp。
+		//   绝不能写成类内 `{ }` 内联体 —— 那样会与 .cpp 里的定义构成
+		//   "同一函数两处定义" ⇒ **C2084「函数已有主体」**(真机踩过)。
+		//   规则:类内给函数体 = 隐式 inline 且**构成定义**;
+		//         若 .cpp 里还要写一份,头文件就必须只留 `;`。
+		virtual void ClearPushedState();
+
 		// 自定义(未处理的)属性
 		void AddCustomAttribute(LPCTSTR pstrName, LPCTSTR pstrAttr);
 		LPCTSTR GetCustomAttribute(LPCTSTR pstrName) const;

+ 156 - 141
DuiLib/Core/UIManager.cpp

@@ -95,72 +95,17 @@ namespace DuiLib
 		}
 	}
 	// ---------------------------------------------------------------------------
-	// [触屏诊断] 保留日志入口以便后续定位,但当前默认关闭。
+	// [触屏诊断] 日志出口已**统一到 `DuiLibDiag`**(Utils/UIDiag.h/.cpp)。
+	//
+	// 原先这里有一份 `namespace TouchTrace { void Log(LPCTSTR, ...); }` 的本地实现,
+	// 与 UIDiag 的写法(`[tick][tid] 文本`、UTF-8、追加写 exe 同目录 touch_trace.log、
+	// 32MB 封顶)完全重复,且因为处于**匿名 namespace** 而无法被别的 .cpp 引用
+	// —— 这正是当初另起 UIDiag 的原因。
+	//
+	// 现在本文件(以及 UIBase.cpp / UIButton.cpp)的所有诊断调用一律写
+	// `DuiLibDiag::Log(...)`,只有 UIDiag.cpp 一处真正落盘。
+	// 关日志 / 改路径 / 改封顶值 ⇒ **只改 UIDiag.cpp 的 Enabled()**,不要在本文件加实现。
 	// ---------------------------------------------------------------------------
-	namespace TouchTrace
-	{
-		inline bool Enabled()
-		{
-				return true;
-		}
-
-		// 极简 TCHAR* → UTF-8 char* 转换(仅用于日志路径;非线程安全,够用即可)
-		inline const char* Narrow(LPCTSTR s)
-		{
-			static char s_buf[MAX_PATH * 3] = { 0 };
-#ifdef _UNICODE
-			s_buf[0] = '\0';
-			if (s != NULL)
-				::WideCharToMultiByte(CP_UTF8, 0, s, -1, s_buf, (int)sizeof(s_buf) - 1, NULL, NULL);
-			return s_buf;
-#else
-			return s;
-#endif
-		}
-
-		void Log(LPCTSTR fmt, ...)
-		{
-			if (!Enabled()) return;
-
-			TCHAR path[MAX_PATH] = { 0 };
-			::GetModuleFileName(NULL, path, MAX_PATH);
-			TCHAR* p = _tcsrchr(path, _T('\\'));
-			if (p != NULL) *(p + 1) = _T('\0');
-			_tcscat_s(path, MAX_PATH, _T("touch_trace.log"));
-
-			TCHAR msg[1024] = { 0 };
-			va_list ap;
-			va_start(ap, fmt);
-			_vsntprintf_s(msg, _countof(msg), _TRUNCATE, fmt, ap);
-			va_end(ap);
-
-			// 统一转 UTF-8 写盘(Unicode/ANSI 工程都能用文本编辑器正常查看)
-			char utf8[2048] = { 0 };
-#ifdef _UNICODE
-			if (::WideCharToMultiByte(CP_UTF8, 0, msg, -1, utf8, (int)sizeof(utf8) - 1, NULL, NULL) <= 0)
-				return;
-#else
-			_tcscpy_s(utf8, _countof(utf8), msg);
-#endif
-			FILE* f = NULL;
-			// 保护:单个日志超过 32MB 就不再追加,避免在收银机上长期运行把日志写到失控。
-			// (全量 INPUT-TRACE 很啰嗦,阈值从 4MB 放宽到 32MB,否则一轮诊断还没跑完就静默停写了。)
-			// 注意:GetFileAttributesEx 是 TCHAR 宏(Unicode 工程下展开为 ...ExW),
-			// 必须传 TCHAR 的 path,不能传 Narrow(path) 得到的 UTF-8 char*(否则 C2664)。
-			WIN32_FILE_ATTRIBUTE_DATA fad = { 0 };
-			if (::GetFileAttributesEx(path, GetFileExInfoStandard, &fad)
-				&& fad.nFileSizeHigh == 0 && fad.nFileSizeLow >= (32u * 1024u * 1024u))
-				return;
-			// fopen_s 只吃 char*(窄字符路径),这里才需要 UTF-8 的 Narrow(path)
-			if (fopen_s(&f, Narrow(path), "a") == 0 && f != NULL)
-			{
-				fprintf(f, "[%8u][T%5u] %s\n", (unsigned)::GetTickCount(), (unsigned)::GetCurrentThreadId(), utf8);
-				// 立刻冲盘:程序被强杀 / 挂死 / 断电时,日志尾部(往往正是关键现场)不能丢。
-				fflush(f);
-				fclose(f);
-			}
-		}
-	}
 
 	static void GetChildWndRect(HWND hWnd, HWND hChildWnd, RECT& rcChildWnd)
 	{
@@ -492,7 +437,7 @@ namespace DuiLib
 			if (dwLen > 0 && dwLen < _countof(szBuf) && szBuf[0] == _T('1'))
 			{
 				m_bRelayDefProcForTouchMove = true;
-				TouchTrace::Log(_T("DEFPROC-RELAY enabled by env (DUI_RELAY_DEFPROC=1)"));
+				DuiLibDiag::Log(_T("DEFPROC-RELAY enabled by env (DUI_RELAY_DEFPROC=1)"));
 			}
 		}
 
@@ -646,7 +591,7 @@ namespace DuiLib
 		}
 		const int nIndex = m_aPreMessages.Find(this);
 		if (nIndex >= 0) m_aPreMessages.Remove(nIndex);
-		TouchTrace::Log(_T("MANAGER-DETACH manager=%p from hwnd=%p"), (void*)this, (void*)m_hWndPaint);
+		DuiLibDiag::Log(_T("MANAGER-DETACH manager=%p from hwnd=%p"), (void*)this, (void*)m_hWndPaint);
 		// 注意:m_hWndPaint 暂不置空 —— AttachPaintWindow 会覆盖它。
 		// 也不要清空控件树 / 字体 / 图片:那些是"内容",迁移后要继续用。
 	}
@@ -719,7 +664,7 @@ namespace DuiLib
 		// 新窗口必须注册触摸,否则收不到 WM_TOUCH。
 		InitTouchSupport();
 
-		TouchTrace::Log(_T("MANAGER-ATTACH manager=%p to hwnd=%p dc=%p (input-state reset)"),
+		DuiLibDiag::Log(_T("MANAGER-ATTACH manager=%p to hwnd=%p dc=%p (input-state reset)"),
 			(void*)this, (void*)m_hWndPaint, (void*)m_hDcPaint);
 	}
 
@@ -1120,7 +1065,7 @@ namespace DuiLib
 			if (hModal == NULL || !::IsWindow(hModal)) return;   // 无模态:沉默
 			if (hWnd == hModal) return;                          // 弹框自己:沉默(正常)
 			HWND hOwner = ::GetWindow(hModal, GW_OWNER);
-			DuiLib::TouchTrace::Log(_T("MODAL-GATE-CHECK where=%s self=%p modal=%p ownerOfModal=%p selfIsOwner=%d"),
+			DuiLibDiag::Log(_T("MODAL-GATE-CHECK where=%s self=%p modal=%p ownerOfModal=%p selfIsOwner=%d"),
 				pszWhere, (void*)hWnd, (void*)hModal, (void*)hOwner,
 				(int)(hOwner == hWnd));
 		}
@@ -1207,7 +1152,7 @@ namespace DuiLib
 	void CPaintManagerUI::SetModalWindow(HWND hWnd)
 	{
 		s_hModalWindow = hWnd;
-		TouchTrace::Log(_T("MODAL-WINDOW set hwnd=%p"), (void*)hWnd);
+		DuiLibDiag::Log(_T("MODAL-WINDOW set hwnd=%p"), (void*)hWnd);
 	}
 
 	HWND CPaintManagerUI::GetModalWindow()
@@ -1223,7 +1168,7 @@ namespace DuiLib
 		BOOL bRegistered = FALSE;
 		if (pfnRegister != NULL)
 			bRegistered = pfnRegister(m_hWndPaint, 0);
-		TouchTrace::Log(_T("REGISTER hwnd=%p manager=%p result=%d error=%u style=0x%08X ex=0x%08X"),
+		DuiLibDiag::Log(_T("REGISTER hwnd=%p manager=%p result=%d error=%u style=0x%08X ex=0x%08X"),
 			(void*)m_hWndPaint, (void*)this, (int)bRegistered, bRegistered ? 0u : ::GetLastError(),
 			(unsigned)::GetWindowLong(m_hWndPaint, GWL_STYLE),
 			(unsigned)::GetWindowLong(m_hWndPaint, GWL_EXSTYLE));
@@ -1262,7 +1207,7 @@ namespace DuiLib
 	// ===========================================================================
 	void CPaintManagerUI::BeginTouchSlideGesture(POINT pt)
 	{
-		TouchTrace::Log(_T("SLIDE-BEGIN pt=%d,%d"), pt.x, pt.y);
+		DuiLibDiag::Log(_T("SLIDE-BEGIN pt=%d,%d"), pt.x, pt.y);
 
 		m_ptLastTouchPos = pt;
 		// 触摸没有"悬浮"语义。新手势开始前先清掉上一次鼠标路径遗留的 HOT 状态,
@@ -1284,14 +1229,14 @@ namespace DuiLib
 		CControlUI* pControl = FindControl(pt);
 		if (pControl == NULL)
 		{
-			TouchTrace::Log(_T("SLIDE-BEGIN no control pt=%d,%d"), pt.x, pt.y);
+			DuiLibDiag::Log(_T("SLIDE-BEGIN no control pt=%d,%d"), pt.x, pt.y);
 			// 没命中控件也要保持 active:随后的 Update 仍需被安全吸收掉,
 			// 由 End 统一收尾(否则 Update 会跑到空指针上)。
 			return;
 		}
 		if (pControl->GetManager() != this)
 		{
-			TouchTrace::Log(_T("SLIDE-BEGIN foreign control=%s manager=%p self=%p"),
+			DuiLibDiag::Log(_T("SLIDE-BEGIN foreign control=%s manager=%p self=%p"),
 				pControl->GetName().GetData(), (void*)pControl->GetManager(), (void*)this);
 			return;
 		}
@@ -1305,7 +1250,7 @@ namespace DuiLib
 				pBar = Touch_ResolveScrollBarThumb(pControl, pt);
 			if (pBar != NULL)
 			{
-				TouchTrace::Log(_T("SLIDE-BEGIN-FATFINGER hit=%s -> scrollbar=%s pt=%d,%d"),
+				DuiLibDiag::Log(_T("SLIDE-BEGIN-FATFINGER hit=%s -> scrollbar=%s pt=%d,%d"),
 					pControl->GetName().GetData(), pBar->GetName().GetData(), pt.x, pt.y);
 				pControl = pBar;
 			}
@@ -1316,7 +1261,7 @@ namespace DuiLib
 		// 但 m_ptLastTouchPos / m_ptTouchStart 仍记真实坐标(去重与位移判定按真实手指轨迹走)。
 		const POINT ptEvent = bTouchIsScrollBar ? Touch_SnapIntoScrollBar(pControl, pt) : pt;
 
-		TouchTrace::Log(_T("SLIDE-BEGIN hit=%s cls=%s scrollbar=%d scrollContainer=%p ptEvent=%d,%d"),
+		DuiLibDiag::Log(_T("SLIDE-BEGIN hit=%s cls=%s scrollbar=%d scrollContainer=%p ptEvent=%d,%d"),
 			pControl->GetName().GetData(), pControl->GetClass(), (int)bTouchIsScrollBar,
 			(void*)Touch_FindScrollContainer(pControl), ptEvent.x, ptEvent.y);
 
@@ -1398,6 +1343,21 @@ namespace DuiLib
 		//   (手指划过屏幕的位移通常数十至数百像素)必然超过。
 		//
 		// ⚠️ 若要调整,只改这一个常量 —— 两条通道共用本函数,改一处即全局生效。
+		//
+		// ⚠️⚠️【不要擅自再加"单包位移"判据 —— 已评估后主动放弃】⚠️⚠️
+		//   动机看似合理:真机日志里"快速划动"时前两包累计才 11px、
+		//   第三包单包突然 55px,若按单包判能提前 ~60ms 判定为滑动。
+		//   但必须意识到:`m_bTouchMoved` **不只是**"清扫按压态的时机",
+		//   它同时是**"这次手势算不算点击"的唯一判据**(见 EndTouchSlideGesture
+		//   的 `bTapLike = !m_bTouchMoved`)。
+		//   ⇒ 放宽它 = 放宽"点击被误判为滑动"的口径 = 有概率让**正常点击点不动**。
+		//   而当前真机日志里 13 次手势中**只有 1 次真实点击**(其余全是有意滑动),
+		//   **样本不足以评估点击侧风险**。
+		//   ⇒ 在没有"点击被误判"的直接证据前,绝不动这个判据。
+		//     (历史教训同源:曾为"理论风险"收窄判据,结果真机主路径直接失效。)
+		//   清扫时机已经由"抬手"提前到"越过阈值那一帧"(157ms → ~78ms),
+		//   这已解决用户可感知的问题;剩余 ~78ms 是"区分点/滑"的**固有代价**,
+		//   不可能再压缩 —— DOWN 那一刻本来就不可能知道用户要干什么。
 		static const LONG kTouchDragThresholdPx = 16;
 		if (ox > kTouchDragThresholdPx || ox < -kTouchDragThresholdPx
 			|| oy > kTouchDragThresholdPx || oy < -kTouchDragThresholdPx)
@@ -1405,7 +1365,7 @@ namespace DuiLib
 
 		if (m_pEventTouch == NULL) return;
 
-		TouchTrace::Log(_T("SLIDE-MOVE touch=%s cls=%s d=%d,%d moved=%d click=%s"),
+		DuiLibDiag::Log(_T("SLIDE-MOVE touch=%s cls=%s d=%d,%d moved=%d click=%s"),
 			m_pEventTouch->GetName().GetData(), m_pEventTouch->GetClass(),
 			(int)dx, (int)dy, (int)m_bTouchMoved,
 			m_pEventClick != NULL ? m_pEventClick->GetName().GetData() : _T("null"));
@@ -1458,7 +1418,7 @@ namespace DuiLib
 				evWheel.ptMouse = pt;
 				evWheel.wKeyState = MapKeyState();
 				evWheel.dwTimestamp = ::GetTickCount();
-				TouchTrace::Log(_T("SLIDE-MOVE->WHEEL control=%s zDelta=%d pt=%d,%d"),
+				DuiLibDiag::Log(_T("SLIDE-MOVE->WHEEL control=%s zDelta=%d pt=%d,%d"),
 					pWheel->GetName().GetData(), zDelta, pt.x, pt.y);
 				pWheel->Event(evWheel);
 			}
@@ -1493,11 +1453,10 @@ namespace DuiLib
 			// [NIM 风格] 这里不需要"解除让路"——让路已改由 m_pEventTouch 这个**标记**表达。
 			// ⚠️ 可变参数必须在 _T 宏的括号**之外**:_T 是单参数宏,
 			//   若把格式化参数一起写进它的括号里,会触发 C4002「类函数宏的调用参数过多」。
-			TouchTrace::Log(_T("SLIDE-END no-control pt=%d,%d (not a tap)"), pt.x, pt.y);
+			DuiLibDiag::Log(_T("SLIDE-END no-control pt=%d,%d (not a tap)"), pt.x, pt.y);
 			return false;
 		}
-
-		TouchTrace::Log(_T("SLIDE-END pt=%d,%d touch=%s moved=%d"),
+		DuiLibDiag::Log(_T("SLIDE-END pt=%d,%d touch=%s moved=%d"),
 			pt.x, pt.y,
 			m_pEventTouch->GetName().GetData(),
 			(int)m_bTouchMoved);
@@ -1561,6 +1520,48 @@ namespace DuiLib
 		m_bSuppressHoverAfterTouch = true;
 		m_bMouseTracking = false;
 
+		// ------------------------------------------------------------------
+		// ★【滑动起手的按压态】手指"划着走"时,按下瞬间命中的那个控件不能留按压态。
+		//
+		// 现象(用户实测):手指触屏滑动,起手碰到的控件虽然不会触发点击,
+		//   但会留下按压态背景(滑完才被别的东西擦掉,或一直留着)。
+		//
+		// 成因:BeginTouchSlideGesture 在 DOWN 时**无条件**派发 UIEVENT_TOUCHDOWN,
+		//   控件(如 CButtonUI::DoEvent)据此置 UISTATE_PUSHED|UISTATE_CAPTURED 并
+		//   Invalidate ⇒ 手指还没滑出控件之前,按压态已经画出来了。
+		//   而 UP 时这里派发的 UIEVENT_TOUCHUP 只清"抬手点所在控件"的状态:
+		//     · CButtonUI 的 TOUCHUP 分支虽然清 PUSHED|CAPTURED,但它**不判 PtInRect**,
+		//       看似能清掉 —— 问题在于它只发给 m_pEventTouch(起手命中的控件),
+		//       而滚动场景下 UpdateTouchSlideGesture 把 TOUCHMOVE 改发给了
+		//       m_pTouchScrollContainer,TOUCHUP 与 TOUCHMOVE 的收件人不一致,
+		//       一旦两个标记不指向同一对象,起手控件就**收不到 TOUCHUP**
+		//       ⇒ PUSHED|CAPTURED 永久残留 ⇒ 按压态背景不消失。
+		//
+		// 修法:判定为滑动时,把起手控件(以及滚动容器)的按压视觉清扫掉,
+		//   走 ClearPushedStateAt 的窄路径(只清视觉、不发通知)。
+		//
+		// ⚠️ 顺序是死线:必须在 TOUCHUP 派发**之前**清。
+		//   `ClearPushedStateAt` 会清 CButtonUI 的 UISTATE_CAPTURED,而那正是
+		//   `UIEVENT_BUTTONUP` 分支的守卫(`if ((m_uButtonState & UISTATE_CAPTURED) != 0)`)。
+		//   放在 TOUCHUP 之后清,会连带把"轻点合成点击"必需的 CAPTURED 一起清掉
+		//   ⇒ 按钮 Activate 不触发。但本分支只在 m_bTouchMoved(已判为滑动、不会合成点击)
+		//   时才进入,所以是安全的。
+		//
+		// ⚠️ 必须在下面 `m_pEventTouch = NULL` **之前**调用 —— 要用到起手控件。
+		// ------------------------------------------------------------------
+		if (m_bTouchMoved)
+		{
+			CControlUI* pSlideOrigin = m_pEventTouch;
+			CControlUI* pSlideScroll = m_pTouchScrollContainer;
+			ClearPushedStateAt(pSlideOrigin, pt);
+			if (pSlideScroll != NULL && pSlideScroll != pSlideOrigin)
+				ClearPushedStateAt(pSlideScroll, pt);
+			DuiLibDiag::Log(_T("SLIDE-END clear-pushed origin=%s scroll=%s pt=%d,%d"),
+				pSlideOrigin != NULL ? pSlideOrigin->GetName().GetData() : _T("null"),
+				pSlideScroll != NULL ? pSlideScroll->GetName().GetData() : _T("null"),
+				pt.x, pt.y);
+		}
+
 		// ⚠️⚠️ 【必须在返回之前清掉 m_pEventTouch】—— 这是"主窗口触摸点击全灭"的元凶之一。⚠️⚠️
 		//
 		// 死锁链条(每一步都真实存在):
@@ -1685,7 +1686,7 @@ namespace DuiLib
 		// 说明这是同一次触摸的另一条通道送来的一份重复消息 —— 整条忽略,绝不能双份驱动。
 		if (m_bPointerGestureActive)
 		{
-			TouchTrace::Log(_T("WM_TOUCH ignored (pointer channel owns this gesture) hwnd=%p inputs=%u"),
+			DuiLibDiag::Log(_T("WM_TOUCH ignored (pointer channel owns this gesture) hwnd=%p inputs=%u"),
 				(void*)m_hWndPaint, (unsigned)LOWORD(wParam));
 			return;
 		}
@@ -1710,7 +1711,7 @@ namespace DuiLib
 		// ------------------------------------------------------------------
 		if (IsModalBackdropWindow(m_hWndPaint))
 		{
-			TouchTrace::Log(_T("MODAL-BACKDROP swallow WM_TOUCH hwnd=%p modal=%p inputs=%u"),
+			DuiLibDiag::Log(_T("MODAL-BACKDROP swallow WM_TOUCH hwnd=%p modal=%p inputs=%u"),
 				(void*)m_hWndPaint, (void*)CPaintManagerUI::GetModalWindow(), (unsigned)LOWORD(wParam));
 			// 顺手清掉可能残留的滑动/候选点击(上一次手势若被异常中断)。
 			// 不派发 TOUCHUP、不派发点击 —— 背景窗口此刻不该有任何交互反应。
@@ -1730,16 +1731,16 @@ namespace DuiLib
 		bool bTouchInputClosed = false;
 
 		UINT cInputs = LOWORD(wParam);
-		TouchTrace::Log(_T("WM_TOUCH begin inputs=%u handle=%p"), cInputs, (HANDLE)lParam);
+		DuiLibDiag::Log(_T("WM_TOUCH begin inputs=%u handle=%p"), cInputs, (HANDLE)lParam);
 		if (cInputs == 0)
 		{
-			TouchTrace::Log(_T("WM_TOUCH ignored: inputs=0"));
+			DuiLibDiag::Log(_T("WM_TOUCH ignored: inputs=0"));
 			return;
 		}
 		std::vector<TouchApi::TOUCHINPUT> inputs(cInputs);
 		if (!pfnGetInfo((HANDLE)lParam, cInputs, inputs.data(), (int)sizeof(TouchApi::TOUCHINPUT)))
 		{
-			TouchTrace::Log(_T("WM_TOUCH GetTouchInputInfo failed error=%u"), ::GetLastError());
+			DuiLibDiag::Log(_T("WM_TOUCH GetTouchInputInfo failed error=%u"), ::GetLastError());
 			return;
 		}
 
@@ -1750,12 +1751,12 @@ namespace DuiLib
 		// BeginTouchSlideGesture 会置 m_pEventTouch,指针通道看到它非空就退让。
 		// 标记的生命周期与"WM_TOUCH 是否真的接管了本次手势"严格一致 ——
 		// 这比时间戳可靠(时间戳会因为"指针通道自己也写它"而恒真,已踩过)。
-		TouchTrace::Log(_T("WM_TOUCH owns this gesture (pointer channel will see m_pEventTouch) hwnd=%p"), (void*)m_hWndPaint);
+		DuiLibDiag::Log(_T("WM_TOUCH owns this gesture (pointer channel will see m_pEventTouch) hwnd=%p"), (void*)m_hWndPaint);
 
 		for (UINT i = 0; i < cInputs; ++i)
 		{
 			TouchApi::TOUCHINPUT& ti = inputs[i];
-			TouchTrace::Log(_T("WM_TOUCH input[%u] id=%u flags=0x%08X screen=%d,%d"),
+			DuiLibDiag::Log(_T("WM_TOUCH input[%u] id=%u flags=0x%08X screen=%d,%d"),
 				i, ti.dwID, ti.dwFlags, TOUCH_COORD_TO_PIXEL(ti.x), TOUCH_COORD_TO_PIXEL(ti.y));
 
 			POINT pt;
@@ -1765,7 +1766,7 @@ namespace DuiLib
 
 			if (ti.dwFlags & TouchApi::TOUCHEVTFLAG_DOWN)
 			{
-				TouchTrace::Log(_T("TOUCHDOWN begin id=%u client=%d,%d"), ti.dwID, pt.x, pt.y);
+				DuiLibDiag::Log(_T("TOUCHDOWN begin id=%u client=%d,%d"), ti.dwID, pt.x, pt.y);
 				// 这里【不再】用 `if (m_pEventClick != NULL) break;` 做通用守卫:Windows 会在 WM_TOUCH
 				// 前后补发合成的 WM_LBUTTONDOWN(同样设置 m_pEventClick),通用守卫会让 m_pEventTouch
 				// 永不设置 → 容器收不到 TOUCHMOVE,列表滚不动。
@@ -1782,7 +1783,7 @@ namespace DuiLib
 			}
 			else if (ti.dwFlags & TouchApi::TOUCHEVTFLAG_MOVE)
 			{
-				TouchTrace::Log(_T("TOUCHMOVE begin id=%u client=%d,%d active=%s scrollContainer=%p"),
+				DuiLibDiag::Log(_T("TOUCHMOVE begin id=%u client=%d,%d active=%s scrollContainer=%p"),
 					ti.dwID, pt.x, pt.y,
 					m_pEventTouch != NULL ? m_pEventTouch->GetName().GetData() : _T("null"),
 					(void*)m_pTouchScrollContainer);
@@ -1798,7 +1799,7 @@ namespace DuiLib
 			}
 			else if (ti.dwFlags & TouchApi::TOUCHEVTFLAG_UP)
 			{
-				TouchTrace::Log(_T("TOUCHUP begin id=%u client=%d,%d active=%s moved=%d"),
+				DuiLibDiag::Log(_T("TOUCHUP begin id=%u client=%d,%d active=%s moved=%d"),
 					ti.dwID, pt.x, pt.y,
 					m_pEventTouch != NULL ? m_pEventTouch->GetName().GetData() : _T("null"),
 					(int)m_bTouchMoved);
@@ -1828,7 +1829,7 @@ namespace DuiLib
 				// ------------------------------------------------------------------
 				TouchUp(pt, bTapLikeFinal);
 
-				TouchTrace::Log(_T("TOUCHUP end synthesized=%d suppress=%d"),
+				DuiLibDiag::Log(_T("TOUCHUP end synthesized=%d suppress=%d"),
 					(int)bTapLikeFinal, (int)m_bSuppressPromotedTouchMouse);
 			}
 		}
@@ -1893,7 +1894,7 @@ namespace DuiLib
 		// ==================================================================
 		if (uMsg != DUI_WM_POINTERUP && IsModalBackdropWindow(m_hWndPaint))
 		{
-			TouchTrace::Log(_T("MODAL-BACKDROP swallow pointer msg=0x%04X hwnd=%p modal=%p"),
+			DuiLibDiag::Log(_T("MODAL-BACKDROP swallow pointer msg=0x%04X hwnd=%p modal=%p"),
 				uMsg, (void*)m_hWndPaint, (void*)CPaintManagerUI::GetModalWindow());
 			// 不 return true 之外的任何动作:不建状态、不派发。
 			// 同时顺手把可能残留的滑动标记收掉(上一次手势若被异常中断)。
@@ -1953,13 +1954,13 @@ namespace DuiLib
 		if (bYieldToTouchChannel
 			&& (uMsg == DUI_WM_POINTERDOWN || uMsg == DUI_WM_POINTERUPDATE))
 		{
-			TouchTrace::Log(_T("POINTER-MSG silent (yield to WM_TOUCH) msg=0x%04X w=0x%08X"), uMsg, (unsigned)wParam);
+			DuiLibDiag::Log(_T("POINTER-MSG silent (yield to WM_TOUCH) msg=0x%04X w=0x%08X"), uMsg, (unsigned)wParam);
 			return true;   // 吃掉,不驱动点击;也不 return false,免得被系统再转成鼠标消息
 		}
 		if (bYieldToTouchChannel && uMsg == DUI_WM_POINTERUP)
 		{
 			// 让路期间的 UP:放行到下面的 switch,由 DUI_WM_POINTERUP 分支只清状态、不派发点击。
-			TouchTrace::Log(_T("POINTER-MSG UP passes through while yielding (state cleanup only) w=0x%08X"), (unsigned)wParam);
+			DuiLibDiag::Log(_T("POINTER-MSG UP passes through while yielding (state cleanup only) w=0x%08X"), (unsigned)wParam);
 		}
 		// 下面各分支统一用这个名字(保持旧代码可读性)
 		const bool bPointerYieldedThisGesture = bYieldToTouchChannel;
@@ -1983,7 +1984,7 @@ namespace DuiLib
 		if (m_bPointerGestureActive && dwPointerId != m_dwActivePointerId
 			&& uMsg != DUI_WM_POINTERUP)
 		{
-			TouchTrace::Log(_T("POINTER-MSG foreign-pointer ignored msg=0x%04X id=%u active=%u"),
+			DuiLibDiag::Log(_T("POINTER-MSG foreign-pointer ignored msg=0x%04X id=%u active=%u"),
 				uMsg, (unsigned)dwPointerId, (unsigned)m_dwActivePointerId);
 			return true;
 		}
@@ -1999,7 +2000,7 @@ namespace DuiLib
 		{
 		case DUI_WM_POINTERDOWN:
 		{
-			TouchTrace::Log(_T("POINTER-MSG DOWN hwnd=%p pt=%d,%d id=%u"), (void*)m_hWndPaint, pt.x, pt.y, (unsigned)dwPointerId);
+			DuiLibDiag::Log(_T("POINTER-MSG DOWN hwnd=%p pt=%d,%d id=%u"), (void*)m_hWndPaint, pt.x, pt.y, (unsigned)dwPointerId);
 			// 记下"本次手势由指针通道驱动":随后同一次触摸的 WM_TOUCH 会被 OnTouchInput 忽略。
 			m_bPointerGestureActive = true;
 			m_dwActivePointerId = dwPointerId;
@@ -2065,7 +2066,7 @@ namespace DuiLib
 		}
 		case DUI_WM_POINTERUP:
 		{
-			TouchTrace::Log(_T("POINTER-MSG UP hwnd=%p pt=%d,%d id=%u yield=%d active=%d slide=%d"),
+			DuiLibDiag::Log(_T("POINTER-MSG UP hwnd=%p pt=%d,%d id=%u yield=%d active=%d slide=%d"),
 				(void*)m_hWndPaint, pt.x, pt.y, (unsigned)dwPointerId,
 				(int)bPointerYieldedThisGesture, (int)m_bPointerGestureActive, (int)m_bTouchSlideActive);
 			// 手势结束:立刻解除"指针驱动中",让下一个手势可以重新选择通道。
@@ -2092,7 +2093,7 @@ namespace DuiLib
 			// ------------------------------------------------------------------
 		if (IsModalBackdropWindow(m_hWndPaint))
 		{
-			TouchTrace::Log(_T("MODAL-BACKDROP UP cleanup-only hwnd=%p modal=%p slide=%d"),
+			DuiLibDiag::Log(_T("MODAL-BACKDROP UP cleanup-only hwnd=%p modal=%p slide=%d"),
 				(void*)m_hWndPaint, (void*)CPaintManagerUI::GetModalWindow(), (int)m_bTouchSlideActive);
 			// 清滑动状态机(不发 TOUCHUP、不动控件树)。
 			m_bTouchSlideActive = false;
@@ -2113,7 +2114,7 @@ namespace DuiLib
 			{
 				// 让路中不应有活跃的滑动状态;万一有(两通道交叉),也要收掉,
 				// 否则 m_bTouchSlideActive 会残留到下一次手势,导致 Update 算错 Δ。
-				TouchTrace::Log(_T("POINTER-MSG UP yielded-path (no click synthesized) pt=%d,%d slide=%d"),
+				DuiLibDiag::Log(_T("POINTER-MSG UP yielded-path (no click synthesized) pt=%d,%d slide=%d"),
 					pt.x, pt.y, (int)m_bTouchSlideActive);
 				if (m_bTouchSlideActive) EndTouchSlideGesture(pt);
 				return false;
@@ -2143,7 +2144,7 @@ namespace DuiLib
 		{
 			// wParam 高位是滚轮增量(与 WM_MOUSEWHEEL 一致)
 			const short zDelta = GET_WHEEL_DELTA_WPARAM(wParam);
-			TouchTrace::Log(_T("POINTER-MSG WHEEL hwnd=%p delta=%d"), (void*)m_hWndPaint, (int)zDelta);
+			DuiLibDiag::Log(_T("POINTER-MSG WHEEL hwnd=%p delta=%d"), (void*)m_hWndPaint, (int)zDelta);
 			LRESULT lRes = 0;
 			if (MessageHandler(WM_MOUSEWHEEL, MAKEWPARAM(0, zDelta), MAKELPARAM(pt.x, pt.y), lRes))
 				return true;
@@ -2158,7 +2159,7 @@ namespace DuiLib
 			// 捕获被系统夺走(典型:手势被中断、窗口失活)。
 			// nim_duilib 注释:若不设置 bHandled,系统会把它转成 WM_BUTTON 类消息 ——
 			// 我们这里明确吃掉并复位状态,避免状态残留导致后续点击全被吞。
-			TouchTrace::Log(_T("POINTER-MSG CAPTURECHANGED hwnd=%p"), (void*)m_hWndPaint);
+			DuiLibDiag::Log(_T("POINTER-MSG CAPTURECHANGED hwnd=%p"), (void*)m_hWndPaint);
 			m_bPointerGestureActive = false;
 			m_dwActivePointerId = 0;
 			m_dwPointerDownTick = 0;
@@ -2199,7 +2200,7 @@ namespace DuiLib
 	{
 		m_pfnDefProcRelay = pfnRelay;
 		m_hWndDefProcRelay = hWndTarget;
-		TouchTrace::Log(_T("DEFPROC-RELAY set manager=%p hwnd=%p pfn=%p enabled=%d"),
+		DuiLibDiag::Log(_T("DEFPROC-RELAY set manager=%p hwnd=%p pfn=%p enabled=%d"),
 			(void*)this, (void*)hWndTarget, (void*)pfnRelay, (int)m_bRelayDefProcForTouchMove);
 	}
 
@@ -2210,7 +2211,7 @@ namespace DuiLib
 		{
 			if (m_pfnDefProcRelay != NULL)
 			{
-				TouchTrace::Log(_T("DEFPROC-RELAY clear manager=%p hwnd=%p pfn=%p relayed=%u"),
+				DuiLibDiag::Log(_T("DEFPROC-RELAY clear manager=%p hwnd=%p pfn=%p relayed=%u"),
 					(void*)this, (void*)m_hWndDefProcRelay, (void*)m_pfnDefProcRelay, m_uDefProcRelayCount);
 			}
 			m_pfnDefProcRelay = NULL;
@@ -2225,7 +2226,7 @@ namespace DuiLib
 		// 绑定窗口已变(如真模态换 HWND):钩子失效,别再调用悬垂的函数指针。
 		if (m_hWndDefProcRelay != NULL && m_hWndDefProcRelay != m_hWndPaint)
 		{
-			TouchTrace::Log(_T("DEFPROC-RELAY skipped: bound=%p cur=%p why=%s"),
+			DuiLibDiag::Log(_T("DEFPROC-RELAY skipped: bound=%p cur=%p why=%s"),
 				(void*)m_hWndDefProcRelay, (void*)m_hWndPaint, pszWhy != NULL ? pszWhy : _T("?"));
 			return;
 		}
@@ -2235,7 +2236,7 @@ namespace DuiLib
 		const bool bLog = (m_uDefProcRelayCount <= 5) || (m_uDefProcRelayCount % 20 == 0);
 		if (bLog)
 		{
-			TouchTrace::Log(_T("DEFPROC-RELAY call #%u msg=0x%04X hwnd=%p lParam=%d,%d why=%s"),
+			DuiLibDiag::Log(_T("DEFPROC-RELAY call #%u msg=0x%04X hwnd=%p lParam=%d,%d why=%s"),
 				m_uDefProcRelayCount, uMsg, (void*)m_hWndPaint,
 				(int)GET_X_LPARAM(lParam), (int)GET_Y_LPARAM(lParam),
 				pszWhy != NULL ? pszWhy : _T("?"));
@@ -2293,11 +2294,25 @@ namespace DuiLib
 			if (pControl == NULL) return;
 		}
 		if (pControl->GetManager() != this) return;
-		if (pControl->GetInterface(DUI_CTR_BUTTON) == NULL) return;
-		{
-			CButtonUI* pButton = static_cast<CButtonUI*>(pControl->GetInterface(DUI_CTR_BUTTON));
-			pButton->ClearPushedState();
-		}
+
+		// --------------------------------------------------------------
+		// ◆【滑动起手误按压】优先走"控件自己实现的清接口"。
+		//
+		// 原先这里只有下面那条 `DUI_CTR_BUTTON` 专用路径 —— 它依赖
+		// `GetInterface(DUI_CTR_BUTTON)`,也就是说**只有 CButtonUI 系**能被清到。
+		// 而业务侧大量自绘控件(如本项目的 CDiandanFoodItemUI : CVerticalLayoutUI)
+		// 的按下态是"换整卡背景图"实现的,压根没有 m_uButtonState,
+		// 也就拿不到 DUI_CTR_BUTTON 接口 ⇒ 被那条早退路静默跳过
+		// ⇒ 触摸滑动起手时留下按压态背景(用户实测 bug)。
+		//
+		// 现在改为:先用基类虚函数 `CControlUI::ClearPushedState()`。
+		//   · CButtonUI 已把它 override 成"清 PUSHED|CAPTURED|HOT",
+		//     所以按钮的行为与改前**完全一致**;
+		//   · 业务自绘控件 override 成"复位自己的卡片样式";
+		//   · 其余控件走基类默认空实现 = 安全 no-op。
+		// pControl 本身就是 CControlUI* 或其派生,基类指针即可完成虚分派。
+		// --------------------------------------------------------------
+		pControl->ClearPushedState();
 	}
 
 	bool CPaintManagerUI::TouchUp(const POINT& pt, bool bTapLike)
@@ -2336,7 +2351,7 @@ namespace DuiLib
 		// ==================================================================
 		if (IsModalBackdropWindow(m_hWndPaint))
 		{
-			TouchTrace::Log(_T("MODAL-BACKDROP TouchUp abort hwnd=%p modal=%p tap=%d pt=%d,%d"),
+			DuiLibDiag::Log(_T("MODAL-BACKDROP TouchUp abort hwnd=%p modal=%p tap=%d pt=%d,%d"),
 				(void*)m_hWndPaint, (void*)CPaintManagerUI::GetModalWindow(),
 				(int)bTapLike, pt.x, pt.y);
 			// 清掉本次手势的痕迹(不发任何通知、不给控件派发 TOUCHUP/BUTTON*)。
@@ -2401,7 +2416,7 @@ namespace DuiLib
 			//   所以自己把命中点的按下态收掉 —— 等价于鼠标路径里
 			//   `WM_LBUTTONUP` 那句 `ClearPushedStateAt` + `bTouchDragged` 的
 			//   "ptMouse 置 (-1,-1) 只清位不 Activate"效果。
-			TouchTrace::Log(_T("TOUCHUP consumed as slide pt=%d,%d"), pt.x, pt.y);
+			DuiLibDiag::Log(_T("TOUCHUP consumed as slide pt=%d,%d"), pt.x, pt.y);
 			ClearPushedStateAt(NULL, pt);
 			m_pEventClick = NULL;
 			m_bSuppressPromotedTouchMouse = false;
@@ -2414,7 +2429,7 @@ namespace DuiLib
 		{
 			// 轻点,但没有候选点击目标(Begin 时 FindControl 落空)。
 			//   仍然要把按下视觉收干净(可能与其它路径交叉),然后吞掉。
-			TouchTrace::Log(_T("TOUCHUP fallback-clear hwnd=%p pt=%d,%d"), (void*)m_hWndPaint, pt.x, pt.y);
+			DuiLibDiag::Log(_T("TOUCHUP fallback-clear hwnd=%p pt=%d,%d"), (void*)m_hWndPaint, pt.x, pt.y);
 			ClearPushedStateAt(NULL, pt);
 			m_bSuppressPromotedTouchMouse = false;
 			m_dwSuppressTick = 0;
@@ -2433,7 +2448,7 @@ namespace DuiLib
 		m_dwSuppressTick = 0;
 		m_bTouchMoved = false;
 
-		TouchTrace::Log(_T("TOUCHUP dispatch click %s cls=%s pt=%d,%d"),
+		DuiLibDiag::Log(_T("TOUCHUP dispatch click %s cls=%s pt=%d,%d"),
 			pClick->GetName().GetData(), pClick->GetClass(), pt.x, pt.y);
 
 		// ==================================================================
@@ -2465,7 +2480,7 @@ namespace DuiLib
 		// ==================================================================
 		if (IsModalBackdropWindow(m_hWndPaint))
 		{
-			TouchTrace::Log(_T("TOUCHUP ABORT modal-opened-midway click=%s modal=%p pt=%d,%d"),
+			DuiLibDiag::Log(_T("TOUCHUP ABORT modal-opened-midway click=%s modal=%p pt=%d,%d"),
 				pClick->GetName().GetData(),
 				(void*)CPaintManagerUI::GetModalWindow(), pt.x, pt.y);
 			// 只收掉按下视觉,**绝不 SendNotify**。
@@ -2478,7 +2493,7 @@ namespace DuiLib
 		const bool bStillOwned = (pClick->GetManager() == this);
 		if (!bStillOwned)
 		{
-			TouchTrace::Log(_T("TOUCHUP ABORT foreign click=%s mgr=%p self=%p"),
+			DuiLibDiag::Log(_T("TOUCHUP ABORT foreign click=%s mgr=%p self=%p"),
 				pClick->GetName().GetData(), (void*)pClick->GetManager(), (void*)this);
 			return true;
 		}
@@ -2497,7 +2512,7 @@ namespace DuiLib
 		}
 		if (!bSameTarget)
 		{
-			TouchTrace::Log(_T("TOUCHUP ABORT covered-by-overlay click=%s hitNow=%s pt=%d,%d"),
+			DuiLibDiag::Log(_T("TOUCHUP ABORT covered-by-overlay click=%s hitNow=%s pt=%d,%d"),
 				pClick->GetName().GetData(),
 				pHitNow != NULL ? pHitNow->GetName().GetData() : _T("null"),
 				pt.x, pt.y);
@@ -2601,7 +2616,7 @@ namespace DuiLib
 
 	void CPaintManagerUI::ClearTouchHoverState(POINT pt)
 	{
-		TouchTrace::Log(_T("CLEAR-HOVER pt=%d,%d hover=%s leaveCount=%d"),
+		DuiLibDiag::Log(_T("CLEAR-HOVER pt=%d,%d hover=%s leaveCount=%d"),
 			pt.x, pt.y,
 			m_pEventHover != NULL ? m_pEventHover->GetName().GetData() : _T("null"),
 			m_aNeedMouseLeaveNeeded.GetSize());
@@ -3232,7 +3247,7 @@ namespace DuiLib
 				HWND hModal = s_hModalWindow;
 				const BOOL bModal = (pfnIsTouch != NULL && hModal != NULL && ::IsWindow(hModal))
 					? pfnIsTouch(hModal, &tfModal) : FALSE;
-				TouchTrace::Log(_T("HEARTBEAT hb self=%p reg=%d tf=0x%X | modal=%p reg=%d tf=0x%X | ownerofModal=%p | fg=%p focus=%p active=%p | managerMsgs=%u | ptrActive=%d ptrId=%u ptrYield=%d"),
+				DuiLibDiag::Log(_T("HEARTBEAT hb self=%p reg=%d tf=0x%X | modal=%p reg=%d tf=0x%X | ownerofModal=%p | fg=%p focus=%p active=%p | managerMsgs=%u | ptrActive=%d ptrId=%u ptrYield=%d"),
 					(void*)m_hWndPaint, (int)bSelf, (unsigned)tfSelf,
 					(void*)hModal, (int)bModal, (unsigned)tfModal,
 					(hModal != NULL) ? (void*)::GetWindow(hModal, GW_OWNER) : NULL,
@@ -3461,7 +3476,7 @@ namespace DuiLib
 				//   MOVE 最先到;若移动不开窗,紧随的 DOWN 就会漏(第二版实证)。
 				if (ConsumeCrossModalPromotedMouse(bClickLike, dwExtra))
 				{
-					TouchTrace::Log(_T("PROMOTED-GEN cross msg=0x%04X hwnd=%p gen=%u click=%d extra=0x%08X pt=%d,%d"),
+					DuiLibDiag::Log(_T("PROMOTED-GEN cross msg=0x%04X hwnd=%p gen=%u click=%d extra=0x%08X pt=%d,%d"),
 						uMsg, (void*)m_hWndPaint, (unsigned)s_nPromotedMouseGeneration,
 						(int)bClickLike, (unsigned)(dwExtra & 0xFFFFFFFF),
 						GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
@@ -3473,7 +3488,7 @@ namespace DuiLib
 					if (bClickLike)
 					{
 						// 残留序列的点击类消息 ⇒ 一律吞,绝不建立候选点击。
-						TouchTrace::Log(_T("PROMOTED-GEN swallow msg=0x%04X hwnd=%p gen=%u extra=0x%08X pt=%d,%d"),
+						DuiLibDiag::Log(_T("PROMOTED-GEN swallow msg=0x%04X hwnd=%p gen=%u extra=0x%08X pt=%d,%d"),
 							uMsg, (void*)m_hWndPaint, (unsigned)s_nPromotedMouseGeneration,
 							(unsigned)(dwExtra & 0xFFFFFFFF),
 							GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
@@ -3508,7 +3523,7 @@ namespace DuiLib
 				{
 					// ★ extra 变了 ⇒ 换了一次手势 ⇒ 残留序列已经结束(哪怕它的 UP 丢了)。
 					//   这是第四版最重要的防御:不再依赖"必须等到 UP 才关窗"。
-					TouchTrace::Log(_T("PROMOTED-GEN close-by-extra msg=0x%04X hwnd=%p gen=%u oldextra=0x%08X newextra=0x%08X"),
+					DuiLibDiag::Log(_T("PROMOTED-GEN close-by-extra msg=0x%04X hwnd=%p gen=%u oldextra=0x%08X newextra=0x%08X"),
 						uMsg, (void*)m_hWndPaint, (unsigned)s_nPromotedMouseGeneration,
 						(unsigned)(s_dwPromotedMouseTaintExtra & 0xFFFFFFFF),
 						(unsigned)(dwExtra & 0xFFFFFFFF));
@@ -3550,10 +3565,10 @@ namespace DuiLib
 			HWND hFg = ::GetForegroundWindow();
 			const BOOL bFg = (pfnIsTouch != NULL && hFg != NULL) ? pfnIsTouch(hFg, &tfFg) : FALSE;
 
-			TouchTrace::Log(_T("MANAGER-MSG hwnd=%p manager=%p msg=0x%04X w=0x%p l=0x%p touch=%p suppress=%d"),
+			DuiLibDiag::Log(_T("MANAGER-MSG hwnd=%p manager=%p msg=0x%04X w=0x%p l=0x%p touch=%p suppress=%d"),
 				(void*)m_hWndPaint, (void*)this, uMsg, (void*)wParam, (void*)lParam,
 				(void*)m_pEventTouch, (int)m_bSuppressPromotedTouchMouse);
-			TouchTrace::Log(_T("TOUCHREG self=%p reg=%d tf=0x%X | modal=%p reg=%d tf=0x%X | fg=%p reg=%d tf=0x%X | focus=%p active=%p"),
+			DuiLibDiag::Log(_T("TOUCHREG self=%p reg=%d tf=0x%X | modal=%p reg=%d tf=0x%X | fg=%p reg=%d tf=0x%X | focus=%p active=%p"),
 				(void*)m_hWndPaint, (int)bSelf, (unsigned)tfSelf,
 				(void*)hModal, (int)bModal, (unsigned)tfModal,
 				(void*)hFg, (int)bFg, (unsigned)tfFg,
@@ -3578,7 +3593,7 @@ namespace DuiLib
 		if (uMsg == WM_LBUTTONDOWN || uMsg == WM_LBUTTONUP || uMsg == WM_LBUTTONDBLCLK)
 		{
 			HWND hModalDbg = CPaintManagerUI::GetModalWindow();
-			TouchTrace::Log(_T("CLICKTRACE msg=0x%04X target=%p promoted=%d modal=%p ownerOfModal=%p touch=%p supp=%d modalbit=%d pending=%d enable=%d extra=0x%p"),
+			DuiLibDiag::Log(_T("CLICKTRACE msg=0x%04X target=%p promoted=%d modal=%p ownerOfModal=%p touch=%p supp=%d modalbit=%d pending=%d enable=%d extra=0x%p"),
 				uMsg, (void*)m_hWndPaint, (int)IsPromotedTouchMouseMessage(),
 				(void*)hModalDbg,
 				(hModalDbg != NULL && ::IsWindow(hModalDbg)) ? (void*)::GetWindow(hModalDbg, GW_OWNER) : NULL,
@@ -3613,7 +3628,7 @@ namespace DuiLib
 		if ((uMsg == WM_LBUTTONDOWN || uMsg == WM_LBUTTONUP || uMsg == WM_LBUTTONDBLCLK)
 			&& IsModalBackdropWindow(m_hWndPaint))
 		{
-			TouchTrace::Log(_T("MODAL-BACKDROP swallow mouse msg=0x%04X hwnd=%p promoted=%d modal=%p"),
+			DuiLibDiag::Log(_T("MODAL-BACKDROP swallow mouse msg=0x%04X hwnd=%p promoted=%d modal=%p"),
 				uMsg, (void*)m_hWndPaint, (int)IsPromotedTouchMouseMessage(),
 				(void*)CPaintManagerUI::GetModalWindow());
 			// 顺手作废可能残留的候选点击/捕获,避免抬手时被派发。
@@ -3650,7 +3665,7 @@ namespace DuiLib
 					m_dwSuppressTick = 0;
 				}
 				// [诊断] down/elapsed 只用于看时序,不再参与任何判定。
-				TouchTrace::Log(_T("PROMOTED-MOUSE swallowed msg=0x%04X touch=%p suppress=%d down=%u elapsed=%u modal=%d"),
+				DuiLibDiag::Log(_T("PROMOTED-MOUSE swallowed msg=0x%04X touch=%p suppress=%d down=%u elapsed=%u modal=%d"),
 					uMsg, (void*)m_pEventTouch, (int)m_bSuppressPromotedTouchMouse,
 					(unsigned)m_dwTouchDownTick,
 					(m_dwSuppressTick != 0) ? (unsigned)(::GetTickCount() - m_dwSuppressTick) : 0u,
@@ -3711,7 +3726,7 @@ namespace DuiLib
 						// 抬起:销账。计数归零时 ReleasePromotedMousePending 会自动解除抑制。
 						ReleasePromotedMousePending();
 					}
-					TouchTrace::Log(_T("MODAL-BEEP swallowed promoted msg=0x%04X hwnd=%p elapsed=%u pending=%d"),
+					DuiLibDiag::Log(_T("MODAL-BEEP swallowed promoted msg=0x%04X hwnd=%p elapsed=%u pending=%d"),
 						uMsg, (void*)m_hWndPaint, (unsigned)dwElapsed, m_nPromotedMousePending);
 					return true;
 				}
@@ -3775,7 +3790,7 @@ namespace DuiLib
 			// [诊断] 未进入归一化的指针消息(通道关闭、或 ENTER/ACTIVATE/HWHEEL 等),
 			// 只记录"系统确实在投指针消息"这一事实,不做任何处理
 			// (绝不能 return true —— 交回系统,保住它自己的指针状态机)。
-			TouchTrace::Log(_T("POINTER-MSG observed-but-passed msg=0x%04X w=0x%08X enabled=%d hwnd=%p"),
+			DuiLibDiag::Log(_T("POINTER-MSG observed-but-passed msg=0x%04X w=0x%08X enabled=%d hwnd=%p"),
 				uMsg, (unsigned)wParam, (int)m_bPointerNormalizeEnabled, (void*)m_hWndPaint);
 		}
 
@@ -4350,7 +4365,7 @@ namespace DuiLib
 			return true;
 			case WM_MOUSELEAVE:
 			{
-				TouchTrace::Log(_T("WM_MOUSELEAVE tracking=%d hover=%s leaveArr=%d click=%s"),
+				DuiLibDiag::Log(_T("WM_MOUSELEAVE tracking=%d hover=%s leaveArr=%d click=%s"),
 					(int)m_bMouseTracking,
 					m_pEventHover != NULL ? m_pEventHover->GetName().GetData() : _T("null"),
 					m_aNeedMouseLeaveNeeded.GetSize(),
@@ -4559,7 +4574,7 @@ namespace DuiLib
 							static_cast<CControlUI*>(aNeedMouseLeaveNeeded[i])->Event(event);
 						}
 
-						TouchTrace::Log(_T("MOUSEMOVE-LEAVE old=%s leaveArr=%d new=%s pt=%d,%d"),
+						DuiLibDiag::Log(_T("MOUSEMOVE-LEAVE old=%s leaveArr=%d new=%s pt=%d,%d"),
 							m_pEventHover->GetName().GetData(), m_aNeedMouseLeaveNeeded.GetSize(),
 							pNewHover != NULL ? pNewHover->GetName().GetData() : _T("null"), pt.x, pt.y);
 						m_pEventHover->Event(event);
@@ -4572,14 +4587,14 @@ namespace DuiLib
 						event.pSender = pNewHover;
 						pNewHover->Event(event);
 						m_pEventHover = pNewHover;
-						TouchTrace::Log(_T("HOVER-ENTER %s cls=%s pt=%d,%d"),
+						DuiLibDiag::Log(_T("HOVER-ENTER %s cls=%s pt=%d,%d"),
 							pNewHover->GetName().GetData(), pNewHover->GetClass(), pt.x, pt.y);
 					}
 				}
 				else
 				{
 					// [第十一轮诊断] 被闸门挡住时明确记录,否则"悬浮态为何没更新"无从判断。
-					TouchTrace::Log(_T("HOVER-GATE-BLOCKED captured=%d promotedMove=%d curHover=%s pt=%d,%d"),
+					DuiLibDiag::Log(_T("HOVER-GATE-BLOCKED captured=%d promotedMove=%d curHover=%s pt=%d,%d"),
 						(int)IsCaptured(), (int)bPromotedTouchMove,
 						m_pEventHover != NULL ? m_pEventHover->GetName().GetData() : _T("null"),
 						pt.x, pt.y);
@@ -4600,7 +4615,7 @@ namespace DuiLib
 			break;
 			case WM_LBUTTONDOWN:
 			{
-				TouchTrace::Log(_T("WM_LBUTTONDOWN promoted=%d pt=%d,%d eventTouch=%s eventClick=%s"),
+				DuiLibDiag::Log(_T("WM_LBUTTONDOWN promoted=%d pt=%d,%d eventTouch=%s eventClick=%s"),
 					(int)IsPromotedTouchMouseMessage(), GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam),
 					m_pEventTouch != NULL ? m_pEventTouch->GetName().GetData() : _T("null"),
 					m_pEventClick != NULL ? m_pEventClick->GetName().GetData() : _T("null"));
@@ -4619,18 +4634,18 @@ namespace DuiLib
 				CControlUI* pControl = FindControl(pt);
 				if (pControl == NULL)
 				{
-					TouchTrace::Log(_T("LBUTTONDOWN no control hwnd=%p pt=%d,%d root=%p"),
+					DuiLibDiag::Log(_T("LBUTTONDOWN no control hwnd=%p pt=%d,%d root=%p"),
 						(void*)m_hWndPaint, pt.x, pt.y, (void*)m_pRoot);
 					break;
 				}
 				if (pControl->GetManager() != this)
 				{
-					TouchTrace::Log(_T("LBUTTONDOWN foreign control hwnd=%p pt=%d,%d name=%s manager=%p self=%p"),
+					DuiLibDiag::Log(_T("LBUTTONDOWN foreign control hwnd=%p pt=%d,%d name=%s manager=%p self=%p"),
 						(void*)m_hWndPaint, pt.x, pt.y, pControl->GetName().GetData(),
 						(void*)pControl->GetManager(), (void*)this);
 					break;
 				}
-				TouchTrace::Log(_T("LBUTTONDOWN hit hwnd=%p name=%s class=%s pos=%d,%d,%d,%d enabled=%d visible=%d"),
+				DuiLibDiag::Log(_T("LBUTTONDOWN hit hwnd=%p name=%s class=%s pos=%d,%d,%d,%d enabled=%d visible=%d"),
 					(void*)m_hWndPaint, pControl->GetName().GetData(), pControl->GetClass(),
 					pControl->GetPos().left, pControl->GetPos().top, pControl->GetPos().right, pControl->GetPos().bottom,
 					(int)pControl->IsEnabled(), (int)pControl->IsVisible());
@@ -4689,7 +4704,7 @@ namespace DuiLib
 			break;
 			case WM_LBUTTONUP:
 			{
-				TouchTrace::Log(_T("WM_LBUTTONUP promoted=%d pt=%d,%d eventTouch=%s eventClick=%s moved=%d"),
+				DuiLibDiag::Log(_T("WM_LBUTTONUP promoted=%d pt=%d,%d eventTouch=%s eventClick=%s moved=%d"),
 					(int)IsPromotedTouchMouseMessage(), GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam),
 					m_pEventTouch != NULL ? m_pEventTouch->GetName().GetData() : _T("null"),
 					m_pEventClick != NULL ? m_pEventClick->GetName().GetData() : _T("null"),
@@ -4757,7 +4772,7 @@ namespace DuiLib
 					ClearTouchHoverState(ptButtonUp);
 				}
 
-				TouchTrace::Log(_T("LBUTTONUP moved=%d click=%s hover=%s leaveArr=%d capture=%d"),
+				DuiLibDiag::Log(_T("LBUTTONUP moved=%d click=%s hover=%s leaveArr=%d capture=%d"),
 					(int)m_bTouchMoved,
 					m_pEventClick != NULL ? m_pEventClick->GetName().GetData() : _T("null"),
 					m_pEventHover != NULL ? m_pEventHover->GetName().GetData() : _T("null"),
@@ -4770,7 +4785,7 @@ namespace DuiLib
 				m_bTouchMoved = false;  // 用完即清,绝不影响后续真实鼠标点击
 				if (pClick == NULL)
 				{
-					TouchTrace::Log(_T("LBUTTONUP no click hwnd=%p pt=%d,%d"),
+					DuiLibDiag::Log(_T("LBUTTONUP no click hwnd=%p pt=%d,%d"),
 						(void*)m_hWndPaint, pt.x, pt.y);
 					ReleaseCapture();
 					break;

+ 10 - 5
DuiLib/Utils/UIDiag.cpp

@@ -1,11 +1,11 @@
 // =============================================================================
-// [第十一轮诊断] 跨 .cpp 日志出口的实现。说明见 Utils/UIDiag.h。
+// 诊断日志唯一出口的实现。说明见 Utils/UIDiag.h。
 //
-// 单独放一个 .cpp(而不是 header-only + inline)的原因:
+// 为什么是独立 .cpp(而不是 header-only + inline):
 //   如果写成头文件里的 inline 函数,每个包含它的 .cpp 都会有一份 static 局部
 //   变量副本(s_buf / path 缓存),日志行为虽然仍正确,但每次都重新
 //   GetModuleFileName + 打开/关闭文件,且在 DLL 边界上语义容易含混。
-//   放 .cpp 里只编译一份,行为与 UIManager.cpp 的 TouchTrace 完全对齐。
+//   放 .cpp 里只编译一份 —— 这也是"全库只有本 .cpp 真正落盘"的实现基础。
 // =============================================================================
 
 #include "StdAfx.h"
@@ -16,7 +16,7 @@
 
 namespace DuiLibDiag
 {
-	// 与 UIManager.cpp 的 TouchTrace::Enabled() 对应 —— 需要关闭时改这里。
+	// ★ 全库唯一的日志开关:需要关闭诊断时改这里(返回 false)。
 	inline bool Enabled()
 	{
 		return true;
@@ -61,14 +61,19 @@ namespace DuiLibDiag
 #endif
 
 		FILE* f = NULL;
-		// 与 TouchTrace 同一把闸:超过 32MB 不再追加,避免长期运行把收银机磁盘写满。
+		// 保护:单个日志超过 32MB 就不再追加,避免在收银机上长期运行把日志写到失控。
+		// (全量 INPUT-TRACE 很啰嗦,阈值从 4MB 放宽到 32MB,否则一轮诊断还没跑完就静默停写了。)
+		// 注意:GetFileAttributesEx 是 TCHAR 宏(Unicode 工程下展开为 ...ExW),
+		// 必须传 TCHAR 的 path,不能传 Narrow(path) 得到的 UTF-8 char*(否则 C2664)。
 		WIN32_FILE_ATTRIBUTE_DATA fad = { 0 };
 		if (::GetFileAttributesEx(path, GetFileExInfoStandard, &fad)
 			&& fad.nFileSizeHigh == 0 && fad.nFileSizeLow >= (32u * 1024u * 1024u))
 			return;
+		// fopen_s 只吃 char*(窄字符路径),这里才需要 UTF-8 的 Narrow(path)
 		if (fopen_s(&f, Narrow(path), "a") == 0 && f != NULL)
 		{
 			fprintf(f, "[%8u][T%5u] %s\n", (unsigned)::GetTickCount(), (unsigned)::GetCurrentThreadId(), utf8);
+			// 立刻冲盘:程序被强杀 / 挂死 / 断电时,日志尾部(往往正是关键现场)不能丢。
 			fflush(f);
 			fclose(f);
 		}

+ 14 - 10
DuiLib/Utils/UIDiag.h

@@ -4,25 +4,29 @@
 #pragma once
 
 // =============================================================================
-// [第十一轮诊断] 跨 .cpp 的日志出口。
+// 诊断日志的**唯一出口**。
 //
 // 为什么需要它:
-//   DuiLib 的触摸诊断日志函数 `TouchTrace::Log` 定义在 UIManager.cpp 的**匿名
-//   namespace** 里 —— 匿名命名空间是内部链接,别的 .cpp(如 Control/UIButton.cpp)
-//   根本引用不到。历史上有过一次尝试:直接在 UIButton.cpp 里写文件日志,后来因为
-//   复制粘贴的路径/格式逻辑太脏而被删掉。
+//   历史上 DuiLib 的触摸诊断函数 `TouchTrace::Log` 定义在 UIManager.cpp 的
+//   **匿名 namespace** 里 —— 匿名命名空间是内部链接,别的 .cpp(如 Control/UIButton.cpp)
+//   根本引用不到。当时为了跨 .cpp 写日志,一度在 UIButton.cpp 里直接抄了一份
+//   写文件的代码,因为复制粘贴的路径/格式逻辑太脏而被删掉;随后才有了本模块。
 //
-//   本模块提供一个**公共的**轻量日志出口,格式与 TouchTrace 完全一致
-//   (`[tick][tid] 文本`,UTF-8,追加写 exe 同目录 touch_trace.log),
-//   这样 UIButton 等控件的诊断可以写进同一个日志文件,用同一条时间轴对照。
+//   **现在 UIManager.cpp / UIBase.cpp / UIButton.cpp 的所有诊断调用都已统一走这里**
+//   (原先 UIManager.cpp 自己那份本地实现已删除,UIBase.h 里的声明也一并删了)。
+//   全库只有本 .cpp 真正落盘,格式 `[tick][tid] 文本`,UTF-8,
+//   追加写 exe 同目录 touch_trace.log。
 //
 // ⚠️ 本模块纯粹是诊断用,不参与任何业务逻辑。问题定位后可以整体删除:
-//    删除时需要同时去掉 UIButton.cpp 里的 LogButtonActivate 调用。
+//    删除时把全库的 `DuiLibDiag::` 调用一并去掉(搜索 `DuiLibDiag::` 与 `UIDiag.h` 确认无遗漏)。
+//
+// ⚠️ 要**关日志**:改 UIDiag.cpp 里 `Enabled()` 的返回值即可,别去调用点加条件 ——
+//    否则以后想再开就得满库找开关。
 // =============================================================================
 
 namespace DuiLibDiag
 {
-	// 与 UIManager.cpp 的 TouchTrace 写法保持一致,写 exe 同目录 touch_trace.log。
+	// 写 exe 同目录 touch_trace.log。
 	void Log(LPCTSTR fmt, ...);
 
 	// 按钮被激活时的现场快照。用途:

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


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


BIN
dll/release/DuiLib.dll


BIN
lib/release/DuiLib.lib


BIN
pdb/release/DuiLib.pdb


+ 33 - 0
zhipuzi_pos_windows/control/CDiandanFoodItemUI.cpp

@@ -83,6 +83,39 @@ void CDiandanFoodItemUI::DoEvent(TEventUI& event)
 	CVerticalLayoutUI::DoEvent(event);
 }
 
+void CDiandanFoodItemUI::ClearPushedState()
+{
+	// ◆【滑动起手误按压 · 根治点】
+	//
+	// 现象(用户实测):手指触屏滑动,**手指刚接触屏幕、还没开始滑动**的那一瞬间,
+	//   接触到的商品控件就已经显示按压态背景了。
+	//
+	// 成因(消息顺序问题,不是"没清干净"):
+	//   1) 手指落下 → 触摸链 `BeginTouchSlideGesture` **无条件**派发 UIEVENT_TOUCHDOWN,
+	//      本卡的 TOUCHDOWN 分支立刻 `SetCardState("pressed")` 并重绘
+	//      ⇒ 按压态在"还没开始滑"的那一刻就画出来了(这一步本身是正确的:
+	//        没有它,用户正常点击时就看不到按下高亮)。
+	//   2) 但"手指是来滑动的"这件事,只有等位移越过 16px 阈值、置上
+	//      `m_bTouchMoved` 之后才知道 —— 太晚了,按压态已经画出来了。
+	//   3) 抬手时 `EndTouchSlideGesture` 判定为滑动、不合成点击,虽然有
+	//      TOUCHUP/清视觉的清扫,但**手指按着不动的整段时间里按压态一直亮着**,
+	//      且滑动过程中手指早已移出本卡 rect,视觉上就是一个"没被按下却压着"的卡片。
+	//
+	// 修法:把本卡的"复位为普通态"暴露给触摸链(override 基类 CControlUI::ClearPushedState),
+	//   让库侧在**判定为滑动**时对起手控件补一次清扫。库侧走的是
+	//   `CPaintManagerUI::ClearPushedStateAt`,它按基类指针调用,虚分派到这里。
+	//
+	// 为什么库侧原生那套清不到本卡:那是 CButtonUI 专用路径
+	//   (`GetInterface(DUI_CTR_BUTTON)` + `CButtonUI::ClearPushedState`),
+	//   而本卡是 CVerticalLayoutUI 派生,`m_youtu_model`/m_pBorderFrame 这套
+	//   换图逻辑只有本类知道,必须由本类自己实现。
+	//
+	// ⚠️ 只改视觉:不 SendNotify、不 Activate、不碰业务数据。
+	//    由 `CVerticalLayoutUI::DoEvent(event);` 所在的正常事件路径之外的
+	//    "手势收尾清扫"调用,语义上等价于 NIM 的 `m_uButtonState = kControlStateNormal`。
+	SetCardState("normal");
+}
+
 void CDiandanFoodItemUI::SetCardState(const std::string& state)
 {
 	bool bR10 = (m_youtu_model == "1");

+ 12 - 0
zhipuzi_pos_windows/control/CDiandanFoodItemUI.h

@@ -30,6 +30,18 @@ public:
 
 	void SetYoutuModel(std::string youtu_model);
 
+	// ◆【滑动起手误按压 · 公共清接口】
+	//
+	// 本卡是 CVerticalLayoutUI 派生类,**没有** CButtonUI 的 m_uButtonState,
+	// 所以库侧的 CPaintManagerUI::ClearPushedStateAt 用它那套
+	// `GetInterface(DUI_CTR_BUTTON)` + `ClearPushedState()` 根本清不到本卡
+	// (见 UIManager.cpp 里该函数的早退路 `if (GetInterface(DUI_CTR_BUTTON) == NULL) return;`)。
+	//
+	// 因此必须由本类自己把"复位为普通态"暴露出去,让触摸链在判定为滑动时调用。
+	// 语义与 ClearPushedState 完全一致:**只改视觉、不发任何通知**。
+	// 基类声明为虚函数,本类 override;库侧按基类指针调用,自动虚分派到这里。
+	virtual void ClearPushedState();
+
 private:
 	// 根据交互态切换整卡背景 +(样式①)顶层描边框
 	void SetCardState(const std::string& state);