WPF 触屏事件后触发鼠标事件的问题及 DataGrid 误触问题

WPF 触屏事件后触发鼠标事件的问题及 DataGrid 误触问题

WPF 触屏事件后触发鼠标事件的问题及 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 

WPF 触屏事件后触发鼠标事件的问题及 DataGrid 误触问题插图

 

但是这次我遇到了一个 DataGrid误触问题,用移开鼠标的方法无效(也有可能是使用方法和时机不对),所以只能另寻它法。

 

注意,本文将在上篇文章《WPF DataGrid 通过自定义表头模拟首行固定》的示例程序基础上进行演示,建议先看看那篇文章。下面开始改造。

 

首先在行样式中添加了两个事件,一个是 PreviewTouchDown,另一个是 PreviewMouseDown:

WPF 触屏事件后触发鼠标事件的问题及 DataGrid 误触问题插图1

 

触屏点击某一行,会先触发 PreviewTouchDown,然后触发 PreviewMouseDown,然后是行改变事件 SelectionChanged,最后依次是 PreviewTouchUp 和 PreviewMouseUp。带有 Preview 前缀的是隧道事件(可视为在事件前触发),没有的是冒泡事件(可视为在事件后触发,此处省略)。

 

那么如何去除触屏事件后连带引发鼠标事件的影响呢?通过在网络上苦苦搜索和尝试,在旧版的微软社区找到了一个可行的方法,帖子为《Prevent a WPF application to interpret touch events as mouse events?》(这个链接之后可能会访问不了)。

提问者就是为了解决触屏操作下触发鼠标事件的问题:

WPF 触屏事件后触发鼠标事件的问题及 DataGrid 误触问题插图2

 

然后里面两个人分别给出了他们的解决方法,先来看看第一个:

WPF 触屏事件后触发鼠标事件的问题及 DataGrid 误触问题插图3

 

这个就是本文采纳的方法,代码文字版如下:

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; //如果判断为 由触屏引发,则将事件标记为已处理;
        }
    }
}

 

再顺便看看第二个人的方法(没有去尝试,感兴趣的朋友可以试试):

WPF 触屏事件后触发鼠标事件的问题及 DataGrid 误触问题插图4

 

二、DataGrid 误触问题及解决方法

上一个部分介绍了去除触屏事件后连带引发鼠标事件影响的方法,也就是通过鼠标事件参数的 StylusDevice 属性来判断是否是由触屏操作引发的(不为 null 则是触屏操作引发),进而进行处理。

 

然而,本次我实际上是要解决一个 DataGrid 表格在触屏下的误触问题,相关业务逻辑是在行改变事件(转为命令了)中的,本来是没有写 PreviewTouchDown 和 PreviewMouseDown 事件的(就是为了解决误触问题而引入),所以将鼠标事件标记为已处理(e.Handled = true;)的方法不能直接使用,还需要修改。原因是,行改变事件 SelectionChanged 是在 PreviewMouseDown 事件之后触发的,如果在 PreviewMouseDown 中将事件标记为已处理,那么行改变事件也就不会触发了。

 

首先来看看误触现象吧(动图):

WPF 触屏事件后触发鼠标事件的问题及 DataGrid 误触问题插图5

 

也就是,我在行改变事件中加了个弹窗,询问用户是否要切换条目,如果选是的话,不作任何处理,如果选否的话,恢复之前的选中项。选是的时候不会有误触现象,选否的时候,鼠标操作的话也正常,而如果在弹窗时通过触屏点击了否,然后在界面空白处(这里是在右侧的信息区)触屏点击几下,就会在表格上,在之前点击要切换到的那一行上产生一个鼠标事件,而且没有触屏事件,这个不用怀疑,通过调试打断点很容易观察到。

 

关于点击几下会触发这个误触,我发现和屏幕支持几点触控有关。比如,公司的触摸屏支持 10 点触控,那么这里就是点击 10 下左右触发(2021年10月11日09:29:05更新:经测试,是点击9下触发);我自己的一个小触摸屏,支持 5 点触控,这边则是在空白处点击 4 下触发。要查看屏幕支持几点触屏,可通过 GitHub 上的一个项目程序 ManipulationDemo 来查看(https://github.com/dotnet-campus/ManipulationDemo):

WPF 触屏事件后触发鼠标事件的问题及 DataGrid 误触问题插图6

 

言归正传,从误触现象的动图中可以看到,已经能够判断出是否是误触了:

WPF 触屏事件后触发鼠标事件的问题及 DataGrid 误触问题插图7

 

那么是怎么判断的呢?来看看代码:

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 ,大家可以帮忙研究研究。

 


2021年10月10日22:19:40 更新:

忘了说了,这个误触还有一个简单的解决方法,就是关闭 WPF 的实时触摸机制,在 App.config 中配置即可:

<configuration>
  <runtime>
    <AppContextSwitchOverrides value="Switch.System.Windows.Input.Stylus.DisableStylusAndTouchSupport=true" />
  </runtime>
</configuration>

 

关闭实时触摸后,多点触控和某些拖动操作就不支持了,所以当时在公司项目中不能用这个方法,如果没有这方面的需求,那么这个方法是比较方便快捷的。

 

发表评论