【WPF学习】第十四章 事件路由

  由上一章可知,WPF中的许多控件都是内容控件,而内容控件可包含任何类型以及大量的嵌套内容。例如,可构建包含图形的按钮,建立混合了文本和图片内容的标签,或者为了实现滚动或折叠的显示效果而在特定容器中放置内容。设置能够屡次重复嵌套,直至达到你所但愿的层次深度。以下所示:测试

<Window x:Class="RouteEvent.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>
        <Label BorderThickness="1" BorderBrush="Black">
            <StackPanel>
                <TextBlock Margin="3">Image and text label</TextBlock>
                <Image Source="face.jpg" Stretch="Fill"  Width="64" Height="64"></Image>
                <TextBlock Margin="3">Courtesy of the StackPanel</TextBlock>
            </StackPanel>
        </Label>
    </Grid>
</Window>

  正如上面所看到的,放在WPF窗口中的全部要素都在必定层次上继承自UIElement类,包括Label、StackPanel、TextBlock和Image。UIElement定义了一些核心事件。例如,每一个继承自UIElement的类都提供了MouseDown事件和MouseUp事件。spa

  但当单击上面这个特殊标签中的图像部分时,想想会发生什么事情。很明显,引起Image.MouseDown事件和Image.MouseUp事件是合情合理的。但若是但愿采用相同的方式来处理标签上的全部单击事件,该怎么办呢?此时,无论单击了图像、某块文本仍是标签内的空白处,都应当使用相同的代码进行相应。设计

  显然,可为每一个元素的MouseDown或MouseUp事件关联同一个事件处理程序,但这样会是标记变得杂乱无章且难以维护。WPF使用路由事件模型提供了一个更好的解决方案。指针

  路由事件实际上如下列三种方式出现:code

  •   与普通.NET事件相似的直接路由事件(direct event)。它们源于一个元素,不传递给其余元素。例如,MouseEnter事件(当鼠标指针移到元素上时发生)是直接路由事件。
  •   在包含层次中向上传递的冒泡路由事件(bubbling event)。例如,MouseDown事件就是冒泡路由事件。该事件首先由被单击的元素引起,接下来被该元素的父元素引起,而后被父元素的父元素引起,依此类推,直到WPF到达元素树的顶部为止。
  •   在包含层次中向下传递的隧道路由事件(tunneling event)。隧道路由事件在事件到达恰当的控件以前为预览事件(甚至终止事件)提供了机会。例如,经过PreviewKeyDown事件可截获是否按下了某个键。首先在窗口级别上,而后是更具体的容器,直至到达当按下键时具备焦点的元素。

  当使用EventManager.RegisterEvent()方法注册路由事件时,须要传递一个RoutingStrategy枚举值,该值用于指示但愿应用于事件的事件行为。xml

  MouseUp事件和MouseDown事件都是冒泡路由事件,所以如今能够肯定在上面特殊的标签示例中会发生什么事情。当单击标签上的图像部分时,按一下顺序触发MouseDown事件:对象

  (1)Image.MouseDown事件blog

  (2)StackPanel.MouseDown事件继承

  (3)Label.MouseDown事件事件

  为标签引起了MouseDown事件后,该事件会传递到下一个控件(在本例中是位于窗口中的Grid控件),而后传递到Grid控件的父元素(窗口)。窗口时整个层次中的顶级元素,而且是事件冒泡顺序的最后一站,它是处理冒泡路由事件(如MouseDown事件)的最后机会。若是用户释放了鼠标按键,就会按相同的顺序触发MouseUp事件。

  没有限制要在某个位置处理冒泡路由事件。实际上,彻底可在任意层次上处理MouseDown事件或MouseUp事件。但一般选择最合适的事件路由层次完成这一任务。

1、RoutedEventArgs类

  在处理冒泡路由事件时,sender参数提供了对整个链条上最后那个连接的引用。例如,在上面的示例中,若是事件在处理以前,从图像向上冒泡到标签,sender参数就会引用标签对象。

  有些状况下,可能但愿肯定事件最初发生的位置。可从RoutedEventArgs类的属性(以下表所示)得到这一信息以及其余细节。因为全部WPF事件参数类继承自RoutedEventArgs,所以任何事件处理程序均可以使用这些属性。

表 RoutedEventArgs类的属性

 

 2、冒泡路由事件

  以下图显示了一个简单窗口,该窗口演示了事件的冒泡过程。当单击标签中的一部时,在列表框中显示事件发生的顺序。图中显示了单击标签中的图像以后窗口的状况。MouseUp事件传递了5级,在窗体中中止向上传递。

 

  图 冒泡的图像单击事件

  要建立该测试窗口,将元素层次结构中的图像以及它上面的每一个元素都关联到同一个事件处理程序——名为SomethingClicked()的方法。下面是所需的XAML标记:

