Browse Source

参考NIM DUILIB,增加对触屏支持

张洋 4 weeks ago
parent
commit
cba4a5c794
4 changed files with 334 additions and 3 deletions
  1. 35 0
      DuiLib/Core/UIContainer.cpp
  2. 17 0
      DuiLib/Core/UIControl.cpp
  3. 249 3
      DuiLib/Core/UIManager.cpp
  4. 33 0
      DuiLib/Core/UIManager.h

+ 35 - 0
DuiLib/Core/UIContainer.cpp

@@ -299,6 +299,41 @@ namespace DuiLib
 			return;
 		}
 
+		// [NIM 触屏模型] 手指拖拽 → 容器内容 1:1 跟手平移(对标 NIM Duilib 的 ScrollableBox)。
+		// 对照 NIM Duilib Box.cpp:903-933 的 ScrollableBox::DoEvent:
+		//     detaValue = ptMouse.y - m_ptLastTouchPos.y;
+		//     detaValue > 0 ? TouchUp(detaValue) : TouchDown(-detaValue);
+		// 其中 TouchUp = scrollPos.cy -= detaValue,即【手指下移 → 内容上滚】;到顶/到底由 SetScrollPos 夹紧。
+		//
+		// 能吃到这里的原因:CPaintManagerUI 把 UIEVENT_TOUCHMOVE 派发给触点下的最底层控件后,
+		// 该控件不认此事件,会沿 CControlUI::DoEvent 末尾的 m_pParent->DoEvent 冒泡到这里。
+		if( event.Type == UIEVENT_TOUCHMOVE )
+		{
+			// 位移增量由 CPaintManagerUI 随事件下发:wParam = dx,lParam = dy
+			int dx = (int)event.wParam;
+			int dy = (int)event.lParam;
+
+			bool bScrolled = false;
+			SIZE szPos = GetScrollPos();
+
+			if( dy != 0 && m_pVerticalScrollBar != NULL && m_pVerticalScrollBar->IsVisible() && m_pVerticalScrollBar->IsEnabled() )
+			{
+				szPos.cy -= dy;
+				bScrolled = true;
+			}
+			if( dx != 0 && m_pHorizontalScrollBar != NULL && m_pHorizontalScrollBar->IsVisible() && m_pHorizontalScrollBar->IsEnabled() )
+			{
+				szPos.cx -= dx;
+				bScrolled = true;
+			}
+
+			if( bScrolled )
+			{
+				SetScrollPos(szPos);
+				return; // 已消费:不再向父冒泡,只让最近的一层可滚动容器跟随手指
+			}
+		}
+
 		if( event.Type == UIEVENT_SETFOCUS ) 
 		{
 			m_bFocused = true;

+ 17 - 0
DuiLib/Core/UIControl.cpp

@@ -843,6 +843,23 @@ namespace DuiLib
 
 	void CControlUI::DoEvent(TEventUI& event)
 	{
+		// [NIM 触屏模型] 触摸事件归一化。
+		// CPaintManagerUI 把 WM_TOUCH 解析成真正的 UIEVENT_TOUCH* 事件并派发到本控件,
+		// 这里把按下/抬起归一化为等价鼠标事件,使所有既有控件(按钮、列表项、Edit、Combo
+		// 以及 60+ 窗口里的自定义控件)无需逐个改写即可原生响应手指操作。
+		//
+		// 这与已废弃的 SynthesizeTap 有本质区别:
+		//   SynthesizeTap = 向窗口句柄 PostMessage 伪造鼠标消息,要绕 Windows 消息队列,
+		//                   在模态弹框里会被路由吞掉导致点击丢失;
+		//   归一化       = 进程内直接换用事件类型后交给控件自己处理,不经消息队列,
+		//                   主窗口与弹框行为完全一致。
+		// 注意:UIEVENT_TOUCHMOVE 不在此归一化,它由 CContainerUI 单独处理为跟手滚动,
+		//     并经本函数末尾的父链冒泡抵达那里。
+		if (event.Type == UIEVENT_TOUCHDOWN)
+			event.Type = UIEVENT_BUTTONDOWN;
+		else if (event.Type == UIEVENT_TOUCHUP)
+			event.Type = UIEVENT_BUTTONUP;
+
 		if (event.Type == UIEVENT_SETCURSOR)
 		{
 			if (GetCursor())

+ 249 - 3
DuiLib/Core/UIManager.cpp

@@ -12,6 +12,16 @@ namespace DuiLib
 	//
 	//
 
+	// 触屏参数(NIM Duilib 模型)
+	// TOUCHINPUT 的坐标以 1/100 像素为单位,必须先换算成像素
+#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 = 10;
+
 	static void GetChildWndRect(HWND hWnd, HWND hChildWnd, RECT& rcChildWnd)
 	{
 		::GetWindowRect(hChildWnd, &rcChildWnd);
@@ -448,7 +458,236 @@ namespace DuiLib
 			m_hWndPaint = hWnd;
 			m_hDcPaint = ::GetDC(hWnd);
 			m_aPreMessages.Add(this);
+
+		// 触屏兼容:每个窗口(主窗口 + 所有弹框)统一注册 WM_TOUCH,由本类库级接管,
+		// 无需 CTouchWindowBase 外挂,也无需逐窗口样板。注册是幂等的,重复调用无害。
+		InitTouchSupport();
+		}
+	}
+
+	// ---------------------------------------------------------------------------
+	// 触摸 API 懒加载包装(对标 NIM Duilib 的 RegisterTouchWindowWrapper 系列)。
+	//
+	// 为什么不能直接调用这些 API:DuiLib 工程自身没有定义 _WIN32_WINNT,
+	// StdAfx.h 会把它兜底设成 XP(0x0501);而 TOUCHINPUT / GetTouchInputInfo /
+	// RegisterTouchWindow 在 winuser.h 里受 `#if _WIN32_WINNT >= 0x0601` 保护,
+	// 直接引用会编译报错(C2065/C3861)。(主工程定义了 0x0601,所以
+	// CTouchWindowBase 里能直接用,那里不受影响。)
+	//
+	// 因此这里完全自给自足:自行声明与 SDK 完全一致的 TOUCHINPUT 布局和事件标志,
+	// 再用 GetProcAddress 取函数地址。这样:
+	//   * 无论 DuiLib 的 target 版本如何都能编译通过;
+	//   * 在没有触摸 API 的系统上自动降级为空操作,不会崩溃。
+	// ---------------------------------------------------------------------------
+	namespace TouchApi
+	{
+		// 与 winuser.h 的 tagTOUCHINPUT 布局逐字段一致
+		typedef struct tagTouchInput
+		{
+			LONG      x;
+			LONG      y;
+			HANDLE    hSource;
+			DWORD     dwID;
+			DWORD     dwFlags;
+			DWORD     dwMask;
+			DWORD     dwTime;
+			ULONG_PTR dwExtraInfo;
+			DWORD     cxContact;
+			DWORD     cyContact;
+		} TOUCHINPUT, *PTOUCHINPUT;
+
+		// 与 winuser.h 完全一致的事件标志
+		const DWORD TOUCHEVENTF_MOVE = 0x0002;
+		const DWORD TOUCHEVENTF_DOWN = 0x0004;
+		const DWORD TOUCHEVENTF_UP   = 0x0008;
+
+		typedef BOOL (WINAPI *PFN_RegisterTouchWindow)(HWND, ULONG);
+		typedef BOOL (WINAPI *PFN_GetTouchInputInfo)(HANDLE, UINT, PTOUCHINPUT, int);
+		typedef BOOL (WINAPI *PFN_CloseTouchInputHandle)(HANDLE);
+
+		inline HMODULE GetUser32()
+		{
+			static HMODULE s_hMod = NULL;
+			static bool s_bInited = false;
+			if (!s_bInited)
+			{
+				s_bInited = true;
+				s_hMod = ::GetModuleHandle(_T("user32.dll"));
+			}
+			return s_hMod;
+		}
+
+		inline PFN_RegisterTouchWindow GetRegisterTouchWindow()
+		{
+			static PFN_RegisterTouchWindow s_pfn = NULL;
+			static bool s_bInited = false;
+			if (!s_bInited)
+			{
+				s_bInited = true;
+				HMODULE hMod = GetUser32();
+				if (hMod != NULL) s_pfn = (PFN_RegisterTouchWindow)::GetProcAddress(hMod, "RegisterTouchWindow");
+			}
+			return s_pfn;
+		}
+
+		inline PFN_GetTouchInputInfo GetGetTouchInputInfo()
+		{
+			static PFN_GetTouchInputInfo s_pfn = NULL;
+			static bool s_bInited = false;
+			if (!s_bInited)
+			{
+				s_bInited = true;
+				HMODULE hMod = GetUser32();
+				if (hMod != NULL) s_pfn = (PFN_GetTouchInputInfo)::GetProcAddress(hMod, "GetTouchInputInfo");
+			}
+			return s_pfn;
+		}
+
+		inline PFN_CloseTouchInputHandle GetCloseTouchInputHandle()
+		{
+			static PFN_CloseTouchInputHandle s_pfn = NULL;
+			static bool s_bInited = false;
+			if (!s_bInited)
+			{
+				s_bInited = true;
+				HMODULE hMod = GetUser32();
+				if (hMod != NULL) s_pfn = (PFN_CloseTouchInputHandle)::GetProcAddress(hMod, "CloseTouchInputHandle");
+			}
+			return s_pfn;
+		}
+	}
+
+	void CPaintManagerUI::InitTouchSupport()
+	{
+		if (m_hWndPaint == NULL || !::IsWindow(m_hWndPaint)) return;
+
+		TouchApi::PFN_RegisterTouchWindow pfnRegister = TouchApi::GetRegisterTouchWindow();
+		if (pfnRegister != NULL)
+			pfnRegister(m_hWndPaint, 0);
+	}
+
+	void CPaintManagerUI::OnTouchInput(WPARAM wParam, LPARAM lParam)
+	{
+		// 对照 NIM Duilib 的 Window::HandleMessage case WM_TOUCH(Window.cpp:1139-1199):
+		//   解析触点 -> 换算坐标 -> FindControl -> 向控件派发真正的触摸事件。
+		// 这里不合成任何鼠标消息:点击由控件自己吃到 TOUCHDOWN/TOUCHUP 后 Activate() 产生,
+		// 不经 Windows 消息队列,因此主窗口与模态弹框行为完全一致。
+		TouchApi::PFN_GetTouchInputInfo pfnGetInfo = TouchApi::GetGetTouchInputInfo();
+		if (pfnGetInfo == NULL) return;
+
+		UINT cInputs = LOWORD(wParam);
+		if (cInputs == 0) return;
+		std::vector<TouchApi::TOUCHINPUT> inputs(cInputs);
+		if (!pfnGetInfo((HANDLE)lParam, cInputs, inputs.data(), (int)sizeof(TouchApi::TOUCHINPUT)))
+			return;
+
+		for (UINT i = 0; i < cInputs; ++i)
+		{
+			TouchApi::TOUCHINPUT& ti = inputs[i];
+
+			POINT pt;
+			pt.x = TOUCH_COORD_TO_PIXEL(ti.x);
+			pt.y = TOUCH_COORD_TO_PIXEL(ti.y);
+			::ScreenToClient(m_hWndPaint, &pt);
+
+			if (ti.dwFlags & TouchApi::TOUCHEVENTF_DOWN)
+			{
+				m_bTouchDown = true;
+				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;
+
+				::SetCapture(m_hWndPaint);
+
+				if (m_pTouchTarget != NULL)
+				{
+					TEventUI event = { 0 };
+					event.Type = UIEVENT_TOUCHDOWN;
+					event.pSender = m_pTouchTarget;
+					event.ptMouse = pt;
+					event.dwTimestamp = ::GetTickCount();
+					event.wKeyState = MapKeyState();
+					m_pTouchTarget->Event(event);
+				}
+			}
+			else if (ti.dwFlags & TouchApi::TOUCHEVENTF_MOVE)
+			{
+				if (!m_bTouchDown) continue;
+
+				LONG dx = pt.x - m_ptLastTouchPos.x;
+				LONG dy = pt.y - m_ptLastTouchPos.y;
+				m_ptLastTouchPos = pt;
+				m_ptLastMousePos = pt;
+
+				m_moveDistSq += (std::int64_t)dx * dx + (std::int64_t)dy * dy;
+				if (m_moveDistSq > (std::int64_t)TOUCH_TAP_SLOP * TOUCH_TAP_SLOP)
+					m_bTouchMoved = true;
+
+				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);
+				}
+			}
+			else if (ti.dwFlags & TouchApi::TOUCHEVENTF_UP)
+			{
+				if (!m_bTouchDown) continue;
+
+				if (::GetCapture() == m_hWndPaint) ::ReleaseCapture();
+
+				// 抬起仍派发给按下时命中的同一个控件(NIM 语义)。
+				// 一旦判定为滑动手势,就把触点坐标挪到控件之外的 (-1,-1):
+				// 按钮的 UIEVENT_BUTTONUP 分支只在 PtInRect 通过时才 Activate(),
+				// 这样既能正常清除 PUSHED/CAPTURED 态,又不会误触发 click。
+				POINT ptUp = pt;
+				if (m_bTouchMoved)
+				{
+					ptUp.x = -1;
+					ptUp.y = -1;
+				}
+
+				if (m_pTouchTarget != NULL)
+				{
+					TEventUI event = { 0 };
+					event.Type = UIEVENT_TOUCHUP;
+					event.pSender = m_pTouchTarget;
+					event.ptMouse = ptUp;
+					event.dwTimestamp = ::GetTickCount();
+					event.wKeyState = MapKeyState();
+					m_pTouchTarget->Event(event);
+				}
+
+				m_bTouchDown = false;
+				m_pTouchTarget = NULL;
+				m_moveDistSq = 0;
+				m_bTouchMoved = false;
+			}
 		}
+
+		TouchApi::PFN_CloseTouchInputHandle pfnClose = TouchApi::GetCloseTouchInputHandle();
+		if (pfnClose != NULL)
+			pfnClose((HANDLE)lParam);
 	}
 
 	void CPaintManagerUI::DeletePtr(void* ptr)
