(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事件之后,窗口就变得可见了,并且元素都已被呈现。
posted @ 2020-08-10 19:42  代码吸血虫  阅读(238)  评论(0)    收藏  举报