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 

%title插图%num

 

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

 

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

 

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

%title插图%num

 

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

 

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

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

%title插图%num

 

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

%title插图%num

 

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

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

 

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

%title插图%num

 

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

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

 

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

 

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

%title插图%num

 

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

 

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

%title插图%num

 

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

%title插图%num

 

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

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
}

 

使用:

%title插图%num

%title插图%num

 

原创文章,转载请注明: 转载自 独立观察员(dlgcy.com)

本文链接地址: [WPF 触屏事件后触发鼠标事件的问题及 DataGrid 误触问题](https://dlgcy.com/wpf-touch-event-promote-to-touch-event-and-datagrid-touch-problem/)

关注微信公众号 独立观察员博客(DLGCY_BLOG) 第一时间获取最新文章

%title插图%num

2条评论

leonierx 发布于21:40 - 2023年2月9日

在winform下对付这个问题还更麻烦,自己继承button重载WndProc过滤wm_touchdown和wm_touchup后,原来的mousedown和mouseup事件还是触发了。

    独立观察员 发布于12:19 - 2023年2月26日

    所以尽量还是不要用 Winform 咯。

发表评论