|
|
@@ -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;
|