> 文档中心 > WPF路由事件RegisterRoutedEvent

WPF路由事件RegisterRoutedEvent

您可能知道,只要在Control对象上启动用户操作并且仅触发指定控件事件处理程序,就会触发本机CLR事件。此事件不会影响其他控件。例如,您可以在WPF中想到的任何事件本质上都是CLR事件。

相反,路由事件是一种事件类型,它可以在元素树中的多个侦听器上调用处理程序,而不仅仅是引发事件的对象。它基本上是一个由Routed Event类的实例支持的CLR事件。它在WPF事件系统中注册。

为了详细解释路由事件,让我们以WPF UI窗口为例,其中有一些嵌套控件。

  • Window
    • StackPanel 1
      • StackPanel 2
        • Textbox 1
        • Textbox 2
        • Button

我希望上面的结构对你来说是清楚的。请记住此UI结构,我们将在需要时引用它。

现在,为了您的利益,我们将简要介绍WPF树结构。

树结构

为了在WPF应用程序中创建和识别UI元素之间的关系,有两种对象树结构。

逻辑树结构
XAML中的UI元素结构表示逻辑树结构。在上面的WPF UI窗口中,如果您查看XAML代码,您将看到逻辑树结构。

可视树结构
描述可视对象的结构,由可视基类表示。它表示渲染到输出屏幕的所有 UI 元素。它用于渲染视觉对象和布局。路由事件主要沿着可视树传播,而不是逻辑树。如果您编译并运行 XAML,您将在 Visual Studio 中看到 Live Visual Tree。视觉树通常是逻辑树的超集,具有关于视觉格式选项的附加信息。

牢记这一点,我们将深入研究路由策略。

路由事件

Routed Events 有以下三种主要的路由策略

  • Direct Event(直接)
  • Bubbling Event(冒泡)
  • Tunneling Event(隧道)

Direct Event

直接事件类似于 Windows 窗体中的事件,这些事件由事件发起的元素引发。

与标准 CLR 事件不同,直接路由事件支持类处理,并且可以在自定义控件样式中的事件设置器和事件触发器中使用。

直接事件的一个很好的例子是 MouseEnter 事件。

Bubbling Event

冒泡事件从事件起源的元素开始。然后它沿着可视树向上移动到可视树中最顶层的元素。您可能已经猜到了,在 WPF 中,最顶层的元素很可能是一个窗口。

现在回到我之前设置的示例,如果您单击按钮,则冒泡事件将朝这个方向移动。

Button > Stackpanel 1 > Window

如果您单击任何文本框,则事件路由将是。

Textbox 1 or 2 > Stackpanel 2 > Stackpanel 1> Window

Tunneling Event

Tunneling 事件的方向与 Bubbling 事件相反。在这里,元素树根上的事件处理程序被调用,然后事件沿着可视树传播到所有子节点,直到它到达事件起源的元素。

冒泡和隧道事件(除了方向)之间的区别在于隧道事件名称总是以“预览”开头。

在 WPF 应用程序中,事件通常实现为隧道/冒泡对。因此,您将有一个 previewMouseDown 和一个 MouseDown 事件。

事件顺序

现在让我们使用适当的事件链更清楚地了解您的概念。

标准 Button 控件派生自ButtonBase,因此它继承了一个Click事件,该事件在用户单击按钮时触发。由于 ButtonBase 继承自UIElement,因此Button还可以访问为 UIElement 定义的所有鼠标按钮事件(如 MouseLeftButtonDown 和 MouseDown)。但是当 Button响应按钮按下时,它会吞下冒泡事件(例如 MouseLeftButtonDown 和 MouseDown)。通常,由于用户按下鼠标按钮而执行某些操作的控件将吞噬相关事件。单击 TextBox 可以使其获得焦点。单击 Button 或 ComboBox 也会导致控件响应单击,因此这些控件也会吞噬非预览事件。请记住,这些类型的控件仅触发隧道事件。

例如,当用户单击按钮时,会有一系列预览事件(隧道)和实际事件(冒泡)在逻辑树上上下移动。

对于左键单击按钮,您通常会按以下顺序看到与单击相关的事件。

  • PreviewMouseLeftButtonDown (Tunnel)
  • PreviewMouseDown (Tunnel)
  • PreviewMouseLeftButtonUp (Tunnel)
  • PreviewMouseUp (Tunnel)
  • Click (Bubble)

