C#+WPF 设备输入事件封装

191 阅读4分钟

前言

本文主要介绍WPF应用对鼠标输入、触摸屏触笔以及触摸事件的封装。

WPF 屏幕点击的设备类型

1、鼠标

可以通过Mouse相关的事件参数MouseButtonEventArgs中的数据,e.StylusDecice==null表示没有触摸设备,所以设备为鼠标

2、触笔 or 触摸

根据StylusDown事件参数StylusDownEventArgs,

e.StylusDevice.TabletDevice.Type == TabletDeviceType.Stylus,True表示触摸设备为触笔,False则为触摸。

注:网上有博客说通过DeviceId来判断是否触笔。这是不对的

e.StylusDevice.Id,触笔的DeviceId在一台电脑上一般是固定的,换台电脑id是会变化的;

触摸的DeviceId则是动态分配变动的。

通过上面设备类型的判断,就可以封装一套设备点击事件,DeviceDown、DeviceUp、DeviceMove。

事件参数中添加设备类型DeviceType(鼠标、触笔、触摸),然后具体业务中可以通过设备类型区分相关的交互操作。

设备输入事件的封装

有个小伙伴问我设备类型具体是如何封装的,那本文就补充下设备输入事件的封装使用,希望给大家提供一点帮助、省去你们磨代码的时间。

除了设备输入类型,输入事件也有多种状态。

简单介绍下事件区分,具体以鼠标事件为例:

private void RegisterMouse()
{
    //鼠标-冒泡
    if (_element is Button button)
    {
        //Button类型的冒泡事件,被内部消化处理了,所以需要重新添加路由事件订阅
        button.AddHandler(UIElement.MouseLeftButtonDownEvent, new MouseButtonEventHandler(Button_MouseLeftButtonDown), true);
        button.AddHandler(UIElement.MouseRightButtonDownEvent, new MouseButtonEventHandler(Button_MouseRightButtonDown), true);
        button.AddHandler(UIElement.MouseLeftButtonUpEvent, new MouseButtonEventHandler(Button_MouseLeftButtonUp), true);
        button.AddHandler(UIElement.MouseRightButtonUpEvent, new MouseButtonEventHandler(Button_MouseRightButtonUp), true);
    }
    else
    {
        _element.MouseLeftButtonDown += Button_MouseLeftButtonDown;
        _element.MouseRightButtonDown += Button_MouseRightButtonDown;
        _element.MouseLeftButtonUp += Button_MouseLeftButtonUp;
        _element.MouseRightButtonUp += Button_MouseRightButtonUp;
    }
    _element.MouseMove += Element_MouseMove;

    //鼠标-隧道事件
    _element.PreviewMouseLeftButtonDown += Button_PreviewMouseLeftButtonDown;
    _element.PreviewMouseRightButtonDown += Button_PreviewMouseRightButtonDown;
    _element.PreviewMouseLeftButtonUp += Button_PreviewMouseLeftButtonUp;
    _element.PreviewMouseRightButtonUp += Button_PreviewMouseRightButtonUp;
    _element.PreviewMouseMove += Element_PreviewMouseMove;
}

上面区分了按钮与其它的FrameworkElement的鼠标事件,因为Button对冒泡事件是做了拦截再暴露Click事件,需要订阅路由事件来完成鼠标的监听。

如上方代码,对鼠标的左右键、按下抬起、移动以及冒泡隧道都做了完整的封装。

鼠标事件:

private void Element_MouseDown(MouseEventArgs e, int deviceId)
{
    if (e.StylusDevice != null) return;
    var positionLazy = new Lazy<Point>(() => e.GetPosition(_element));
    var deviceInputArgs= new DeviceInputArgs()
    {
        DeviceId = deviceId,
        DeviceType = DeviceType.Mouse,
        PositionLazy = positionLazy,
        PointsLazy = new Lazy<StylusPointCollection>(() =>
        {
            var point = positionLazy.Value;
            return new StylusPointCollection(new List<StylusPoint>() { new StylusPoint(point.X, point.Y) });
        }),
        GetPositionFunc = (element, args) => e.GetPosition(element),
        SourceArgs = e
    };
    _deviceDown?.Invoke(_element, deviceInputArgs);
}

触摸面积获取:

