WPF高级教程(七)路由事件

对着背影说爱祢 提交于 2020-02-26 11:09:02

介绍

与依赖项属性一样,路由事件是WPF对于传统.NET事件的升级,使得事件拥有更强的传播能力。

定义,注册和包装

// 我们来看一个Click事件定义的例子
public abstract class ButtonBase : ContentControl
{
    // 定义路由事件
    public static readonly RoutedEvent ClickEvent;
    
    // 注册路由事件
    static ButtonBase()
    {
        ButtonBase.ClickEvent = EventManager.RegisterRoutedEvent("Click", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ButtonBase));
    }
    
    // 普通事件包装路由事件
    public event RoutedEventHandler Click
    {
        add
        {
            // AddHandler 和 RemoveHandler都是再FrameworkElement中定义的
            base.AddHandler(ButtonBase.ClickEvent, value);
        }
        remove
        {
            base.RemoveHandler(ButtonBase.ClickEvent, value);
        }
    }
}

共享

通过上面的定义我们可以看到,与依赖项属性一样,路由事件也是静态定义,包装为普通事件使用,那么我们自然就可以推测我们可以像依赖项属性一样,将别的类的路由事件作为己用,这里我们需要使用 RoutedEvent.AddOwner()方法。

// UIElement类添加Mouse类的MouseUp事件
UIElement.MouseUpEvent = Mouse.MouseUpEvent.AddOwner(typeof(UIElement));

使用

路由事件的引发

使用RaiseEvent方法引发路由事件

RoutedEventArgs e = new RoutedEventArgs(ButtonBase.ClickEvent, this);
base.RaiseEvent(e);

路由事件的处理

监听事件

// 在xaml中直接处理
<button Click="cmdOK_Click">OK</Button>
// 后台代码连接事件
img.MouseUp += new MouseButtonEventHandler(img_MouseUp);
// 甚至可以简化代码
img.MouseUp += img_MouseUp;
// 直接调用AddHandler方法,不通过事件包装器绑定
img.AddHandler(Image.MouseUpEvent, new MouseButtonEventHandler(image_MouseUp));
// 由于我们之前讲过的,这是一个共享事件,UIElement Image Mouse中的MouseUp方法都是共享的,也可以AddHandler到UIElement中处理,这两种是等价的。唯一的区别就是不写Image不太容易看出MouseUp是由Image引发的
img.AddHandler(UIElement.MouseUpEvent, new MouseButtonEventHandler(image_MouseUp));

事件处理程序都有一个Args的参数,Args都继承自RoutedEventArgs,包含下面的属性
在这里插入图片描述

断开路由事件

断开路由事件不能在xaml中实现,必须使用代码 -=

// 使用-=运算符
img.MouseUp -= img_MouseUp;
// 直接使用RemoveHandler方法
img.RemoveHandler(Image.MouseUpEvent, new MouseButtonEventHandler(image_MouseUp));

路由事件的分类

分类描述

  • 直接路由事件 起源于一个元素,不会传递给下一个元素
  • 冒泡路由事件 沿着元素树向上传递(MouseUp事件),如果不处理的话,一直传递到元素树最上层元素
  • 隧道路由事件 沿着元素树向下传递(KeyDown事件),隧道事件为提前处理事件提供了机会,比如PreviewKeyDown事件

设置事件种类

在使用EventManager.RegistEvent方法注册一个事件的时候需要传递一个RoutingStrategy的枚举值,设置事件的种类。

1. 冒泡路由事件

xaml代码:

<Window x:Class="Charles.WPF.View.TestBubblingEvent"
        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:Charles.WPF.View"
        mc:Ignorable="d"
        Title="TestBubblingEvent" Height="359" Width="329" MouseUp="SomethingClick">
    <Grid>
        <Grid Margin="3" MouseUp="SomethingClick">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"></RowDefinition>
                <RowDefinition Height="*"></RowDefinition>
                <RowDefinition Height="Auto"></RowDefinition>
                <RowDefinition Height="Auto"></RowDefinition>
            </Grid.RowDefinitions>
            <Label Margin="5" Grid.Row="0" HorizontalAlignment="Left"
                   Background="AliceBlue" BorderBrush="Black" BorderThickness="1" MouseUp="SomethingClick">
                <StackPanel MouseUp="SomethingClick">
                    <TextBlock Margin="3" MouseUp="SomethingClick">Image and text label</TextBlock>
                    <Image Source="/Image/1.jpg" Stretch="None" MouseUp="SomethingClick" Width="30" Height="30"></Image>
                    <TextBlock Margin="3" MouseUp="SomethingClick">Courtesy of the StackPanel.</TextBlock>
                </StackPanel>
            </Label>
            <ListBox Grid.Row="1" Margin="5" Name="lstMessage"></ListBox>
            <CheckBox Grid.Row="2" Margin="5" Padding="3" HorizontalAlignment="Right"
                      Name="chk_Handle">Handle first event</CheckBox>
            <Button Grid.Row="3" Margin="5" Padding="3" HorizontalAlignment="Right" Name="cmd_Clear" Click="cmd_Clear_Click">
                Clear List
            </Button>
        </Grid>
    </Grid>