但是对于包含在 StackPanel 中的标签,它包含在窗口中,鼠标左键单击标签的完整事件序列是,

  • PreviewMouseLeftButtonDown for Window (Tunnel)
  • PreviewMouseDown for Window (Tunnel)
  • PreviewMouseLeftButtonDown for StackPanel (Tunnel)
  • PreviewMouseDown for StackPanel (Tunnel)
  • PreviewMouseLeftButtonDown for Label (Tunnel)
  • PreviewMouseDown for Label (Tunnel)
  • MouseLeftButtonDown for Label (Bubble)
  • MouseDown for Label (Bubble)
  • MouseLeftButtonDown for StackPanel (Bubble)
  • MouseDown for StackPanel (Bubble)
  • MouseLeftButtonDown for Window (Bubble)
  • MouseDown for Window (Bubble)
  • PreviewMouseLeftButtonUp for Window (Tunnel)
  • PreviewMouseUp for Window (Tunnel)
  • PreviewMouseLeftButtonUp for StackPanel (Tunnel)
  • PreviewMouseUp for StackPanel (Tunnel)
  • PreviewMouseLeftButtonUp for Label (Tunnel)
  • PreviewMouseUp for Label (Tunnel)
  • MouseLeftButtonUp for Label (Bubble)
  • MouseUp for Label (Bubble)
  • MouseLeftButtonUp for Stackpanel (Bubble)
  • MouseUp for StackPanel (Bubble)
  • MouseLeftButtonUp for Window (Bubble)
  • MouseUp for Window (Bubble)

现在准备好亲身体验一下路由事件的这一美丽特性。在本例中,我将只向您展示 Bubble 事件,但不要担心!很快我也会发布更多关于隧道事件的示例。

互联网上的许多简单教程都提供了基于按钮的示例。它非常基础和简单,它隐藏了使用路由事件的重要方面。因此,我决定使用按钮以外的控件(更具体地说是 TextBlock!)以正确指示路由事件的功能。

动手演示

在这里,我将创建一个自定义路由事件。您需要按照下面给出的步骤在 C# 中定义自定义路由事件。

  • 使用系统调用RegisterRoutedEvent声明并注册您的路由事件。
  • 指定路由策略,即 Bubble、Tunnel 或 Direct。
  • 提供事件处理程序。

按照下面给出的步骤,

  • 创建一个名为 WpfCustomRoutedEventTutorial 的新 WPF 项目。
  • 右键单击您的解决方案并选择 Add > New Item... 在以下对话框中,选择 Custom Control (WPF) 并将其命名为 MyCustomControl。
  • 单击添加按钮,您将看到两个新文件(Themes/Generic.xaml 和 MyCustomControl.cs)将添加到您的解决方案中。

以下 XAML 代码设置 Generic.xaml 文件中自定义控件的样式。

                                  

下面给出的是 MyCustomControl 类的 C# 代码,该类继承自 Control 类,其中为自定义控件创建了自定义路由事件 Click。代码被大量注释,以便用勺子喂你逻辑。不用担心。

using System.Windows;  using System.Windows.Controls;    namespace WpfCustomRoutedEventTutorial  {      public class MyCustomControl : Control      {   static MyCustomControl()   {DefaultStyleKeyProperty.OverrideMetadata(typeof(MyCustomControl), new FrameworkPropertyMetadata(typeof(MyCustomControl)));   } public override void OnApplyTemplate()   {base.OnApplyTemplate();  var custextblock = GetTemplateChild("tbCustomControl") as TextBlock;if (custextblock != null){    custextblock.MouseDown += Custom_MouseClick;}   }     /* Event handler for mouse click */   private void Custom_MouseClick(object sender, System.Windows.Input.MouseButtonEventArgs e)   {RaiseMouseClickEvent();   }     /* Event handler for mouse wheel rotate */   void Custom_MouseWheel(object sender, RoutedEventArgs e)   {RaiseMouseWheelEvent();   }   public static readonly RoutedEvent CustomWheelEvent =    EventManager.RegisterRoutedEvent("MyCustomWheelRotate", RoutingStrategy.Bubble,    typeof(RoutedEventHandler), typeof(MyCustomControl)); public event RoutedEventHandler MyCustomWheelRotate   {add { AddHandler(CustomWheelEvent, value); }remove { RemoveHandler(CustomWheelEvent, value); }   }   public static readonly RoutedEvent CustomClickEvent =    EventManager.RegisterRoutedEvent("MyCustomClick", RoutingStrategy.Bubble,    typeof(RoutedEventHandler), typeof(MyCustomControl));  public event RoutedEventHandler MyCustomClick   {add { AddHandler(CustomClickEvent, value); }remove { RemoveHandler(CustomClickEvent, value); }   }     protected virtual void RaiseMouseWheelEvent()   {RoutedEventArgs args = new RoutedEventArgs(CustomWheelEvent);RaiseEvent(args);   }   protected virtual void RaiseMouseClickEvent()   {RoutedEventArgs args = new RoutedEventArgs(CustomClickEvent);RaiseEvent(args);   }      }  }  

