WPF 触屏事件后触发鼠标事件的问题及 DataGrid 误触问题
一、触屏事件连带触发鼠标事件的问题
这个是 WPF 已知的问题,网络上也有一些讨论,但是没有一个完美的方法来解决。本文也就是讲解其中的一种方法,亲测可行。
先来说说具体现象:触屏操作时,如果程序里使用了触屏事件(如:PreviewTouchDown、TouchDown、PreviewTouchUp、TouchUp),那么相应地会接着触发鼠标事件(PreviewMouseDown、MouseDown、PreviewMouseUp、MouseUp),这个据说是微软为了在触屏设备上兼容老程序,让这些程序能够接收从触屏事件转换来的鼠标事件,从而能正常工作。
所以,有一个说法是,只使用鼠标事件就行了,比如就单单使用 PreviewMouseDown 事件,或者按钮的话直接使用 Click 事件,或者使用命令(Command),这种方法理论上是可以的,但是实际情况下,有的时候会发现,这样用的话,触屏操作很不灵敏,可能要点好几次才触发。
这个触屏事件提升为鼠标事件的一个表现就是,触屏拖动或者点击,会在屏幕上 “残留” 鼠标,当然,是不可见的,或者表现为一个小星号。所以,从这个角度出发,产生了这样一种方法:点击后将鼠标移开。
这个方法能满足部分场景,比如之前有这样一个问题,在 DataGrid 表格上方有一个 DatePicker 日期选择控件,日期展开后,下拉的悬浮框会遮在表格上,当在下拉的悬浮框中选择日期后下拉框收起,这时却在表格上产生了某个条目的选中效果。针对于这个情况,就可以使用移开鼠标的方案,相关帮助类见下方链接:
https://gitee.com/dlgcy/WPFTemplateLib/blob/master/WpfHelpers/ClickAndTouchHelper2.cs
但是这次我遇到了一个 DataGrid 的误触问题,用移开鼠标的方法无效(也有可能是使用方法和时机不对),所以只能另寻它法。
注意,本文将在上篇文章《WPF DataGrid 通过自定义表头模拟首行固定》的示例程序基础上进行演示,建议先看看那篇文章。下面开始改造。
首先在行样式中添加了两个事件,一个是 PreviewTouchDown,另一个是 PreviewMouseDown:
触屏点击某一行,会先触发 PreviewTouchDown,然后触发 PreviewMouseDown,然后是行改变事件 SelectionChanged,最后依次是 PreviewTouchUp 和 PreviewMouseUp。带有 Preview 前缀的是隧道事件(可视为在事件前触发),没有的是冒泡事件(可视为在事件后触发,此处省略)。
那么如何去除触屏事件后连带引发鼠标事件的影响呢?通过在网络上苦苦搜索和尝试,在旧版的微软社区找到了一个可行的方法,帖子为《Prevent a WPF application to interpret touch events as mouse events?》(这个链接之后可能会访问不了)。
提问者就是为了解决触屏操作下触发鼠标事件的问题:
然后里面两个人分别给出了他们的解决方法,先来看看第一个:
这个就是本文采纳的方法,代码文字版如下:
public static class PreventTouchToMousePromotion { public static void Register(FrameworkElement root) { root.PreviewMouseDown += Evaluate; root.PreviewMouseMove += Evaluate; root.PreviewMouseUp += Evaluate; } private static void Evaluate(object sender, MouseEventArgs e) { //StylusDevice属性,触屏操作连带触发时不为null,鼠标触发时为null; if (e.StylusDevice != null) { e.Handled = true; //如果判断为 由触屏引发,则将事件标记为已处理; } } }
再顺便看看第二个人的方法(没有去尝试,感兴趣的朋友可以试试):
二、DataGrid 误触问题及解决方法
上一个部分介绍了去除触屏事件后连带引发鼠标事件影响的方法,也就是通过鼠标事件参数的 StylusDevice 属性来判断是否是由触屏操作引发的(不为 null 则是触屏操作引发),进而进行处理。
然而,本次我实际上是要解决一个 DataGrid 表格在触屏下的误触问题,相关业务逻辑是在行改变事件(转为命令了)中的,本来是没有写 PreviewTouchDown 和 PreviewMouseDown 事件的(就是为了解决误触问题而引入),所以将鼠标事件标记为已处理(e.Handled = true;)的方法不能直接使用,还需要修改。原因是,行改变事件 SelectionChanged 是在 PreviewMouseDown 事件之后触发的,如果在 PreviewMouseDown 中将事件标记为已处理,那么行改变事件也就不会触发了。
首先来看看误触现象吧(动图):
也就是,我在行改变事件中加了个弹窗,询问用户是否要切换条目,如果选是的话,不作任何处理,如果选否的话,恢复之前的选中项。选是的时候不会有误触现象,选否的时候,鼠标操作的话也正常,而如果在弹窗时通过触屏点击了否,然后在界面空白处(这里是在右侧的信息区)触屏点击几下,就会在表格上,在之前点击要切换到的那一行上产生一个鼠标事件,而且没有触屏事件,这个不用怀疑,通过调试打断点很容易观察到。
关于点击几下会触发这个误触,我发现和屏幕支持几点触控有关。比如,公司的触摸屏支持 10 点触控,那么这里就是点击 10 下左右触发(2021年10月11日09:29:05更新:经测试,是点击9下触发);我自己的一个小触摸屏,支持 5 点触控,这边则是在空白处点击 4 下触发。要查看屏幕支持几点触屏,可通过 GitHub 上的一个项目程序 ManipulationDemo 来查看(https://github.com/dotnet-campus/ManipulationDemo):
言归正传,从误触现象的动图中可以看到,已经能够判断出是否是误触了:
那么是怎么判断的呢?来看看代码:
private void EventSetter_PreviewTouchDown(object sender, TouchEventArgs e) { //真实触摸时会触发 PreviewTouchDown 事件,而误触时(点击弹窗取消后在空白处点击多次会误触表格)则不会(因为那个只触发鼠标事件); _vm.IsRealTouch = true; } /* 注意:触摸事件之后还会触发鼠标事件 */ private void EventSetter_PreviewMouseDown(object sender, MouseButtonEventArgs e) { //StylusDevice属性,触屏操作连带触发时不为null,鼠标触发时为null if (e.StylusDevice != null) {//触屏 //e.Handled = true; } else {//鼠标 _vm.IsRealTouch = true; //避免后续判断不正常; } }
在 ViewModel 中新增了一个标记变量 IsRealTouch,用来记录是真实的触控或者鼠标点击意图,还是误触。真实触摸时会触发 PreviewTouchDown 事件,而误触时(点击弹窗取消后在空白处点击多次会误触表格)则不会(因为那个只触发鼠标事件),所以只要在鼠标事件 PreviewMouseDown 中能够判断出是否是触屏操作连带触发的就行了,而这个问题在前一部分已经解决了。所以,在触摸事件,以及鼠标事件的单纯鼠标触发的情况下,都对 IsRealTouch 赋值为 true 即可。
行改变事件(命令)中还需要给 IsRealTouch 复位,代码如下:
SelectionChangedCmd ??= new RelayCommand(o => IsCanSelectionChanged, o => { try { IsCanSelectionChanged = false; var args = o as SelectionChangedEventArgs; EditType = EditTypeEnum.Show; var isOk = MessageBox.Show($"是否切换?(是否是误触?{!IsRealTouch})", "触屏误触问题演示", MessageBoxButton.YesNo); if (isOk == MessageBoxResult.No) { if (SelectedUser != _originUser) { SelectedUser = _originUser; } } } catch (Exception ex) { Console.WriteLine(ex); } finally { IsRealTouch = false; _originUser = SelectedUser; IsCanSelectionChanged = true; } });
可以看到,这样就能识别出是否是误触了。这里是演示,在实际使用时,识别到是误触,就可以直接返回而不用弹窗了。
问题解决了,那么原因呢?对于触屏操作产生鼠标事件,这个是微软为了兼容性而导致的,前面也说过了。至于为什么会有个触点残留在原来的位置,而且点击其它地方一定次数就会触发,这个问题我也没找到原因,请知道的朋友不吝赐教。有两个猜测,一是模态弹窗对事件有影响,一是命令对事件有影响,目前没想到怎么验证。
另外,之前说过弹窗点击是的情况下,后续没有误触现象,所以也有理由怀疑是从代码中改变了选中项(已绑定到 DataGrid 的选中项)所以会有这个问题。从代码中改变选中项又会触发行改变事件,所以加了个 IsCanSelectionChanged 来避免重入,当然,加不加这个避免重入的,都有误触现象。有点晕。
最后奉上源码地址:https://gitee.com/dlgcy/DLGCY_WPFPractice/tree/Blog20211010 ,大家可以帮忙研究研究。
三、一些更新说明
3.1 关闭实时触摸(2021.10.10)
忘了说了,这个误触还有一个简单的解决方法,就是关闭 WPF 的实时触摸机制,在 App.config 中配置即可:
<configuration> <runtime> <AppContextSwitchOverrides value="Switch.System.Windows.Input.Stylus.DisableStylusAndTouchSupport=true" /> </runtime> </configuration>
关闭实时触摸后,多点触控和某些拖动操作就不支持了,所以当时在公司项目中不能用这个方法,如果没有这方面的需求,那么这个方法是比较方便快捷的。
3.2 MVVM 方式的大致说明(2022.07.19)
界面上使用事件转命令,主要是 PreviewTouchDown 和 PreviewMouseDown,另外的 MouseDown 可视为业务方法:
<b:Interaction.Triggers> <b:EventTrigger EventName="PreviewTouchDown"> <b:InvokeCommandAction Command="{Binding PreviewTouchDownCmd}" PassEventArgsToCommand="True"/> </b:EventTrigger> <b:EventTrigger EventName="PreviewMouseDown"> <b:InvokeCommandAction Command="{Binding PreviewMouseDownCmd}" PassEventArgsToCommand="True"/> </b:EventTrigger> <b:EventTrigger EventName="MouseDown"> <b:InvokeCommandAction Command="{Binding MouseDownCmd}" PassEventArgsToCommand="True"/> </b:EventTrigger> </b:Interaction.Triggers>
然后 VIewModel 的基类中添加如下代码(基本和之前的代码一样):
#region 误触问题 /// <summary> /// 是否是真实触控(用于解决误触问题) /// </summary> public bool IsRealTouch { get; set; } #region 触屏按下隧道事件转命令 private DelegateCommand<TouchEventArgs> _PreviewTouchDownCmd; /// <summary> /// 触屏按下隧道事件转命令 /// </summary> public DelegateCommand<TouchEventArgs> PreviewTouchDownCmd => _PreviewTouchDownCmd ?? (_PreviewTouchDownCmd = new DelegateCommand<TouchEventArgs>(e => { // 真实触摸时会触发 PreviewTouchDown 事件,而误触时(点击弹窗取消后在空白处点击多次会误触表格)则不会(因为那个只触发鼠标事件); IsRealTouch = true; })); #endregion /* 注意:触摸事件之后还会触发鼠标事件 */ #region 鼠标按下隧道事件转命令 private DelegateCommand<MouseButtonEventArgs> _PreviewMouseDownCmd; /// <summary> /// 鼠标按下隧道事件转命令 /// </summary> public DelegateCommand<MouseButtonEventArgs> PreviewMouseDownCmd => _PreviewMouseDownCmd ?? (_PreviewMouseDownCmd = new DelegateCommand<MouseButtonEventArgs>(e => { //StylusDevice 属性,触屏操作连带触发时不为 null,鼠标触发时为 null if (e.StylusDevice != null) {// 触屏 //e.Handled = true; } else {// 鼠标 IsRealTouch = true; // 避免后续判断不正常; } })); #endregion #region 鼠标按下事件转命令 private DelegateCommand<MouseEventArgs> _MouseDownCmd; /// <summary> /// 鼠标按下事件转命令 /// </summary> public DelegateCommand<MouseEventArgs> MouseDownCmd => _MouseDownCmd ?? (_MouseDownCmd = new DelegateCommand<MouseEventArgs>(e => { try { if (IsRealTouch) { ActionWhenRealTouch(); } else { e.Handled = true; //实际上不需要 } } catch (Exception ex) { LogManager.GetCurrentClassLogger().Error(ex); } finally { IsRealTouch = false; } })); /// <summary> /// [子类需重写] 真实触控时执行的方法(业务方法) /// </summary> public virtual void ActionWhenRealTouch() { } #endregion #endregion
MouseDownCmd 可当作例子,还提供了一个代表业务逻辑的 ActionWhenRealTouch () 方法,子类中重写即可。如果是子类 ViewModel 中的其它命令,只需要使用 IsRealTouch 并在最后重置即可。
3.3 防止触摸事件提升为鼠标事件(2022.10.10)
提取出了【防止触摸事件提升为鼠标事件】的方法和相应的附加属性帮助类:
帮助类:
using System.Windows; using System.Windows.Input; namespace WPFTemplateLib.WpfHelpers { /// <summary> /// 防止触摸事件提升为鼠标事件 /// https://social.msdn.microsoft.com/Forums/vstudio/en-US/9b05e550-19c0-46a2-b19c-40f40c8bf0ec/ /// http://dlgcy.com/wpf-touch-event-promote-to-touch-event-and-datagrid-touch-problem/ /// </summary> public static class PreventTouchToMousePromotion { /// <summary> /// 防止触摸事件提升为鼠标事件(启用) /// </summary> /// <param name="root">根元素</param> public static void PreventTouchToMouse_Register(this FrameworkElement root) { root.PreviewMouseDown += Evaluate; root.PreviewMouseMove += Evaluate; root.PreviewMouseUp += Evaluate; } private static void Evaluate(object sender, MouseEventArgs e) { //StylusDevice属性,触屏操作连带触发时不为null,鼠标触发时为null; if (e.StylusDevice != null) { e.Handled = true; //如果判断为 由触屏引发,则将事件标记为已处理; } } /// <summary> /// 防止触摸事件提升为鼠标事件(禁用) /// </summary> /// <param name="root">根元素</param> public static void PreventTouchToMouse_UnRegister(this FrameworkElement root) { root.PreviewMouseDown -= Evaluate; root.PreviewMouseMove -= Evaluate; root.PreviewMouseUp -= Evaluate; } } }
附加属性:
using System.Windows; using WPFTemplateLib.WpfHelpers; namespace WPFTemplateLib.Attached { /// <summary> /// WPF触摸操作附加属性帮助类 /// </summary> public class WpfTouchAttached : DependencyObject { #region 是否防止触摸事件提升为鼠标事件 public static bool GetIsPreventTouchToMouse(DependencyObject obj) { return (bool)obj.GetValue(IsPreventTouchToMouseProperty); } public static void SetIsPreventTouchToMouse(DependencyObject obj, bool value) { obj.SetValue(IsPreventTouchToMouseProperty, value); } /// <summary> /// 是否防止触摸事件提升为鼠标事件 /// </summary> public static readonly DependencyProperty IsPreventTouchToMouseProperty = DependencyProperty.RegisterAttached("IsPreventTouchToMouse", typeof(bool), typeof(WpfTouchAttached), new PropertyMetadata(false, IsPreventTouchToMouseChanged)); private static void IsPreventTouchToMouseChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var target = d as FrameworkElement; if (target == null) return; if ((bool)e.NewValue) { target.PreventTouchToMouse_Register(); } else { target.PreventTouchToMouse_UnRegister(); } } #endregion } }
3.4 用于后台事件的情况可使用的基类(2022.10.10)
以用户控件为例:
/// <summary> /// 用户控件基类 /// </summary> public class BaseUserControl : UserControl { #region 误触问题 /// <summary> /// 是否是真实触控或操作(用于解决误触问题,业务方法执行完成后要置为 false) /// </summary> public bool IsRealTouch { get; set; } public void PreviewTouchDown_ForPreventErrorTouch(object sender, TouchEventArgs e) { //真实触摸时会触发 PreviewTouchDown 事件,而误触时(关闭弹窗后在空白处点击多次会误触)则不会; IsRealTouch = true; } /* 注意:正常情况触摸事件之后还会触发鼠标事件,误触时只会触发鼠标事件 */ public void PreviewMouseDown_ForPreventErrorTouch(object sender, MouseButtonEventArgs e) { //StylusDevice属性,触屏操作别的地方连带触发(误触)时不为null,鼠标直接操作触发或者正常触摸后接着触发时为null。 if (e.StylusDevice != null) {//触屏 //e.Handled = true; } else {//鼠标 IsRealTouch = true; //避免后续判断不正常; } } #endregion }
使用:
2条评论