WPF路由事件RegisterRoutedEvent

2 篇文章 0 订阅
1 篇文章 0 订阅

您可能知道,只要在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 文件中自定义控件的样式。

<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 并且第三个文本块不会被更新。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值