WPF 開啟Pointer訊息存在的坑

lindexi發表於2024-09-01

本文記錄在 WPF 開啟 Pointer 訊息的坑

螢幕鍵盤

啟用了Pointer之後,呼叫 TextBox.Focus() 方法時,有一定的可能起不來螢幕鍵盤,必須點在控制元件之上才行,觸控在它之上才行

後續的 Win10 版本似乎修復了這個問題,暫時還沒了解到具體是從哪個版本開始修復

使用螢幕絕對座標而不是視窗座標

預設 Pointer 訊息是使用螢幕絕對座標而不是視窗座標

可能存在獲取 Stylus 事件時觸控點不準,此時可以透過獲取 Touch 代替,詳細請看 WPF will have a touch offset after trun on the WM_Pointer message · Issue #3360 · dotnet/wpf 此問題應該在 Fix raw stylus data to support per-monitor DPI by rladuca · Pull Request #2891 · dotnet/wpf 修復

開啟 Pointer 訊息之後無法隱藏觸控反饋點

開啟 Pointer 訊息之後,呼叫 Stylus.IsPressAndHoldEnabled="False" 無效

在沒有開啟 Pointer 訊息,將會在 System.Windows.Interop.HwndSource 的 Initialize 方法透過判斷是否開啟 Pointer 訊息執行 HwndStylusInputProvider 邏輯

            if (StylusLogic.IsStylusAndTouchSupportEnabled)
            {
                // Choose between Wisp and Pointer stacks
                if (StylusLogic.IsPointerStackEnabled)
                {
                	// 開啟 Pointer 的邏輯
                    _stylus = new SecurityCriticalDataClass<IStylusInputProvider>(new HwndPointerInputProvider(this));
                }
                else
                {
                    _stylus = new SecurityCriticalDataClass<IStylusInputProvider>(new HwndStylusInputProvider(this));
                }
            }

在 HwndStylusInputProvider 將會讀取 IsPressAndHoldEnabledProperty 屬性,然後使用 WM_TABLET_QUERYSYSTEMGESTURESTATUS 返回 1 的方式告訴系統不顯示觸控反饋點。也就是 WPF 隱藏觸控反饋點是透過 How do I disable the press-and-hold gesture for my window 的方法

如果不設定 Stylus.IsPressAndHoldEnabled="False" 也可以自己手動監聽訊息,在訊息 WM_TABLET_QUERYSYSTEMGESTURESTATUS 裡面返回 1 就可以告訴系統不顯示觸控反饋點

       private IntPtr Hook(IntPtr hwnd, int msg, IntPtr wparam, IntPtr lparam, ref bool handled)
        {
            const int WM_TABLET_DEFBASE =0x02C0;
            const int WM_TABLET_QUERYSYSTEMGESTURESTATUS = WM_TABLET_DEFBASE + 12;
            const int WM_TABLET_FLICK = WM_TABLET_DEFBASE + 11;

            if (msg == WM_TABLET_QUERYSYSTEMGESTURESTATUS)
            {
                uint flags = 0;

                flags |= TABLET_PRESSANDHOLD_DISABLED;
                flags |= TABLET_TAPFEEDBACK_DISABLED;
                flags |= TABLET_TOUCHUI_FORCEON;
                flags |= TABLET_TOUCHUI_FORCEOFF;
                flags |= TABLET_FLICKS_DISABLED;

                handled = true;
                return new IntPtr(flags);
            }
            else if (msg == WM_TABLET_FLICK)
            {
                handled = true;
                return new IntPtr(1);
            }

            return IntPtr.Zero;
        }

        private const uint TABLET_PRESSANDHOLD_DISABLED = 0x00000001;
        private const uint TABLET_TAPFEEDBACK_DISABLED = 0x00000008;
        private const uint TABLET_TOUCHUI_FORCEON = 0x00000100;
        private const uint TABLET_TOUCHUI_FORCEOFF = 0x00000200;
        private const uint TABLET_FLICKS_DISABLED = 0x00010000;

但如果開啟了 Pointer 訊息,那麼這個機制將會無效,即使依然是手動監聽訊息,如 https://github.com/lindexi/lindexi_gd/tree/81b2a63a/KemjawyecawDurbahelal 的程式碼,也是無效的

問題報告給了 WPF 官方,請看 WPF can not work well with set IsPressAndHoldEnabled to false when enable pointer message · Issue #3379 · dotnet/wpf 但預計不會在 WPF 中修復,原因是這是 Windows 的 WM_Pointer 機制的坑,和 WPF 其實沒有關係

現在 WM_Pointer 開啟之後,可以透過 DwnShowContact 達成類似的功能,或者是透過 SetWindowFeedbackSetting 禁用整個視窗的觸控反饋效果

另一個解決方法是在關閉系統全域性觸控反饋點,關閉方法請看 3 Ways to Enable or Disable Touch Feedback in Windows 10

不存在互斥觸控互動

其實這個也算是一個特性,但是行為有變更。在 Win10 提出的一個新互動裡面,允許未啟用的視窗接收到滑鼠滾輪訊息。這一套是和 Pointer 一起提出的,我問了微軟的大佬,收到了 MVP 內部郵件,可惜我沒看明白,大概的意思是這個互動是 Win10 提供的,和 Pointer 走的是差不多的邏輯

這也就導致了原本支援互斥獨佔的觸控互動,在開啟 Pointer 的應用下被無效。表現是如當前觸控被某個獲取焦點的視窗捕獲,此時觸控點到一個後臺的視窗,未啟用的視窗上,那此視窗依然可以收到觸控訊息,無論這個視窗是在哪個程序上,只需要此視窗所在的程序開啟 Pointer 訊息即可

