|
|
@@ -21,9 +21,12 @@ namespace DuiLib
|
|
|
#ifndef TOUCH_COORD_TO_PIXEL
|
|
|
#define TOUCH_COORD_TO_PIXEL(l) ((l) / 100)
|
|
|
#endif
|
|
|
- // [触屏模型变更] 不再使用 tap-slop 阈值:100% 照搬 NIM Duilib,触摸“点击”交给 Windows
|
|
|
- // 为触摸模拟的 WM_LBUTTONDOWN/UP(原生鼠标路径),滚动交给原生 UIEVENT_TOUCHMOVE,
|
|
|
- // 二者由 m_pEventTouch 守卫互斥,无需自定滑动阈值。
|
|
|
+ // [触屏模型] 触摸只做两件事,不做任何“触摸→鼠标”的归一化:
|
|
|
+ // ①点击:交给 Windows 为触摸自动模拟的 WM_LBUTTONDOWN/UP(原生鼠标路径),
|
|
|
+ // 由 CButtonUI 等控件原生 Activate();
|
|
|
+ // ②滚动:WM_TOUCH 直接派发 UIEVENT_TOUCHDOWN/MOVE/UP(见 OnTouchInput),
|
|
|
+ // 容器据此 1:1 跟手平移、滚动条据此拖滑块。
|
|
|
+ // “轻点 vs 拖拽”不用距离阈值互斥,而是在 WM_LBUTTONUP 用 m_bTouchMoved 抑制误点。
|
|
|
|
|
|
// WM_TOUCH 消息 ID 同样受 `#if _WIN32_WINNT >= 0x0601` 保护,DuiLib 工程未定义
|
|
|
// _WIN32_WINNT(StdAfx.h 兜底为 XP)→ 必须自行声明,否则 switch 里引用该消息 ID
|
|
|
@@ -92,6 +95,11 @@ namespace DuiLib
|
|
|
_tcscpy_s(utf8, _countof(utf8), msg);
|
|
|
#endif
|
|
|
FILE* f = NULL;
|
|
|
+ // 保护:单个日志超过 4MB 就不再追加,避免在收银机上长期运行把日志写到失控。
|
|
|
+ WIN32_FILE_ATTRIBUTE_DATA fad = { 0 };
|
|
|
+ if (::GetFileAttributesEx(Narrow(path), GetFileExInfoStandard, &fad)
|
|
|
+ && fad.nFileSizeHigh == 0 && fad.nFileSizeLow >= (4u * 1024u * 1024u))
|
|
|
+ return;
|
|
|
if (fopen_s(&f, Narrow(path), "a") == 0 && f != NULL)
|
|
|
{
|
|
|
fprintf(f, "[%8u][T%5u] %s\n", (unsigned)::GetTickCount(), (unsigned)::GetCurrentThreadId(), utf8);
|
|
|
@@ -638,6 +646,57 @@ namespace DuiLib
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ // ---------------------------------------------------------------------------
|
|
|
+ // [触屏] 手指容差(滚动条专治)
|
|
|
+ // 滚动条默认仅 16px 宽(CScrollBarUI::DEFAULT_SCROLLBAR_SIZE),指尖按偏 5~10px 就落到
|
|
|
+ // 右侧内容上:内容会跟手滚动,但“滑块拖不动”——用户感知就是“滚动条没法在触屏上滑动”。
|
|
|
+ // 两个措施:①把触点横向收进滚动条 rect,再交给滚动条自己判滑块;②若直接命中的不是
|
|
|
+ // 滚动条,但触点容差范围内正好压在某个滚动条的滑块上,则改判为命中该滚动条。
|
|
|
+ // ---------------------------------------------------------------------------
|
|
|
+ namespace
|
|
|
+ {
|
|
|
+ const int kScrollBarTouchSlop = 12; // 手指离滚动条 rect 的最大容差(像素)
|
|
|
+
|
|
|
+ // Windows 会把“触摸提升(promoted)出来的合成鼠标消息”打上 MOUSEEVENTF_FROMTOUCH 标记
|
|
|
+ // (位于 GetMessageExtraInfo() 的高位,MSDN 推荐判定值 0xFF515700)。
|
|
|
+ // 这里**只**用它来区分“真鼠标移动”与“触摸补发的鼠标移动”,从而决定要不要更新悬浮态;
|
|
|
+ // 消息本身绝不丢弃——历史教训:整条吞掉会让点击状态机(m_pEventClick)失效、点击全灭。
|
|
|
+ inline bool IsPromotedTouchMouseMessage()
|
|
|
+ {
|
|
|
+ return (::GetMessageExtraInfo() & 0xFF515700) == 0xFF515700;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 把触点收进滚动条自身 rect:垂直滚动条主要收敛 x(y 原样保留,纵向拖拽量完好)。
|
|
|
+ inline POINT Touch_SnapIntoScrollBar(const CControlUI* pBar, POINT pt)
|
|
|
+ {
|
|
|
+ if (pBar == NULL) return pt;
|
|
|
+ RECT rc = pBar->GetPos();
|
|
|
+ if (pt.x < rc.left) pt.x = rc.left;
|
|
|
+ if (pt.x > rc.right) pt.x = rc.right;
|
|
|
+ if (pt.y < rc.top) pt.y = rc.top;
|
|
|
+ if (pt.y > rc.bottom) pt.y = rc.bottom;
|
|
|
+ return pt;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 沿“命中控件 → 祖先链”找最近的、满足容差条件的滚动条滑块;找不到返回 NULL。
|
|
|
+ inline CControlUI* Touch_ResolveScrollBarThumb(CControlUI* pStart, POINT pt)
|
|
|
+ {
|
|
|
+ for (CControlUI* p = pStart; p != NULL; p = p->GetParent())
|
|
|
+ {
|
|
|
+ if (p->GetInterface(DUI_CTR_SCROLLBAR) != NULL) return NULL; // 本身已是滚动条,无需改判
|
|
|
+ CContainerUI* pCont = static_cast<CContainerUI*>(p->GetInterface(DUI_CTR_CONTAINER));
|
|
|
+ if (pCont == NULL) continue;
|
|
|
+ CScrollBarUI* pBars[2] = { pCont->GetVerticalScrollBar(), pCont->GetHorizontalScrollBar() };
|
|
|
+ for (int i = 0; i < 2; ++i)
|
|
|
+ {
|
|
|
+ if (pBars[i] != NULL && pBars[i]->IsThumbHit(pt, kScrollBarTouchSlop))
|
|
|
+ return pBars[i];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return NULL;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
void CPaintManagerUI::InitTouchSupport()
|
|
|
{
|
|
|
if (m_hWndPaint == NULL || !::IsWindow(m_hWndPaint)) return;
|
|
|
@@ -649,20 +708,20 @@ namespace DuiLib
|
|
|
|
|
|
void CPaintManagerUI::OnTouchInput(WPARAM wParam, LPARAM lParam)
|
|
|
{
|
|
|
- // 100% 照搬 NIM Duilib 的 Window::HandleMessage case WM_TOUCH(Window.cpp:1139-1199)。
|
|
|
- // 设计要点(不要自创归一化 / 状态机):
|
|
|
+ // 触屏模型(参考 NIM Duilib 的 Window::HandleMessage case WM_TOUCH,但**不可**照搬它的鼠标守卫,
|
|
|
+ // 旧版 DuiLib 的点击状态机强依赖原生 WM_LBUTTONDOWN/UP,照搬会让点击全灭——已实测踩过):
|
|
|
// 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) 守卫:DOWN 与 MOVE 都【只对“滚动条”让路】(滑块拖拽由原生鼠标独占),其余控件一律接管。
|
|
|
- // 不能用 `m_pEventClick != NULL` 做通用守卫——Windows 为触摸补发的合成 WM_LBUTTONDOWN 也会
|
|
|
- // 设置 m_pEventClick,一旦 WM_TOUCH 不是先到,通用守卫会让容器收不到 TOUCHMOVE(列表滚不动)。
|
|
|
- // 5) 抬起后置 m_bSuppressHoverAfterTouch,抑制 Windows 补发的合成 MOUSEMOVE 与 WM_MOUSELEAVE
|
|
|
- // 的自回发把刚清掉的悬浮态重新点亮(“悬浮态不消失 / 多控件同时悬浮”的真凶)。
|
|
|
+ // 2) 只向控件派发“原生触摸事件” UIEVENT_TOUCHDOWN / UIEVENT_TOUCHMOVE / UIEVENT_TOUCHUP:
|
|
|
+ // · CContainerUI 认 TOUCHMOVE → 内容 1:1 跟手平移(对标 NIM ScrollableBox);
|
|
|
+ // · CScrollBarUI 认 TOUCHDOWN/MOVE/UP → 拖滑块 / 点轨道翻页(不依赖合成鼠标移动);
|
|
|
+ // · 其余控件忽略触摸事件(未认领的会沿 DoEvent 冒泡给父容器)。
|
|
|
+ // 3) 按钮“点击”不在这里合成!它来自 Windows 为触摸自动提升(promote)的 WM_LBUTTONDOWN/UP,
|
|
|
+ // 走 MessageHandler 里原生的鼠标点击路径(CButtonUI 等据此 Activate())。
|
|
|
+ // 4) 不做任何“触摸→鼠标”归一化、不做 tap-slop 互斥、不做通用 m_pEventClick 守卫;
|
|
|
+ // “轻点 vs 拖拽”只用 m_bTouchMoved 在 WM_LBUTTONUP 处抑制误点。
|
|
|
+ // 5) 触摸**不参与悬浮态**:WM_MOUSEMOVE 里用 MOUSEEVENTF_FROMTOUCH 标记把“提升出来的鼠标消息”
|
|
|
+ // 挡在悬浮 enter/leave 之外(触摸没有“光标停留”语义)——这是“点过的控件一直高亮 /
|
|
|
+ // 多个控件同时高亮”的根治手段;此处再叠加 m_bSuppressHoverAfterTouch 抑制抬起后的复活。
|
|
|
TouchApi::PFN_GetTouchInputInfo pfnGetInfo = TouchApi::GetGetTouchInputInfo();
|
|
|
if (pfnGetInfo == NULL) return;
|
|
|
|
|
|
@@ -686,32 +745,52 @@ namespace DuiLib
|
|
|
|
|
|
if (ti.dwFlags & TouchApi::TOUCHEVTFLAG_DOWN)
|
|
|
{
|
|
|
- // 说明:这里【不再】用 `if (m_pEventClick != NULL) break;`。
|
|
|
- // 该守卫本意是“真实鼠标交互中忽略触摸”,但 Windows 会在 WM_TOUCH 前后补发合成的
|
|
|
- // WM_LBUTTONDOWN(同样会设置 m_pEventClick)。若保证不了 WM_TOUCH 一定先到,
|
|
|
- // 这句 break 会让 m_pEventTouch 永不被设置 → 容器 TOUCHMOVE 跟手滚动彻底失效。
|
|
|
- // 由于点击/滚动本质上互不冲突(点击走原生鼠标路径,滚动走 TOUCHMOVE),
|
|
|
- // 这里只在“命中控件是滚动条”时让路,其余一律接管。
|
|
|
+ // 这里【不再】用 `if (m_pEventClick != NULL) break;` 做通用守卫:Windows 会在 WM_TOUCH
|
|
|
+ // 前后补发合成的 WM_LBUTTONDOWN(同样设置 m_pEventClick),通用守卫会让 m_pEventTouch
|
|
|
+ // 永不设置 → 容器收不到 TOUCHMOVE,列表滚不动。
|
|
|
+ //
|
|
|
+ // 滚动条也不再“交回原生鼠标”:实测部分触摸收银机在手指拖拽期间【不补发合成的
|
|
|
+ // WM_MOUSEMOVE】,导致滑块拖不动。现在改为由 WM_TOUCH 直接驱动——CScrollBarUI 已把
|
|
|
+ // TOUCHDOWN/MOVE/UP 与鼠标事件等价处理,这里让滚动条走常规接管流程,
|
|
|
+ // 后续 TOUCHMOVE 由它消费(不会冒泡到容器把内容滚走)。
|
|
|
|
|
|
::SetFocus(m_hWndPaint);
|
|
|
m_ptLastTouchPos = pt;
|
|
|
+ // 先把手势状态复位:即使这一按没命中任何控件,也不能让上一轮的 m_pEventTouch 残留
|
|
|
+ // 下去(否则后续 TOUCHMOVE 会发给一个早已松开的手指所对应的控件)。
|
|
|
+ m_pEventTouch = NULL;
|
|
|
+ m_bTouchMoved = false;
|
|
|
+ m_ptTouchStart = pt;
|
|
|
|
|
|
CControlUI* pControl = FindControl(pt);
|
|
|
if (pControl == NULL) break;
|
|
|
if (pControl->GetManager() != this) break;
|
|
|
|
|
|
- // 滚动条:滑块拖拽/点击由原生鼠标路径(模拟的 WM_LBUTTONDOWN/MOVE/UP)驱动;
|
|
|
- // 若此处再派发 UIEVENT_TOUCHMOVE 会冒泡到容器把【内容】滚走,与滑块拖拽打架→滑块拖不动、点轨道误翻页。
|
|
|
- // 直接交回原生鼠标,本函数提前退出(悬浮态由 WM_TOUCH UP 统一清理)。
|
|
|
- if (pControl->GetInterface(_T("ScrollBar")) != NULL)
|
|
|
+ // [触屏] 手指容差改判:直接命中的不是滚动条时,看看是不是“手指离滚动条滑块很近”。
|
|
|
+ // 典型场景:列表右侧竖直滚动条仅 16px 宽,用户想拖滑块却按到了旁边的列表项上。
|
|
|
+ if (pControl->GetInterface(DUI_CTR_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;
|
|
|
+ CControlUI* pBar = Touch_ResolveScrollBarThumb(pControl, pt);
|
|
|
+ if (pBar != NULL)
|
|
|
+ {
|
|
|
+ TouchTrace::Log(_T("TOUCHDOWN-FATFINGER hit=%s -> scrollbar=%s pt=%d,%d"),
|
|
|
+ pControl->GetName().GetData(), pBar->GetName().GetData(), pt.x, pt.y);
|
|
|
+ pControl = pBar;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ bool bTouchIsScrollBar = (pControl->GetInterface(DUI_CTR_SCROLLBAR) != NULL);
|
|
|
+ // 命中滚动条时把触点收进滚动条 rect(横向按偏也能拖滑块);事件点用收敛后的,
|
|
|
+ // 但 m_ptLastTouchPos / m_ptTouchStart 仍记真实坐标(去重与位移判定按真实手指轨迹走)。
|
|
|
+ POINT ptEvent = bTouchIsScrollBar ? Touch_SnapIntoScrollBar(pControl, pt) : pt;
|
|
|
+
|
|
|
+ if (bTouchIsScrollBar)
|
|
|
+ {
|
|
|
+ TouchTrace::Log(_T("TOUCHDOWN-SCROLLBAR touch-driven hit=%s pt=%d,%d -> %d,%d"),
|
|
|
+ pControl->GetName().GetData(), pt.x, pt.y, ptEvent.x, ptEvent.y);
|
|
|
}
|
|
|
|
|
|
m_pEventTouch = pControl;
|
|
|
- m_bTouchMoved = false;
|
|
|
m_ptTouchStart = pt;
|
|
|
m_bSuppressHoverAfterTouch = false; // 新手势开始,解除上一轮抬起后的悬浮抑制
|
|
|
pControl->SetFocus();
|
|
|
@@ -723,41 +802,39 @@ namespace DuiLib
|
|
|
TEventUI event = { 0 };
|
|
|
event.Type = UIEVENT_TOUCHDOWN;
|
|
|
event.pSender = pControl;
|
|
|
- event.ptMouse = pt;
|
|
|
+ event.ptMouse = ptEvent;
|
|
|
event.dwTimestamp = ::GetTickCount();
|
|
|
event.wKeyState = MapKeyState();
|
|
|
pControl->Event(event);
|
|
|
}
|
|
|
else if (ti.dwFlags & TouchApi::TOUCHEVTFLAG_MOVE)
|
|
|
{
|
|
|
- // 鼠标/指针交互进行中 -> 忽略触摸。但注意:一次触摸手势本身会触发 Windows 模拟的
|
|
|
- // WM_LBUTTONDOWN,从而把 m_pEventClick 置为【当前命中控件】(容器/按钮/滚动条都会)。
|
|
|
- // 若整句 break,会把容器的 UIEVENT_TOUCHMOVE 一起吞掉→列表无法跟手滚动。
|
|
|
- // 故只对“滚动条”让路(滚动条滑块拖拽由原生鼠标驱动,见 DOWN 的 ScrollBar break),
|
|
|
- // 其余控件(容器/按钮)照常派发 TOUCHMOVE:容器据此 1:1 跟手滚动,按钮不处理 TOUCHMOVE 无害。
|
|
|
- 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;
|
|
|
- }
|
|
|
+ // 【不要】在这里对滚动条让路。滚动条现在完全由 WM_TOUCH 驱动:DOWN 已把 m_pEventTouch
|
|
|
+ // 设为"滚动条自身"(不再交回原生鼠标),CScrollBarUI 也已把 TOUCHDOWN/MOVE/UP 与鼠标
|
|
|
+ // 事件等价处理。若此处仍 `m_pEventClick->GetInterface("ScrollBar") != NULL` 就 break,
|
|
|
+ // TOUCHMOVE 永远到不了滚动条 → 滑块拖不动(历史症状)。滚动条收到 TOUCHMOVE 会自行消费
|
|
|
+ // (CScrollBarUI::DoEvent 命中已捕获分支后 return true),不会冒泡到容器把内容滚走。
|
|
|
// 与上一包完全相同的点 -> 去重,避免无谓派发(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;
|
|
|
- // 相对手势起点的累计位移超过阈值 -> 判定为拖拽(用于松开时抑制点击)
|
|
|
+ // 相对手势起点的累计位移超过阈值 -> 判定为拖拽(用于松开时抑制点击)。
|
|
|
LONG ox = pt.x - m_ptTouchStart.x;
|
|
|
LONG oy = pt.y - m_ptTouchStart.y;
|
|
|
- if (ox > 5 || ox < -5 || oy > 5 || oy < -5)
|
|
|
+ // 阈值取 8px:真实滚动位移远大于此,而指尖“按下去时的轻微滚动”通常在 8px 内。
|
|
|
+ // 阈值过小会把抖动误判成拖拽 → 该次抬起的 BUTTONUP 被抑制 → 按钮停在按下态。
|
|
|
+ if (ox > 8 || ox < -8 || oy > 8 || oy < -8)
|
|
|
m_bTouchMoved = true;
|
|
|
if (m_pEventTouch == NULL) break;
|
|
|
|
|
|
TEventUI event = { 0 };
|
|
|
event.Type = UIEVENT_TOUCHMOVE;
|
|
|
event.pSender = m_pEventTouch;
|
|
|
- event.ptMouse = pt;
|
|
|
+ // 滚动条:触点同样收进滚动条 rect(与 DOWN 保持一致,避免首帧跳变)。
|
|
|
+ event.ptMouse = (m_pEventTouch->GetInterface(DUI_CTR_SCROLLBAR) != NULL)
|
|
|
+ ? Touch_SnapIntoScrollBar(m_pEventTouch, pt) : pt;
|
|
|
event.dwTimestamp = ::GetTickCount();
|
|
|
event.wKeyState = MapKeyState();
|
|
|
event.wParam = (WPARAM)(int)dx;
|
|
|
@@ -813,7 +890,7 @@ namespace DuiLib
|
|
|
m_bSuppressHoverAfterTouch = true;
|
|
|
m_bMouseTracking = false;
|
|
|
|
|
|
- // 滚动条等场景本函数未接管(m_pEventTouch 为 NULL),捕获由原生鼠标路径负责,这里不释放。
|
|
|
+ // 本函数未接管该次触摸(如 DOWN 时 FindControl 落空)→ 沿旧路径交给原生鼠标,这里不释放。
|
|
|
if (m_pEventTouch == NULL) break;
|
|
|
|
|
|
ReleaseCapture();
|
|
|
@@ -821,7 +898,9 @@ namespace DuiLib
|
|
|
TEventUI event = { 0 };
|
|
|
event.Type = UIEVENT_TOUCHUP;
|
|
|
event.pSender = m_pEventTouch;
|
|
|
- event.ptMouse = pt;
|
|
|
+ // 与 DOWN/MOVE 一致:滚动条用收敛点,保证 ptMouse 轨迹连续、位移计算不失真。
|
|
|
+ event.ptMouse = (m_pEventTouch->GetInterface(DUI_CTR_SCROLLBAR) != NULL)
|
|
|
+ ? Touch_SnapIntoScrollBar(m_pEventTouch, pt) : pt;
|
|
|
event.dwTimestamp = ::GetTickCount();
|
|
|
event.wKeyState = MapKeyState();
|
|
|
TouchTrace::Log(_T("TOUCHUP touch=%s moved=%d hover=%p leaveArr=%d click=%p capture=%d"),
|
|
|
@@ -2126,7 +2205,13 @@ namespace DuiLib
|
|
|
event.lParam = lParam;
|
|
|
event.dwTimestamp = ::GetTickCount();
|
|
|
event.wKeyState = MapKeyState();
|
|
|
- if (!IsCaptured())
|
|
|
+ // 悬浮态总闸:触摸提升出来的合成鼠标消息(MOUSEEVENTF_FROMTOUCH)不参与
|
|
|
+ // 悬浮 enter/leave。触摸没有“光标停留”语义,一旦让它参与,就会出现
|
|
|
+ // “点过的控件一直高亮 / 连点几个控件后多个同时高亮、怎么点都清不掉”的顽疾
|
|
|
+ // (按钮的 UISTATE_HOT、列表项的 UISTATE_HOT 全靠 MOUSEENTER/MOUSELEAVE 维护)。
|
|
|
+ // 该判断只影响悬浮段,消息依旧往下走 → 点击/拖拽状态机不受影响。
|
|
|
+ const bool bPromotedTouchMove = IsPromotedTouchMouseMessage();
|
|
|
+ if (!IsCaptured() && !bPromotedTouchMove)
|
|
|
{
|
|
|
pNewHover = FindControl(pt);
|
|
|
if (pNewHover != NULL && pNewHover->GetManager() != this) break;
|