</Window>

后台代码:

public partial class TestBubblingEvent : Window
{
    protected int eventCount = 0;
    public TestBubblingEvent()
    {
        InitializeComponent();
    }

    private void SomethingClick(object sender, MouseButtonEventArgs e)
    {
        eventCount++;
        string message = "#" + eventCount.ToString() + ":\r\n" +
            " Sender: " + sender.ToString() + ":\r\n" +
            " Source: " + e.Source + ":\r\n" +
            " Original Source: " + e.OriginalSource;
        lstMessage.Items.Add(message);
        e.Handled = (bool)chk_Handle.IsChecked;
    }

    private void cmd_Clear_Click(object sender, RoutedEventArgs e)
    {
        lstMessage.Items.Clear();
    }
}

这个例子说明了冒泡事件的事件传递顺序,点击图片的时候,事件由Image触发,层层向上传递,不碰到e.Handled = True不会终止传递。

一些冒泡事件的技巧:

  • Sender是当前事件触发的控件,Source是触发源
  • 事件的处理使用 MouseButtonEventArgs 和 RoutedEventArgs都是可以的
  • 我们也监听了窗口的MouseUp事件,这就让我们在窗口的任意空白位置点击之后都会触发MouseUp事件,但是我们发现,点击按钮的时候不触发窗口的MouseUp事件,而是触发Button的Click事件,这是因为Button的源代码中挂起了MouseUp事件,并且引发了一个更高级的Click事件
  • WinForm中,大多数控件都拥有Click事件,在WPF中,只有少数控件拥有Click事件
  • 有一种方法可以监听到Handled为True的事件,虽然这是不推荐的,但是是可以实现的,通过传递最后一个参数true,可以接收到挂起的事件(不推荐)
    cmdClear.AddHandler(UIElement.MouseUpEvent, new MouseButtonEventHandler(cmdClear_MouseUp), true);
    
  • 上面我们讲到,大部分控件都没有Click事件,那Click事件如何冒泡呢?事实上,Click事件支持冒泡,但是处理Click事件需要一些特殊的技巧
    // StatckPanel 没有Click但是要监听里面Button的Click
    // 用下面的方法是不行的
    <StackPanel Click="DoSomething">
        <Button/>
        <Button/>
        <Button/>
    </StackPanel>
    
    这样写会报错,因为StackPanel并没有Click事件,这时候我们想要监听所有Button的Click事件,需要用到一个附件事件的技巧
    <StackPanel Button.Click="DoSomething">
        <Button/>
        <Button/>
        <Button/>
    </StackPanel>
    
    使用类名加上事件可以获取StackPanel中Button的Click事件,这就是附加属性的使用。在代码中,需要注意不能使用 += 的方法进行事件处理方法的绑定,因为+=默认就绑定到StackPanel上了,需要使用AndHandler方法
    // StackPanel 的名字为 pnlButtons
    pnlButtons.AddHandler(Button.Click, new RoutedEventHandler(DoSomething));
    
    要确定是StackPanel中哪个按钮引发了事件,可以使用下面的方法
    // 方法1. 使用控件的x:Name属性
    private void DoSomething(object sender, RoutedEventArgs args)
    {
        // cmd1是按钮控件1的Name属性
        if(sender == cmd1)
        {
            // 触发的是第一个按钮
        }
        else if(sender == cmd1)
        ...
    }
    // 方法2. 给按钮添加Tag
    <Button Tag="first button"/>
    
    object tag = ((FrameworkElement)sender).Tag;
    

2. 隧道路由事件

  • 隧道路由事件的工作方式和冒泡路由事件相同,但是方向相反
  • 隧道路由事件都是以Preview开头的事件
  • 隧道路由事件和冒泡路由事件公用同一个RoutedEventArgs,所以如果把隧道路由事件标记为已处理,冒泡路由事件就不会触发,这个属性很适合用于做预处理
  • 隧道事件总是在冒泡事件之前触发
  • 从下图可以看到,事件的触发先下去后上来,所以在任意中间元素中让e.Handler=true则冒泡事件都不会触发
    在这里插入图片描述
标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!