您可能知道,只要在Control对象上启动用户操作并且仅触发指定控件的事件处理程序,就会触发本机CLR事件。此事件不会影响其他控件。例如,您可以在WPF中想到的任何事件本质上都是CLR事件。
相反,路由事件是一种事件类型,它可以在元素树中的多个侦听器上调用处理程序,而不仅仅是引发事件的对象。它基本上是一个由Routed Event类的实例支持的CLR事件。它在WPF事件系统中注册。
为了详细解释路由事件,让我们以WPF UI窗口为例,其中有一些嵌套控件。
- Window
- StackPanel 1
- StackPanel 2
- Textbox 1
- Textbox 2
- Button
- StackPanel 2
- StackPanel 1
我希望上面的结构对你来说是清楚的。请记住此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 文件中自定义控件的样式。
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfCustomRoutedEventTutorial">
<Style TargetType="{x:Type local:MyCustomControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:MyCustomControl}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<TextBlock x:Name = "tbCustomControl" Text = "I am Custom Textblock. Click or Double-click Me!" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
下面给出的是 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 中的实现,用于使用两个路由事件添加您自己的自定义控件。
<Window x:Class="WpfCustomRoutedEventTutorial.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfCustomRoutedEventTutorial"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525" MouseDown="Window_Click">
<Grid>
<StackPanel Margin = "20" MouseDown="StackPanel_Click">
<StackPanel Margin = "10">
<TextBlock Name = "txt1" FontSize = "18" Margin = "5" Text = "This is a TextBlock 1" />
<TextBlock Name = "txt2" FontSize = "18" Margin = "5" Text = "This is a TextBlock 2" />
<TextBlock Name = "txt3" FontSize = "18" Margin = "5" Text = "This is a TextBlock 3" />
</StackPanel>
<local:MyCustomControl MyCustomWheelRotate="MyCustomControl_MouseWheel" MyCustomClick="MyCustomControl_MouseClick" />
</StackPanel>
</Grid>
</Window>
这是 C# 中的自定义路由事件实现,它将根据您触发的事件更改三个文本框的文本。
using System.Windows;
namespace WpfCustomRoutedEventTutorial
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
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 并且第三个文本块不会被更新。