如果你清楚到这里,你或多或少就通过了。现在,只剩下客户端逻辑了,也就是说,在自定义控件上创建自己的自定义路由事件之后,您想在 WPF 应用程序中使用这个自定义控件,对吧?

这是 MainWindow.xaml 中的实现,用于使用两个路由事件添加您自己的自定义控件。

                            

这是 C# 中的自定义路由事件实现,它将根据您触发的事件更改三个文本框的文本。

using System.Windows;    namespace WpfCustomRoutedEventTutorial  {      ///       /// Interaction logic for MainWindow.xaml      ///       public partial class MainWindow : Window      {   public MainWindow()   {InitializeComponent();   }   private void MyCustomControl_MouseWheel(object sender, RoutedEventArgs e)   {txt1.Text = "Wheel rotated! It is the custom routed event of your custom control";   }     private void MyCustomControl_MouseClick(object sender, RoutedEventArgs e)   {txt1.Text = "Clicked! It is the custom routed event of your custom control";   }     /* The event will be routed to its parent which is a stackpanel.   */   private void StackPanel_Click(object sender, RoutedEventArgs e)   {txt2.Text = "Only the Click event is bubbled to Stack Panel";   }     /* The event will then again be routed to its parent which is the window.   */   private void Window_Click(object sender, RoutedEventArgs e)   {txt3.Text = "Only the Click event is bubbled to Window";   }      }  }  

当上面的代码编译执行后,会产生如下窗口,其中包含一个自定义控件。

现在您指向文本块并旋转鼠标滚轮。你会看到这个。红色文本仅用于指示(它不是输出的一部分)。

现在,如果您单击文本块,您将看到以下内容。

当您单击文本块时,会触发 MouseEnter 事件。在这种情况下,Routed 事件源自 TextBlock 并沿方向移动。

TextBlock > StackPanel > Window.

因此,这些控件的每个事件处理程序都会被触发,并且文本框文本会被更改。到现在为止还挺好。

但是为什么当鼠标滚轮旋转时 Routed 事件没有冒泡?

要理解这一点,您需要知道为什么在第一种情况下触发 MouseEnter 事件时 Routed 事件会冒泡。

如果您在 MainWindow.xaml 文件中注意到,您会看到我在父控件(即 Window 和 StackPanel)中都设置了 MouseDown 事件。MyCustomControl 的 MyCustomClick 事件是触发 Custom_MouseClick() 处理程序时引发的路由事件,这也是引发 MouseDown 事件时。因此,很明显,相同的事件沿可视树向上路由。如果它是一个 MouseDown 事件,那么只有 MouseDown 事件会冒泡,因此,只有所有父控件的 MouseDown 处理程序会被触发。

在鼠标滚轮事件案例中,您会注意到在自定义控件中触发的是 MouseWheel 事件,但在 StackPanel 和 Window 等父控件中没有设置 MouseWheel 事件处理程序。因此,除了原始控件 TextBlock 之外,不会在任何地方处理此 MouseWheel 事件,因为它已经定义了鼠标滚轮事件处理程序。

实际上,您可以在 StackPanel 和 Window 控件中设置鼠标滚轮事件处理程序,并亲自检查真正的乐趣。这是你的家庭作业伙计们!!!

如果在任何时间点,您想在任何特定级别停止路由事件,那么您需要设置 e.Handled = true;

让我们更改 StackPanel_Click 事件,如下所示。

private void StackPanel_Click(object sender, RoutedEventArgs e)   {txt2.Text = "Only the Click event is bubbled to Stack Panel";e.Handled = true;   }  

当你点击 TextBlock 时,你会观察到点击事件不会被路由到 Window 并且会停在 StackPanel 并且第三个文本块不会被更新。