<Window x:Class="RouteEvent.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="359" Width="329"
        MouseUp="SomethingClicked">
    <Grid Margin="3" MouseUp="SomethingClicked">
        <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" BorderThickness="1" BorderBrush="Black"
               MouseUp="SomethingClicked">
            <StackPanel MouseUp="SomethingClicked">
                <TextBlock Margin="3" MouseUp="SomethingClicked">Image and text label</TextBlock>
                <Image Source="face.jpg" Stretch="Fill"  Width="16" Height="16" MouseUp="SomethingClicked"></Image>
                <TextBlock Margin="3" MouseUp="SomethingClicked">Courtesy of the StackPanel</TextBlock>
            </StackPanel>
        </Label>
        <ListBox Grid.Row="1" Margin="5" Name="lstMessages"></ListBox>
        <CheckBox Grid.Row="2" Margin="5" Name="chkHandle">Handle first event</CheckBox>
        <Button Grid.Row="3" Margin="5" Padding="3" HorizontalAlignment="Right"
                Name="cmdClear" Click="cmdClear_Click">Clear list</Button>
    </Grid>
</Window>

  SomethingClicked()方法简单地检查RoutedEventArgs对象的属性,而且给列表框添加消息:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace RouteEvent
{
    /// <summary>
    /// MainWindow.xaml 的交互逻辑
    /// </summary>
    public partial class MainWindow : Window
    {
        protected int eventCounter = 0;
        public MainWindow()
        {
            InitializeComponent();
        }
        private void SomethingClicked(object sender, RoutedEventArgs e)
        {
            eventCounter++;
            string message = "#" + eventCounter.ToString() + ":\r\n" +
                " Sender: " + sender.ToString() + "\r\n" +
                " Source: " + e.Source + "\r\n" +
                " Original Source: " + e.OriginalSource + "\r\n";
            lstMessages.Items.Add(message);
            e.Handled = (bool)chkHandle.IsChecked;
        }

        private void cmdClear_Click(object sender, RoutedEventArgs e)
        {
            eventCounter = 0;
            lstMessages.Items.Clear();
        }
    }
}

  在本例中还有一个细节。若是选中chkHandle复选框,SomethingClicked()方法就将RoutedEventArgs.Handled属性设为true,从而在事件第一次发生时就终止事件的冒泡过程。所以,这时在列表框中就只能看到第一个事件,以下图所示:

 

   由于SomethingClicked()方法处理由Window对象引起的MouseUp事件,因此也能截获在列表框和窗口表面空白处的鼠标单击事件。但当单击Clear按钮时(这会删除全部列表框条目)不会引起MouseUp事件,这时由于按钮包含了一些有趣的代码,这些代码会挂起MouseUp事件,并引起更高级的Click事件。同时,Handled标记被设置为true,从而会阻止MouseUp事件继续传递。

3、处理挂起的事件

  有一种方法可接受被标记处理过的事件。不是经过XAML关联事件处理程序,而是必须使用前面介绍的AddHandler()方法。AddHandler()方法提供了一个重载版本,该版本能够接收一个Boolean值做为它的第三个参数。若是将该参数设置为true,那么即便设置了Handled标记,也将接收到事件:

cmdClear.AddHandler(UIElement.MouseUpEvent,new MouseButtonEventHandler(cmdClear_MouseUp),true);

  这一般并非正确的设计决策。为防止可能形成的困惑,按钮被设计为会挂起MouseUp事件。毕竟,可采用多种方式使用键盘“单击”按钮,这是Windows中很是广泛的约定。若是为按钮错误地处理了MouseUp事件,而没有处理Click事件,那么事件处理代码就只能对鼠标单击作出相应,而不能对相应的键盘操做作出相应。

4、附加事件

  上面这个有趣的标签示例是一个很是简单的事件冒泡示例,由于全部的元素都支持MouseUp事件。然而,许多控件有各自的特殊事件。按钮即是一个例子——它添加了Click事件,而其余任何基类都没有定义该事件。

  这致使两难的境地。假设在StackPanel面板中封装了一堆按钮,并但愿在一个事件处理程序中处理全部这些按钮的单击事件。粗略的方法是将每一个按钮的Click事件关联到同一个事件处理程序。但Click事件支持事件冒泡,从而提供了一种更好的选择。可经过处理更高层次元素的Click事件(如包含按钮的StackPanel面板)来处理全部按钮的Click事件。

  但看似浅显的代码却不能工做:

<StackPanel Click="DoSomething" Margin="5">
    <Button Name="cmd1">Command 1</Button>
    <Button Name="cmd2">Command 2</Button>
    <Button Name="cmd3">Command 3</Button>
    ...
</StackPanel>

  问题在于StackPanel面板没有Click事件,因此XAML解析器会将其解释错误。解决方案是以“类名.事件名"的形式使用不一样的关联事件语法。下面是更正后的示例:

