张洋 4 weeks ago
parent
commit
c5be12662f
2 changed files with 161 additions and 7 deletions
  1. 155 7
      DuiLib/Core/UIManager.cpp
  2. 6 0
      DuiLib/Core/UIManager.h

+ 155 - 7
DuiLib/Core/UIManager.cpp

@@ -1,5 +1,9 @@
 #include "StdAfx.h"
 #include <zmouse.h>
+// [触屏诊断] 显式引入,避免依赖 tchar.h 的传递包含
+#include <stdio.h>
+#include <stdarg.h>
+#include <tchar.h>
 
 #pragma warning(push)
 #pragma warning(disable:4838 4244)
@@ -30,6 +34,72 @@ namespace DuiLib
 #define WM_TOUCH 0x0240
 #endif
 
+	// ---------------------------------------------------------------------------
+	// [触屏诊断] 临时打点:DuiLib 自身无日志库,故用无依赖的追加文件法。
+	// 输出到 exe 同目录 touch_trace.log(每行带 tick/线程)。定位完成后可整段删除。
+	// 开关:环境变量 ZP_TOUCH_TRACE=0 可关闭;默认开启。
+	// ---------------------------------------------------------------------------
+	namespace TouchTrace
+	{
+		inline bool Enabled()
+		{
+			static int s_state = -1;
+			if (s_state < 0)
+			{
+				TCHAR buf[8] = { 0 };
+				DWORD n = ::GetEnvironmentVariable(_T("ZP_TOUCH_TRACE"), buf, 8);
+				s_state = (n > 0 && buf[0] == _T('0')) ? 0 : 1;
+			}
+			return s_state == 1;
+		}
+
+		// 极简 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
+		}
+
+		inline 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;
+			if (fopen_s(&f, Narrow(path), "a") == 0 && f != NULL)
+			{
+				fprintf(f, "[%8u][T%5u] %s\n", (unsigned)::GetTickCount(), (unsigned)::GetCurrentThreadId(), utf8);
+				fclose(f);
+			}
+		}
+	}
+
 	static void GetChildWndRect(HWND hWnd, HWND hChildWnd, RECT& rcChildWnd)
 	{
 		::GetWindowRect(hChildWnd, &rcChildWnd);
@@ -588,9 +658,11 @@ namespace DuiLib
 		//      走 MessageHandler 里原生的鼠标点击路径(CButtonUI 等据此 Activate())。NIM 从不过滤
 		//      这些模拟鼠标,只在 WM_LBUTTON* 分支加了 m_pEventTouch!=NULL 守卫,保证一次手指手势
 		//      要么滚动、要么点击,不会双触发。
-		//   4) 守卫:DOWN 时若真实鼠标已在交互(m_pEventClick 被真实鼠标置位)则忽略触摸;MOVE 仅对“滚动条”让路
-		//      (原生鼠标独占滑块拖拽),容器照常收 TOUCHMOVE 跟手滚动。注意 m_pEventClick 也会被触摸模拟的
-		//      WM_LBUTTONDOWN 置位,故不能仅因它非空就吞掉 TOUCHMOVE,否则列表滚不动。
+		//   4) 守卫:DOWN 与 MOVE 都【只对“滚动条”让路】(滑块拖拽由原生鼠标独占),其余控件一律接管。
+		//      不能用 `m_pEventClick != NULL` 做通用守卫——Windows 为触摸补发的合成 WM_LBUTTONDOWN 也会
+		//      设置 m_pEventClick,一旦 WM_TOUCH 不是先到,通用守卫会让容器收不到 TOUCHMOVE(列表滚不动)。
+		//   5) 抬起后置 m_bSuppressHoverAfterTouch,抑制 Windows 补发的合成 MOUSEMOVE 与 WM_MOUSELEAVE
+		//      的自回发把刚清掉的悬浮态重新点亮(“悬浮态不消失 / 多控件同时悬浮”的真凶)。
 		TouchApi::PFN_GetTouchInputInfo pfnGetInfo = TouchApi::GetGetTouchInputInfo();
 		if (pfnGetInfo == NULL) return;
 
@@ -614,8 +686,12 @@ namespace DuiLib
 
 			if (ti.dwFlags & TouchApi::TOUCHEVTFLAG_DOWN)
 			{
-				// 鼠标/指针交互进行中 -> 忽略触摸(NIM 同款守卫)
-				if (m_pEventClick != NULL) break;
+				// 说明:这里【不再】用 `if (m_pEventClick != NULL) break;`。
+				// 该守卫本意是“真实鼠标交互中忽略触摸”,但 Windows 会在 WM_TOUCH 前后补发合成的
+				// WM_LBUTTONDOWN(同样会设置 m_pEventClick)。若保证不了 WM_TOUCH 一定先到,
+				// 这句 break 会让 m_pEventTouch 永不被设置 → 容器 TOUCHMOVE 跟手滚动彻底失效。
+				// 由于点击/滚动本质上互不冲突(点击走原生鼠标路径,滚动走 TOUCHMOVE),
+				// 这里只在“命中控件是滚动条”时让路,其余一律接管。
 
 				::SetFocus(m_hWndPaint);
 				m_ptLastTouchPos = pt;
@@ -628,13 +704,21 @@ namespace DuiLib
 				// 若此处再派发 UIEVENT_TOUCHMOVE 会冒泡到容器把【内容】滚走,与滑块拖拽打架→滑块拖不动、点轨道误翻页。
 				// 直接交回原生鼠标,本函数提前退出(悬浮态由 WM_TOUCH UP 统一清理)。
 				if (pControl->GetInterface(_T("ScrollBar")) != NULL)
+				{
+					TouchTrace::Log(_T("TOUCHDOWN-SCROLLBAR handback hit=%s pt=%d,%d click=%p capture=%d"),
+						pControl->GetName().GetData(), pt.x, pt.y, (void*)m_pEventClick, (int)m_bMouseCapture);
 					break;
+				}
 
 				m_pEventTouch = pControl;
 				m_bTouchMoved = false;
 				m_ptTouchStart = pt;
+				m_bSuppressHoverAfterTouch = false;  // 新手势开始,解除上一轮抬起后的悬浮抑制
 				pControl->SetFocus();
 				SetCapture();                              // mgr 级:m_bMouseCapture=true + ::SetCapture
+				TouchTrace::Log(_T("TOUCHDOWN hit=%s cls=%s pt=%d,%d click=%p capture=%d"),
+					pControl->GetName().GetData(), pControl->GetClass(), pt.x, pt.y,
+					(void*)m_pEventClick, (int)m_bMouseCapture);
 
 				TEventUI event = { 0 };
 				event.Type = UIEVENT_TOUCHDOWN;
@@ -651,7 +735,12 @@ namespace DuiLib
 				// 若整句 break,会把容器的 UIEVENT_TOUCHMOVE 一起吞掉→列表无法跟手滚动。
 				// 故只对“滚动条”让路(滚动条滑块拖拽由原生鼠标驱动,见 DOWN 的 ScrollBar break),
 				// 其余控件(容器/按钮)照常派发 TOUCHMOVE:容器据此 1:1 跟手滚动,按钮不处理 TOUCHMOVE 无害。
-				if (m_pEventClick != NULL && m_pEventClick->GetInterface(_T("ScrollBar")) != NULL) break;
+				if (m_pEventClick != NULL && m_pEventClick->GetInterface(_T("ScrollBar")) != NULL)
+				{
+					TouchTrace::Log(_T("TOUCHMOVE-SKIP(click=scrollbar) click=%s pt=%d,%d"),
+						m_pEventClick->GetName().GetData(), pt.x, pt.y);
+					break;
+				}
 				// 与上一包完全相同的点 -> 去重,避免无谓派发(NIM 同款守卫)
 				if (m_ptLastTouchPos.x == pt.x && m_ptLastTouchPos.y == pt.y) break;
 				// 本次位移增量(相对上一包),随事件下发,滚动容器据此做 1:1 跟手平移
@@ -673,6 +762,10 @@ namespace DuiLib
 				event.wKeyState = MapKeyState();
 				event.wParam = (WPARAM)(int)dx;
 				event.lParam = (LPARAM)(int)dy;
+				TouchTrace::Log(_T("TOUCHMOVE 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"));
 				m_pEventTouch->Event(event);
 			}
 			else if (ti.dwFlags & TouchApi::TOUCHEVTFLAG_UP)
@@ -713,6 +806,13 @@ namespace DuiLib
 					if (m_hwndTooltip != NULL) ::SendMessage(m_hwndTooltip, TTM_TRACKACTIVATE, FALSE, (LPARAM)&m_ToolTip);
 				}
 
