WPF 触屏事件后触发鼠标事件的问题及 DataGrid 误触问题
目录
一、触屏事件连带触发鼠标事件的问题
二、DataGrid 误触问题及解决方法
独立观察员 2021 年 10 月 10 日
一、触屏事件连带触发鼠标事件的问题
这个是 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 下左右触发;我自己的一个小触摸屏,支持 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 ,大家可以帮忙研究研究。