本文记录在 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博客


本文会经常更新,请阅读原文: https://blog.lindexi.com/post/WPF-%E5%BC%80%E5%90%AFPointer%E6%B6%88%E6%81%AF%E5%AD%98%E5%9C%A8%E7%9A%84%E5%9D%91.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。

如果你想持续阅读我的最新博客,请点击 RSS 订阅,推荐使用RSS Stalker订阅博客,或者收藏我的博客导航

知识共享许可协议 本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名林德熙(包含链接: https://blog.lindexi.com ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系

微软最具价值专家


无盈利,不卖课,做纯粹的技术博客

以下是广告时间

推荐关注 Edi.Wang 的公众号

欢迎进入 Eleven 老师组建的 .NET 社区

以上广告全是友情推广,无盈利