+				// 关键:手指抬起后禁止“悬浮态复活”。
+				// Windows 会为触摸补发合成的 WM_MOUSEMOVE;且真实 WM_MOUSELEAVE 在 m_bMouseTracking
+				// 为真时会自己给自己重发 WM_MOUSEMOVE(见 WM_MOUSELEAVE 分支)→ 刚清掉的悬浮态立刻
+				// 被重新点亮。这里置抑制位 + 主动撤下鼠标跟踪,直到用户真的移动鼠标才恢复。
+				m_bSuppressHoverAfterTouch = true;
+				m_bMouseTracking = false;
+
 				// 滚动条等场景本函数未接管(m_pEventTouch 为 NULL),捕获由原生鼠标路径负责,这里不释放。
 				if (m_pEventTouch == NULL) break;
 
@@ -724,6 +824,9 @@ namespace DuiLib
 				event.ptMouse = pt;
 				event.dwTimestamp = ::GetTickCount();
 				event.wKeyState = MapKeyState();
+				TouchTrace::Log(_T("TOUCHUP touch=%s moved=%d hover=%p leaveArr=%d click=%p capture=%d"),
+					m_pEventTouch->GetName().GetData(), (int)m_bTouchMoved, (void*)m_pEventHover,
+					m_aNeedMouseLeaveNeeded.GetSize(), (void*)m_pEventClick, (int)m_bMouseCapture);
 				m_pEventTouch->Event(event);
 				m_pEventTouch = NULL;
 			}
