张洋 3 weeks ago
parent
commit
2374833d0c

+ 145 - 5
DuiLib/Core/UIBase.cpp

@@ -1,4 +1,5 @@
 #include "StdAfx.h"
+#include "UIManager.h"
 
 #ifdef _DEBUG
 #include <shlwapi.h>
@@ -7,9 +8,34 @@
 
 namespace DuiLib {
 
+// WM_TOUCH 消息 ID 在 winuser.h 里受 `#if _WIN32_WINNT >= 0x0601` 保护,
+// 而 DuiLib 工程的 _WIN32_WINNT 兜底为 XP(0x0501),直接引用会编译失败,这里自行声明。
+#ifndef WM_TOUCH
+#define WM_TOUCH 0x0240
+#endif
+// WM_POINTER* 系列(Win8+),同样不在 XP 段里。仅用于模态期间屏蔽 owner 的输入。
+#define DUI_WM_POINTER_FIRST 0x0246
+#define DUI_WM_POINTER_LAST  0x0259
+
 namespace {
     typedef BOOL (WINAPI* PFN_REGISTER_TOUCH_WINDOW)(HWND, ULONG);
 
+    // 与 SDK tagTOUCHINPUT 布局一致的私有副本(同样受 _WIN32_WINNT 保护,不能直接用)
+    struct TRACE_TOUCHINPUT
+    {
+        LONG      x;
+        LONG      y;
+        HANDLE    hSource;
+        DWORD     dwID;
+        DWORD     dwFlags;
+        DWORD     dwMask;
+        DWORD     dwTime;
+        ULONG_PTR dwExtraInfo;
+        DWORD     cxContact;
+        DWORD     cyContact;
+    };
+    typedef BOOL (WINAPI* PFN_GET_TOUCH_INPUT_INFO)(HANDLE, UINT, TRACE_TOUCHINPUT*, int);
+
     void RegisterTouchWindowIfAvailable(HWND hWnd)
     {
         if (hWnd == NULL) return;
@@ -20,6 +46,47 @@ namespace {
         if (pfnRegister != NULL)
             pfnRegister(hWnd, 0);
     }
+
+    // 诊断:WM_TOUCH 究竟落在哪个窗口上(弹框自己?还是被禁用/被挡住的 owner?)。
+    // ⚠️ 只 peek,绝不调用 CloseTouchInputHandle:句柄必须交给 CPaintManagerUI::OnTouchInput 关闭。
+    void TraceTouchArrival(HWND hWnd, WPARAM wParam, LPARAM lParam)
+    {
+        HMODULE hUser32 = ::GetModuleHandle(_T("user32.dll"));
+        if (hUser32 == NULL) return;
+        PFN_GET_TOUCH_INPUT_INFO pfnGet =
+            reinterpret_cast<PFN_GET_TOUCH_INPUT_INFO>(::GetProcAddress(hUser32, "GetTouchInputInfo"));
+        int x = 0, y = 0;
+        DWORD flags = 0;
+        UINT cInputs = LOWORD(wParam);
+        TRACE_TOUCHINPUT ti;
+        ::ZeroMemory(&ti, sizeof(ti));
+        if (pfnGet != NULL && cInputs > 0 && pfnGet((HANDLE)lParam, 1, &ti, (int)sizeof(ti)))
+        {
+            x = (int)(ti.x / 100);
+            y = (int)(ti.y / 100);
+            flags = ti.dwFlags;
+        }
+        DuiLib::TouchTrace::Log(_T("WNDPROC-TOUCH hwnd=%p owner=%p parent=%p enabled=%d visible=%d pt=%d,%d flags=0x%X inputs=%u modal=%p fg=%p"),
+            (void*)hWnd, (void*)::GetWindow(hWnd, GW_OWNER), (void*)::GetParent(hWnd),
+            (int)::IsWindowEnabled(hWnd), (int)::IsWindowVisible(hWnd),
+            x, y, (unsigned)flags, cInputs,
+            (void*)DuiLib::CPaintManagerUI::GetModalWindow(), (void*)::GetForegroundWindow());
+    }
+
+    // 上一次"抢回前台"的时间戳(限流用,避免频繁改激活状态打断输入)
+    DWORD s_dwLastModalFocusFix = 0;
+
+    // 模态期间需要屏蔽的输入类消息(发给被弹框挡住的 owner 时直接丢弃,不再派发)
+    bool IsModalBlockedInputMessage(UINT uMsg)
+    {
+        if (uMsg == WM_TOUCH) return true;                                  // 0x0240
+        if (uMsg >= DUI_WM_POINTER_FIRST && uMsg <= DUI_WM_POINTER_LAST) return true;
+        if (uMsg >= 0x0200 && uMsg <= 0x020E) return true;                  // WM_MOUSEMOVE .. WM_MOUSELAST
+        if (uMsg >= 0x00A0 && uMsg <= 0x00AD) return true;                  // WM_NCMOUSEMOVE .. WM_NCXBUTTONDBLCLK
+        if (uMsg >= WM_KEYFIRST && uMsg <= WM_KEYLAST) return true;         // 键盘
+        if (uMsg == WM_MOUSEACTIVATE) return true;
+        return false;
+    }
 }
 
 /////////////////////////////////////////////////////////////////////////////////////
@@ -326,24 +393,94 @@ UINT CWindowWnd::ShowModal()
     ASSERT(::IsWindow(m_hWnd));
     UINT nRet = 0;
     HWND hWndParent = GetWindowOwner(m_hWnd);
+    const bool bBlockParent = (hWndParent != NULL && ::IsWindow(hWndParent));
+
     RegisterTouchWindowIfAvailable(m_hWnd);
+
+    // -------------------------------------------------------------------------
+    // 【重要】这里不再调用 ::EnableWindow(hWndParent, FALSE)。
+    //
+    // 原因:Windows 8/10 把触摸输入改成了 Pointer 栈,触摸消息(WM_TOUCH / WM_POINTER*)
+    // 的投递目标与"窗口是否被禁用 / 是否是活动窗口"强相关。实测在 Win10 上,只要 owner
+    // 被 EnableWindow(FALSE) 禁用,手势就会被系统转投给被禁用的 owner 而不再到达弹框,
+    // 于是弹框既收不到 WM_TOUCH(滚动条无法触摸滑动)、也没有可靠的提升鼠标消息可用
+    // (点击无反应);而在 Win7 上鼠标/触摸提升路径不受此影响,所以旧写法一直正常。
+    //
+    // 模态性改用两条等价手段保证:
+    //   ① 弹框置顶 + 激活,抢到前台;
+    //   ② 消息循环里丢弃所有发给 owner(及其子窗口)的输入类消息(见 IsModalBlockedInputMessage)。
+    //      —— owner 保持 enabled,它的鼠标/键盘/触摸都不会被派发,等价于"点不动";
+    //   ③ CPaintManagerUI 层再加一道:有模态弹框时,非弹框窗口收到 WM_TOUCH 直接忽略。
+    // -------------------------------------------------------------------------
     ::ShowWindow(m_hWnd, SW_SHOWNORMAL);
-    ::EnableWindow(hWndParent, FALSE);
+    ::SetWindowPos(m_hWnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
+    ::BringWindowToTop(m_hWnd);
+    ::SetForegroundWindow(m_hWnd);
+    ::SetActiveWindow(m_hWnd);
+    ::SetFocus(m_hWnd);
+    // 显示之后再注册一次(幂等,重复调用无害):确保最终窗口样式下的触摸注册生效。
+    RegisterTouchWindowIfAvailable(m_hWnd);
+
+    // 支持弹框嵌套:保存外层模态窗口,退出时恢复
+    HWND hPrevModal = CPaintManagerUI::GetModalWindow();
+    CPaintManagerUI::SetModalWindow(m_hWnd);
+    DuiLib::TouchTrace::Log(_T("MODAL-ENTER hwnd=%p owner=%p foreground=%p active=%p focus=%p enabled=%d style=0x%08X ex=0x%08X"),
+        (void*)m_hWnd, (void*)hWndParent, (void*)::GetForegroundWindow(), (void*)::GetActiveWindow(),
+        (void*)::GetFocus(), (int)::IsWindowEnabled(m_hWnd),
+        (unsigned)::GetWindowLong(m_hWnd, GWL_STYLE), (unsigned)::GetWindowLong(m_hWnd, GWL_EXSTYLE));
+
     MSG msg = { 0 };
     while( ::IsWindow(m_hWnd) && ::GetMessage(&msg, NULL, 0, 0) ) {
+        if( msg.message == WM_QUIT ) break;
+
+        // 模态期间:owner 及其子窗口的任何输入消息都不派发(等价于原来的禁用效果)
+        if( bBlockParent && msg.hwnd != NULL
+            && (msg.hwnd == hWndParent || ::IsChild(hWndParent, msg.hwnd))
+            && IsModalBlockedInputMessage(msg.message) )
+        {
+            continue;
+        }
+
         if( msg.message == WM_CLOSE && msg.hwnd == m_hWnd ) {
             nRet = (int)msg.wParam;
-            ::EnableWindow(hWndParent, TRUE);
             ::SetFocus(hWndParent);
         }
+
+        // Win10 前台锁比 Win7 严格:弹框偶尔会丢掉前台/激活状态,之后触摸就会被路由到别处。
+        // 这里做限流补偿(≥800ms 一次,且【绝不在手势消息期间】执行,避免打断输入路由)。
+        if( !IsModalBlockedInputMessage(msg.message) )
+        {
+            const DWORD dwNow = ::GetTickCount();
+            if( (LONG)(dwNow - s_dwLastModalFocusFix) >= 800
+                && (::GetActiveWindow() != m_hWnd || ::GetForegroundWindow() != m_hWnd) )
+            {
+                s_dwLastModalFocusFix = dwNow;
+                DuiLib::TouchTrace::Log(_T("MODAL-REACTIVATE hwnd=%p active=%p foreground=%p"),
+                    (void*)m_hWnd, (void*)::GetActiveWindow(), (void*)::GetForegroundWindow());
+                ::BringWindowToTop(m_hWnd);
+                ::SetForegroundWindow(m_hWnd);
+                ::SetActiveWindow(m_hWnd);
+                ::SetFocus(m_hWnd);
+            }
+        }
+
         if( !CPaintManagerUI::TranslateMessage(&msg) ) {
             ::TranslateMessage(&msg);
             ::DispatchMessage(&msg);
         }
-        if( msg.message == WM_QUIT ) break;
     }
-    ::EnableWindow(hWndParent, TRUE);
-    ::SetFocus(hWndParent);
+
+    CPaintManagerUI::SetModalWindow(hPrevModal);
+    if( ::IsWindow(m_hWnd) )
+        ::SetWindowPos(m_hWnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
+    if( hWndParent != NULL ) {
+        if( ::IsWindow(hWndParent) ) {
+            ::EnableWindow(hWndParent, TRUE);
+            ::SetForegroundWindow(hWndParent);
+            ::SetFocus(hWndParent);
+        }
+    }
+    DuiLib::TouchTrace::Log(_T("MODAL-EXIT hwnd=%p owner=%p ret=%u"), (void*)m_hWnd, (void*)hWndParent, nRet);
     if( msg.message == WM_QUIT ) ::PostQuitMessage((int)msg.wParam);
     return nRet;
 }
@@ -522,6 +659,9 @@ LRESULT CALLBACK CWindowWnd::__WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPAR
             return lRes;
         }
     }
+    // 诊断:确认 WM_TOUCH 到底投递给了哪个窗口(弹框 / owner / 其它)。
+    if( uMsg == WM_TOUCH ) TraceTouchArrival(hWnd, wParam, lParam);
+
     if( pThis != NULL ) {
         return pThis->HandleMessage(uMsg, wParam, lParam);
     } 

+ 43 - 1
DuiLib/Core/UIManager.cpp

@@ -729,6 +729,26 @@ namespace DuiLib
 		}
 	}
 
