Browse Source

修复触摸的相关问题

张洋 3 weeks ago
parent
commit
1aacbb0d06

+ 22 - 1
DuiLib/Control/UIScrollBar.cpp

@@ -1,4 +1,4 @@
-#include "StdAfx.h"
+#include "StdAfx.h"
 #include "UIScrollBar.h"
 
 namespace DuiLib
@@ -688,6 +688,7 @@ namespace DuiLib
 			if( (m_uThumbState & UISTATE_CAPTURED) != 0 ) {
 				if( !m_bHorizontal ) {
 					__int64 fMouseRange = (event.ptMouse.y - m_ptLastMouse.y) * m_nRange;
+					if (event.Type == UIEVENT_TOUCHMOVE) fMouseRange = -fMouseRange;
 					int nBtnSize = 0;
 					if(GetShowButton1()) nBtnSize += m_cxyFixed.cx;
 					if(GetShowButton2()) nBtnSize += m_cxyFixed.cx;
@@ -698,12 +699,32 @@ namespace DuiLib
 				}
 				else {
 					__int64 fMouseRange = (event.ptMouse.x - m_ptLastMouse.x) * m_nRange;
+					if (event.Type == UIEVENT_TOUCHMOVE) fMouseRange = -fMouseRange;
 					int nBtnSize = 0;
 					if(GetShowButton1()) nBtnSize += m_cxyFixed.cy;
 					if(GetShowButton2()) nBtnSize += m_cxyFixed.cy;
 					int hRange = m_rcItem.right - m_rcItem.left - m_rcThumb.right + m_rcThumb.left - nBtnSize;
 					if (hRange != 0) m_nLastScrollOffset = fMouseRange / abs(hRange);
 				}
+
+				if (event.Type == UIEVENT_TOUCHMOVE && m_nLastScrollOffset != 0)
+				{
+					if (m_pOwner != NULL)
+					{
+						SIZE szPos = m_pOwner->GetScrollPos();
+						if (m_bHorizontal) szPos.cx += m_nLastScrollOffset;
+						else szPos.cy += m_nLastScrollOffset;
+						m_pOwner->SetScrollPos(szPos);
+					}
+					else
+					{
+						SetScrollPos(m_nScrollPos + m_nLastScrollOffset);
+					}
+					m_ptLastMouse = event.ptMouse;
+					m_nLastScrollOffset = 0;
+					m_nLastScrollPos = m_nScrollPos;
+					Invalidate();
+				}
 			}
 			else {
 				if( (m_uThumbState & UISTATE_HOT) != 0 ) {

+ 18 - 0
DuiLib/Core/UIBase.cpp

@@ -7,6 +7,21 @@
 
 namespace DuiLib {
 
+namespace {
+    typedef BOOL (WINAPI* PFN_REGISTER_TOUCH_WINDOW)(HWND, ULONG);
+
+    void RegisterTouchWindowIfAvailable(HWND hWnd)
+    {
+        if (hWnd == NULL) return;
+        HMODULE hUser32 = ::GetModuleHandle(_T("user32.dll"));
+        if (hUser32 == NULL) return;
+        PFN_REGISTER_TOUCH_WINDOW pfnRegister =
+            reinterpret_cast<PFN_REGISTER_TOUCH_WINDOW>(::GetProcAddress(hUser32, "RegisterTouchWindow"));
+        if (pfnRegister != NULL)
+            pfnRegister(hWnd, 0);
+    }
+}
+
 /////////////////////////////////////////////////////////////////////////////////////
 //
 //
@@ -272,6 +287,7 @@ HWND CWindowWnd::Create(HWND hwndParent, LPCTSTR pstrName, DWORD dwStyle, DWORD
 		m_hWnd = ::CreateWindowEx(dwExStyle, GetWindowClassName(), pstrName, dwStyle, x, y, cx, cy, hwndParent, hMenu, CPaintManagerUI::GetInstance(), this);
 	}
     ASSERT(m_hWnd!=NULL);
+    RegisterTouchWindowIfAvailable(m_hWnd);
     return m_hWnd;
 }
 
@@ -284,6 +300,7 @@ HWND CWindowWnd::Subclass(HWND hWnd)
     m_bSubclassed = true;
     m_hWnd = hWnd;
     ::SetWindowLongPtr(hWnd, GWLP_USERDATA, reinterpret_cast<LPARAM>(this));
+    RegisterTouchWindowIfAvailable(m_hWnd);
     return m_hWnd;
 }
 
@@ -309,6 +326,7 @@ UINT CWindowWnd::ShowModal()
     ASSERT(::IsWindow(m_hWnd));
     UINT nRet = 0;
     HWND hWndParent = GetWindowOwner(m_hWnd);
+    RegisterTouchWindowIfAvailable(m_hWnd);
     ::ShowWindow(m_hWnd, SW_SHOWNORMAL);
     ::EnableWindow(hWndParent, FALSE);
     MSG msg = { 0 };

+ 270 - 68
DuiLib/Core/UIManager.cpp

@@ -22,11 +22,11 @@ namespace DuiLib
 #define TOUCH_COORD_TO_PIXEL(l) ((l) / 100)
 #endif
 	// [触屏模型] 触摸只做两件事,不做任何“触摸→鼠标”的归一化:
-	//   ①点击:交给 Windows 为触摸自动模拟的 WM_LBUTTONDOWN/UP(原生鼠标路径),
+	//   ①点击:由 TOUCHUP 在确认未拖动后派发 BUTTONDOWN/UP,
 	//         由 CButtonUI 等控件原生 Activate();
 	//   ②滚动:WM_TOUCH 直接派发 UIEVENT_TOUCHDOWN/MOVE/UP(见 OnTouchInput),
 	//         容器据此 1:1 跟手平移、滚动条据此拖滑块。
-	//   “轻点 vs 拖拽”不用距离阈值互斥,而是在 WM_LBUTTONUP 用 m_bTouchMoved 抑制误点。
+	//   “轻点 vs 拖拽”使用 m_bTouchMoved 区分,拖动不会激活控件。
 
 	// WM_TOUCH 消息 ID 同样受 `#if _WIN32_WINNT >= 0x0601` 保护,DuiLib 工程未定义
 	// _WIN32_WINNT(StdAfx.h 兜底为 XP)→ 必须自行声明,否则 switch 里引用该消息 ID
@@ -38,22 +38,13 @@ namespace DuiLib
 #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;
+			return false;
 		}
 
 		// 极简 TCHAR* → UTF-8 char* 转换(仅用于日志路径;非线程安全,够用即可)