/// <summary>
/// 获取含面积的触摸点集合
/// </summary>
/// <param name="stylusEventArgs"></param>
/// <param name="uiElement"></param>
/// <returns></returns>
public static Rect GetTouchPointArea(TouchEventArgs stylusEventArgs, UIElement uiElement)
{
    Rect touchArea = Rect.Empty;
    var touchPoints = stylusEventArgs.GetIntermediateTouchPoints(uiElement);
    foreach (var stylusPoint in touchPoints)
    {
        var stylusPointArea = stylusPoint.Bounds;
        touchArea.Union(stylusPointArea);
    }
    return touchArea;
}

返回新的触笔输入点集:

/// <summary>
/// 获取触摸点集
/// </summary>
/// <param name="stylusEventArgs"></param>
/// <param name="element"></param>
/// <returns></returns>
private StylusPointCollection GetStylusPoints(StylusEventArgs stylusEventArgs, FrameworkElement element)
{
    // 临时去除description
    var pointCollection = new StylusPointCollection();
    var stylusPointCollection = stylusEventArgs.GetStylusPoints(element);
    foreach (var stylusPoint in stylusPointCollection)
    {
        pointCollection.Add(new StylusPoint(stylusPoint.X, stylusPoint.Y, stylusPoint.PressureFactor));
    }
    return pointCollection;
}

这里是直接把StylusPoint.Description舍弃,防止在应用层未能统一好这个设备描述、导致异常

当然这些获取信息根据业务需要来获取、此处设备事件封装不损耗处理延时,所以需要添加Lazy函数如:

PositionLazy = new Lazy< Point>(() => e.GetPosition(_element)), PointsLazy = new Lazy< StylusPointCollection>(() => GetStylusPoints(e, _element)),

暴露的通用设备按下事件,可以看如下定义:

/// <summary>
/// 设备按下
/// </summary>
public event EventHandler<DeviceInputArgs> DeviceDown
{
    add => _deviceDown.Add(value, value.Invoke);
    remove => _deviceDown.Remove(value);
}

UI控件订阅DeviceDown、DeviceMove、DeviceUp事件,从事件参数DeviceInputArgs获取详细数据:

/// <summary>
/// 设备ID
/// <para>默认为鼠标设备,鼠标左键-1,鼠标右键-2 </para>
/// </summary>
public int DeviceId { get; set; }
/// <summary>
/// 设备类型
/// </summary>
public DeviceType DeviceType { get; set; }

/// <summary>
/// 位置
/// </summary>
public Point Position
{
    get => PositionLazy?.Value ?? default;
    set => PositionLazy = new Lazy<Point>(() => value);
}

/// <summary>
/// 触笔点集
/// </summary>
public StylusPointCollection Points
{
    get => PointsLazy?.Value;
    set => PointsLazy = new Lazy<StylusPointCollection>(() => value);
}

/// <summary>
/// 获取相对元素的位置
/// </summary>
public Func<FrameworkElement, Point> GetPosition
{
    get => relativeElement => GetPositionFunc(relativeElement, SourceArgs);
    set => GetPositionFunc = (relativeElement, args) => value(relativeElement);
}

/// <summary>
/// 事件触发源数据
/// </summary>
public InputEventArgs SourceArgs { get; set; }

/// <summary>
/// 触摸面积
/// </summary>
public Rect TouchArea => TouchAreaLazy?.Value ?? Rect.Empty;

DeviceInputArgs继承Windows路由事件参数类RoutedEventArgs

为何要封装通用事件?

因为两点原因:

1、大部分业务并不需要区分鼠标、触笔、触摸类型

2、有些业务如白板书写、多指操作需要区分鼠标、触笔、触摸类型,这类场景因为操作事件订阅太多,业务逻辑搞复杂了

通过将鼠标、触笔、触摸事件封装为通用输入事件,下游应用业务对设备输入事件处理逻辑就简单化了。

示例源码

Github:github.com/kybs00/Devi…

最后

如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。

也可以加入微信公众号 [DotNet技术匠] 社区,与其他热爱技术的同行一起交流心得,共同成长!

优秀是一种习惯,欢迎大家留言学习!

作者:唐宋元明清2188

出处:cnblogs.com/kybs0/p/18325065

声明:网络内容,仅供学习,尊重版权,侵权速删,歉意致谢!