(3)WPF深入解析路由事件
摘自
https://www.cnblogs.com/zhili/tag/WPF/
1、路由事件的解析
与依赖属性相同,路由事件也是事件的包装。与普通事件不同的是,路由事件是一种用于元素树的事件,当路由事件触发后,它可以向上或向下遍历可视树和逻辑树,他用一种简单而持久的方式在每个元素上触发,而不需要任何定制的代码(如果用传统的方式实现一个操作,执行整个事件的调用则需要执行代码将事件串联起来)
1.1、自定义路由事件
路由事件的定义与依赖属性的定义类似,路由事件由只读的静态字段表示,在一个静态构造函数通过EventManager.RegisterRoutedEvent函数注册,并且通过一个.NET事件定义进行包装。
如下展示了ButtonBase的Click事件的定义方法,自定义路由事件也可以通过相同的方法定义
[Localizability(LocalizationCategory.Button), DefaultEvent("Click")] public abstract class ButtonBase : ContentControl, ICommandSource { // 路由事件定义 public static readonly RoutedEvent ClickEvent; // 事件注册 static ButtonBase() { //RoutingStrategy.Bubble 冒泡路由事件 ClickEvent = EventManager.RegisterRoutedEvent("Click", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ButtonBase)); CommandProperty = DependencyProperty.Register("Command", typeof(ICommand), typeof(ButtonBase), new FrameworkPropertyMetadata(null, new PropertyChangedCallback(ButtonBase.OnCommandChanged))); ....... } // 包装器,通过它来操作我们刚才注册的路由事件 public event RoutedEventHandler Click { add { base.AddHandler(ClickEvent, value); } remove { base.RemoveHandler(ClickEvent, value); } } ....... } }
1.2、共享路由事件(继承)
与依赖属性一样,可以在类之间共享路由事件的定义。即实现路由事件的继承。例如UIElement类和ContentElement类都使用了MouseUp事件,但MouseUp事件是由System.Windows.Input.Mouse类定义的。UIElement类和ContentElement类只是通过RouteEvent.AddOwner方法重用了MouseUp事件。你可以在UIElement类的静态构造函数找到下面的代码:
static UIElement() { _typeofThis = typeof(UIElement); PreviewMouseUpEvent = Mouse.PreviewMouseUpEvent.AddOwner(_typeofThis); MouseUpEvent = Mouse.MouseUpEvent.AddOwner(_typeofThis); }
1.3、触发和处理路由事件
尽管路由事件通过传统的.NET事件进行包装,但路由事件并不是通过.NET事件触发的,而是使用RaiseEvent方法触发事件,所有元素都从UIElement类继承了该方法。
下面代码是具体ButtonBase类中触发路由事件的代码:
protected virtual void OnClick() { RoutedEventArgs e = new RoutedEventArgs(ClickEvent, this); base.RaiseEvent(e);// 通过RaiseEvent方法触发路由事件 CommandHelpers.ExecuteCommandSource(this); }
触发路由事件后的处理方法,有两种注册方法:
//方法1: <TextBlock Margin="3" MouseUp="SomethingClick" Name="tbxTest"> text label </TextBlock> // 后台cs代码 private void SomethingClick(object sender, MouseButtonEventArgs e) { } //方法2: tbxTest.MouseUp += new MouseButtonEventHandler(SomethingClick); // 或者省略委托类型 tbxTest.MouseUp += SomethingClick;
1.4、三种路由事件(路由策略)
路由事件的特殊性在于其传递性,WPF中的路由事件分为三种。
- 与普通的.NET事件类似的直接路由事件(Direct event):它源自一个元素,并且不传递给其他元素。例如,MouseEnter事件(当鼠标移动到一个元素上面时触发)就是一个直接路由事件。
- 在包含层次中向上传递的冒泡路由事件(Bubbling event):例如,MouseDown事件就是一个冒泡路由事件。它首先被单击的元素触发,接下来就是该元素的父元素触发,依此类推,直到WPF到达元素树的顶部为止。
- 在包含层次中向下传递的隧道路由事件(Tunneling event):例如PreviewKeyDown就是一个隧道路由事件。在一个窗口上按下某个键,首先是窗口,然后是更具体的容器,直到到达按下键时具有焦点的元素。
WPF一般都成对地定义冒泡路由事件和隧道路由事件。这意味着如果发现一个冒泡的MouseUp事件,则对应的PreviewMouseUp就是一个隧道路由事件。另外,隧道路由事件总是在冒泡路由事件之前被触发。
当使用EventManager.RegisterEvent方法注册一个路由事件时,需要传递一个RoutingStrategy枚举值来设置的路由策略。
1)冒泡路由事件

单击窗口中的笑脸图像之后,程序的运行结果如下左图所示,从结果可以发现,MouseUp事件由下向上传递了5级,直到窗口级别结束。
如果选择了Handle first event复选框的话,SomethingClicked方法会将RoutedEventArgs.Handled属性设置为true,表示事件已被处理,该事件还会往上向上冒泡,但对应的事件不会再处理。因此,此时列表中只能看到Image的事件,具体运行结果如下右图所示:

2)隧道路由事件
隧道路由事件与冒泡路由事件正好相反,隧道路由事件对于来执行一些预处理操作非常有用,例如,根据键盘上特定的键执行特定操作,或过滤掉特定的鼠标操作等这样的场景,都可以在隧道路由事件处理程序中进行处理。
如果将隧道路由事件标记为已处理的,那么冒泡路由事件就不会发生。这是因为这两个事件共享同一个RoutedEventArgs类的实例
下面的示例演示了PreviewKeyDown事件的隧道过程。XAML代码如下所示。