@@ -589,9 +580,9 @@ namespace DuiLib
 		// 名字必须避开 SDK 的同名宏:部分 SDK 把它们定义成【宏】,且不受 _WIN32_WINNT 版本段保护,
 		// 同名会被预处理器展开——声明变成 `const DWORD 0x0002 = 0x0002;`(C2059),
 		// 限定使用变成 `TouchApi::0x0004`(C2589)。故改用 TOUCHEVTFLAG 前缀。
-		const DWORD TOUCHEVTFLAG_MOVE = 0x0002;
-		const DWORD TOUCHEVTFLAG_DOWN = 0x0004;
-		const DWORD TOUCHEVTFLAG_UP   = 0x0008;
+		const DWORD TOUCHEVTFLAG_MOVE = 0x0001;
+		const DWORD TOUCHEVTFLAG_DOWN = 0x0002;
+		const DWORD TOUCHEVTFLAG_UP   = 0x0004;
 
 		typedef BOOL (WINAPI *PFN_RegisterTouchWindow)(HWND, ULONG);
 		typedef BOOL (WINAPI *PFN_GetTouchInputInfo)(HANDLE, UINT, PTOUCHINPUT, int);
@@ -666,7 +657,7 @@ namespace DuiLib
 		// 消息本身绝不丢弃——历史教训:整条吞掉会让点击状态机(m_pEventClick)失效、点击全灭。
 		inline bool IsPromotedTouchMouseMessage()
 		{
-			return (::GetMessageExtraInfo() & 0xFF515700) == 0xFF515700;
+			return (::GetMessageExtraInfo() & 0xFFFFFF80) == 0xFF515700;
 		}
 
 		// 把触点收进滚动条自身 rect:垂直滚动条主要收敛 x(y 原样保留,纵向拖拽量完好)。
