【WPF】路由事件

总结WPF中的路由事件,我将学到的内容分为四部分来逐渐掌握

第一部分:wpf中内置的路由事件

以Button的Click事件来说明内置路由事件的使用

XAML代码:

<Window x:Class="WpfApp1112.Window3"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Window3" Height="300" Width="300">
    <Grid x:Name="gridRoot" Background="Lime" >
        <Grid x:Name="gridA" Margin="10" Background="Blue">
            <Grid.ColumnDefinitions>
                <ColumnDefinition/>
                <ColumnDefinition/>
            </Grid.ColumnDefinitions>
            <Canvas x:Name="canvasLeft" Grid.Column="0" Background="Red" Margin="10">
                <Button x:Name="btnLeft" Content="Left" Width="40" Height="100" Margin="10"/>
            </Canvas>
            <Canvas x:Name="canvasRight" Grid.Column="1" Background="Yellow" Margin="10">
                <Button x:Name="btnRight" Content="Right" Width="40" Height="100" Margin="10"/>
            </Canvas>
        </Grid>        
    </Grid>
</Window>

下面让gridRoot控件侦听Button.Click事件,用AddHandler方法把侦听的事件和事件处理器关联起来。

        public Window3()
        {
            InitializeComponent();
            this.gridRoot.AddHandler(Button.ClickEvent, new RoutedEventHandler(this.ButtonClicked));
        }
        private void ButtonClicked(object sender,RoutedEventArgs e)
        {
            MessageBox.Show((e.OriginalSource  as FrameworkElement).Name);             
        }

因为路由事件是从内部一层一层传递出来到最外层的gridRoot,并且由gridRoot元素将事件消息交给ButtonClicked方法来处理,所以传入ButtonClicked方法的sender参数是gridRoot而不是被单击的Button,所以用e.OriginalSource来查看事件的源头,但必须转换成正确的类型。运行结果如下:

以上是在后台为元素添加路由事件,也可以在XAML里完成,如下:

......
<Grid x:Name="gridRoot" Background="Lime" Button.Click="ButtonClicked"> <Grid x:Name="gridA" Margin="10" Background="Blue">
......

第二部分:自定义路由事件

自定义路由事件分为三个步骤:

(1)声明并注册路由事件

(2)为路由事件添加CLR事件包装

(3)创建可以激发路由事件的方法

下面来创建一个路由事件,这个事件的用途是报告事件发生的时间。

后台代码如下:

    /// <summary>
    /// Window4.xaml 的交互逻辑
    /// </summary>
    public partial class Window4 : Window
    {
        public Window4()
        {
            InitializeComponent();
        }
        //路由事件处理器
        private void ReportTimeHandler(object sender, ReportTimeEventArgs e)
        {
            FrameworkElement element = sender as FrameworkElement;
            string timeStr = e.ClickTime.ToLongTimeString();
            string content = string.Format("{0}到达{1}", timeStr, element.Name);
            this.listBox.Items.Add(content);

            //if (element.Name == "grid_2")
            //{
            //    e.Handled = true;  //stop
            //}
        }
    }

    /// <summary>
    /// 为了让事件消息能携带按钮被单击时的时间,我们创建一个RoutedEventArgs类的派送类,为其添加ClickTime属性;
    /// </summary>
    public class ReportTimeEventArgs : RoutedEventArgs
    {
        public ReportTimeEventArgs(RoutedEvent routedEvent, object source)
            : base(routedEvent, source)
        {
        }
        public DateTime ClickTime
        {
            get;
            set;
        }
    }

    /// <summary>
    /// 创建一派生自Button的TimeButton类型
    /// </summary>
    public class TimeButton : Button
    { 
        //声明和注册路由事件
        public static readonly RoutedEvent ReportTimeEvent = EventManager.RegisterRoutedEvent
        ("ReportTime", RoutingStrategy.Bubble, typeof(EventHandler<ReportTimeEventArgs>), typeof(TimeButton));

        //CLR事件包装器
        public event RoutedEventHandler ReportTime
        {
            add { this.AddHandler(ReportTimeEvent, value); }
            remove { this.RemoveHandler(ReportTimeEvent, value); }
        }

        //激发路由事件,借用Click事件的激发方法
        protected override void OnClick()
        {
            base.OnClick();

            ReportTimeEventArgs args = new ReportTimeEventArgs(ReportTimeEvent, this);
            args.ClickTime = DateTime.Now;
            this.RaiseEvent(args);
        }        
    }

XAML代码如下:

<Window x:Class="WpfApp1112.Window4"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfApp1112"       
        Title="Window4" Height="300" Width="300" Name="window4"
        local:TimeButton.ReportTime="ReportTimeHandler">
    <Grid x:Name="grid_1" local:TimeButton.ReportTime="ReportTimeHandler">
        <Grid x:Name="grid_2" local:TimeButton.ReportTime="ReportTimeHandler">
            <Grid x:Name="grid_3" local:TimeButton.ReportTime="ReportTimeHandler">
                <StackPanel x:Name="stackPanel_1"
                            local:TimeButton.ReportTime="ReportTimeHandler">
                    <ListBox x:Name="listBox"/>
                    <local:TimeButton x:Name="timeButton" Width="80" Height="80" 
                                      Content="报时" local:TimeButton.ReportTime="ReportTimeHandler"/>
                </StackPanel>
            </Grid>
        </Grid>
    </Grid>
