WPF 自定义附加事件

我们都知道路由事件,而附加事件用的比较少。

但如果是通用的场景,类似附加属性,附加事件就很有必要的。

举个例子,输入设备有很多种,WPF中输入事件主要分为鼠标、触摸、触笔:WPF 屏幕点击的设备类型 - 唐宋元明清2188 - 博客园 (cnblogs.com)

有这么多输入事件Mouse、Touch、Stylus,另外按钮Click还处理了冒泡事件,事件类型就更多了。

但绝大部分业务其实并不关心事件类型,只需要一个触发事件就行了。

所以我们有这样的需求:设计并提供一个通用的输入事件,大家只需要拿到事件进行业务操作。另外一些小场景,如果需要具体事件如触摸点集,可以从事件源参数内部去获取。

具体的通用输入事件,我们另外讨论,这里主要描述如何自定义附加事件

附加事件

WPF官方对附加事件的描述 - 附加事件概述 - WPF .NET Framework | Microsoft Learn

所以我们先定义一个附加事件类:

 1     public class DeviceEvents  2     {  3         /// <summary>  4         /// 按压事件  5         /// </summary>  6         public static readonly RoutedEvent PreviewDeviceDownEvent = EventManager.RegisterRoutedEvent("PreviewDeviceDown", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(DeviceEvents));  7         public static void AddPreviewDeviceDownHandler(DependencyObject d, RoutedEventHandler handler)  8         {  9             (d as UIElement)?.AddHandler(DeviceEvents.PreviewDeviceDownEvent, handler); 10         } 11         public static void RemovePreviewDeviceDownHandler(DependencyObject d, RoutedEventHandler handler) 12         { 13             (d as UIElement)?.RemoveHandler(DeviceEvents.PreviewDeviceDownEvent, handler); 14         } 15     }

然后,我们看下如何封装原有事件PreviewMouseDown、PreviewStylusDown,并转换成此PreviewDeviceDown事件

在界面上添加下事件:

1     <Button x:Name="TestButton"   2             Content="测试" Height="30" Width="120" 3             local:DeviceEvents.PreviewDeviceDown="TestButton_OnPreviewDeviceDown"/>

我们会发现界面加载时,AddPreviewDeviceDownHandler不会触发调用。附加事件,与附加属性原理不一样,附加事件相当于在编译时注入委托(事件处理程序)AddPreviewDeviceDownHandler,界面加载时不会执行这个委托注册。

所以,我们可以通过另一个附加属性来开关附加事件,并且内部集成事件的触发。具体设计如下:

 1     public class DeviceEvents  2     {  3         private static readonly List<DeviceEventTransformer> EventTransformers = new List<DeviceEventTransformer>();  4   5         /// <summary>  6         /// 是否开启附加事件  7         /// </summary>  8         public static readonly DependencyProperty IsOpenProperty = DependencyProperty.RegisterAttached(  9             "IsOpen", typeof(bool), typeof(DeviceEvents), new PropertyMetadata(default(bool), OnIsOpenPropertyChanged)); 10         private static void OnIsOpenPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 11         { 12             if (d is UIElement element && e.NewValue is bool isOpen) 13             { 14                 var eventTransformer = EventTransformers.FirstOrDefault(i => i.Target == element); 15                 if (isOpen) 16                 { 17                     if (eventTransformer == null) 18                     { 19                         eventTransformer = new DeviceEventTransformer(element); 20                         EventTransformers.Add(eventTransformer); 21                     } 22                     eventTransformer.Register(); 23                 } 24                 else 25                 { 26                     eventTransformer?.UnRegister(); 27                 } 28             } 29         } 30  31         public static void SetIsOpen(DependencyObject element, bool value) 32         { 33             element.SetValue(IsOpenProperty, value); 34         } 35         public static bool GetIsOpen(DependencyObject element) 36         { 37             return (bool)element.GetValue(IsOpenProperty); 38         } 39  40         /// <summary> 41         /// 按压事件 42         /// </summary> 43         public static readonly RoutedEvent PreviewDeviceDownEvent = EventManager.RegisterRoutedEvent("PreviewDeviceDown", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(DeviceEvents)); 44         public static void AddPreviewDeviceDownHandler(DependencyObject d, RoutedEventHandler handler) 45         { 46             (d as UIElement)?.AddHandler(DeviceEvents.PreviewDeviceDownEvent, handler); 47         } 48         public static void RemovePreviewDeviceDownHandler(DependencyObject d, RoutedEventHandler handler) 49         { 50             (d as UIElement)?.RemoveHandler(DeviceEvents.PreviewDeviceDownEvent, handler); 51         } 52     } 53     /// <summary> 54     /// 事件转换器 55     /// </summary> 56     public class DeviceEventTransformer 57     { 58         private readonly UIElement _uiElement; 59  60         public DeviceEventTransformer(UIElement uiElement) 61         { 62             _uiElement = uiElement; 63         } 64         public UIElement Target => _uiElement; 65  66         public void Register() 67         { 68             _uiElement.PreviewMouseDown += UiElement_PreviewMouseDown; 69             _uiElement.PreviewStylusDown += UiElement_PreviewStylusDown; 70         } 71  72         public void UnRegister() 73         { 74             _uiElement.PreviewMouseDown -= UiElement_PreviewMouseDown; 75             _uiElement.PreviewStylusDown -= UiElement_PreviewStylusDown; 76         } 77  78         private void UiElement_PreviewMouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e) 79         { 80             OnPreviewDeviceDown(e); 81         } 82         private void UiElement_PreviewStylusDown(object sender, StylusDownEventArgs e) 83         { 84             OnPreviewDeviceDown(e); 85         } 86  87         private void OnPreviewDeviceDown(InputEventArgs e) 88         { 89             RoutedEventArgs reArgs = new RoutedEventArgs(DeviceEvents.PreviewDeviceDownEvent, e); 90             _uiElement.RaiseEvent(reArgs); 91         } 92     }

DeviceEventTransformer是一个事件封装器,如果只是后台代码,也可以直接使用DeviceEventTransformer订阅相关事件。

上面事件实现是一个案例,这里的代码比较粗糙,没有对鼠标、触摸做互斥处理。另外其它的事件类型如冒泡、隧道、Down、Up等,后续可以将DeviceEvents封装完整。

附加事件使用

在界面上,我们可以直接在任意控件上添加此事件:

WPF 自定义附加事件

先添加DeviceEvents.IsOpen="True"开启设备附加事件,
然后订阅相应的附加事件DeviceEvents.PreviewDeviceDown="TestGrid_OnPreviewDeviceDown"

Demo:DeviceEventsDemo: WPF附加事件DEMO (gitee.com)

 

发表评论

相关文章