WPF中文网

什么是路由事件

我们曾在《LogicalTree逻辑树》那一章节中了解了WPF的XAML代码其实是由控件的代码组成的一棵树结构。针对于一个窗体而言,树根就是Window对象。考虑下面这个布局:

<Window x:Class="HelloWorld.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:HelloWorld" 
        xmlns:controls="clr-namespace:HelloWorld.Controls"
        xmlns:helper="clr-namespace:HelloWorld.MVVM"
        mc:Ignorable="d" 
        Title="WPF中文网 - www.wpfsoft.com" Height="350" Width="500">
    <Window.DataContext>
        <local:MainViewModel/>
    </Window.DataContext>
    <Border>
        <Canvas>
            <Button Content="确定" Width="80" Height="30" Canvas.Left="150" Canvas.Top="100"/>
            <Button Content="取消" Width="80" Height="30" Canvas.Left="280" Canvas.Top="100"/>
        </Canvas>
    </Border>
</Window>

这个布局最终形成的逻辑树应该是下面这个样子

<Window>
    <Border>
        <Canvas>
            <Button/>
            <Button/>
        </Canvas>
    </Border>
</Window>    

有了这棵树,当我们单击树上的确定按钮或者取消按钮时,会引发单击事件,但是,这个事件是如何被WPF侦听的呢?在这里,我们便引出了WPF的路由事件的概念。

一、路由事件的概念

在WPF的元素树中,若某一个元素引发了一个事件,那么这个事件会沿着整棵树进行传播,而开发者可以在事件传播的沿途进行侦听(有点像设立关卡打劫)。一旦侦听到这个事件,便可以执行事件的回调函数。当然,只有被声明为RoutedEvent路由事件才具备传播功能。

我们以上面的代码为例,假如用户单击了确定按钮,此时,从整个窗体的视角来看,窗体会说,哎呀,你把我给单击了,且单击的是我其中的确定按钮,于是,这个单击事件首先会经历第一个关卡——Window对象。如果开发者恰好订阅了Window对象的单击事件,那首先被执行的就是Window窗体的单击事件回调函数。紧接着单击事件会经历第二道关卡——Border对象,第三道关卡——Canvas对象,直到确定按钮为止。可见这个单击事件一路经历了4个控件,它们分别是Window->Border->Canvas->Button,开发者可以在这4个控件上去订阅单击事件。像这个从根目录一直路由到事件源对象的路由事件,我们称为隧道事件(预览事件),这类事件都是Preview单词开头。

但是,从Button按钮的视角,用户肯定是先单击的我呀,我这里才是事件源,事件应该就像小孩向水中投石之后,平静的水面会泛起一圈圈的涟漪,最终消失在岸边——即最外层的Window窗体对象。那么,此时的事件路由方向就反过来了,Button->Canvas->Border->Window。像这种从事件源一直路由到元素树根的路由事件,我们称为冒泡事件。

除了这两种事件,WPF还支持直接事件,即只有事件源才能响应触发的事件,我们称为直接事件。三种事件由定义事件时通过RoutingStrategy 枚举进行标识。

//
// 摘要:
//     指示路由事件的路由策略。
public enum RoutingStrategy
{
    //
    // 摘要:
    //     路由事件使用隧道策略,以便事件实例通过树向下路由(从根到源元素)。
    Tunnel = 0,
    //
    // 摘要:
    //     路由事件使用冒泡策略,以便事件实例通过树向上路由(从事件元素到根)。
    Bubble = 1,
    //
    // 摘要:
    //     路由事件不通过元素树路由,但支持其他路由的事件功能。
    Direct = 2
}

第二、路由事件的订阅

接下来,我们分别演示隧道事件和冒泡事件如何使用。

2.1 隧道事件

前端代码

<Window x:Class="HelloWorld.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:HelloWorld" 
        xmlns:controls="clr-namespace:HelloWorld.Controls"
        xmlns:helper="clr-namespace:HelloWorld.MVVM"
        mc:Ignorable="d" 
        PreviewMouseUp="Window_PreviewMouseUp"
        Title="WPF中文网 - www.wpfsoft.com" Height="350" Width="500">
    <Window.DataContext>
        <local:MainViewModel/>
    </Window.DataContext>
    <Border PreviewMouseUp="Border_PreviewMouseUp">
        <Canvas PreviewMouseUp="Canvas_PreviewMouseUp">
            <Button PreviewMouseUp="Button_PreviewMouseUp"
                    Content="确定" Width="80" Height="30" 
                    Canvas.Left="150" Canvas.Top="100"/>
            <Button PreviewMouseUp="Button_PreviewMouseUp_1" 
                    Content="取消" Width="80" Height="30" 
                    Canvas.Left="280" Canvas.Top="100"/>
        </Canvas>
    </Border>
</Window>