@@ -1017,10 +1256,11 @@ namespace DuiLib
 		//   * 手指刚按下准备滑动就被当成“点击” —— 误把商品加入购物车;
 		//   * 滑动时经过的每个商品 item 都被置上悬浮态(UISTATE_HOT) 且无人清除 —— 多个 item 同时悬浮;
 		//   * 抬起后悬浮态卡死不消失。
-		// 触摸手势的实际处理(滚动 / 轻触判定 / 惯性)由 CTouchWindowBase 通过 WM_TOUCH 完成,
-		// 轻触时它会以“纯净鼠标消息”(ExtraInfo=0) 重新发一次 down+up 给真正需要点击的控件。
+		// 触摸手势的实际处理(跟手滚动 / 轻触点击)由 CPaintManagerUI 通过 WM_TOUCH 完成,
+		// 派发给控件的 UIEVENT_TOUCHDOWN/TOUCHUP;控件自行判断是否 Activate() 触发 click,
+// 全程不再合成任何鼠标消息(旧的 SynthesizeTap 方案已废弃)。
 		//
-		// 作用域天然正确:只有调用过 RegisterTouchWindow 的窗口(主窗口已在 OnCreate 注册)才会产生带
+		// 作用域天然正确:只有调用过 RegisterTouchWindow 的窗口(所有窗口已在 CPaintManagerUI::Init 注册)才会产生带
 		//   FROMTOUCH 标记的模拟鼠标;DuiLib 的下拉框/菜单等未注册触摸的弹出窗口不会产生该标记,照常进入
 		//   DuiLib,触摸点选不受影响。因此不必再用窗口属性做额外限定。
 		// 注:未用 IsTouchWindow,因其声明要求 _WIN32_WINNT>=0x0601,本项目部分目标 SDK 没有该符号。
