WPF 自定義附加事件

唐宋元明清2188發表於2022-12-29

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

但如果是通用的場景,類似附加屬性,附加事件就很有必要的。

舉個例子,輸入裝置有很多種,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封裝完整。

附加事件使用

在介面上,我們可以直接在任意控制元件上新增此事件:

先新增DeviceEvents.IsOpen="True"開啟裝置附加事件,

然後訂閱相應的附加事件DeviceEvents.PreviewDeviceDown="TestGrid_OnPreviewDeviceDown"

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

 

相關文章