WPF 设备输入事件封装
本文主要介绍WPF应用对鼠标输入、触摸屏触笔以及触摸事件的封装
之前有简单说明设备输入类型 WPF 屏幕点击的设备类型 - 唐宋元明清2188 - 博客园 (cnblogs.com)
1、鼠标 - 通过Mouse相关的事件参数MouseButtonEventArgs中的数据,e.StylusDecice==null表示没有触摸设备,所以设备为鼠标
2、触笔 or 触摸 - 根据StylusDown事件参数StylusDownEventArgs,e.StylusDevice.TabletDevice.Type == TabletDeviceType.Stylus,True表示触摸设备为触笔,False则为触摸。
通过上面设备类型的判断,就可以封装一套设备点击事件,DeviceDown、DeviceUp、DeviceMove。事件参数中添加设备类型DeviceType(鼠标、触笔、触摸),然后具体业务中可以通过设备类型区分相关的交互操作。
博客园有个小伙伴问我设备类型具体是如何封装的,那本文就补充下设备输入事件的封装使用,希望给大家提供一点帮助、省去你们磨代码的时间。
除了设备输入类型,输入事件也有多种状态。简单介绍下事件区分,具体以鼠标事件为例:
1 private void RegisterMouse() 2 { 3 //鼠标-冒泡 4 if (_element is Button button) 5 { 6 //Button类型的冒泡事件,被内部消化处理了,所以需要重新添加路由事件订阅 7 button.AddHandler(UIElement.MouseLeftButtonDownEvent, new MouseButtonEventHandler(Button_MouseLeftButtonDown), true); 8 button.AddHandler(UIElement.MouseRightButtonDownEvent, new MouseButtonEventHandler(Button_MouseRightButtonDown), true); 9 button.AddHandler(UIElement.MouseLeftButtonUpEvent, new MouseButtonEventHandler(Button_MouseLeftButtonUp), true); 10 button.AddHandler(UIElement.MouseRightButtonUpEvent, new MouseButtonEventHandler(Button_MouseRightButtonUp), true); 11 } 12 else 13 { 14 _element.MouseLeftButtonDown += Button_MouseLeftButtonDown; 15 _element.MouseRightButtonDown += Button_MouseRightButtonDown; 16 _element.MouseLeftButtonUp += Button_MouseLeftButtonUp; 17 _element.MouseRightButtonUp += Button_MouseRightButtonUp; 18 } 19 _element.MouseMove += Element_MouseMove; 20 21 //鼠标-隧道事件 22 _element.PreviewMouseLeftButtonDown += Button_PreviewMouseLeftButtonDown; 23 _element.PreviewMouseRightButtonDown += Button_PreviewMouseRightButtonDown; 24 _element.PreviewMouseLeftButtonUp += Button_PreviewMouseLeftButtonUp; 25 _element.PreviewMouseRightButtonUp += Button_PreviewMouseRightButtonUp; 26 _element.PreviewMouseMove += Element_PreviewMouseMove; 27 }
上面区分了按钮与其它的FrameworkElement的鼠标事件,因为Button对冒泡事件是做了拦截再暴露Click事件,需要订阅路由事件来完成鼠标的监听。
如上方代码,对鼠标的左右键、按下抬起、移动以及冒泡隧道都做了完整的封装。
鼠标事件:
1 private void Element_MouseDown(MouseEventArgs e, int deviceId) 2 { 3 if (e.StylusDevice != null) return; 4 var positionLazy = new Lazy<Point>(() => e.GetPosition(_element)); 5 var deviceInputArgs= new DeviceInputArgs() 6 { 7 DeviceId = deviceId, 8 DeviceType = DeviceType.Mouse, 9 PositionLazy = positionLazy, 10 PointsLazy = new Lazy<StylusPointCollection>(() => 11 { 12 var point = positionLazy.Value; 13 return new StylusPointCollection(new List<StylusPoint>() { new StylusPoint(point.X, point.Y) }); 14 }), 15 GetPositionFunc = (element, args) => e.GetPosition(element), 16 SourceArgs = e 17 }; 18 _deviceDown?.Invoke(_element, deviceInputArgs); 19 }
触摸面积获取:
1 /// <summary> 2 /// 获取含面积的触摸点集合 3 /// </summary> 4 /// <param name="stylusEventArgs"></param> 5 /// <param name="uiElement"></param> 6 /// <returns></returns> 7 public static Rect GetTouchPointArea(TouchEventArgs stylusEventArgs, UIElement uiElement) 8 { 9 Rect touchArea = Rect.Empty; 10 var touchPoints = stylusEventArgs.GetIntermediateTouchPoints(uiElement); 11 foreach (var stylusPoint in touchPoints) 12 { 13 var stylusPointArea = stylusPoint.Bounds; 14 touchArea.Union(stylusPointArea); 15 } 16 return touchArea; 17 }
返回新的触笔输入点集:
1 /// <summary> 2 /// 获取触摸点集 3 /// </summary> 4 /// <param name="stylusEventArgs"></param> 5 /// <param name="element"></param> 6 /// <returns></returns> 7 private StylusPointCollection GetStylusPoints(StylusEventArgs stylusEventArgs, FrameworkElement element) 8 { 9 // 临时去除description 10 var pointCollection = new StylusPointCollection(); 11 var stylusPointCollection = stylusEventArgs.GetStylusPoints(element); 12 foreach (var stylusPoint in stylusPointCollection) 13 { 14 pointCollection.Add(new StylusPoint(stylusPoint.X, stylusPoint.Y, stylusPoint.PressureFactor)); 15 } 16 return pointCollection; 17 }
这里是直接把StylusPoint.Description舍弃,防止在应用层未能统一好这个设备描述、导致异常。
当然这些获取信息根据业务需要来获取、此处设备事件封装不损耗处理延时,所以需要添加Lazy函数如:
PositionLazy = new Lazy<Point>(() => e.GetPosition(_element)),
PointsLazy = new Lazy<StylusPointCollection>(() => GetStylusPoints(e, _element)),
暴露的通用设备按下事件,可以看如下定义:
1 /// <summary> 2 /// 设备按下 3 /// </summary> 4 public event EventHandler<DeviceInputArgs> DeviceDown 5 { 6 add => _deviceDown.Add(value, value.Invoke); 7 remove => _deviceDown.Remove(value); 8 }
UI控件订阅DeviceDown、DeviceMove、DeviceUp事件,从事件参数DeviceInputArgs获取详细数据:
1 /// <summary> 2 /// 设备ID 3 /// <para>默认为鼠标设备,鼠标左键-1,鼠标右键-2 </para> 4 /// </summary> 5 public int DeviceId { get; set; } 6 /// <summary> 7 /// 设备类型 8 /// </summary> 9 public DeviceType DeviceType { get; set; } 10 11 /// <summary> 12 /// 位置 13 /// </summary> 14 public Point Position 15 { 16 get => PositionLazy?.Value ?? default; 17 set => PositionLazy = new Lazy<Point>(() => value); 18 } 19 20 /// <summary> 21 /// 触笔点集 22 /// </summary> 23 public StylusPointCollection Points 24 { 25 get => PointsLazy?.Value; 26 set => PointsLazy = new Lazy<StylusPointCollection>(() => value); 27 } 28 29 /// <summary> 30 /// 获取相对元素的位置 31 /// </summary> 32 public Func<FrameworkElement, Point> GetPosition 33 { 34 get => relativeElement => GetPositionFunc(relativeElement, SourceArgs); 35 set => GetPositionFunc = (relativeElement, args) => value(relativeElement); 36 } 37 38 /// <summary> 39 /// 事件触发源数据 40 /// </summary> 41 public InputEventArgs SourceArgs { get; set; } 42 43 /// <summary> 44 /// 触摸面积 45 /// </summary> 46 public Rect TouchArea => TouchAreaLazy?.Value ?? Rect.Empty;
DeviceInputArgs继承Windows路由事件参数类RoutedEventArgs
为何要封装通用事件?因为2点原因:
1. 大部分业务并不需要区分鼠标、触笔、触摸类型
2. 有些业务如白板书写、多指操作需要区分鼠标、触笔、触摸类型,这类场景因为操作事件订阅太多,业务逻辑搞复杂了
通过将鼠标、触笔、触摸事件封装为通用输入事件,下游应用业务对设备输入事件处理逻辑就简单化了。
封装Demo可详见:kybs00/DeviceInputEventDemo (github.com)
关键字:鼠标、触笔、触摸