由上一章可知,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事件。

  但当单击上面这个特殊标签中的图像部分时,想一想会发生什么事情。很明显,引发Image.MouseDown事件和Image.MouseUp事件是合情合理的。但如果希望采用相同的方式来处理标签上的所有单击事件,该怎么办呢?此时,不管单击了图像、某块文本还是标签内的空白处,都应当使用相同的代码进行相应。

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

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

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

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

  MouseUp事件和MouseDown事件都是冒泡路由事件,因此现在可以确定在上面特殊的标签示例中会发生什么事情。当单击标签上的图像部分时,按一下顺序触发MouseDown事件:

  (1)Image.MouseDown事件

  (2)StackPanel.MouseDown事件

  (3)Label.MouseDown事件

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

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

一、RoutedEventArgs类

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

  有些情况下,可能希望确定事件最初发生的位置。可从RoutedEventArgs类的属性(如下表所示)获得这一信息以及其他细节。由于所有WPF事件参数类继承自RoutedEventArgs,因此任何事件处理程序都可以使用这些属性。

表 RoutedEventArgs类的属性

二、冒泡路由事件

  如下图显示了一个简单窗口,该窗口演示了事件的冒泡过程。当单击标签中的一部时,在列表框中显示事件发生的顺序。图中显示了单击标签中的图像之后窗口的情况。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 = ;
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 = ;
lstMessages.Items.Clear();
}
}
}

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

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

三、处理挂起的事件

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

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

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

四、附加事件

  上面这个有趣的标签示例是一个非常简单的事件冒泡示例,因为所有的元素都支持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属性。在此不列举出具体实例。

五、隧道路由事件

  随着路由事件的工作方式和冒泡路由事件相同,当方向相反。例如,如果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 = ;
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 = ;
lstMessages.Items.Clear();
}
}
}

【WPF学习】第十四章 事件路由的更多相关文章

  1. 【WPF学习】第十三章 理解路由事件

    每个.NET开发人员都熟悉“事件”的思想——当有意义的事情发生时,由对象(如WPF元素)发送的用于通知代码的消息.WPF通过事件路由(event routing)的概念增强了.NET事件模型.事件路由 ...

  2. C#图解教程 第十四章 事件

    事件 发布者和订阅者源代码组件概览声明事件订阅事件触发事件标准事件的用法 通过扩展EventArgs来传递数据移除事件处理程序 事件访问器 事件 发布者和订阅者 很多程序都有一个共同的需求,既当一个特 ...

  3. 【WPF学习】第四章 加载和编译XAML

    前面已经介绍过,尽管XAML和WPF这两种技术具有相互补充的作用,但他们也是相互独立的.因此,完全可以创建不使用XAML和WPF应用程序. 总之,可使用三种不同的编码方式来创建WPF应用程序: 只使用 ...

  4. Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十四章:曲面细分阶段

    原文:Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十四章:曲面细分阶段 代码工程地址: https://github. ...

  5. 《Linux命令行与shell脚本编程大全》 第十四章 学习笔记

    第十四章:呈现数据 理解输入与输出 标准文件描述符 文件描述符 缩写 描述 0 STDIN 标准输入 1 STDOUT 标准输出 2 STDERR 标准错误 1.STDIN 代表标准输入.对于终端界面 ...

  6. C++ Primer Plus学习:第十四章

    第十四章 C++中的代码重用 包含对象成员的类 将类的对象作为新类的成员.称为has-a关系.使用公有继承的时候,类可以继承接口,可能还有实现(纯虚函数不提供实现,只提供接口).使用包含时,可以获得实 ...

  7. 【odoo14】第十四章、CMS网站开发

    第十四章.CMS网站开发** Odoo有一个功能齐全的内容管理系统(CMS).通过拖放功能,你的最终用户可以在几分钟内设计一个页面,但是在Odoo CMS中开发一个新功能或构建块就不是那么简单了.在本 ...

  8. JavaScript高级程序设计:第十四章

    第十四章 一.表单的基础知识 在HTML中,表单是由<form>元素来表示的,而在javascript中,表单对应的则是HTMLFormElement类型.HTMLFormElement继 ...

  9. 第十四章——循环神经网络(Recurrent Neural Networks)(第二部分)

    本章共两部分,这是第二部分: 第十四章--循环神经网络(Recurrent Neural Networks)(第一部分) 第十四章--循环神经网络(Recurrent Neural Networks) ...

随机推荐

  1. css给span加float:right右浮动后内容换行下移

    转自:https://www.jb51.net/css/67309.html 在div css布局中 当span标签右浮动时会产生换行狭义的现象 <!DOCTYPE html PUBLIC &q ...

  2. day01_2spring3

    Bean基于XML和基于注解的装配 一.Bean基于XML的装配 1.生命周期接着day01_1来讲(了解) Bean生命周期的如图所示:用红色框起来的都是我们要研究的! 如图Bean is Read ...

  3. 根据CPU内核创建多进程

    from multiprocessing import Pool import psutil cpu_count = psutil.cpu_count(logical=False) #1代表单核CPU ...

  4. 【Python】循环控制保留字

  5. 使用NSIS制作可执行程序的安装包

    使用NSIS制作可执行程序的安装包: 1,NSIS下载地址:https://pan.baidu.com/s/1GzzQNXgAlJPJWgjBzVwceA 下载完成之后解压缩,打开安装程序,默认安装即 ...

  6. java json转换工具类

    在java项目中,通常会用到json类型的转换,常常需要对 json字符串和对象进行相互转换. 在制作自定义的json转换类之前,先引入以下依赖 <!--json相关工具--><de ...

  7. SpringBoot整合WEB开发--(四)@ControllerAdvice

    1.全局异常处理: @ControllerAdvice处理全局数据,一般搭配@ExceptionHandler,@ModelAttribute以及@InitBinder使用. @ControllerA ...

  8. 链接测试工具:Xenu

    Xenu 是一款深受业界好评,并被广泛使用的死链接检测工具.时常检测网站并排除死链接,对网站的 SEO(搜索引擎优化) 非常重要,因为大量死链接存在会降低用户和搜索引擎对网站的信任. 最大支持100线 ...

  9. YAML(YML)语法详解

    ansible playbook是由yaml(yml)语法书写,结构清晰,可读性强,所以必须掌握yaml(yml)基础语法 语法 描述 锁进  YAML使用固定的缩进风格表示层级结构,每个缩进由两个空 ...

  10. Redis06——Redis五大数据类型 list

    list 单键多值 Redis列表是简单的字符串列表,按照插入顺序排序,可以添加左边/右边 底层实际上是一个双向链表,对两端的操作性能好,但是通过索引下标的操作中间节点性能较差  lpush/rpus ...