在文本框中按下一个键时,事件首先在窗口触发,然后在整个层次结构中向下传递。具体的运行结果如下左图所示:
当勾选了Handle first event 复选框时,当在输入框中按下一个键时,listbox中显示的记录只有1条记录,因为窗口触发的PrevieKeyDown事件处理已经把隧道路由事件标识为已处理,所以PreviewKeyDown事件将不会向下传递。我们按下的键上对应的字符并没有在输入框中显示,因为此时并没有触发Textbox中的KeyDown事件,因为改变文本框内容的处理是在KeyDown事件中处理的。

1.5、附件事件
假设有这样一个场景,StackPanel面板中包含了一堆按钮,并且希望在一个事件处理程序中处理所有这些按钮的单击事件。在更高层次元素来关联Click事件来处理所有按钮的单击事件,具体的XAML代码实现如下所示:
<Window x:Class="AttachClickEvent.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525"> <StackPanel Margin="3" Button.Click="DoSomething"> <Button Name="btn1">Button 1</Button> <Button Name="btn2">Button 2</Button> <Button Name="btn3">Button 3</Button> </StackPanel> </Window>
也可以在代码中关联附加事件,但是需要使用UIElement.AddHandle方法,而不能使用+=运算符的方式。具体实现代码如下所示:
// StackPanel面板命名为ButtonsPanel ButtonsPanel.AddHandler(Button.ClickEvent, new RoutedEventHandler(DoSomething));
1.6、事件的生命周期
- BeginInit&EndInit
FrameworkElement类实现了ISupportInitialize接口,该接口提供了两个用于控制初始化过程的方法。
第一个是BeginInit方法,在实例化元素后立即调用该方法。BeginInit方法被调用之后,XAML解析器设置所有元素的属性并添加内容。
第二个是EndInit方法,当初始化完成后,该方法被调用。此时引发Initialized事件。更准确地说,XAML解析器负责调用BeginInit方法和EndInit方法。 - Initializeds事件
当创建窗口时,每个元素分支都以自下而上的方式被初始化。 - Loaded事件
一旦初始化过程完成后,就会引发Loaded事件。
Loaded事件和Initialized事件的发生过程相反。包含所有元素的窗口首先引发Loaded事件,然后才是更深层次的嵌套元素。当所有元素都引发了Loaded事件之后,窗口就变得可见了,并且元素都已被呈现。

浙公网安备 33010602011771号