@@ -1869,8 +1972,15 @@ namespace DuiLib
 			return true;
 			case WM_MOUSELEAVE:
 			{
+				TouchTrace::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(),
+					m_pEventClick != NULL ? m_pEventClick->GetName().GetData() : _T("null"));
 				if (m_hwndTooltip != NULL) ::SendMessage(m_hwndTooltip, TTM_TRACKACTIVATE, FALSE, (LPARAM)&m_ToolTip);
-				if (m_bMouseTracking)
+				// 触屏抬起刚清过悬浮态时,不要执行下面这段“自回发 WM_MOUSEMOVE”:
+				// 光标通常仍在窗口内 → 会立刻重建悬浮态,导致“悬浮态不消失/多控件同时悬浮”。
+				if (!m_bSuppressHoverAfterTouch && m_bMouseTracking)
 				{
 					POINT pt = { 0 };
 					RECT rcWnd = { 0 };
@@ -1906,6 +2016,34 @@ namespace DuiLib
 					m_bMouseTracking = true;
 				}
 
+				// 触屏抬起后的“悬浮态复活”抑制:
+				// 抬起瞬间会置 m_bSuppressHoverAfterTouch=true。此后 Windows 补发的合成 MOUSEMOVE
+				// (以及 WM_MOUSELEAVE 触发的自回发 WM_MOUSEMOVE)一律不再参与悬浮态 enter/leave,
+				// 只更新坐标。真实鼠标移动(与上次触摸落点明显不同)才解除抑制。
+				{
+					POINT ptCheck = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
+					if ((INT_PTR)lParam == -1)
+					{
+						// (LPARAM)-1 = “鼠标离开窗口”的自回发,坐标为垃圾值,直接丢弃
+						break;
+					}
+					if (m_bSuppressHoverAfterTouch)
+					{
+						const int kResumeSlop = 3; // 与上次触摸落点的最小差异,超过视为真实鼠标移动
+						int ddx = ptCheck.x - m_ptLastTouchPos.x;
+						int ddy = ptCheck.y - m_ptLastTouchPos.y;
+						if (ddx > kResumeSlop || ddx < -kResumeSlop || ddy > kResumeSlop || ddy < -kResumeSlop)
+						{
+							m_bSuppressHoverAfterTouch = false; // 真实鼠标移动,恢复悬浮逻辑
+						}
+						else
+						{
+							m_ptLastMousePos = ptCheck;
+							break; // 吞掉这次合成移动,不给控件改悬浮态
+						}
+					}
+				}
+
 				// Generate the appropriate mouse messages
 				POINT pt = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
 				// 是否移动