而原先的互動是如果觸控被某個前臺視窗捕獲,那麼其他視窗將啥都收不到,包括 WM_Touch 訊息或者實時觸控訊息

滑動過程開啟視窗觸控失效

在進行 Manipulation 過程中,開啟或者啟用了視窗,將導致此視窗不接受觸控訊息而觸控失效。例如另一個程序的文字框獲取焦點時,在滑動 ListView 列表時,開啟了視窗或者啟用現有的視窗到前臺獲取焦點,在此視窗內進行觸控,可能會收不到觸控事件

原因是在進行 Manipulation 將會設定一些特殊的內部欄位引數,原本不走 Pointer 時,將會自然走到 MouseDevice.cs 的邏輯,觸發了 Activate 邏輯,讓 WPF 框架層處理視窗啟用互動邏輯。但是在 Pointer 層時,走的是 PointerLogic.cs 的邏輯,沒有啟用互動的邏輯。修復方法是在 PointerLogic.cs 的邏輯也呼叫 MouseDevice.cs 的 PushActivateInputReport 方法啟用互動

此問題已修復,參閱 Port touch activation fix from 4.8 by SamBent · Pull Request #5836 · dotnet/wpf

以上是在 .NET Core 版本的修復,對應的 .NET Framework 在 2022 的一月系統質量更新補丁,如 50088XX 系列補丁,參閱 https://support.microsoft.com/kb/5008890

.NET Framework January 2022 Security and Quality Rollup Updates - .NET Blog

觸控偏移

WPF 已知問題 開啟 WM_Pointer 訊息之後 獲取副屏觸控資料座標偏移

開啟 RegisterTouchWindow 不用禁用實時觸控依然可以收到訊息

在沒有開啟 WM_Pointer 訊息之前,需要先禁用實時觸控才能收到 WM_Touch 訊息,詳細請看 WPF 禁用實時觸控

然而在開啟 POINTER 訊息之後,只要呼叫 RegisterTouchWindow 方法,即可讓視窗收到 WM_TOUCH 訊息。如以下測試程式碼所示

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        SourceInitialized += OnSourceInitialized;
    }

    private void OnSourceInitialized(object? sender, EventArgs e)
    {
        var windowInteropHelper = new WindowInteropHelper(this);
        var hwnd = windowInteropHelper.Handle;

        PInvoke.RegisterTouchWindow(new HWND(hwnd), 0);

        HwndSource source = HwndSource.FromHwnd(hwnd)!;
        source.AddHook(Hook);
    }

    private unsafe IntPtr Hook(IntPtr hwnd, int msg, IntPtr wparam, IntPtr lparam, ref bool handled)
    {
        if ((uint)msg is PInvoke.WM_TOUCH)
        {
            var touchInputCount = wparam.ToInt32();

            var pTouchInputs = stackalloc TOUCHINPUT[touchInputCount];
            if (PInvoke.GetTouchInputInfo(new HTOUCHINPUT(lparam), (uint)touchInputCount, pTouchInputs,
                    sizeof(TOUCHINPUT)))
            {
                for (var i = 0; i < touchInputCount; i++)
                {
                    var touchInput = pTouchInputs[i];
                    var point = new System.Drawing.Point(touchInput.x / 100, touchInput.y / 100);
                    PInvoke.ScreenToClient(new HWND(hwnd), ref point);

                    Debug.WriteLine($"Touch {touchInput.dwID} XY={point.X}, {point.Y}");
                }

                PInvoke.CloseTouchInputHandle(new HTOUCHINPUT(lparam));
            }
        }

        return IntPtr.Zero;
    }
}

只需要按照 WPF dotnet core 如何開啟 Pointer 訊息的支援 提供的方法,在 App 建構函式里面使用如下程式碼進行開啟 WM_POINTER 訊息,和對比不開啟的除錯輸出,即可看到除錯下的輸出差別

    public App()
    {
        AppContext.SetSwitch("Switch.System.Windows.Input.Stylus.EnablePointerSupport", true);
    }

以上程式碼放在 githubgitee 上,可以使用如下命令列拉取程式碼。我整個程式碼倉庫比較龐大,使用以下命令列可以進行部分拉取,拉取速度比較快

先建立一個空資料夾,接著使用命令列 cd 命令進入此空資料夾,在命令列裡面輸入以下程式碼,即可獲取到本文的程式碼

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin 6354ef906a85be0d2cdcb6d545c094b098c34544

以上使用的是國內的 gitee 的源,如果 gitee 不能訪問,請替換為 github 的源。請在命令列繼續輸入以下程式碼,將 gitee 源換成 github 源進行拉取程式碼。如果依然拉取不到程式碼,可以發郵件向我要程式碼

git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin 6354ef906a85be0d2cdcb6d545c094b098c34544

獲取程式碼之後,進入 WPFDemo/GibibealaFoheyufairha 資料夾,即可獲取到原始碼

更多閱讀

WPF 如何確定應用程式開啟了 Pointer 觸控訊息的支援

win10 支援預設把觸控提升 Pointer 訊息

WPF dotnet core 如何開啟 Pointer 訊息的支援

WPF can not work well with set IsPressAndHoldEnabled to false when enable pointer message · Issue #3379 · dotnet/wpf

Stylus.SetIsPressAndHoldEnabled does not work when Switch.System.Windows.Input.Stylus.EnablePointerSupport is enabled · Issue #5939 · dotnet/wpf

How do I disable the press-and-hold gesture for my window

WM_TABLET_QUERYSYSTEMGESTURESTATUS message (Tpcshrd.h)

WM_TABLET_FLICK message (Tpcshrd.h)

c# - Calling SetGestureConfig method affects onmousemove override of control - Stack Overflow

SetGestureConfig 函式-中文整理_Augusdi的專欄-CSDN部落格

相關文章