本文记录在 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 stacksif (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);}
以上代码放在 github 和 gitee 上,可以使用如下命令行拉取代码。我整个代码仓库比较庞大,使用以下命令行可以进行部分拉取,拉取速度比较快
先创建一个空文件夹,接着使用命令行 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博客