+	// ---------------------------------------------------------------------------
+	// 当前模态弹框句柄(由 CWindowWnd::ShowModal 进入/退出时设置)。
+	// Win8/10 指针栈下,触摸消息(WM_TOUCH / WM_POINTER)并不总是投递给"视觉上在最前面的弹框":
+	// 当弹框的 owner 被 EnableWindow(FALSE) 禁用时,部分系统会把手势转投给 owner。
+	// 这里做一道保险:只要当前有模态弹框,任何【非该弹框】的窗口收到 WM_TOUCH 一律忽略,
+	// 避免"弹框还开着,手指却把主窗口点动了"的穿透。
+	// ---------------------------------------------------------------------------
+	static HWND s_hModalWindow = NULL;
+
+	void CPaintManagerUI::SetModalWindow(HWND hWnd)
+	{
+		s_hModalWindow = hWnd;
+		TouchTrace::Log(_T("MODAL-WINDOW set hwnd=%p"), (void*)hWnd);
+	}
+
+	HWND CPaintManagerUI::GetModalWindow()
+	{
+		return s_hModalWindow;
+	}
+
 	void CPaintManagerUI::InitTouchSupport()
 	{
 		if (m_hWndPaint == NULL || !::IsWindow(m_hWndPaint)) return;
@@ -760,6 +780,20 @@ namespace DuiLib
 		//      挡在悬浮 enter/leave 之外(触摸没有“光标停留”语义)——这是“点过的控件一直高亮 /
 		//      多个控件同时高亮”的根治手段;此处再叠加 m_bSuppressHoverAfterTouch 抑制抬起后的复活。
 		TouchApi::PFN_GetTouchInputInfo pfnGetInfo = TouchApi::GetGetTouchInputInfo();
+
+		// 有模态弹框、但本次 WM_TOUCH 却投到了【别的】窗口(典型:投给了被弹框挡住的 owner,
+		// 尤其是 owner 曾被 EnableWindow(FALSE) 禁用过的情况):
+		// 直接同步转发给真正的模态弹框,让弹框照常完成点击 / 滑动。
+		// 注:MSDN 明确 WM_TOUCH 的 lParam 句柄在经 SendMessage 转发后即失效,
+		//     所以转发后本窗口【不能】再 CloseTouchInputHandle,也不能再解析,直接返回。
+		if (s_hModalWindow != NULL && ::IsWindow(s_hModalWindow) && s_hModalWindow != m_hWndPaint)
+		{
+			TouchTrace::Log(_T("WM_TOUCH forward to modal from=%p modal=%p inputs=%u"),
+				(void*)m_hWndPaint, (void*)s_hModalWindow, (UINT)LOWORD(wParam));
+			::SendMessage(s_hModalWindow, WM_TOUCH, wParam, lParam);
+			return;
+		}
+
 		if (pfnGetInfo == NULL) return;
 
 		UINT cInputs = LOWORD(wParam);
@@ -997,11 +1031,13 @@ namespace DuiLib
 					m_aNeedMouseLeaveNeeded.GetSize(), (void*)m_pEventClick, (int)m_bMouseCapture);
 				m_pEventTouch->Event(event);
 
