|
|
@@ -37,6 +37,35 @@ namespace DuiLib
|
|
|
#define WM_TOUCH 0x0240
|
|
|
#endif
|
|
|
|
|
|
+ // ======================================================================
|
|
|
+ // ★★【悬浮态残留 · 跨 manager】这套"触摸抬手后抑制悬浮复活"的状态必须是
|
|
|
+ // **线程级(文件级静态)**的,绝不能用 CPaintManagerUI 的成员。★★
|
|
|
+ //
|
|
|
+ // 反面教材(本项目真机日志实证):属性弹框 CDiandanNatureWnd **自带独立的
|
|
|
+ // CPaintManagerUI m_pm**,与主窗口的 manager 不是同一个对象
|
|
|
+ // (真机日志:弹框的 `MANAGER-MSG hwnd=000E04BA manager=0A29F5F8`,
|
|
|
+ // 主窗口的 `MANAGER-MSG hwnd=001304D0 manager=07DA8C58`)。
|
|
|
+ //
|
|
|
+ // 用户点弹框里的「选好了」关掉弹框时:
|
|
|
+ // · 那次触摸由**弹框的 manager** 处理 ⇒ 抑制位与参考点写在弹框那一份上;
|
|
|
+ // · 弹框一销毁,系统立刻给**主窗口**补投一条 extra=0 / fromtouch=0 的
|
|
|
+ // WM_MOUSEMOVE(语义"指针真的停在这")⇒ 落在**主窗口的 manager** 上。
|
|
|
+ // 主窗口那份状态压根不知道刚刚发生过触摸:抑制位要么是假,要么参考点还停在
|
|
|
+ // 更早那次主窗口触摸的位置 ⇒ ±8px 判据必然失败 ⇒ 解除抑制 + FindControl(pt)
|
|
|
+ // 命中背后商品卡 ⇒ **悬浮态点亮且此后永不熄灭**。
|
|
|
+ // (真机日志:`MODAL-DLG exit` → `HOVER-ENTER diandan_fooditem pt=778,630`,
|
|
|
+ // 而主窗口那份参考点还停在 1225,446 —— 相距 447 像素。)
|
|
|
+ //
|
|
|
+ // 与 `s_nPromotedMouseGeneration` 的理由完全相同:这是**线程消息队列**现象,
|
|
|
+ // 与 manager 归属无关 ⇒ 状态必须线程级。
|
|
|
+ // (本项目 UI 单线程;若将来多线程,把这两个 static 换成 TLS 即可。)
|
|
|
+ //
|
|
|
+ // 参考点存**屏幕坐标**:WM_MOUSEMOVE 的 lParam 是"接收窗口的客户区坐标",
|
|
|
+ // 而触摸可能发生在另一个窗口里 —— 客户区坐标一换窗口就不可比。
|
|
|
+ // ======================================================================
|
|
|
+ static bool s_bSuppressHoverAfterTouch = false;
|
|
|
+ static POINT s_ptLastTouchScreen = { 0, 0 };
|
|
|
+
|
|
|
// ---------------------------------------------------------------------------
|
|
|
// WM_POINTER* 消息 ID(Win8+)。参考 nim_duilib NativeWindow_Windows::OnPointerMsgs。
|
|
|
//
|
|
|
@@ -654,7 +683,7 @@ namespace DuiLib
|
|
|
if (m_bMouseCapture || m_bDragMode)
|
|
|
ReleaseCapture();
|
|
|
m_bMouseTracking = false;
|
|
|
- m_bSuppressHoverAfterTouch = true;
|
|
|
+ s_bSuppressHoverAfterTouch = true;
|
|
|
|
|
|
m_hWndPaint = hWnd;
|
|
|
m_hDcPaint = ::GetDC(hWnd);
|
|
|
@@ -833,6 +862,172 @@ namespace DuiLib
|
|
|
return s_nPromotedMouseGeneration;
|
|
|
}
|
|
|
|
|
|
+ // ======================================================================
|
|
|
+ // ★★【第二次合成点击 · 根治】"触摸通道处理过的这次手势,其提升鼠标副本一律作废" ★★
|
|
|
+ // ======================================================================
|
|
|
+ //
|
|
|
+ // 【背景:一次手势会被处理两遍】
|
|
|
+ // 手指点一下,DuiLib 会经历两遍:
|
|
|
+ // ① 触摸通道(WM_TOUCH / WM_POINTER*)合成 BUTTONDOWN/BUTTONUP —— 业务的
|
|
|
+ // 点击回调在这里跑,`ShowModal()` 的嵌套模态循环也在这里开;
|
|
|
+ // ② Windows 随后把**同一次触摸**提升成鼠标消息(MOVE + LBUTTONDOWN +
|
|
|
+ // LBUTTONUP,extra 带 MOUSEEVENTF_FROMTOUCH)。它是纯重复,必须作废。
|
|
|
+ //
|
|
|
+ // 【为什么 ② 一定还会来,但 ② 投给谁却会变】
|
|
|
+ // ② 投给谁取决于**投递那一刻**的窗口状态,与 ① 时刻可能完全不同:
|
|
|
+ // · 该点此刻已被弹框覆盖 ⇒ 投给**弹框**(前台且启用)⇒ 窗口过程收得到,能拦;
|
|
|
+ // · 该点此刻仍在 owner 上 ⇒ 投给**被禁用的 owner** ⇒ 系统直接
|
|
|
+ // MessageBeep(0) + 丢弃 ⇒ **窗口过程根本收不到**(真机日志实证:该坐标
|
|
|
+ // 附近 0 条提升消息)⇒ 这就是 Win7 上"弹框一出来就咚一声"的全部成因。
|
|
|
+ //
|
|
|
+ // 【旧判据为什么在 Win7 上必然漏】(真机日志 touch_trace.log,2451 行 / 25 次手势)
|
|
|
+ // · "代数水位":水位在**任意一条**提升消息到达时就追平。于是"点『选好了』
|
|
|
+ // 关闭弹框"这一次手势的副本到达时,脏已被前一次手势用掉 ⇒ 无脏可判 ⇒
|
|
|
+ // **放行** ⇒ 透传到背后商品卡 ⇒ 弹框被重新点开。25 次里漏了 6 次,
|
|
|
+ // 全部是"关闭弹框的那一次手势"。
|
|
|
+ // · "extra 指纹":Win7 上 extra **恒为 0xFF51578A**(日志中 59/59 条完全
|
|
|
+ // 相同),指纹去重彻底失效 —— `close-by-extra` 一次都没触发。
|
|
|
+ // (Win10 那边 extra 低字节每次手势都变,所以同一套方案在 Win10 上
|
|
|
+ // "看起来是对的"。这正是"Win10 好了、Win7 没好"的分界。)
|
|
|
+ //
|
|
|
+ // 【新判据:只依赖一件事实 —— 触摸通道收到了这次手势】
|
|
|
+ // 收到了 ⇒ 提升副本必是重复 ⇒ 作废。与模态边界、代数、extra 全都无关,
|
|
|
+ // 因此对"关闭弹框的那一次手势"同样有效(那正是旧判据唯一漏掉的一类)。
|
|
|
+ //
|
|
|
+ // ⚠️ 标志**只由触摸通道**置位(BeginTouchSlideGesture —— 两条通道共用的
|
|
|
+ // "新手势开始"入口)。若某台机器两条通道都收不到(只会收到提升鼠标),
|
|
|
+ // 标志恒假 ⇒ 提升鼠标照常放行 ⇒ **不会把整机点不动**。
|
|
|
+ // 这条自限边界是本方案敢用的前提,务必保留。
|
|
|
+ //
|
|
|
+ // 【为什么还要比坐标】
|
|
|
+ // 若某次手势的提升副本被系统吃掉(上面那种"咚"的情况),标志会一直挂着。
|
|
|
+ // 此时若出现"触摸通道收不到、只能靠提升鼠标"的窗口(例如弹框的触摸注册
|
|
|
+ // 被样式变更隐式注销),不加坐标判据就会把它的真实点击一起吞掉 ⇒ 点不动。
|
|
|
+ // 加上 ±16px(与滑动阈值同一量级)判据后,陈旧标志不会误伤无关位置的点击。
|
|
|
+ // 坐标一律换算到**屏幕坐标**再比 —— ② 可能落在**另一个窗口的客户区**里
|
|
|
+ // (弹框会拦截同一物理点的消息,lParam 是弹框的客户区坐标)。
|
|
|
+ // ======================================================================
|
|
|
+ static bool s_bPromotedMouseDuplicate = false; // 有"待作废的提升副本"
|
|
|
+ static POINT s_ptPromotedMouseDuplicate = { 0, 0 }; // 那次手势的**屏幕**坐标
|
|
|
+ static const int kPromotedDuplicateSlop = 16; // 手指抖动容差(像素)
|
|
|
+
|
|
|
+ // 三者都只在本文件内使用 ⇒ 一律 static(内部链接),避免与别的 TU 里的
|
|
|
+ // 同名符号在 DuiLib 命名空间里撞车(C2668 的典型成因)。
|
|
|
+ static void ArmPromotedMouseDuplicate(const POINT& ptScreen)
|
|
|
+ {
|
|
|
+ s_bPromotedMouseDuplicate = true;
|
|
|
+ s_ptPromotedMouseDuplicate = ptScreen;
|
|
|
+ }
|
|
|
+
|
|
|
+ static void DisarmPromotedMouseDuplicate()
|
|
|
+ {
|
|
|
+ s_bPromotedMouseDuplicate = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 本条提升鼠标消息是否属于"待作废的那次手势副本"。
|
|
|
+ // 参数 ptClient 是消息 lParam 里的**接收窗口客户区**坐标。
|
|
|
+ static bool IsPromotedMouseDuplicate(HWND hWndPaint, const POINT& ptClient)
|
|
|
+ {
|
|
|
+ if (!s_bPromotedMouseDuplicate) return false;
|
|
|
+ POINT ptScreen = ptClient;
|
|
|
+ if (hWndPaint != NULL && ::IsWindow(hWndPaint))
|
|
|
+ ::ClientToScreen(hWndPaint, &ptScreen);
|
|
|
+ const LONG dx = ptScreen.x - s_ptPromotedMouseDuplicate.x;
|
|
|
+ const LONG dy = ptScreen.y - s_ptPromotedMouseDuplicate.y;
|
|
|
+ return (dx <= kPromotedDuplicateSlop && dx >= -kPromotedDuplicateSlop
|
|
|
+ && dy <= kPromotedDuplicateSlop && dy >= -kPromotedDuplicateSlop);
|
|
|
+ }
|
|
|
+ // ======================================================================
|
|
|
+ // ★★【Win7"咚"声 · 根治】进模态循环之前,把"在途的提升鼠标副本"从消息队列里摘掉 ★★
|
|
|
+ //
|
|
|
+ // 【现象】
|
|
|
+ // Win7 上"点主窗口商品卡 → 弹出弹框"时会听到一声"咚"。它是**系统**发出的
|
|
|
+ // `MessageBeep(0)`:属于**弹框出现之前**那一次手势的提升鼠标副本,在弹框已经把
|
|
|
+ // owner `EnableWindow(FALSE)` 禁用之后才被投递 ⇒ 撞上"点击禁用窗口"的默认处理。
|
|
|
+ //
|
|
|
+ // 【已经逐条排除的方向(都有真机日志支撑,不要再回头走)】
|
|
|
+ // · 写在窗口过程里的闸门(MODAL-BEEP / PROMOTED-DUP):`swallowed` 恒为 0 ——
|
|
|
+ // 消息**根本没进窗口过程**,判据再准也无从生效。
|
|
|
+ // · WH_MOUSE_LL 低级鼠标钩子:钩子过程运行在**安装它的线程**(正是消息接收线程)
|
|
|
+ // 上,队列里的鼠标消息先于钩子被抽出 ⇒ 真机日志里提升副本照旧投递,一条没少。
|
|
|
+ // · 让 owner 在"副本在途"期间保持启用(能消音,但不可接受):owner 的鼠标消息
|
|
|
+ // 会因此落到 manager 上,而真模态期间 manager 已经搬到弹框
|
|
|
+ // (DetachPaintWindow → AttachPaintWindow)⇒ 此时 `m_hWndPaint` 指向**弹框**,
|
|
|
+ // owner 的客户区坐标会被拿去**弹框的控件树**里 FindControl,可能凭空命中
|
|
|
+ // 弹框上的某个控件。
|
|
|
+ //
|
|
|
+ // 【根治点:让它根本不存在】
|
|
|
+ // 既然拦不住"投递阶段"的系统行为,就在**投递之前**把它从队列里摘掉。
|
|
|
+ // 调用点在 `CWindowWnd::ShowModal` 进模态循环之前(见那里的长注释)。
|
|
|
+ // 为什么那个时刻一定赶得上:这条副本是在**同一次触摸**的输入处理里被排进队列的,
|
|
|
+ // 而调用方的触摸 UP 此刻**已经被处理完**(业务回调就是正在执行的这段代码)
|
|
|
+ // ⇒ 副本必然已经在队列里(真机日志亦实证:它被卡在队列里,直到弹框关闭、
|
|
|
+ // owner 重新启用之后才被投递)。而"摘除"与"创建弹框"之间没有任何消息泵
|
|
|
+ // ⇒ 摘除对投递而言是**原子**的。
|
|
|
+ //
|
|
|
+ // 【为什么不会误伤】
|
|
|
+ // · 判据只有一件事:触摸通道确实收到了这次手势
|
|
|
+ // (IsPromotedMouseDuplicatePending —— 与 PROMOTED-DUP 同源、同一处置位),
|
|
|
+ // 与模态边界 / 代数水位 / extra 指纹全都无关。
|
|
|
+ // · 摘除范围只限"**owner 窗口**的鼠标类消息",且只在这一个瞬间摘一次。
|
|
|
+ // · 被摘掉的本来就是**重复**的:这次点击已经由触摸通道的 tap 判定处理完毕
|
|
|
+ // (否则根本不会走到 ShowModal 这里)。
|
|
|
+ //
|
|
|
+ // 【用户的两条边界同时守住】
|
|
|
+ // · 场景①【从主窗口控件点出弹框 ⇒ 咚 必须消除】:触摸通道收到了手势 ⇒ 副本在
|
|
|
+ // 队列里 ⇒ 摘掉 ⇒ **无声**。
|
|
|
+ // · 场景②【弹框已打开后,点弹框外空白区 ⇒ 咚 属正常,用户明确要求不改】:
|
|
|
+ // 那一处属于被禁用的 owner,它的 WM_TOUCH 在**输入阶段**就被系统丢弃
|
|
|
+ // ⇒ BeginTouchSlideGesture 不会被调用 ⇒ 标志为假 ⇒ 不摘 ⇒ 那一声"咚"照旧。
|
|
|
+ //
|
|
|
+ // 【自限】
|
|
|
+ // 若某台机器压根不产生提升副本(本项目 Win10 走 WM_POINTER*,fromtouch 消息 0 条),
|
|
|
+ // 标志恒假 ⇒ 本机制**从不启用** ⇒ 零影响。
|
|
|
+ // ======================================================================
|
|
|
+
|
|
|
+ // 【同源访问器】把"提升副本在途"这一事实暴露给 UIBase.cpp(摘除的时机在那边)。
|
|
|
+ bool CPaintManagerUI::IsPromotedMouseDuplicatePending()
|
|
|
+ {
|
|
|
+ return s_bPromotedMouseDuplicate;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ★把"本次手势在途的提升鼠标副本"从本线程消息队列里就地摘掉,返回摘掉的条数。
|
|
|
+ // 调用点:CWindowWnd::ShowModal 进模态循环之前(那里的长注释解释了为什么必须摘)。
|
|
|
+ //
|
|
|
+ // ⚠️ 这里**故意不销账**(不调 DisarmPromotedMouseDuplicate):
|
|
|
+ // 标志继续挂着,PROMOTED-DUP 那道闸门对"稍后才到的提升副本"仍然有效 ——
|
|
|
+ // 摘除动作只是把"已经排在队列里的那些"提前清掉,不该顺带削弱仍在生效的保护。
|
|
|
+ // 陈旧标志本身不会误伤:IsPromotedMouseDuplicate() 还要比屏幕坐标(±16px)。
|
|
|
+ int CPaintManagerUI::DropQueuedPromotedMouseMessages(HWND hWndTarget)
|
|
|
+ {
|
|
|
+ if (hWndTarget == NULL || !::IsWindow(hWndTarget)) return 0;
|
|
|
+
|
|
|
+ int nDropped = 0;
|
|
|
+ MSG msg;
|
|
|
+ // 范围 WM_MOUSEFIRST..WM_MOUSELAST 覆盖 MOVE / 左中右键 DOWN-UP-DBLCLK /
|
|
|
+ // WHEEL / XBUTTON 全族 —— 提升副本只在其中。
|
|
|
+ // 只摘"投给该窗口"的(弹框出现之前那次手势的副本,其归属窗口必然是这个 owner:
|
|
|
+ // 消息的归属在**投递那一刻**才按"光标下的窗口"结算,而那时弹框还不存在)。
|
|
|
+ while (::PeekMessage(&msg, hWndTarget, WM_MOUSEFIRST, WM_MOUSELAST, PM_REMOVE))
|
|
|
+ ++nDropped;
|
|
|
+
|
|
|
+ return nDropped;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 【悬浮态残留 · 跨窗口参考点】把"本窗口客户区点"记成**屏幕**坐标后,写进
|
|
|
+ // **线程级**参考点 `s_ptLastTouchScreen`(见文件头那一大段说明:这套状态不能是
|
|
|
+ // manager 成员,因为补投的 MOVE 会落在另一个 manager 上)。
|
|
|
+ // 用屏幕坐标的原因:抑制"抬起后悬浮态复活"的 ±8px 判据必须用与窗口无关的坐标系,
|
|
|
+ // 否则窗口一换(尤其弹框销毁、消息改投主窗口)判据就会失准。
|
|
|
+ // 调用约定:凡写 `m_ptLastTouchPos` 的**触摸**路径,都应同步调一次本函数。
|
|
|
+ void CPaintManagerUI::SetLastTouchScreenPoint(POINT ptClient)
|
|
|
+ {
|
|
|
+ POINT ptScreen = ptClient;
|
|
|
+ if (m_hWndPaint != NULL && ::IsWindow(m_hWndPaint))
|
|
|
+ ::ClientToScreen(m_hWndPaint, &ptScreen);
|
|
|
+ s_ptLastTouchScreen = ptScreen;
|
|
|
+ }
|
|
|
+
|
|
|
namespace
|
|
|
{
|
|
|
const int kScrollBarTouchSlop = 12; // 手指离滚动条 rect 的最大容差(像素)
|
|
|
@@ -1226,6 +1421,24 @@ namespace DuiLib
|
|
|
// 本标志是"滑动状态机活着"的唯一判据(End 之前一直为真)。
|
|
|
m_bTouchSlideActive = true;
|
|
|
|
|
|
+ // ★【第二次合成点击 · 根治】触摸通道接手了这次手势 ⇒ 它的提升鼠标副本要作废。
|
|
|
+ //
|
|
|
+ // 只在这一处置位(连同 EndTouchSlideGesture 里的销账),理由:
|
|
|
+ // · WM_TOUCH 与 WM_POINTER* 两条通道**都经由本函数**开始手势 ⇒ 一处即全覆盖;
|
|
|
+ // · 由"触摸通道确实收到了手势"来驱动,而不是由"模态边界/代数"来猜 ⇒
|
|
|
+ // 对"关闭弹框的那一次手势"同样生效(旧判据唯一漏掉的一类)。
|
|
|
+ // · pt 是**本窗口客户区**坐标,提升副本可能落在别的窗口客户区里,
|
|
|
+ // 所以这里换算成屏幕坐标存起来,供 MessageHandler 侧比对。
|
|
|
+ // 详见文件上方 ArmPromotedMouseDuplicate 那段注释。
|
|
|
+ {
|
|
|
+ POINT ptScreen = pt;
|
|
|
+ ::ClientToScreen(m_hWndPaint, &ptScreen);
|
|
|
+ ArmPromotedMouseDuplicate(ptScreen);
|
|
|
+ // 同一个触点的**屏幕坐标**副本:抑制"抬起后悬浮态复活"的 ±8px 判据必须用它
|
|
|
+ // (客户区坐标一换窗口就不可比,见 s_ptLastTouchScreen 的注释)。
|
|
|
+ s_ptLastTouchScreen = ptScreen;
|
|
|
+ }
|
|
|
+
|
|
|
CControlUI* pControl = FindControl(pt);
|
|
|
if (pControl == NULL)
|
|
|
{
|
|
|
@@ -1270,7 +1483,7 @@ namespace DuiLib
|
|
|
if (!bTouchIsScrollBar)
|
|
|
m_pEventClick = pControl;
|
|
|
// 触摸没有悬浮语义;整个手势期间以及抬起后的合成鼠标移动都不应重建 HOT。
|
|
|
- m_bSuppressHoverAfterTouch = true;
|
|
|
+ s_bSuppressHoverAfterTouch = true;
|
|
|
pControl->SetFocus();
|
|
|
SetCapture(); // mgr 级:m_bMouseCapture=true + ::SetCapture
|
|
|
|
|
|
@@ -1309,6 +1522,33 @@ namespace DuiLib
|
|
|
event.dwTimestamp = ::GetTickCount();
|
|
|
event.wKeyState = MapKeyState();
|
|
|
pControl->Event(event);
|
|
|
+
|
|
|
+ // ------------------------------------------------------------------
|
|
|
+ // ★★【触摸按压态 · 主动冲刷一次绘制】★★
|
|
|
+ //
|
|
|
+ // 现象(Win7 真机实测):点商品卡时"很多商品没有按压态显示"。
|
|
|
+ // 成因**不是**控件没置状态,而是**没机会把它画出来**:
|
|
|
+ // · WM_PAINT 是**低优先级**消息 —— 只有消息队列空下来,GetMessage 才会合成它;
|
|
|
+ // · 真机上这次手势的 DOWN→UP 只差 0~16ms(同一个时钟滴答)。TOUCHDOWN 这一发
|
|
|
+ // 把 pressed 置上并 Invalidate 之后,队列里紧接着就排着 UP,WM_PAINT 一直
|
|
|
+ // 轮不上;等 UP 处理完,状态早被复位成 normal 了 ⇒ 画出来的是普通帧。
|
|
|
+ // · 反过来,**滑动时按压态反而看得见** —— 因为手指按住不放,队列有空隙,
|
|
|
+ // WM_PAINT 合成得出来。这与用户此前"滑动起手就显示按压态"的反馈完全吻合,
|
|
|
+ // 也反过来证明"置/复位是配对的、只是没机会绘制"这一判断成立。
|
|
|
+ //
|
|
|
+ // 修法:TOUCHDOWN 派发完,立刻用 ::UpdateWindow 把已经堆起来的脏区**同步**刷一次。
|
|
|
+ // · 与库内既有写法一致:CContainerUI::EndDown / EndRight 就是这么刷的;
|
|
|
+ // · ::UpdateWindow 只在"本窗口已有脏区"时才发 WM_PAINT,不制造多余重绘;
|
|
|
+ // · 不碰任何状态、不派发任何事件,纯显示层。
|
|
|
+ //
|
|
|
+ // ⚠️ 副作用(已知并接受):滑动起手那一帧的按压态也会**真的画出来**。
|
|
|
+ // 这不是本行引入的新语义 —— 清扫时机是"抬手时才清扫",用户已明确选择过。
|
|
|
+ // 若将来要让它不显示,改的是清扫时机,不是这里。
|
|
|
+ // ------------------------------------------------------------------
|
|
|
+ if (m_hWndPaint != NULL && ::IsWindow(m_hWndPaint))
|
|
|
+ {
|
|
|
+ ::UpdateWindow(m_hWndPaint);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
void CPaintManagerUI::UpdateTouchSlideGesture(POINT pt)
|
|
|
@@ -1445,10 +1685,11 @@ namespace DuiLib
|
|
|
if (m_pEventTouch == NULL)
|
|
|
{
|
|
|
m_ptLastTouchPos = pt;
|
|
|
+ SetLastTouchScreenPoint(pt);
|
|
|
m_pTouchScrollContainer = NULL;
|
|
|
m_bTouchMoved = false;
|
|
|
m_nTouchWheelAccum = 0;
|
|
|
- m_bSuppressHoverAfterTouch = true;
|
|
|
+ s_bSuppressHoverAfterTouch = true;
|
|
|
m_bMouseTracking = false;
|
|
|
// [NIM 风格] 这里不需要"解除让路"——让路已改由 m_pEventTouch 这个**标记**表达。
|
|
|
// ⚠️ 可变参数必须在 _T 宏的括号**之外**:_T 是单参数宏,
|
|
|
@@ -1462,6 +1703,7 @@ namespace DuiLib
|
|
|
(int)m_bTouchMoved);
|
|
|
|
|
|
m_ptLastTouchPos = pt;
|
|
|
+ SetLastTouchScreenPoint(pt);
|
|
|
|
|
|
// 注:`m_pEventTouch == NULL` 的早退路已提到本函数最开头(见上),
|
|
|
// 到这里 m_pEventTouch 必然非空,可以安全解引用。
|
|
|
@@ -1517,7 +1759,7 @@ namespace DuiLib
|
|
|
// Windows 会为触摸补发合成的 WM_MOUSEMOVE;且真实 WM_MOUSELEAVE 在 m_bMouseTracking
|
|
|
// 为真时会自己给自己重发 WM_MOUSEMOVE(见 WM_MOUSELEAVE 分支)→ 刚清掉的悬浮态立刻
|
|
|
// 被重新点亮。这里置抑制位 + 主动撤下鼠标跟踪,直到用户真的移动鼠标才恢复。
|
|
|
- m_bSuppressHoverAfterTouch = true;
|
|
|
+ s_bSuppressHoverAfterTouch = true;
|
|
|
m_bMouseTracking = false;
|
|
|
|
|
|
// ------------------------------------------------------------------
|
|
|
@@ -1653,7 +1895,7 @@ namespace DuiLib
|
|
|
// "轻点 vs 拖拽"只用 m_bTouchMoved 在 UP 处抑制误点。
|
|
|
// 5) 触摸**不参与悬浮态**:WM_MOUSEMOVE 里用 MOUSEEVENTF_FROMTOUCH 标记把"提升出来的鼠标消息"
|
|
|
// 挡在悬浮 enter/leave 之外(触摸没有"光标停留"语义)——这是"点过的控件一直高亮 /
|
|
|
- // 多个控件同时高亮"的根治手段;此处再叠加 m_bSuppressHoverAfterTouch 抑制抬起后的复活。
|
|
|
+ // 多个控件同时高亮"的根治手段;此处再叠加 s_bSuppressHoverAfterTouch 抑制抬起后的复活。
|
|
|
TouchApi::PFN_GetTouchInputInfo pfnGetInfo = TouchApi::GetGetTouchInputInfo();
|
|
|
|
|
|
// 【已移除】"把 WM_TOUCH 同步转发给模态弹框"的兜底块。
|
|
|
@@ -2398,7 +2640,7 @@ namespace DuiLib
|
|
|
// 而且回调返回时前台已经变了 ⇒ 复位动作打在错的对象/时机上。
|
|
|
//
|
|
|
// 鼠标路径为什么没这问题:它的清理动作全部在业务回调**之前**:
|
|
|
- // ClearTouchHoverState / ClearPushedStateAt / m_bSuppressHoverAfterTouch
|
|
|
+ // ClearTouchHoverState / ClearPushedStateAt / s_bSuppressHoverAfterTouch
|
|
|
// → 才轮到 `pClick->Event(BUTTONUP)` → `SendNotify(CLICK)` → `ShowModal()`
|
|
|
//
|
|
|
// 但**触摸链不能照搬"事前 ClearPushedStateAt"**!
|
|
|
@@ -3470,6 +3712,48 @@ namespace DuiLib
|
|
|
|| uMsg == WM_LBUTTONDBLCLK);
|
|
|
const bool bUpLike = (uMsg == WM_LBUTTONUP || uMsg == WM_LBUTTONDBLCLK);
|
|
|
|
|
|
+ // ==============================================================
|
|
|
+ // ★★【第二次合成点击 · 根治】第一道,也是唯一必需的一道闸 ★★
|
|
|
+ //
|
|
|
+ // 判据:本条提升消息,是不是"触摸通道刚刚处理过的那次手势"的副本。
|
|
|
+ // · 依据只有"触摸通道收到了这次手势"这一个事实 + 屏幕坐标吻合,
|
|
|
+ // **与模态边界 / 代数水位 / extra 指纹全都无关** ⇒ 对"关闭弹框
|
|
|
+ // 的那一次手势"同样有效(旧判据在 Win7 上唯一漏掉的就是它)。
|
|
|
+ // · 命中的处理:点击类一律吞(并且不建立候选点击);抬手类到达即销账。
|
|
|
+ // 移动类放行(不产生点击;仍需维持重绘/光标,悬浮态由"提升移动
|
|
|
+ // 不参与 hover"的总闸挡掉)。
|
|
|
+ //
|
|
|
+ // ⚠️ 必须放在下面"代数水位"那套**之前**:那套是按模态边界近似出来的
|
|
|
+ // 版本,在 Win7 上(extra 恒定 ⇒ 指纹失效;水位一次手势就追平)
|
|
|
+ // 必然漏掉"关闭弹框那一次"。本闸门是它的严格超集。
|
|
|
+ // 先不删旧的,等这两台机器都验过再决定去留。
|
|
|
+ // ==============================================================
|
|
|
+ {
|
|
|
+ POINT ptMsg = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
|
|
|
+ if (IsPromotedMouseDuplicate(m_hWndPaint, ptMsg) && bClickLike)
|
|
|
+ {
|
|
|
+ DuiLibDiag::Log(_T("PROMOTED-DUP swallow msg=0x%04X hwnd=%p extra=0x%08X pt=%d,%d"),
|
|
|
+ uMsg, (void*)m_hWndPaint, (unsigned)(dwExtra & 0xFFFFFFFF),
|
|
|
+ ptMsg.x, ptMsg.y);
|
|
|
+ // 从根上不产生候选点击(抬手自然也不会派发)。
|
|
|
+ if (uMsg == WM_LBUTTONDOWN)
|
|
|
+ m_pEventClick = NULL;
|
|
|
+ if (m_bMouseCapture)
|
|
|
+ ReleaseCapture();
|
|
|
+ // 【悬浮态残留】把"抑制参考点"钉在本条坐标上:残留序列之后
|
|
|
+ // 系统还会补投一条 extra=0 的普通 MOVE,参考点不挪的话那条
|
|
|
+ // MOVE 会解除抑制并重建 hover。
|
|
|
+ s_bSuppressHoverAfterTouch = true;
|
|
|
+ m_bMouseTracking = false;
|
|
|
+ m_ptLastTouchPos = ptMsg;
|
|
|
+ SetLastTouchScreenPoint(ptMsg);
|
|
|
+ // 抬手类到达 ⇒ 这次手势的提升副本已配完 ⇒ 销账。
|
|
|
+ if (bUpLike)
|
|
|
+ DisarmPromotedMouseDuplicate();
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
// 跨代 ⇒ 打开"作废窗口"并记下 extra 指纹。
|
|
|
//
|
|
|
// ⚠️ 第三版:**移动也开窗**。真机残留是三件套 MOVE+DOWN+UP,
|
|
|
@@ -3502,15 +3786,16 @@ namespace DuiLib
|
|
|
// (表示"指针真的停在这")。它不是提升消息,绕不过悬浮总闸;
|
|
|
// 而此时 m_ptLastTouchPos 还停在弹框内"选好了"的位置(如 496,561),
|
|
|
// 与残留点(如 779,645)相距远超 8px ⇒ WM_MOUSEMOVE 里那条
|
|
|
- // ±8px 判据会**主动解除** m_bSuppressHoverAfterTouch,
|
|
|
+ // ±8px 判据会**主动解除** s_bSuppressHoverAfterTouch,
|
|
|
// 于是 FindControl(pt) 命中背后商品并建立 hover(HOVER-ENTER)。
|
|
|
// 这里先把抑制位重新立起来、并把参考点挪到残留点上,那条补投的
|
|
|
// MOVE 坐标与参考点几乎重合 ⇒ 落在 ±8px 内被 break,不建立悬浮态。
|
|
|
// 纯状态判据,不看时间。
|
|
|
- m_bSuppressHoverAfterTouch = true;
|
|
|
+ s_bSuppressHoverAfterTouch = true;
|
|
|
m_bMouseTracking = false;
|
|
|
- m_ptLastTouchPos.x = GET_X_LPARAM(lParam);
|
|
|
- m_ptLastTouchPos.y = GET_Y_LPARAM(lParam);
|
|
|
+ POINT ptResidue = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
|
|
|
+ m_ptLastTouchPos = ptResidue;
|
|
|
+ SetLastTouchScreenPoint(ptResidue);
|
|
|
// 抬手类到达 ⇒ 序列收尾 ⇒ 关窗(纯状态,不看时间)。
|
|
|
if (bUpLike)
|
|
|
ClearPromotedMouseGenTaint();
|
|
|
@@ -3530,10 +3815,11 @@ namespace DuiLib
|
|
|
ClearPromotedMouseGenTaint();
|
|
|
// ★【悬浮态残留】同上:新手势的残留点也要成为抑制参考点,
|
|
|
// 否则紧跟的 extra=0 补投 MOVE 会解除抑制并建立悬浮态。
|
|
|
- m_bSuppressHoverAfterTouch = true;
|
|
|
+ s_bSuppressHoverAfterTouch = true;
|
|
|
m_bMouseTracking = false;
|
|
|
- m_ptLastTouchPos.x = GET_X_LPARAM(lParam);
|
|
|
- m_ptLastTouchPos.y = GET_Y_LPARAM(lParam);
|
|
|
+ POINT ptNewGen = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
|
|
|
+ m_ptLastTouchPos = ptNewGen;
|
|
|
+ SetLastTouchScreenPoint(ptNewGen);
|
|
|
// 关窗后本条按正常流程继续处理(它属于新手势,应当放行)。
|
|
|
}
|
|
|
}
|
|
|
@@ -4321,7 +4607,7 @@ namespace DuiLib
|
|
|
break;
|
|
|
case WM_MOUSEHOVER:
|
|
|
{
|
|
|
- if (m_bSuppressHoverAfterTouch) return true;
|
|
|
+ if (s_bSuppressHoverAfterTouch) return true;
|
|
|
m_bMouseTracking = false;
|
|
|
POINT pt = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
|
|
|
CControlUI* pHover = FindControl(pt);
|
|
|
@@ -4373,7 +4659,7 @@ namespace DuiLib
|
|
|
if (m_hwndTooltip != NULL) ::SendMessage(m_hwndTooltip, TTM_TRACKACTIVATE, FALSE, (LPARAM)&m_ToolTip);
|
|
|
// 触屏抬起刚清过悬浮态时,不要执行下面这段“自回发 WM_MOUSEMOVE”:
|
|
|
// 光标通常仍在窗口内 → 会立刻重建悬浮态,导致“悬浮态不消失/多控件同时悬浮”。
|
|
|
- if (!m_bSuppressHoverAfterTouch && m_bMouseTracking)
|
|
|
+ if (!s_bSuppressHoverAfterTouch && m_bMouseTracking)
|
|
|
{
|
|
|
POINT pt = { 0 };
|
|
|
RECT rcWnd = { 0 };
|
|
|
@@ -4415,7 +4701,7 @@ namespace DuiLib
|
|
|
RelayDefWindowProc(uMsg, wParam, lParam, _T("promoted-touch-move/tracking"));
|
|
|
return true;
|
|
|
}
|
|
|
- if (m_bSuppressHoverAfterTouch)
|
|
|
+ if (s_bSuppressHoverAfterTouch)
|
|
|
{
|
|
|
// [第十一轮修正] 这里曾经无条件丢弃消息(return true)。
|
|
|
// 后果:弹框弹出时 AttachPaintWindow 会置起本抑制位(为了让新弹框
|
|
|
@@ -4445,15 +4731,42 @@ namespace DuiLib
|
|
|
break;
|
|
|
|
|
|
POINT ptCheck = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
|
|
|
- if (m_bSuppressHoverAfterTouch)
|
|
|
+ if (s_bSuppressHoverAfterTouch)
|
|
|
{
|
|
|
// 合成消息通常仍停在触摸结束点;只有明显离开该点的鼠标移动
|
|
|
// 才能重新建立鼠标悬浮语义。
|
|
|
- const int dx = ptCheck.x - m_ptLastTouchPos.x;
|
|
|
- const int dy = ptCheck.y - m_ptLastTouchPos.y;
|
|
|
+ //
|
|
|
+ // ⚠️⚠️ 这里有两个**必须同时成立**的前提,缺一个就会让"点『选好了』
|
|
|
+ // 关掉弹框后,背后商品卡保持悬浮态不消失"复现。真机日志两次实证:
|
|
|
+ //
|
|
|
+ // ① 参考点必须换算到**屏幕坐标**再比 —— lParam 是"**接收窗口**的
|
|
|
+ // 客户区坐标",而触摸可能发生在**另一个窗口**里。
|
|
|
+ // 用户点弹框里的「选好了」时,同一物理点在
|
|
|
+ // · 弹框的客户区里是 (496,547)
|
|
|
+ // · 主窗口的客户区里是 (778,630)
|
|
|
+ // 若按客户区比较,等于拿两个不同原点的数相减 ⇒ 判据必然失准。
|
|
|
+ //
|
|
|
+ // ② 抑制位与参考点必须是**线程级**的(见文件头 `s_bSuppressHoverAfterTouch`
|
|
|
+ // 那段说明)—— 因为属性弹框**自带独立的 CPaintManagerUI m_pm**,
|
|
|
+ // 与主窗口的 manager 不是同一个对象(真机日志:弹框 0A29F5F8 /
|
|
|
+ // 主窗口 07DA8C58)。那次触摸记在**弹框**那份状态上;而弹框一销毁,
|
|
|
+ // 系统把补投的 `extra=0` WM_MOUSEMOVE 发给**主窗口** ⇒ 落到主窗口的
|
|
|
+ // manager 上。若状态是成员,主窗口那份要么抑制位为假、要么参考点
|
|
|
+ // 还停在更早那次触摸(真机日志里是 1225,446,与本次点相距 447px)
|
|
|
+ // ⇒ 判据失败 ⇒ 解除抑制 + FindControl(pt) 命中背后商品卡 ⇒ 悬浮态
|
|
|
+ // 点亮且此后永不熄灭(`MODAL-DLG exit` → `HOVER-ENTER diandan_fooditem`)。
|
|
|
+ // 所以这两个状态已提升为文件级静态变量,与 manager 无关。
|
|
|
+ //
|
|
|
+ // 注意那条补投 MOVE 的 extra=0x00000000,它绕不过"提升移动不参与 hover"
|
|
|
+ // 的总闸,只能靠**提前把状态改好**来防(此处判据 + 各触摸路径的写入)。
|
|
|
+ POINT ptCheckScreen = ptCheck;
|
|
|
+ if (m_hWndPaint != NULL && ::IsWindow(m_hWndPaint))
|
|
|
+ ::ClientToScreen(m_hWndPaint, &ptCheckScreen);
|
|
|
+ const int dx = ptCheckScreen.x - s_ptLastTouchScreen.x;
|
|
|
+ const int dy = ptCheckScreen.y - s_ptLastTouchScreen.y;
|
|
|
if (dx <= 8 && dx >= -8 && dy <= 8 && dy >= -8)
|
|
|
break;
|
|
|
- m_bSuppressHoverAfterTouch = false;
|
|
|
+ s_bSuppressHoverAfterTouch = false;
|
|
|
}
|
|
|
|
|
|
// Start tracking this entire window again...
|
|
|
@@ -4754,7 +5067,7 @@ namespace DuiLib
|
|
|
// DoEvent(BUTTONUP) 复位的。
|
|
|
ClearPushedStateAt(NULL, ptButtonUp);
|
|
|
// (c) 抬起后禁止"悬浮态复活"(系统会补发合成的 WM_MOUSEMOVE)。
|
|
|
- m_bSuppressHoverAfterTouch = true;
|
|
|
+ s_bSuppressHoverAfterTouch = true;
|
|
|
m_bMouseTracking = false;
|
|
|
}
|
|
|
|