@@ -698,6 +689,45 @@ namespace DuiLib
 			}
 			return NULL;
 		}
+
+		inline CScrollBarUI* Touch_FindScrollBar(CControlUI* pStart, POINT pt)
+		{
+			for (CControlUI* p = pStart; p != NULL; p = p->GetParent())
+			{
+				CContainerUI* pContainer = static_cast<CContainerUI*>(p->GetInterface(DUI_CTR_CONTAINER));
+				if (pContainer == NULL) continue;
+
+				CScrollBarUI* bars[2] = {
+					pContainer->GetVerticalScrollBar(),
+					pContainer->GetHorizontalScrollBar()
+				};
+				for (int i = 0; i < 2; ++i)
+				{
+					if (bars[i] == NULL || !bars[i]->IsVisible() || !bars[i]->IsEnabled())
+						continue;
+					RECT rc = bars[i]->GetPos();
+					::InflateRect(&rc, kScrollBarTouchSlop, kScrollBarTouchSlop);
+					if (::PtInRect(&rc, pt)) return bars[i];
+				}
+			}
+			return NULL;
+		}
+
+		inline CContainerUI* Touch_FindScrollContainer(CControlUI* pStart)
+		{
+			for (CControlUI* p = pStart; p != NULL; p = p->GetParent())
+			{
+				CContainerUI* pContainer = static_cast<CContainerUI*>(p->GetInterface(DUI_CTR_CONTAINER));
+				if (pContainer == NULL) continue;
+
+				CScrollBarUI* pVertical = pContainer->GetVerticalScrollBar();
+				CScrollBarUI* pHorizontal = pContainer->GetHorizontalScrollBar();
+				if ((pVertical != NULL && pVertical->IsVisible() && pVertical->IsEnabled())
+					|| (pHorizontal != NULL && pHorizontal->IsVisible() && pHorizontal->IsEnabled()))
+					return pContainer;
+			}
+			return NULL;
+		}
 	}
 
 	void CPaintManagerUI::InitTouchSupport()
@@ -729,17 +759,24 @@ namespace DuiLib
 		if (pfnGetInfo == NULL) return;
 
 		UINT cInputs = LOWORD(wParam);
-		if (cInputs == 0) return;
+		TouchTrace::Log(_T("WM_TOUCH begin inputs=%u handle=%p"), cInputs, (HANDLE)lParam);
+		if (cInputs == 0)
+		{
+			TouchTrace::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());
 			return;
+		}
 
 		for (UINT i = 0; i < cInputs; ++i)
 		{
 			TouchApi::TOUCHINPUT& ti = inputs[i];
-
-			// 仅处理主触点(dwID==0 为保留值,NIM 同款跳过)
-			if (ti.dwID == 0) continue;
+			TouchTrace::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;
 			pt.x = TOUCH_COORD_TO_PIXEL(ti.x);
@@ -748,6 +785,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);
 				// 这里【不再】用 `if (m_pEventClick != NULL) break;` 做通用守卫:Windows 会在 WM_TOUCH
 				// 前后补发合成的 WM_LBUTTONDOWN(同样设置 m_pEventClick),通用守卫会让 m_pEventTouch
 				// 永不设置 → 容器收不到 TOUCHMOVE,列表滚不动。
@@ -759,21 +797,36 @@ namespace DuiLib
 
 				::SetFocus(m_hWndPaint);
 				m_ptLastTouchPos = pt;
+				// 触摸没有“悬浮”语义。新手势开始前先清掉上一次鼠标路径遗留的 HOT 状态,
+				// 防止首个触摸 DOWN 恰好发生在合成 WM_MOUSEMOVE 之前。
+				ClearTouchHoverState(pt);
 				// 先把手势状态复位:即使这一按没命中任何控件,也不能让上一轮的 m_pEventTouch 残留
 				// 下去(否则后续 TOUCHMOVE 会发给一个早已松开的手指所对应的控件)。
 				m_pEventTouch = NULL;