</Window>

在前台我们为Window、grid_1、grid_2、grid_3、stackPanel_1、timeButton元素都注册了ReportTime事件。

运行结果如下:

下面来说明下以上代码中的几个重点:

1、注册代码的四个参数:

public static readonly RoutedEvent ReportTimeEvent = EventManager.RegisterRoutedEvent
        ("ReportTime", RoutingStrategy.Bubble, typeof(EventHandler<ReportTimeEventArgs>), typeof(TimeButton));

第一个参数:是string类型,是路由事件的名称,应与CLR事件包装器的名称和路由事件的变量名称前缀一样,即路由事件ReportTimeEvent的前缀ReportTime一样。

第二个参数:路由事件的策略,有三种:

                 Bubble(冒泡式)------路由事件由事件的激发者出发向它的上级容器一层一层路由,直至最外层容器(window或page)。

                 Tunnel(隧道式)------与Bubble相反,从外层向内层一层一层路由。

                 Direct(直达式)-------直接将事件消息送达事件处理器。

ReportTimeEvent策略为Tunnel时,运行结果为:

ReportTimeEvent策略为Direct时,运行结果为:

如果我们想让一个路由事件到某个结点不想让它再继续传递,我们只需要把路由事件携带的事件参数必须是RoutedEventArgs类或其派生类的实例,RoutedEventArgs类具有一个bool类型属性Handled,一旦被设置为true,就表示路由事件“已经被处理”了,路由事件就不会再往下传递了。修改代码如下:

        //路由事件处理器
        private void ReportTimeHandler(object sender, ReportTimeEventArgs e)
        {
            FrameworkElement element = sender as FrameworkElement;
            string timeStr = e.ClickTime.ToLongTimeString();
            string content = string.Format("{0}到达{1}", timeStr, element.Name);
            this.listBox.Items.Add(content);

            if (element.Name == "grid_2")
            {
                e.Handled = true;  //stop
            }
        }

路由策略不管是Bubble或是Tunnel,都是到达元素grid_2时就结束了,运行结果如下:

第三部分:RoutedEventArgs的Source和OriginalSource

简单的总结就是:Source表示的是LogicalTree上的消息源头,而OriginalSource则表示VisualTree上的源头。

第四部分:附加事件

路由事件的宿主拥有可视化实体的界面元素,比如:Button、Slider、TextBox.......

附加事件则不具备显示在用户界面上的能力。比如:

           Binding类:SourceUpdated事件、TargetUpdated事件。

           Mouse类:MouseEnter事件、MouseLeave事件、MouseDown事件、MouseUp事件等。

           Keyboard类:KeyDown事件、KeyUp事件等。

下面用示例来说明,设计一个名为Student的类,如果Student实例的Name属性值发生变化就激发一个路由事件,用界面元素来捕捉这个事件。

后台代码如下:

    /// <summary>
    /// MainWindow.xaml 的交互逻辑
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            Student.AddNameChangedHandler(this.gridMain, new RoutedEventHandler(this.StudentNameChangedHandler));
        }
        private void StudentNameChangedHandler(object sender, RoutedEventArgs e)
        {
            MessageBox.Show((e.OriginalSource as Student).Id.ToString());
        }

        /// <summary>
        /// Click事件处理器
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btn1_Click(object sender, RoutedEventArgs e)
        {
            Student stu = new Student() { Id = 101, Name = "Tim" };
            stu.Name = "Tom";

            //准备事件消息并发送路由事件
            RoutedEventArgs arg = new RoutedEventArgs(Student.NameChangedEvent, stu);
            this.btn1.RaiseEvent(arg);
        }
    }

    public class Student
    {
        //声明并注册事件
        public static readonly RoutedEvent NameChangedEvent = EventManager.RegisterRoutedEvent
            ("NameChanged",RoutingStrategy.Bubble,typeof(RoutedEventHandler),typeof(Student));

        //为界面元素添加路由事件侦听
        public static void AddNameChangedHandler(DependencyObject d, RoutedEventHandler h)
        {
            UIElement e = d as UIElement;
            if (e != null)
            {
                e.AddHandler(Student.NameChangedEvent, h);
            }
        }
        //移除侦听
        public static void RemoveNameChangedHandler(DependencyObject d, RoutedEventHandler h)
        {
            UIElement e = d as UIElement;
            if (e != null)
            {
                e.RemoveHandler(Student.NameChangedEvent, h);
            }
        }

        public int Id { get; set; }
        public string Name{get;set ;}
    }

XAML代码如下:

<Window x:Class="WpfApp1120.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid x:Name="gridMain">
        <Button x:Name="btn1" Content="OK" Width="80" Height="80" Click="btn1_Click"/>        
    </Grid>
</Window>

注意:Student类非派生自UIElement,因此不具备AddHandler和RemoveHandler这两个方法,所以不能使用CLR属性作为包装器(因为CLR属性包装器的add和remove分支分别调用当前对象的AddHandler 和 RemoveHandler)。所以微软规定:

1、为目标UI元素添加附近事件侦听器的包装器是一个名为 Add(事件名称)Handler的Public static 方法。

2、解除UI元素对附加事件侦听的包装器是名为Removea(事件名称)Handler 的Public static 方法。

如上面实例中的 AddNameChangedHandler 和 RemoveNameChangedHandler 方法。

原文地址:https://www.cnblogs.com/lenlen-de/p/3433877.html