+				bool bClickSynthesized = false;
 				if (m_pEventClick != NULL && !m_bTouchMoved
 					&& m_pEventClick->GetInterface(DUI_CTR_SCROLLBAR) == NULL)
 				{
 					CControlUI* pClick = m_pEventClick;
 					m_pEventClick = NULL;
+					bClickSynthesized = true;
 					TEventUI clickEvent = { 0 };
 					clickEvent.Type = UIEVENT_BUTTONDOWN;
 					clickEvent.pSender = pClick;
@@ -1021,7 +1057,13 @@ namespace DuiLib
 
 				m_pEventTouch = NULL;
 				m_pTouchScrollContainer = NULL;
-				m_bSuppressPromotedTouchMouse = true;
+				// 只有在本次 TOUCHUP 真的合成了点击时才屏蔽随后提升的鼠标消息(避免重复触发)。
+				// 若本次【没有】合成(拖动滚动 / 未命中控件 / 命中滚动条),必须放行提升的鼠标消息:
+				// 这是 Win10 上弹框还能"点得动"的兜底;WM_LBUTTONUP 原生分支已有 bTouchDragged
+				// 保护(ptMouse 置 (-1,-1)),拖拽结束不会误激活控件,所以放行是安全的。
+				m_bSuppressPromotedTouchMouse = bClickSynthesized;
+				TouchTrace::Log(_T("TOUCHUP end synthesized=%d suppress=%d"),
+					(int)bClickSynthesized, (int)m_bSuppressPromotedTouchMouse);
 			}
 		}
 

+ 5 - 0
DuiLib/Core/UIManager.h

@@ -484,6 +484,11 @@ namespace DuiLib {
 		void InitTouchSupport();
 		void OnTouchInput(WPARAM wParam, LPARAM lParam);
 		void ClearTouchHoverState(POINT pt);
+		// 当前处于模态状态的弹框句柄(由 CWindowWnd::ShowModal 设置/清除)。
+		// 目的是:万一 Windows 把 WM_TOUCH 错投给了被弹框挡住的 owner(主窗口),
+		// 非模态窗口一律忽略触摸,避免“弹框还开着、主窗口却在响应手指”的穿透。
+		static void SetModalWindow(HWND hWnd);
+		static HWND GetModalWindow();
 
 	private:
 		CStdPtrArray* GetFoundControls();

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
pdb/release/DuiLib.pdb