@@ -2005,6 +2143,9 @@ namespace DuiLib
 							static_cast<CControlUI*>(aNeedMouseLeaveNeeded[i])->Event(event);
 						}
 
+						TouchTrace::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);
 						m_pEventHover = NULL;
 						if (m_hwndTooltip != NULL) ::SendMessage(m_hwndTooltip, TTM_TRACKACTIVATE, FALSE, (LPARAM)&m_ToolTip);
@@ -2015,6 +2156,8 @@ namespace DuiLib
 						event.pSender = pNewHover;
 						pNewHover->Event(event);
 						m_pEventHover = pNewHover;
+						TouchTrace::Log(_T("HOVER-ENTER %s cls=%s pt=%d,%d"),
+							pNewHover->GetName().GetData(), pNewHover->GetClass(), pt.x, pt.y);
 					}
 				}
 				if (m_pEventClick != NULL)
@@ -2101,6 +2244,11 @@ namespace DuiLib
 				// 触屏拖拽手势(已发生位移)-> 松开不触发“点击”,避免“拖拽又误点”。
 				// 但滚动条滑块拖拽依赖 BUTTONUP 来解除 UISTATE_CAPTURED 并停掉 50ms 定时器,
 				// 绝不能在此提前清空 m_pEventClick,否则滚动条卡死在捕获态。故对滚动条放行 BUTTONUP。
+				TouchTrace::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"),
+					m_aNeedMouseLeaveNeeded.GetSize(), (int)m_bMouseCapture);
 				if (m_bTouchMoved && m_pEventClick != NULL && m_pEventClick->GetInterface(_T("ScrollBar")) == NULL)
 					m_pEventClick = NULL;
 				m_bTouchMoved = false;  // 用完即清,绝不影响后续真实鼠标点击

+ 6 - 0
DuiLib/Core/UIManager.h

@@ -525,6 +525,12 @@ namespace DuiLib {
 		POINT m_ptLastTouchPos = { 0, 0 };  // 上一包触点:用于去重 + 计算跟手滚动的位移增量
 		POINT m_ptTouchStart = { 0, 0 };    // 本次手势起点:用于判断是否发生位移(拖拽 vs 轻点)
 		bool m_bTouchMoved = false;         // 本次触摸是否发生位移(>阈值视为拖拽),松开时抑制点击
+		// 手指抬起后,Windows 仍会补发合成的 WM_MOUSEMOVE;且真实 WM_MOUSELEAVE 在
+		// m_bMouseTracking 为真时会【自己给自己重发 WM_MOUSEMOVE】(UIManager.cpp WM_MOUSELEAVE 分支),
+		// 导致刚清掉的悬浮态立刻被重新点亮 → “悬浮态不消失 / 多个控件同时悬浮”。
+		// 抬起后置 true,丢弃随后这批“非真实”的 MOUSEMOVE 悬浮逻辑;一旦收到用户真实移动鼠标
+		// (真实鼠标有 MK_LBUTTON 之外的持续位移特征,这里用“距上次触摸位置明显移动”判定)即解除。
+		bool m_bSuppressHoverAfterTouch = false;
 
 		//
 		CControlUI* m_pRoot;