+				m_pTouchScrollContainer = NULL;
 				m_bTouchMoved = false;
 				m_ptTouchStart = pt;
 
 				CControlUI* pControl = FindControl(pt);
-				if (pControl == NULL) break;
-				if (pControl->GetManager() != this) break;
+				if (pControl == NULL)
+				{
+					TouchTrace::Log(_T("TOUCHDOWN no control client=%d,%d"), pt.x, pt.y);
+					break;
+				}
+				if (pControl->GetManager() != this)
+				{
+					TouchTrace::Log(_T("TOUCHDOWN foreign control=%s manager=%p self=%p"),
+						pControl->GetName().GetData(), (void*)pControl->GetManager(), (void*)this);
+					break;
+				}
 
 				// [触屏] 手指容差改判:直接命中的不是滚动条时,看看是不是“手指离滚动条滑块很近”。
 				// 典型场景:列表右侧竖直滚动条仅 16px 宽,用户想拖滑块却按到了旁边的列表项上。
 				if (pControl->GetInterface(DUI_CTR_SCROLLBAR) == NULL)
 				{
-					CControlUI* pBar = Touch_ResolveScrollBarThumb(pControl, pt);
+					CControlUI* pBar = Touch_FindScrollBar(pControl, pt);
+					if (pBar == NULL)
+						pBar = Touch_ResolveScrollBarThumb(pControl, pt);
 					if (pBar != NULL)
 					{
 						TouchTrace::Log(_T("TOUCHDOWN-FATFINGER hit=%s -> scrollbar=%s pt=%d,%d"),
@@ -783,6 +836,9 @@ namespace DuiLib
 				}
 
 				bool bTouchIsScrollBar = (pControl->GetInterface(DUI_CTR_SCROLLBAR) != NULL);
+				TouchTrace::Log(_T("TOUCHDOWN hit name=%s class=%s scrollbar=%d scrollContainer=%p"),
+					pControl->GetName().GetData(), pControl->GetClass(), (int)bTouchIsScrollBar,
+					(void*)Touch_FindScrollContainer(pControl));
 				// 命中滚动条时把触点收进滚动条 rect(横向按偏也能拖滑块);事件点用收敛后的,
 				// 但 m_ptLastTouchPos / m_ptTouchStart 仍记真实坐标(去重与位移判定按真实手指轨迹走)。
 				POINT ptEvent = bTouchIsScrollBar ? Touch_SnapIntoScrollBar(pControl, pt) : pt;
@@ -794,8 +850,13 @@ namespace DuiLib
 				}
 
 				m_pEventTouch = pControl;
+				m_pTouchScrollContainer = bTouchIsScrollBar ? NULL : Touch_FindScrollContainer(pControl);
+				if (!bTouchIsScrollBar)
+					m_pEventClick = pControl;
 				m_ptTouchStart = pt;
-				m_bSuppressHoverAfterTouch = false;  // 新手势开始,解除上一轮抬起后的悬浮抑制
+				// 触摸没有悬浮语义;整个手势期间以及抬起后的合成鼠标移动都不应
+				// 重建 HOT。真实鼠标移动时在 WM_MOUSEMOVE 中解除该抑制。
+				m_bSuppressHoverAfterTouch = true;
 				pControl->SetFocus();
 				SetCapture();                              // mgr 级:m_bMouseCapture=true + ::SetCapture
 				TouchTrace::Log(_T("TOUCHDOWN hit=%s cls=%s pt=%d,%d click=%p capture=%d"),
@@ -809,9 +870,18 @@ namespace DuiLib
 				event.dwTimestamp = ::GetTickCount();
 				event.wKeyState = MapKeyState();
 				pControl->Event(event);
+
+				// 触摸按下只建立候选点击,不立即发送 BUTTONDOWN。
+				// 用户可能正在从商品项或空白区域开始拖动滚动容器;若此时先发送
+				// BUTTONDOWN,控件会先进入按下态,随后很容易被提升的鼠标消息激活。
+				// 轻点的 BUTTONDOWN/BUTTONUP 在 TOUCHUP 且未越过阈值时成对发送。
 			}
 			else if (ti.dwFlags & TouchApi::TOUCHEVTFLAG_MOVE)
 			{
+				TouchTrace::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);
 				// 【不要】在这里对滚动条让路。滚动条现在完全由 WM_TOUCH 驱动:DOWN 已把 m_pEventTouch
 				// 设为"滚动条自身"(不再交回原生鼠标),CScrollBarUI 也已把 TOUCHDOWN/MOVE/UP 与鼠标
 				// 事件等价处理。若此处仍 `m_pEventClick->GetInterface("ScrollBar") != NULL` 就 break,
@@ -834,7 +904,9 @@ namespace DuiLib
 
 				TEventUI event = { 0 };
 				event.Type = UIEVENT_TOUCHMOVE;
-				event.pSender = m_pEventTouch;
+				CControlUI* pTouchTarget = m_pTouchScrollContainer != NULL
+					? static_cast<CControlUI*>(m_pTouchScrollContainer) : m_pEventTouch;
+				event.pSender = pTouchTarget;
 				// 滚动条:触点同样收进滚动条 rect(与 DOWN 保持一致,避免首帧跳变)。
 				event.ptMouse = (m_pEventTouch->GetInterface(DUI_CTR_SCROLLBAR) != NULL)
 					? Touch_SnapIntoScrollBar(m_pEventTouch, pt) : pt;
@@ -846,11 +918,16 @@ namespace DuiLib
 					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);
