张洋 4 weeks ago
parent
commit
0fd5502298
2 changed files with 87 additions and 160 deletions
  1. 72 144
      DuiLib/Core/UIManager.cpp
  2. 15 16
      DuiLib/Core/UIManager.h

+ 72 - 144
DuiLib/Core/UIManager.cpp

@@ -17,10 +17,9 @@ namespace DuiLib
 #ifndef TOUCH_COORD_TO_PIXEL
 #define TOUCH_COORD_TO_PIXEL(l) ((l) / 100)
 #endif
-	// tap slop:手指位移超过该距离即判为滑动手势,抬起时不再产生点击。
-	// NIM 原实现只靠「抬起点是否仍落在控件内」来判定是否 click;此处额外加滑动阈值,
-	// 是因为 POS 收银场景下手指微颤/小幅拖动极易被判成点击,从而误把商品加进购物车。
-	static constexpr LONG TOUCH_TAP_SLOP = 16;
+	// [触屏模型变更] 不再使用 tap-slop 阈值:100% 照搬 NIM Duilib,触摸“点击”交给 Windows
+	// 为触摸模拟的 WM_LBUTTONDOWN/UP(原生鼠标路径),滚动交给原生 UIEVENT_TOUCHMOVE,
+	// 二者由 m_pEventTouch 守卫互斥,无需自定滑动阈值。
 
 	// WM_TOUCH 消息 ID 同样受 `#if _WIN32_WINNT >= 0x0601` 保护,DuiLib 工程未定义
 	// _WIN32_WINNT(StdAfx.h 兜底为 XP)→ 必须自行声明,否则 switch 里引用该消息 ID
