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

原文:【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事件。

  但当单击上面这个特殊标签中的图像部分时,想一想会发生什么事情。很明显,引发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 = 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事件继续传递。

三、处理挂起的事件

  有一种方法可接受被标记处理过的事件。不是通过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 = 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();
        }
    }
}

原文地址:https://www.cnblogs.com/lonelyxmas/p/12285953.html

时间: 2024-10-11 16:39:33

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

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

每个.NET开发人员都熟悉“事件”的思想——当有意义的事情发生时,由对象(如WPF元素)发送的用于通知代码的消息.WPF通过事件路由(event routing)的概念增强了.NET事件模型.事件路由允许源自某个元素的事件由另一个元素引发.例如,使用事件路由,来自工具栏按钮的单击事件可在被代码处理之前上传到工具栏,然后上传到包含工具栏的窗口. 事件路由为在最合适的位置编写紧凑的.组织良好的用于处理事件的代码提供了灵活性.要使用WPF内容模型,事件路由也是必需的,内容模型允许使用许多不同的元素构建

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

前面已经介绍过,尽管XAML和WPF这两种技术具有相互补充的作用,但他们也是相互独立的.因此,完全可以创建不使用XAML和WPF应用程序. 总之,可使用三种不同的编码方式来创建WPF应用程序: 只使用代码.这是在Visual Studio中为Windows窗体应用程序使用的传统方法.它通过代码语句生成用户界面. 使用代码和未经编译的标记(XAML).这种具体方式对于某些特殊情况是很有意义的,例如创建高度动态化的用户界面.这种方式在运行时使用System.Windows.Markup名称空间中的X

AI - 深度学习入门十四章- 摘要1

原文链接:https://yq.aliyun.com/topic/111 01 - 一入侯门"深"似海,深度学习深几许 什么是"学习"? "如果一个系统,能够通过执行某个过程,就此改进了它的性能,那么这个过程就是学习". 学习的核心目的,就是改善性能. 什么是机器学习? 定义1: 对于计算机系统而言,通过运用数据及某种特定的方法(比如统计的方法或推理的方法),来提升机器系统的性能,就是机器学习. 定义2: 对于某类任务(Task,简称T)和某项性

汇编语言学习第十四章-端口

本博文系列参考自<<汇编语言>>第三版,作者:王爽 各种存储器都通过地址总线,数据总线以及控制总线与CPU相连.CPU对这些各种存储器组成的存储单元进行统一编址,统一寻址.除了各种存储器和CPU相连之外,还有以下几种芯片和CPU相连: (1)各种接口卡(比如网卡,显卡)上的芯片,它们控制接口卡工作 (2)主板上的接口芯片,CPU通过它们对部分外设进行访问 (3)其他芯片,用来存储相关的系统信息,或进行相关的输入输出处理 在这些芯片中,都有一些可以用CPU读写的寄存器,虽然这些寄存器

【WPF学习】第四十四章 图画

原文:[WPF学习]第四十四章 图画 通过上一章的学习,Geometry抽象类表示形状或路径.Drawing抽象类扮演了互补的角色,它表示2D图画(Drawing)--换句话说,它包含了显示矢量图像或位图需要的所有信息. 尽管有几类画图类,但只有GeometryDrawing类能使用已经学习过的几何图形.它增加了决定如何绘制图形的画笔和填充细节.可将GeometryDrawing对象视为矢量插图中的形状.例如,可将标准的窗口元文件格式(.wmf)转换成准备插入用户界面的GeometryDrawi

【WPF学习】第二十四章 基于范围的控件

原文:[WPF学习]第二十四章 基于范围的控件 WPF提供了三个使用范围概念的控件.这些控件使用在特定最小值和最大值之间的数值.这些控件--ScrollBar.ProgressBar以及Slider--都继承自RangeBase类(该类又继承自Control类).尽管它们使用相同的抽象概念(范围),但工作方式却又很大的区别. 下表显示了RangeBase类定义的属性: 表 RangeBase类的属性 通常不比直接使用ScrollBar控件.更高级的ScrollViewer控件(封装了两个Scro

《Linux内核设计与实现》第八周学习总结——第四章 进程调度

<Linux内核设计与实现>第八周学习总结——第四章 进程调度 第4章 进程调度35 调度程序负责决定将哪个进程投入运行,何时运行以及运行多长时间,进程调度程序可看做在可运行态进程之间分配有限的处理器时间资源的内核子系统.只有通过调度程序的合理调度,系统资源才能最大限度地发挥作用,多进程才会有并发行的效果. 调度程序没有太复杂的原理,最大限度地利用处理器时间的原则是只要有可以执行的进程,那么就总会有进程正在执行,但是只要系统中可运行的进程的数目比处理器的个数多,就注定某一给定时刻会有一些进程不

Python基础教程(第十四章 网络编程)

本文内容全部出自<Python基础教程>第二版,在此分享自己的学习之路. ______欢迎转载:http://www.cnblogs.com/Marlowes/p/5538341.html______ Created on Marlowes 本章将会给读者展示一些例子,这些例子会使用多种Python的方法编写一个将网络(比如因特网)作为重要组成部分的程序.Python是一个很强大的网络编程工具,这么说有很多原因,首先,Python内有很多针对常见网络协议的库,在库顶部可以获得抽象层,这样就可以

javascript高级程序设计 第十四章--表单脚本

javascript高级程序设计 第十四章--表单脚本 在HTML中表单由<form>元素表示,在js中表单对应的是HTMLFormElement类型,这个类型也有很多属性和方法:取得表单元素的引用还是为它添加id特性,用DOM操作来获取表单元素:提交表单:把<input>或<button>元素的type特性设置为"submit",图像按钮把<input>元素的type特性设置为"image",也可以调用submit(