+				pTouchTarget->Event(event);
 			}
 			else if (ti.dwFlags & TouchApi::TOUCHEVTFLAG_UP)
 			{
+				TouchTrace::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);
 				m_ptLastTouchPos = pt;
+				ClearTouchHoverState(pt);
 
 				// 触屏抬起模拟“鼠标离开”:清除悬浮态。触摸永远不会生成 WM_MOUSELEAVE,
 				// 若只清 m_pEventHover,m_aNeedMouseLeaveNeeded 数组里登记过的控件
@@ -894,7 +971,12 @@ namespace DuiLib
 				m_bMouseTracking = false;
 
 				// 本函数未接管该次触摸(如 DOWN 时 FindControl 落空)→ 沿旧路径交给原生鼠标,这里不释放。
-				if (m_pEventTouch == NULL) break;
+				if (m_pEventTouch == NULL)
+				{
+					m_pTouchScrollContainer = NULL;
+					m_bTouchMoved = false;
+					break;
+				}
 
 				ReleaseCapture();
 
@@ -910,7 +992,31 @@ namespace DuiLib
 					m_pEventTouch->GetName().GetData(), (int)m_bTouchMoved, (void*)m_pEventHover,
 					m_aNeedMouseLeaveNeeded.GetSize(), (void*)m_pEventClick, (int)m_bMouseCapture);
 				m_pEventTouch->Event(event);
+
+				if (m_pEventClick != NULL && !m_bTouchMoved
+					&& m_pEventClick->GetInterface(DUI_CTR_SCROLLBAR) == NULL)
+				{
+					CControlUI* pClick = m_pEventClick;
+					m_pEventClick = NULL;
+					TEventUI clickEvent = { 0 };
+					clickEvent.Type = UIEVENT_BUTTONDOWN;
+					clickEvent.pSender = pClick;
+					clickEvent.ptMouse = pt;
+					clickEvent.dwTimestamp = ::GetTickCount();
+					clickEvent.wKeyState = MapKeyState();
+					pClick->Event(clickEvent);
+
+					clickEvent.Type = UIEVENT_BUTTONUP;
+					pClick->Event(clickEvent);
+				}
+				else if (m_pEventClick != NULL)
+				{
+					// 拖动或滚动条触摸从未发送 BUTTONDOWN,直接丢弃候选点击。
+					m_pEventClick = NULL;
+				}
+
 				m_pEventTouch = NULL;
+				m_pTouchScrollContainer = NULL;
 			}
 		}
 
@@ -919,6 +1025,45 @@ namespace DuiLib
 			pfnClose((HANDLE)lParam);
 	}
 