@@ -580,23 +579,16 @@ namespace DuiLib
 
 	void CPaintManagerUI::OnTouchInput(WPARAM wParam, LPARAM lParam)
 	{
-		// 对照 NIM Duilib 的 Window::HandleMessage case WM_TOUCH(Window.cpp:1139-1199):
-		//   解析触点 -> 换算坐标 -> FindControl -> 向控件派发真正的鼠标语义事件。
-		// 这里不合成任何鼠标消息:点击由控件自己吃到 BUTTONDOWN/BUTTONUP 后 Activate() 产生,
-		// 不经 Windows 消息队列,因此主窗口与模态弹框行为完全一致。
-		//
-		// ★ 关键:触摸的 DOWN/UP 必须 1:1 复用 DuiLib 既有的“鼠标点击状态机”
-		//   (CPaintManagerUI::SetCapture / m_pEventClick / ReleaseCapture),
-		//   绝不能用 raw ::SetCapture + 私有 m_pTouchTarget 另搞一套。原因:
-		//   所有控件(CButtonUI/COptionUI/...)的 BUTTONUP 都依赖“当初 BUTTONDOWN 时
-		//   已被 SetCapture 且 m_pEventClick 指向自己”这一前提,才能在抬手时正确 Activate(),
-		//   并把 m_pEventClick 清空、m_bMouseCapture 复位,使下一拍干净。
-		//   之前用 raw ::SetCapture + 私有 m_pTouchTarget,与鼠标状态机失配,导致:
-		//     (1) 按下去有 PUSHED 悬浮态、抬起来 BUTTONUP 丢拍 -> 不触发点击;
-		//     (2) 下一拍点别的控件时,上一拍残留的按下态才被结算 -> 点击延迟一拍;
-		//     (3) 多窗口(主窗 + 模态弹框)下 raw ::SetCapture 的 HWND 归属与 m_pEventClick
-		//         两套状态互相打架,弹框按钮始终收不到完整的 DOWN/UP 周期 -> 全无响应。
-		//   所以这里直接走与 WM_LBUTTONDOWN / WM_LBUTTONUP 完全一致的代码路径。
+		// 100% 照搬 NIM Duilib 的 Window::HandleMessage case WM_TOUCH(Window.cpp:1139-1199)。
+		// 设计要点(不要自创归一化 / 状态机):
+		//   1) 独立的 m_pEventTouch 追踪“本次触摸命中控件”,DOWN 设定、UP 清空;绝不复用 m_pEventClick。
+		//   2) 只向控件派发“原生触摸事件” UIEVENT_TOUCHDOWN / UIEVENT_TOUCHMOVE / UIEVENT_TOUCHUP,
+		//      由 CContainerUI::DoEvent 把 TOUCHMOVE 当作 1:1 跟手滚动(对标 NIM ScrollableBox)。
+		//   3) 按钮“点击”不在这里合成!它来自 Windows 为触摸自动模拟的 WM_LBUTTONDOWN/UP,
+		//      走 MessageHandler 里原生的鼠标点击路径(CButtonUI 等据此 Activate())。NIM 从不过滤
+		//      这些模拟鼠标,只在 WM_LBUTTON* 分支加了 m_pEventTouch!=NULL 守卫,保证一次手指手势
+		//      要么滚动、要么点击,不会双触发。
+		//   4) 守卫:若 m_pEventClick 已存在(鼠标/指针交互进行中),直接忽略本次触摸,互不打架。
 		TouchApi::PFN_GetTouchInputInfo pfnGetInfo = TouchApi::GetGetTouchInputInfo();
 		if (pfnGetInfo == NULL) return;
 
@@ -610,6 +602,9 @@ namespace DuiLib
 		{
 			TouchApi::TOUCHINPUT& ti = inputs[i];
 
+			// 仅处理主触点(dwID==0 为保留值,NIM 同款跳过)
+			if (ti.dwID == 0) continue;
+
 			POINT pt;
 			pt.x = TOUCH_COORD_TO_PIXEL(ti.x);
 			pt.y = TOUCH_COORD_TO_PIXEL(ti.y);
@@ -617,122 +612,64 @@ namespace DuiLib
 
 			if (ti.dwFlags & TouchApi::TOUCHEVTFLAG_DOWN)
 			{
-				m_bTouchDown = true;
-				m_ptTouchDownPos = pt;
+				// 鼠标/指针交互进行中 -> 忽略触摸(NIM 同款守卫)
+				if (m_pEventClick != NULL) break;
+
+				::SetFocus(m_hWndPaint);
 				m_ptLastTouchPos = pt;
-				m_ptLastMousePos = pt;
-				m_moveDistSq = 0;
-				m_bTouchMoved = false;
 
-				m_pTouchTarget = FindControl(pt);
-				if (m_pTouchTarget != NULL && m_pTouchTarget->GetManager() != this)
-					m_pTouchTarget = NULL;
+				CControlUI* pControl = FindControl(pt);
+				if (pControl == NULL) break;
+				if (pControl->GetManager() != this) break;
 
-				if (m_pTouchTarget != NULL)
-				{
-					// —— 复用鼠标点击状态机(与 WM_LBUTTONDOWN 完全一致)——
-					SetCapture();                    // mgr:m_bMouseCapture=true + ::SetCapture(hWndPaint)
-					m_pEventClick = m_pTouchTarget;  // 点击目标槽,UP 时据此派发 BUTTONUP
-					m_pTouchTarget->SetFocus();
+				m_pEventTouch = pControl;
+				pControl->SetFocus();
+				SetCapture();                              // mgr 级:m_bMouseCapture=true + ::SetCapture
 
-					TEventUI event = { 0 };
-					// 归一化:直接派发等价鼠标按下事件,使重写了 DoEvent 的控件(CButtonUI 等)也能原生响应手指。
-					event.Type = UIEVENT_BUTTONDOWN;
-					event.pSender = m_pTouchTarget;
-					event.ptMouse = pt;
-					event.dwTimestamp = ::GetTickCount();
-					event.wKeyState = MapKeyState();
-					m_pTouchTarget->Event(event);
-				}
+				TEventUI event = { 0 };
+				event.Type = UIEVENT_TOUCHDOWN;
+				event.pSender = pControl;
+				event.ptMouse = pt;
+				event.dwTimestamp = ::GetTickCount();
+				event.wKeyState = MapKeyState();
+				pControl->Event(event);
 			}
 			else if (ti.dwFlags & TouchApi::TOUCHEVTFLAG_MOVE)
 			{
-				if (!m_bTouchDown) continue;
-
+				// 鼠标/指针交互进行中 -> 忽略触摸
+				if (m_pEventClick != NULL) break;
+				// 与上一包完全相同的点 -> 去重,避免无谓派发(NIM 同款守卫)
+				if (m_ptLastTouchPos.x == pt.x && m_ptLastTouchPos.y == pt.y) break;
+				// 本次位移增量(相对上一包),随事件下发,滚动容器据此做 1:1 跟手平移
 				LONG dx = pt.x - m_ptLastTouchPos.x;
 				LONG dy = pt.y - m_ptLastTouchPos.y;
 				m_ptLastTouchPos = pt;
-				m_ptLastMousePos = pt;
+				if (m_pEventTouch == NULL) break;
 
-				// tap/drag 判定用“距按下点的净位移平方”,而非逐包位移累加:
-				// 触屏抖动手指会来回小幅移动,累加会让净位移≈0 的轻触被误判成滑动。
-				LONG netDx = pt.x - m_ptTouchDownPos.x;
-				LONG netDy = pt.y - m_ptTouchDownPos.y;
-				m_moveDistSq = (std::int64_t)netDx * netDx + (std::int64_t)netDy * netDy;
-
-				if (!m_bTouchMoved)
-				{
-					if (m_moveDistSq > (std::int64_t)TOUCH_TAP_SLOP * TOUCH_TAP_SLOP)
-					{
-						// 越过“轻触死区”-> 判定为滑动。吞掉越过死区这一包的移动量,
-						// 让滚动从死区边界干净起步,避免突然跳动;后续包才真正跟手滚动。
-						m_bTouchMoved = true;
-						continue;
-					}
-					// 仍在轻触死区内:吸收位移,不滚动、不派发 TOUCHMOVE
-					// (否则“点一下不滑动”也会被抖动的 MOVE 包驱动成滚动,且抬起时点击被吞掉)。
-					continue;
-				}
-
-				if (dx == 0 && dy == 0) continue;
-
-				// 滑动已确认:派发给当前触点下的控件。绝大多数控件不认 UIEVENT_TOUCHMOVE,
-				// 会沿 CControlUI::DoEvent 末尾的 m_pParent->DoEvent 天然冒泡到最近的滚动容器
-				// (等价于 NIM 里 ScrollableBox 收到 kEventTouchMove),由它做 1:1 跟手平移。
-				CControlUI* pTarget = FindControl(pt);
-				if (pTarget != NULL && pTarget->GetManager() != this) pTarget = NULL;
-				if (pTarget == NULL) pTarget = m_pTouchTarget;
-				if (pTarget != NULL)
-				{
-					TEventUI event = { 0 };
-					event.Type = UIEVENT_TOUCHMOVE;
-					event.pSender = pTarget;
-					event.ptMouse = pt;
-					event.dwTimestamp = ::GetTickCount();
-					event.wKeyState = MapKeyState();
-					// 位移增量随事件下发,滚动容器无需各自维护"上一次触点",避免多容器之间失步
-					event.wParam = (WPARAM)(int)dx;
-					event.lParam = (LPARAM)(int)dy;
-					pTarget->Event(event);
-				}
+				TEventUI event = { 0 };
+				event.Type = UIEVENT_TOUCHMOVE;
+				event.pSender = m_pEventTouch;
+				event.ptMouse = pt;
+				event.dwTimestamp = ::GetTickCount();
+				event.wKeyState = MapKeyState();
+				event.wParam = (WPARAM)(int)dx;
+				event.lParam = (LPARAM)(int)dy;
+				m_pEventTouch->Event(event);
 			}
 			else if (ti.dwFlags & TouchApi::TOUCHEVTFLAG_UP)
 			{
-				if (!m_bTouchDown) continue;
-
-				// —— 复用鼠标点击状态机(与 WM_LBUTTONUP 完全一致)——
-				if (m_pEventClick != NULL)
-				{
-					// 一旦判定为滑动手势,就把触点坐标挪到控件之外的 (-1,-1):
-					// 按钮的 UIEVENT_BUTTONUP 分支只在 PtInRect 通过时才 Activate(),
-					// 这样既能正常清除 PUSHED/CAPTURED 态,又不会误触发 click。
-					POINT ptUp = pt;
-					if (m_bTouchMoved)
-					{
-						ptUp.x = -1;
-						ptUp.y = -1;
-					}
-
-					ReleaseCapture();                // mgr:m_bMouseCapture=false + ::ReleaseCapture
-
-					TEventUI event = { 0 };
-					// 归一化:直接派发等价鼠标抬起事件(MOVE 仍保持 UIEVENT_TOUCHMOVE 留给 CContainerUI 滚动)。
-					event.Type = UIEVENT_BUTTONUP;
-					event.pSender = m_pEventClick;
-					event.ptMouse = ptUp;
-					event.dwTimestamp = ::GetTickCount();
-					event.wKeyState = MapKeyState();
-
-					CControlUI* pClick = m_pEventClick;
-					m_pEventClick = NULL;           // 抬手即清空,保证下一拍干净(消除“点击延迟一拍”)
-					pClick->Event(event);
-				}
+				m_ptLastTouchPos = pt;
+				ReleaseCapture();
+				if (m_pEventTouch == NULL) break;
 
-				// 无论如何复位触摸手势态,避免状态泄漏(如手势被系统取消、未收到 UP 等)
-				m_bTouchDown = false;
-				m_pTouchTarget = NULL;
-				m_moveDistSq = 0;
-				m_bTouchMoved = false;
+				TEventUI event = { 0 };
+				event.Type = UIEVENT_TOUCHUP;
+				event.pSender = m_pEventTouch;
+				event.ptMouse = pt;
+				event.dwTimestamp = ::GetTickCount();
+				event.wKeyState = MapKeyState();
+				m_pEventTouch->Event(event);
+				m_pEventTouch = NULL;
 			}
 		}
 
@@ -1300,28 +1237,11 @@ namespace DuiLib
 	{
 		if (m_hWndPaint == NULL) return false;
 
-		// [触屏兼容修复] 过滤 Windows 为“触摸手势”模拟出来的鼠标消息(ExtraInfo 带 MOUSEEVENTF_FROMTOUCH 标记)。
-		// 背景:调用 RegisterTouchWindow 后,Windows 在把 WM_TOUCH 交给我们的同时,还会“模拟”出一套
-		//   WM_LBUTTONDOWN / WM_LBUTTONMOVE / WM_LBUTTONUP(以及可能的 WM_LBUTTONDBLCLK)。
-		// 这些模拟消息若进入 DuiLib,会导致:
-		//   * 手指刚按下准备滑动就被当成“点击” —— 误把商品加入购物车;
-		//   * 滑动时经过的每个商品 item 都被置上悬浮态(UISTATE_HOT) 且无人清除 —— 多个 item 同时悬浮;
-		//   * 抬起后悬浮态卡死不消失。
-		// 触摸手势的实际处理(跟手滚动 / 轻触点击)由 CPaintManagerUI 通过 WM_TOUCH 完成,
-		// 派发成等价鼠标事件 UIEVENT_BUTTONDOWN/BUTTONUP 交给控件;控件按原生鼠标语义判定 Activate() 触发 click,
-// 全程不再合成任何鼠标消息(旧的 SynthesizeTap 方案已废弃)。
-		//
-		// 作用域天然正确:只有调用过 RegisterTouchWindow 的窗口(所有窗口已在 CPaintManagerUI::Init 注册)才会产生带
-		//   FROMTOUCH 标记的模拟鼠标;DuiLib 的下拉框/菜单等未注册触摸的弹出窗口不会产生该标记,照常进入
-		//   DuiLib,触摸点选不受影响。因此不必再用窗口属性做额外限定。
-		// 注:未用 IsTouchWindow,因其声明要求 _WIN32_WINNT>=0x0601,本项目部分目标 SDK 没有该符号。
-		if ((uMsg == WM_LBUTTONDOWN || uMsg == WM_LBUTTONUP || uMsg == WM_LBUTTONDBLCLK
-			|| uMsg == WM_MOUSEMOVE)
-			&& (::GetMessageExtraInfo() & 0xFF515700) == 0xFF515700)
-		{
-			lRes = 0;
-			return true; // 标记为已处理,阻止 DuiLib 继续处理
-		}
+		// [触屏兼容] 不再过滤 Windows 为触摸模拟的鼠标消息(MOUSEEVENTF_FROMTOUCH)。
+		// 100% 照搬 NIM Duilib:触摸的“点击”正来自这些模拟的 WM_LBUTTONDOWN/UP,
+		// 由 MessageHandler 原生鼠标路径交给 CButtonUI 等控件 Activate() 触发。
+		// 为防止一次手指手势既滚动又点击,仅在 WM_LBUTTON*/WM_MOUSEMOVE 分支加了
+		// `if (m_pEventTouch != NULL) return;` 守卫(手势进行中 m_pEventTouch 非空)。
 
 		// Cycle through listeners
 		for (int i = 0; i < m_aMessageFilters.GetSize(); i++)
@@ -1918,6 +1838,8 @@ namespace DuiLib
 			break;
 			case WM_MOUSEMOVE:
 			{
+				// 触屏手势进行中 -> 忽略模拟鼠标移动(NIM 同款守卫)
+				if (m_pEventTouch != NULL) { lRes = 0; return true; }
 				// Start tracking this entire window again...
 				if (!m_bMouseTracking)
 				{
@@ -2057,6 +1979,8 @@ namespace DuiLib
 			break;
 			case WM_LBUTTONDOWN:
 			{
+				// 触屏手势进行中 -> 忽略模拟鼠标,避免“既滚动又点击”双触发(NIM 同款守卫)
+				if (m_pEventTouch != NULL) { lRes = 0; return true; }
 				// We alway set focus back to our app (this helps
 				// when Win32 child windows are placed on the dialog
 				// and we need to remove them on focus change).
@@ -2100,6 +2024,8 @@ namespace DuiLib
 			break;
 			case WM_LBUTTONDBLCLK:
 			{
+				// 触屏手势进行中 -> 忽略模拟鼠标(NIM 同款守卫)
+				if (m_pEventTouch != NULL) { lRes = 0; return true; }
 				if (!m_bNoActivate) ::SetFocus(m_hWndPaint);
 
 				POINT pt = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
@@ -2122,6 +2048,8 @@ namespace DuiLib
 			break;
 			case WM_LBUTTONUP:
 			{
+				// 触屏手势进行中 -> 忽略模拟鼠标(NIM 同款守卫)
+				if (m_pEventTouch != NULL) { lRes = 0; return true; }
 				POINT pt = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
 				m_ptLastMousePos = pt;
 				if (m_pEventClick == NULL) break;

+ 15 - 16
DuiLib/Core/UIManager.h

@@ -469,16 +469,15 @@ namespace DuiLib {
 		bool PreMessageHandler(UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT& lRes);
 		void UsedVirtualWnd(bool bUsed);
 
-		// 触屏支持(NIM Duilib 模型,库级统一接管:主窗口 + 所有弹框通用)。
-		// WM_TOUCH 在 MessageHandler 里被解析为真正的 DuiLib 触摸事件,直接派发给命中的控件:
-		//   DOWN → UIEVENT_BUTTONDOWN(归一化) 控件按原生鼠标语义处理按下(按钮置 PUSHED/CAPTURED)
-		//   MOVE → UIEVENT_TOUCHMOVE  经 DoEvent 冒泡链由 CContainerUI 接收,做 1:1 跟手平移
-		//   UP   → UIEVENT_BUTTONUP(归一化)   控件按原生鼠标语义处理抬起(按钮 PtInRect 通过才 Activate → click)
-		// 全程不合成任何鼠标消息(已废弃 SynthesizeTap):点击由控件 Activate()+SendNotify 直接产生,
-		// 不经过 Windows 消息队列,所以主窗口与模态弹框行为完全一致。
-		// MOVE 在“轻触死区”(TOUCH_TAP_SLOP=16px 净位移)内被吸收,不派发 TOUCHMOVE,避免“点一下就滚动”;
-		// 越过后才判定为滑动并跟手滚动,抬起时 ptMouse 设 (-1,-1) 抑制误触 click。
-		// 注册在 Init 中完成,无需逐窗口样板,也无需 CTouchWindowBase 外挂。
+		// 触屏支持(100% 照搬 NIM Duilib 模型,库级统一接管:主窗口 + 所有弹框通用)。
+		// WM_TOUCH 在 MessageHandler 里解析为原生 DuiLib 触摸事件,直接派发给命中的控件:
+		//   DOWN → UIEVENT_TOUCHDOWN   仅用于滚动容器感知手势起点(绝大多数控件忽略)
+		//   MOVE → UIEVENT_TOUCHMOVE   经 DoEvent 冒泡链由 CContainerUI 接收,做 1:1 跟手平移
+		//   UP   → UIEVENT_TOUCHUP     仅用于滚动容器收尾(绝大多数控件忽略)
+		// 按钮“点击”【不】在此合成:它来自 Windows 为触摸自动模拟的 WM_LBUTTONDOWN/UP,
+		// 走 MessageHandler 原生鼠标路径 → CButtonUI 等据此 Activate() 触发 click。
+		// NIM 从不过滤这些模拟鼠标,只在 WM_LBUTTON*/WM_MOUSEMOVE 分支用 m_pEventTouch!=NULL 守卫
+		// 防止“一次手指手势既滚动又点击”。注册在 Init 中完成,无需逐窗口样板。
 		void InitTouchSupport();
 		void OnTouchInput(WPARAM wParam, LPARAM lParam);
 
@@ -517,12 +516,12 @@ namespace DuiLib {
 		bool m_bShowUpdateRect;
 
 		// 触屏状态(库级统一接管后,每个窗口的 CPaintManagerUI 各持一份)
-		bool m_bTouchDown = false;          // 手指是否按下
-		POINT m_ptLastTouchPos = { 0, 0 };  // 上一次触点:与本次触点差分,得到跟手滚动的平移增量
-		POINT m_ptTouchDownPos = { 0, 0 };  // 按下时的触点:与本次触点算“净位移”,用于 tap/drag 判定
-		CControlUI* m_pTouchTarget = NULL;  // 按下时命中的控件,抬起时复用同一目标(NIM 语义)
-		std::int64_t m_moveDistSq = 0;      // 距按下点的“净位移平方”,用于 tap slop(轻触/滑动)判定
-		bool m_bTouchMoved = false;         // 是否已超过 tap slop:超过则不再判为点击
+		// 100% 照搬 NIM Duilib(Window.cpp:1139-1199):用独立成员 m_pEventTouch 追踪“本次触摸命中控件”,
+		// DOWN 时设定、UP 时清空;绝不复用 m_pEventClick(那是鼠标点击状态机专用)。
+		// 按钮点击不在此合成,而是来自 Windows 为触摸自动模拟的 WM_LBUTTONDOWN/UP(见 MessageHandler 的
+		// WM_LBUTTON* 分支)——NIM 从不过滤这些模拟鼠标,仅在鼠标分支加了 m_pEventTouch!=NULL 守卫防止双触发。
+		CControlUI* m_pEventTouch = NULL;   // 本次触摸命中的控件(对应 NIM 的 m_pEventTouch)
+		POINT m_ptLastTouchPos = { 0, 0 };  // 上一包触点:用于去重 + 计算跟手滚动的位移增量
 
 		//
 		CControlUI* m_pRoot;