后端代码

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private void Window_PreviewMouseUp(object sender, MouseButtonEventArgs e)
    {
        Console.WriteLine($"Window对象的隧道事件PreviewMouseUp被触发");
    }

    private void Border_PreviewMouseUp(object sender, MouseButtonEventArgs e)
    {
        Console.WriteLine($"Border对象的隧道事件PreviewMouseUp被触发");

    }

    private void Canvas_PreviewMouseUp(object sender, MouseButtonEventArgs e)
    {
        Console.WriteLine($"Canvas对象的隧道事件PreviewMouseUp被触发");

    }

    private void Button_PreviewMouseUp(object sender, MouseButtonEventArgs e)
    {
        Console.WriteLine($"Button确定按钮的隧道事件PreviewMouseUp被触发");

    }

    private void Button_PreviewMouseUp_1(object sender, MouseButtonEventArgs e)
    {
        Console.WriteLine($"Button取消按钮的隧道事件PreviewMouseUp被触发");

    }
}

我们为Window、Border、Canvas、Button的PreviewMouseUp隧道事件都订阅了回调函数,然后F5调试运行,并单击确定按钮,观察输出结果如下:

Window对象的隧道事件PreviewMouseUp被触发
Border对象的隧道事件PreviewMouseUp被触发
Canvas对象的隧道事件PreviewMouseUp被触发
Button确定按钮的隧道事件PreviewMouseUp被触发

从输出结果看,虽然单击的确定按钮,但是首先被触发的却是元素树的根元素Window对象,最后才是Button对象。另外,取消按钮与确定按钮没有嵌套关系,它们是平级关系,所以单击确定按钮时,不会触发取消按钮的PreviewMouseUp事件。

2.2 冒泡事件

接下来,我们来演示冒泡事件的使用

前端代码

<Window x:Class="HelloWorld.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:HelloWorld" 
        xmlns:controls="clr-namespace:HelloWorld.Controls"
        xmlns:helper="clr-namespace:HelloWorld.MVVM"
        mc:Ignorable="d" 
        MouseUp="Window_MouseUp"
        Title="WPF中文网 - www.wpfsoft.com" Height="350" Width="500">
    <Window.DataContext>
        <local:MainViewModel/>
    </Window.DataContext>
    <Border MouseUp="Border_MouseUp" Background="Transparent">
        <Canvas MouseUp="Canvas_MouseUp" Background="Transparent">
            <Button MouseUp="Button_MouseUp" 
                    Content="确定" Width="80" Height="30" 
                    Canvas.Left="150" Canvas.Top="100"/>
            <Button MouseUp="Button_MouseUp_1"
                    Content="取消" Width="80" Height="30" 
                    Canvas.Left="280" Canvas.Top="100"/>
        </Canvas>
    </Border>
</Window>

后端代码

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private void Window_MouseUp(object sender, MouseButtonEventArgs e)
    {
        Console.WriteLine($"Window对象的冒泡事件MouseUp被触发");
    }

    private void Border_MouseUp(object sender, MouseButtonEventArgs e)
    {
        Console.WriteLine($"Border对象的冒泡事件MouseUp被触发");
    }
    private void Canvas_MouseUp(object sender, MouseButtonEventArgs e)
    {
        Console.WriteLine($"Canvas对象的冒泡事件MouseUp被触发");
    }

    private void Button_MouseUp(object sender, MouseButtonEventArgs e)
    {
        Console.WriteLine($"Button1的冒泡事件MouseUp被触发");
    }       
    

    private void Button_MouseUp_1(object sender, MouseButtonEventArgs e)
    {
        Console.WriteLine($"Button2的冒泡事件MouseUp被触发");
    }
}

然后F5运行单击确定按钮,此时我们会发现并未有任何输出,这是因为Button的MouseUp事件已经在内部被处理掉了,由Click事件代替了,所以中断事件的广播方式是:e.Handled = true;

但是, 我们任然可以在窗体的空白处单击鼠标,此时会有出下输出:

Canvas对象的冒泡事件MouseUp被触发
Border对象的冒泡事件MouseUp被触发
Window对象的冒泡事件MouseUp被触发

重庆教主说

注意哦,在上面的代码中,Canvas、Border的背景颜色必须赋值(哪怕是透明色)才能响应鼠标的单击事件。

总结:第一点,从输出结果看,隧道事件是从根元素路由到事件源,冒泡事件是从事件源路由到根元素。第二点,如果同时订阅了隧道事件和冒泡事件,那么两条路由路线都将执行。谁先谁后?隧道事件先完成路由!

Window对象的隧道事件PreviewMouseUp被触发
Border对象的隧道事件PreviewMouseUp被触发
Canvas对象的隧道事件PreviewMouseUp被触发
Canvas对象的冒泡事件MouseUp被触发
Border对象的冒泡事件MouseUp被触发
Window对象的冒泡事件MouseUp被触发

当前课程源码下载:(注明:本站所有源代码请按标题搜索)

文件名:086-《什么是路由事件》-源代码
链接:https://pan.baidu.com/s/1yu-q4tUtl0poLVgmcMfgBA
提取码:wpff

——重庆教主 2023年10月27日

copyright @重庆教主 WPF中文网 联系站长:(QQ)23611316 (微信)movieclip (QQ群).NET小白课堂:864486030 | 本文由WPF中文网原创发布,谢绝转载 渝ICP备2023009518号-1