@@ -1953,6 +2193,12 @@ namespace DuiLib
 				m_pEventRClick = NULL;
 			}
 			break;
+			case WM_TOUCH:
+			{
+				OnTouchInput(wParam, lParam);
+				lRes = 0;
+				return true;
+			}
 			case WM_MOUSEWHEEL:
 			{
 				if (m_pRoot == NULL) break;

+ 33 - 0
DuiLib/Core/UIManager.h

@@ -2,6 +2,13 @@
 #define __UIMANAGER_H__
 
 #pragma once
+
+// 本头文件原本不含任何 include(符号由 StdAfx.h/UIlib.h 提供),
+// 但触屏相关的成员与状态里直接使用了 std::int64_t 与 std::vector,
+// 显式补齐以保证在 DuiLib 库工程(_WIN32_WINNT 兜底为 XP)里也能独立通过编译。
+#include <cstdint>
+#include <vector>
+
 namespace DuiLib {
 	/////////////////////////////////////////////////////////////////////////////////////
 	//
@@ -46,6 +53,14 @@ namespace DuiLib {
 		UIEVENT_CONTEXTMENU,
 		UIEVENT_SCROLLWHEEL,
 		UIEVENT__MOUSEEND,
+		// 触摸事件(NIM Duilib 模型)。必须排在 __MOUSEEND 之后:
+		// CContainerUI::DoEvent 开头对 UIEVENT__MOUSEBEGIN~__MOUSEEND 区间做了 IsMouseEnabled 早退分支,
+		// 排在区间之外才能保证触摸事件始终走正常派发与冒泡。
+		UIEVENT__TOUCHBEGIN,
+		UIEVENT_TOUCHDOWN,
+		UIEVENT_TOUCHMOVE,
+		UIEVENT_TOUCHUP,
+		UIEVENT__TOUCHEND,
 		UIEVENT_KILLFOCUS,
 		UIEVENT_SETFOCUS,
 		UIEVENT_WINDOWSIZE,
@@ -454,6 +469,17 @@ namespace DuiLib {
 		bool PreMessageHandler(UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT& lRes);
 		void UsedVirtualWnd(bool bUsed);
 
+		// 触屏支持(NIM Duilib 模型,库级统一接管:主窗口 + 所有弹框通用)。
+		// WM_TOUCH 在 MessageHandler 里被解析为真正的 DuiLib 触摸事件,直接派发给命中的控件:
+		//   DOWN → UIEVENT_TOUCHDOWN  控件自行处理按下(按钮置 PUSHED/CAPTURED)
+		//   MOVE → UIEVENT_TOUCHMOVE  经 DoEvent 冒泡链由 CContainerUI 接收,做 1:1 跟手平移
+		//   UP   → UIEVENT_TOUCHUP    控件自行处理抬起(按钮 PtInRect 通过才 Activate → click)
+		// 全程不合成任何鼠标消息(已废弃 SynthesizeTap):点击由控件 Activate()+SendNotify 直接产生,
+		// 不经过 Windows 消息队列,所以主窗口与模态弹框行为完全一致。
+		// 注册在 Init 中完成,无需逐窗口样板,也无需 CTouchWindowBase 外挂。
+		void InitTouchSupport();
+		void OnTouchInput(WPARAM wParam, LPARAM lParam);
+
 	private:
 		CStdPtrArray* GetFoundControls();
 		static CControlUI* CALLBACK __FindControlFromNameHash(CControlUI* pThis, LPVOID pData);
@@ -488,6 +514,13 @@ namespace DuiLib {
 		bool m_bNoActivate;
 		bool m_bShowUpdateRect;
 
+		// 触屏状态(库级统一接管后,每个窗口的 CPaintManagerUI 各持一份)
+		bool m_bTouchDown = false;          // 手指是否按下
+		POINT m_ptLastTouchPos = { 0, 0 };  // 上一次触点:与本次触点差分,得到跟手滚动的平移增量
+		CControlUI* m_pTouchTarget = NULL;  // 按下时命中的控件,抬起时复用同一目标(NIM 语义)
+		std::int64_t m_moveDistSq = 0;      // 累计位移平方,用于 tap slop(轻触/滑动)判定
+		bool m_bTouchMoved = false;         // 是否已超过 tap slop:超过则不再判为点击
+
 		//
 		CControlUI* m_pRoot;
 		CControlUI* m_pFocus;