+	void CPaintManagerUI::ClearTouchHoverState(POINT pt)
+	{
+		TouchTrace::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());
+		TEventUI event = { 0 };
+		event.Type = UIEVENT_MOUSELEAVE;
+		event.ptMouse = pt;
+		event.wKeyState = MapKeyState();
+		event.dwTimestamp = ::GetTickCount();
+
+		CStdPtrArray controls(m_aNeedMouseLeaveNeeded.GetSize());
+		controls.Resize(m_aNeedMouseLeaveNeeded.GetSize());
+		::CopyMemory(controls.GetData(), m_aNeedMouseLeaveNeeded.GetData(),
+			m_aNeedMouseLeaveNeeded.GetSize() * sizeof(LPVOID));
+
+		for (int i = 0; i < controls.GetSize(); ++i)
+		{
+			CControlUI* pControl = static_cast<CControlUI*>(controls[i]);
+			if (pControl != NULL)
+			{
+				event.pSender = pControl;
+				pControl->Event(event);
+			}
+		}
+
+		if (m_pEventHover != NULL)
+		{
+			event.pSender = m_pEventHover;
+			m_pEventHover->Event(event);
+			m_pEventHover = NULL;
+		}
+
+		m_aNeedMouseLeaveNeeded.Empty();
+		if (m_hwndTooltip != NULL)
+			::SendMessage(m_hwndTooltip, TTM_TRACKACTIVATE, FALSE, (LPARAM)&m_ToolTip);
+	}
+
 	void CPaintManagerUI::DeletePtr(void* ptr)
 	{
 		if (ptr)
@@ -1478,6 +1623,15 @@ namespace DuiLib
 	{
 		if (m_hWndPaint == NULL) return false;
 
+		// 触摸点击由 WM_TOUCH 直接驱动 BUTTONDOWN/BUTTONUP。
+		// 部分收银机仍会补发鼠标消息,但这些消息不能再次进入普通鼠标状态机,
+		// 否则会造成重复点击或覆盖触摸手势状态。
+		if ((uMsg == WM_LBUTTONDOWN || uMsg == WM_LBUTTONUP || uMsg == WM_LBUTTONDBLCLK)
+			&& IsPromotedTouchMouseMessage())
+		{
+			return true;
+		}
+
 		// [触屏兼容] 不再过滤 Windows 为触摸模拟的鼠标消息(MOUSEEVENTF_FROMTOUCH)。
 		// 触摸的“点击”正来自这些模拟的 WM_LBUTTONDOWN/UP,由原生鼠标路径交给 CButtonUI 等控件
 		// Activate() 触发——而 DuiLib 的点击状态机依赖 WM_LBUTTONDOWN 设置 m_pEventClick,故绝不能
@@ -2011,6 +2165,7 @@ namespace DuiLib
 			break;
 			case WM_MOUSEHOVER:
 			{
+				if (m_bSuppressHoverAfterTouch) return true;
 				m_bMouseTracking = false;
 				POINT pt = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
 				CControlUI* pHover = FindControl(pt);
@@ -2086,6 +2241,40 @@ namespace DuiLib
 			break;
 			case WM_MOUSEMOVE:
 			{
+				const bool bPromotedTouchMove = IsPromotedTouchMouseMessage();
+				if (bPromotedTouchMove && m_pEventTouch != NULL)
+				{
+					POINT ptTouch = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
+					const int kTouchSlop = 8;
+					const int dxTouch = ptTouch.x - m_ptTouchStart.x;
+					const int dyTouch = ptTouch.y - m_ptTouchStart.y;
+					if (dxTouch > kTouchSlop || dxTouch < -kTouchSlop
+						|| dyTouch > kTouchSlop || dyTouch < -kTouchSlop)
+						m_bTouchMoved = true;
+					m_ptLastMousePos = ptTouch;
+					return true;
+				}
+				if (m_bSuppressHoverAfterTouch)
+				{
+					m_ptLastMousePos = POINT{ GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
+					return true;
+				}
+
+				if ((INT_PTR)lParam == -1)
+					break;
+
+				POINT ptCheck = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
+				if (m_bSuppressHoverAfterTouch)
+				{
+					// 合成消息通常仍停在触摸结束点;只有明显离开该点的鼠标移动
+					// 才能重新建立鼠标悬浮语义。
+					const int dx = ptCheck.x - m_ptLastTouchPos.x;
+					const int dy = ptCheck.y - m_ptLastTouchPos.y;
+					if (dx <= 8 && dx >= -8 && dy <= 8 && dy >= -8)
+						break;
+					m_bSuppressHoverAfterTouch = false;
+				}
+
 				// Start tracking this entire window again...
 				if (!m_bMouseTracking)
 				{
@@ -2098,34 +2287,6 @@ 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) };
 				// 是否移动
@@ -2213,7 +2374,6 @@ namespace DuiLib
 				// “点过的控件一直高亮 / 连点几个控件后多个同时高亮、怎么点都清不掉”的顽疾
 				// (按钮的 UISTATE_HOT、列表项的 UISTATE_HOT 全靠 MOUSEENTER/MOUSELEAVE 维护)。
 				// 该判断只影响悬浮段,消息依旧往下走 → 点击/拖拽状态机不受影响。
-				const bool bPromotedTouchMove = IsPromotedTouchMouseMessage();
 				if (!IsCaptured() && !bPromotedTouchMove)
 				{
 					pNewHover = FindControl(pt);
@@ -2264,6 +2424,14 @@ namespace DuiLib
 			break;
 			case WM_LBUTTONDOWN:
 			{
+				TouchTrace::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"));
+				// WM_TOUCH 后 Windows 可能补发 WM_LBUTTONDOWN。触摸点击由 TOUCHUP
+				// 的 tap 判定统一处理,不能让这条合成消息提前建立鼠标点击状态。
+				if (m_pEventTouch != NULL)
+					break;
 				// 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).
@@ -2303,6 +2471,7 @@ namespace DuiLib
 				event.wKeyState = (WORD)wParam;
 				event.dwTimestamp = ::GetTickCount();
 				pControl->Event(event);
+
 			}
 			break;
 			case WM_LBUTTONDBLCLK:
@@ -2329,32 +2498,65 @@ namespace DuiLib
 			break;
 			case WM_LBUTTONUP:
 			{
-				// 触屏拖拽手势(已发生位移)-> 松开不触发“点击”,避免“拖拽又误点”。
-				// 但滚动条滑块拖拽依赖 BUTTONUP 来解除 UISTATE_CAPTURED 并停掉 50ms 定时器,
-				// 绝不能在此提前清空 m_pEventClick,否则滚动条卡死在捕获态。故对滚动条放行 BUTTONUP。
+				TouchTrace::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"),
+					(int)m_bTouchMoved);
+				const bool bPromotedTouchUp = IsPromotedTouchMouseMessage();
+				// TOUCHUP 已经负责 tap/drag 判定;若提升的鼠标抬起先到,不能
+				// 绕过该判定触发 BUTTONUP。
+				if (m_pEventTouch != NULL)
+					break;
+				POINT ptButtonUp = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
+				if (!bPromotedTouchUp)
+				{
+					// 普通鼠标点击也不会天然产生 MOUSELEAVE。
+					// 点击后光标仍停留在控件上时,必须主动清除 HOT 状态,
+					// 否则直到移动到下一个控件前都会保持悬浮态。
+					ClearTouchHoverState(ptButtonUp);
+				}
+				if (bPromotedTouchUp)
+				{
+					ClearTouchHoverState(ptButtonUp);
+					m_bSuppressHoverAfterTouch = true;
+					m_bMouseTracking = false;
+				}
+
 				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;  // 用完即清,绝不影响后续真实鼠标点击
 				POINT pt = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
 				m_ptLastMousePos = pt;
-				if (m_pEventClick == NULL) break;
+				CControlUI* pClick = m_pEventClick;
+				const bool bTouchDragged = m_bTouchMoved;
+				m_pEventClick = NULL;
+				m_bTouchMoved = false;  // 用完即清,绝不影响后续真实鼠标点击
+				if (pClick == NULL)
+				{
+					ReleaseCapture();
+					break;
+				}
+
 				ReleaseCapture();
 				TEventUI event = { 0 };
 				event.Type = UIEVENT_BUTTONUP;
-				event.pSender = m_pEventClick;
+				event.pSender = pClick;
 				event.wParam = wParam;
 				event.lParam = lParam;
 				event.ptMouse = pt;
+				if (bTouchDragged && pClick->GetInterface(DUI_CTR_SCROLLBAR) == NULL)
+				{
+					// 清除按钮的 PUSHED/CAPTURED 状态,但让 CButtonUI 的命中测试失败,
+					// 从而不会在滑动结束时调用 Activate。
+					event.ptMouse.x = -1;
+					event.ptMouse.y = -1;
+				}
 				event.wKeyState = (WORD)wParam;
 				event.dwTimestamp = ::GetTickCount();
 
-				CControlUI* pClick = m_pEventClick;
-				m_pEventClick = NULL;
 				pClick->Event(event);
 			}
 			break;
@@ -5135,4 +5337,4 @@ namespace DuiLib
 	}
 } // namespace DuiLib
 
-#pragma warning(pop)
+#pragma warning(pop)

+ 6 - 6
DuiLib/Core/UIManager.h

@@ -14,6 +14,7 @@ namespace DuiLib {
 	//
 
 	class CControlUI;
+	class CContainerUI;
 	class CRichEditUI;
 	class CIDropTarget;
 
@@ -482,6 +483,7 @@ namespace DuiLib {
 		// 注册在 Init 中完成,无需逐窗口样板。
 		void InitTouchSupport();
 		void OnTouchInput(WPARAM wParam, LPARAM lParam);
+		void ClearTouchHoverState(POINT pt);
 
 	private:
 		CStdPtrArray* GetFoundControls();
@@ -518,12 +520,10 @@ namespace DuiLib {
 		bool m_bShowUpdateRect;
 
 		// 触屏状态(库级统一接管后,每个窗口的 CPaintManagerUI 各持一份)
-		// 旧 DuiLib 点击状态机依赖:WM_LBUTTONDOWN 设置 m_pEventClick,WM_LBUTTONUP 据此触发点击
-		// (CButtonUI::Activate)。因此【绝不能】在鼠标分支里用 m_pEventTouch 守卫吞掉 Windows 为触摸
-		// 模拟的 WM_LBUTTONDOWN/UP——否则 m_pEventClick 永不被设置,触摸与鼠标点击全失效。
-		// 正确做法:放开模拟鼠标(FROMTOUCH 不过滤),点击自然走原生路径;仅用 m_bTouchMoved 标记
-		// “本次手势是否发生位移”,若位移则松开时不触发点击(避免拖拽=滚动 又 误点按钮)。
-		CControlUI* m_pEventTouch = NULL;   // 本次触摸命中的控件(用于派发 UIEVENT_TOUCHMOVE 做跟手滚动)
+		// 鼠标点击仍由 WM_LBUTTONDOWN/UP 维护 m_pEventClick;触摸期间则由
+		// m_pEventTouch 和 m_bTouchMoved 独立维护,避免合成鼠标消息提前激活控件。
+		CControlUI* m_pEventTouch = NULL;   // 本次触摸命中的控件
+		CContainerUI* m_pTouchScrollContainer = NULL; // 本次触摸对应的最近可滚动容器
 		POINT m_ptLastTouchPos = { 0, 0 };  // 上一包触点:用于去重 + 计算跟手滚动的位移增量
 		POINT m_ptTouchStart = { 0, 0 };    // 本次手势起点:用于判断是否发生位移(拖拽 vs 轻点)
 		bool m_bTouchMoved = false;         // 本次触摸是否发生位移(>阈值视为拖拽),松开时抑制点击

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


+ 2 - 2
zhipuzi_pos_windows/zhipuzi_pos_windows.cpp

@@ -89,9 +89,9 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
     pLogin->CenterWindow();
 
 	pLogin->ShowWindow(true);
-
+	
     CPaintManagerUI::MessageLoop();
-
+	
     ::CoUninitialize();
 
 	//销毁一些环境