<StackPanel Button.Click="DoSomething" Margin="5">
    <Button Name="cmd1">Command 1</Button>
    <Button Name="cmd2">Command 2</Button>
    <Button Name="cmd3">Command 3</Button>
    ...
</StackPanel>

  如今,事件处理程序能够接收到StackPanel面板包含的全部按钮的单击事件了。

  可在代码中关联附加事件,但须要使用UIElement.AddHandler()方法,而不能使用+=运算符语法。下面是一个示例(该例假定StackPanel面板已被命名为pnlButtons):

pnlButtons.AddHandler(Button.Click,new RoutedEventHandler(DoSomething));

  在DoSomething()事件处理程序中,可以使用多种方法肯定是哪一个按钮引起了事件。能够比较按钮的文本(对与本地化这可能会引发问题),也能够比较按钮的名称(这是脆弱的方法,由于当构建应用程序时没法捕获输入错误的名称)。最好确保每一个按钮在XAML中都有Name属性设置,从而能够经过窗口类的一个字段访问相应的对象,并使用事件发送者比较应用。下面列举一个示例:

private void DoSomething(object sender,RoutedEventArgs e)
{
    if(sender==cmd1)
    {
        ...
    }
    else if(sender==cmd2)
    {
        ...
    }
    else if(sender==cmd3)
    {
        ...
    }
    ...
}

  另外一个选择是简单地随按钮传递一段能够在代码中使用的信息。好比设置每一个按钮的Tag属性。在此不列举出具体实例。

5、隧道路由事件

  随着路由事件的工做方式和冒泡路由事件相同,当方向相反。例如,若是MouseUp事件是隧道路由事件(实际上不是),在特殊的标签示例中单击图形将致使MouseUp事件首先在窗口中被引起,而后在Grid控件中被引起,接下来在StackPanel面板中呗引起,依此类推,直至到达实际源头,即标签中的图像为止。

  隧道路由事件易于识别,他们都以单词Preview开头。并且,WPF一般成对地定义冒泡路由事件和隧道路由事件。这意味着若是发现冒泡的MouseUp事件,就还能够找到PreviewMouseUp隧道事件。隧道路由事件总在冒泡路由事件以前被触发。以下图所示:

 

  更有趣的是,若是将隧道路由事件标记为已处理过,那就不会发生冒泡路由事件。这是由于两个事件共享RoutedEventArgs类的同一个实例。

  若是须要执行一些预处理(根据键盘上特定的键执行动做或过滤掉特定的鼠标动做),隧道路由事件是很是有用的。

  以下面实例所示,该例测试PreviewKeyDown事件的隧道过程。当在文本框按下一个键时,事件首先在窗口触发,而后再整个层次结构中向下传递。若是在任何位置将PreviewKeyDown事件标记为已处理过,就不会发生冒泡的KeyDown事件。

 

 下面是所需的XAML标记:

<Window x:Class="TunnelRouteEvent.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="359" Width="329"
        PreviewKeyDown="SomethingClicked">
    <Grid Margin="3" PreviewKeyDown="SomethingClicked">
        <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" BorderThickness="1" BorderBrush="Black"
               PreviewKeyDown="SomethingClicked">
            <StackPanel PreviewKeyDown="SomethingClicked">
                <TextBlock Margin="3" PreviewKeyDown="SomethingClicked">Image and text label</TextBlock>
                <Image Source="face.jpg" Stretch="Fill"  Width="16" Height="16" PreviewKeyDown="SomethingClicked"></Image>
                <TextBox Margin="3" PreviewKeyDown="SomethingClicked"></TextBox>
            </StackPanel>
        </Label>
        <ListBox Grid.Row="1" Margin="5" Name="lstMessages"></ListBox>
        <CheckBox Grid.Row="2" Margin="5" Name="chkHandle">Handle first event</CheckBox>
        <Button Grid.Row="3" Margin="5" Padding="3" HorizontalAlignment="Right"
                Name="cmdClear" Click="cmdClear_Click">Clear list</Button>
    </Grid>
</Window>

后台代码以下所示:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace TunnelRouteEvent
{
    /// <summary>
    /// MainWindow.xaml 的交互逻辑
    /// </summary>
    public partial class MainWindow : Window
    {
        protected int eventCounter = 0;
        public MainWindow()
        {
            InitializeComponent();
        }
        private void SomethingClicked(object sender, RoutedEventArgs e)
        {
            eventCounter++;
            string message = "#" + eventCounter.ToString() + ":\r\n" +
                " Sender: " + sender.ToString() + "\r\n" +
                " Source: " + e.Source + "\r\n" +
                " Original Source: " + e.OriginalSource + "\r\n" +
                " Event: " + e.RoutedEvent;
            lstMessages.Items.Add(message);
            e.Handled = (bool)chkHandle.IsChecked;
        }

        private void cmdClear_Click(object sender, RoutedEventArgs e)
        {
            eventCounter = 0;
            lstMessages.Items.Clear();
        }
    }
}
相关文章
相关标签/搜索