WPF 事件

1.1 逻辑树与可视树

如果把一片树叶放在显微镜下观察,你会发现这片叶子也像一棵树----有自己的基部并向上生长出多级分叉。在WPF的Logic Tree上,扮演叶子的一般都是控件。如果我们把WPF中的控件也放在显微镜下观察,你会发现WPF控件本身也是一棵由更细微级别的组件(他们不是控件,而是一些可视化组件,派生至Visual类)组成的树。

在WPF中有两种树:逻辑树(Logical Tree)和可视树(Visual Tree),XAML是表达WPF的一棵树。逻辑树完全是由布局组件和控件构成。如果我们把逻辑树延伸至Template组件级别,我们就得到了可视树,所以可视树把树分的更细致。

1.2 事件的来龙去脉

微软把消息机制封装成了更容易让人理解的事件模型。

事件模型隐藏了消息机制的很多细节,让程序开发变的简单。繁琐的消息驱动机制在事件模型中被简化为了3个关键点:

事件的拥有者:即消息的发送者。事件的宿主可以在某些条件下激发它拥有的事件,事件被触发则消息被发送。

事件的响应者:即消息的接收者、处理者。事件接收者使用其事件处理器(EventHandler)对事件做出响应。

事件的订阅关系:事件的拥有者可以随时激发事件,但事件发生后会不会得到响应要看有没有事件响应者,或者说要看这个事件是否被关注。如果对象A关注对象B的某个事件是否发生,则称A订阅了B的某个事件。更进一步讲,事件实际上是一个使用Event关键字修饰的委托类型的成员变量,事件处理器则是一个函数,说A订阅了B的某个事件,本质就是让B.Event和A.EventHandler关联起来。所谓事件激发就是B.Event被调用,这时,与其关联的A.EventHandler就会被调用。

在这种模型里,事件的响应者通过订阅关系直接关联在事件拥有者的事件上,为了与WPF路由事件模型分开,我们把这种事件模型称为直接事件模型或CLR事件模型。每条消息是“发送---响应”关系,必须显示的建立点对点订阅关系。

1.3 初试路由事件

为了降低由事件订阅带来的耦合度和代码量,WPF推出了路由事件机制。路由事件和直接事件的区别在于,直接事件激发时,发送者直接将消息通过事件订阅交给事件的响应者,事件响应者使用其事件处理器方法对事件的发生做出响应驱动逻辑程序按客户需求运行。路由事件的拥有者和事件响应者之间则没有直接显示的订阅关系,事件的拥有者只负责激发事件,事件将由谁响应它并不知道,事件的响应者则安装有事件侦听器,针对某类事件进行侦听。当有某类事件传递至此时事件响应者就使用事件处理器来响应事件并决定事件是否可以继续传递。

举个例子,在Visual Tree上有一个button控件,当它被单击的时候就相当于自己喊了一声“我被单击了”,这样一个button.Click开始在Visual Tree上开始传播。当事件经过某个节点的时候如果这个节点没有安装用于侦听button.Click事件的“耳朵”,那么它会无视这个事件,让它继续畅通无阻的继续传播,如果某个节点安装了针对button.Click的侦听器,它的事件处理器就会被调用,在事件处理器内部,程序员可以查看路由事件原始的出发点是哪个控件,上一站是哪里,还可以决定事件传递到此为止还是可以继续往下传递----路由事件就是这样依靠“口耳相传”的办法将消息传递给“关心”它的控件的。虽然WPF推出了路由事件机制,但它仍然支持传统的直接事件模型

下面通过一个例子初试一下路由事件:

<Window x:Class="WpfApplication8.wnd831"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="200" Width="200">
    <Grid x:Name="_gridRoot" Background="Lime">
        <Grid x:Name="_gridA" Background="Blue" Margin="10">
            <Grid.ColumnDefinitions>
                <ColumnDefinition></ColumnDefinition>
                <ColumnDefinition></ColumnDefinition>
            </Grid.ColumnDefinitions>
            <Canvas x:Name="_canvasLeft" Background="Red" Margin="10" Grid.Column="0">
                <Button x:Name="_buttonLeft" Content="Left" Margin="10" Width="45" Height="110"/>
            </Canvas>
            <Canvas x:Name="_canvasRight" Background="LightSlateGray" Margin="10" Grid.Column="1">
                <Button x:Name="_buttonRight" Content="Right" Margin="10" Width="45" Height="110"/>
            </Canvas>
        </Grid>
    </Grid>
</Window>

我们点击按钮时,无论是_buttonLeft还是_buttonRight单击都能显示按钮的名称。两个按钮到顶部的window有唯一条路,左边的按钮对应的路:_buttonLeft->_canvasLeft->_gridA->_GridRoot->_Window,右边按钮对应的路:_buttonRight->_canvasRight->_gridA->_GridRoot->_Window。如果GridRoot订阅两个处理器,那么处理器应该是相同的。后台代码为:

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

            // 为指定的路由事件添加路由事件处理程序
            _gridRoot.AddHandler(Button.ClickEvent, new RoutedEventHandler(ButtonClicked));
        }

        public void ButtonClicked(object sender, RoutedEventArgs e)
        {
            MessageBox.Show((e.OriginalSource as FrameworkElement).Name);
        }
    }

下面先解释一下路由事件是怎么沿着可视树来传播的,当Button被点击,Button就开始发送消息了,可视树上的元素如果订阅了Button的点击事件,那么才会根据消息来作出相应的反应,如果没有订阅的话,就无视它发出的消息,当然我们还可以控制它的消息的传播方式,是从树根到树叶传播,还是树叶向树根传播以及是直接到达目的传播,不仅如此,还能控制消息传到某个元素时,停止传播。具体的会在后面记录到。

其次是this._GridRoot.AddHandler(Button.ClickEvent,new RoutedEventHandler(this.ButtonClicked));订阅事件时,第一个参数是路由事件类型,在这里用的是Button的ClickEvent,就像依赖属性一样,类名加上依赖属性,这里是类名加上路由事件。另外一个是e.OriginalSourcee.Source的区别,由于消息每传一站,都要把消息交到下一个控件(此控件成为了消息的发送地点),e.Source为逻辑树上的源头,要想获取原始发消息的控件(可视树的源头)要用e.OriginalSource。

1.4 自定义路由事件

创建自定义路由事件大体分为3个步骤:

声明并注册路由事件。

为路由事件添加CLR事件包装。

创建可以激发路由事件的方法。

声明路由事件参数:

    /// Event Handler
    delegate void ReportTimeRouteEventHandler(object sender, ReportTimeRoutedEventArgs e);

    /// <summary>
    /// 包含时间的路由事件参数
    /// </summary>
    public class ReportTimeRoutedEventArgs : RoutedEventArgs
    {
        public ReportTimeRoutedEventArgs(RoutedEvent routedEvent, object source)
            : base(routedEvent, source){}

        public DateTime ClickTime { get; set; }
    }

创建路由事件:

    /// <summary>
    /// 继承Button
    /// </summary>
    public class TimeButton : Button
    {
        /// <summary>
        /// 声明和注册路由事件
        /// </summary>
        public static readonly RoutedEvent ReportTimeEvent = EventManager.RegisterRoutedEvent(
            "ReportTime",
            RoutingStrategy.Bubble,
            typeof(ReportTimeRouteEventHandler)/*typeof(EventHandler<ReportTimeRoutedEventArgs>)*/,
            typeof(TimeButton));

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

        /// <summary>
        /// 激发路由事件,借用Click事件激发
        /// </summary>
        protected override void OnClick()
        {
            base.OnClick();

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

注册路由事件时注意:

第一个参数是一个String类型,被称为路由事件的名称,按微软的建议,这个字符串应该与RountEvent变量的前缀和CLR事件包装器名称一致。本例中,路由事件的名称是ReportTimeEvent,则此字符串是ReportTime,CLR事件名亦为ReportTime。

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

Bubble,冒泡式:路由事件由事件激发者出发向它的上一层容器一层一层路由,直至最外层的容器(Windows或Page)。因为是由树的底部想树的顶部移动,而且从事件激发元素到UI树的树根只有确定的一条路径,所以这种策略被形象的命名为“冒泡式”。

Tunnel,隧道式:事件的路由刚好和冒泡式相反,是由树的树根向事件激发者移动,这就想当于在树根和目标控件之间挖了一条隧道,事件只能沿着隧道移动,所以称为“隧道式”。

Direct,直达式:模仿CLR直接事件,直接将事件消息送达事件处理器。

第三个参数用于指定事件处理器的类型。事件处理器的返回值类型和参数列表必须与此参数指定的委托保持一致,不然会导致在编译的时候报异常。

第四个参数用于指定路由事件的宿主(拥有者)是哪个类型。

XAML:让控件都监听类型为ReportTime的路由事件:

<Window x:Class="WpfApplication8.wnd832"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfApplication8"
        Title="wnd832" Height="300" Width="300">
    <Grid x:Name="grid_1"  local:TimeButton.ReportTime="ReportTimeHandle">
        <Grid x:Name="grid_2" local:TimeButton.ReportTime="ReportTimeHandle">
            <Grid x:Name="grid_3" local:TimeButton.ReportTime="ReportTimeHandle">
                <StackPanel x:Name="dock1" local:TimeButton.ReportTime="ReportTimeHandle">
                    <ListBox x:Name="listBox1" local:TimeButton.ReportTime="ReportTimeHandle"/>
                    <local:TimeButton x:Name="btn1" Content="报时" Width="80" Height="80" local:TimeButton.ReportTime="ReportTimeHandle"/>
                </StackPanel>
            </Grid>
        </Grid>
    </Grid>
</Window>

事件处理器:

public void ReportTimeHandle(object sender, ReportTimeRoutedEventArgs e)
{
    FrameworkElement ele = sender as FrameworkElement;
    string content = string.Format("{0}到达{1}", e.ClickTime.ToLongTimeString(), ele.Name);
    listBox1.Items.Add(content);

    //当事件传递到grid_2就停止了
    if (ele == grid_2)
    {
        e.Handled = true;
    }
}

参考《深入浅出WPF》

时间: 2024-08-09 13:22:57

WPF 事件的相关文章

WPF快速入门系列(3)——深入解析WPF事件机制

一.引言 WPF除了创建了一个新的依赖属性系统之外,还用更高级的路由事件功能替换了普通的.NET事件. 路由事件是具有更强传播能力的事件——它可以在元素树上向上冒泡和向下隧道传播,并且沿着传播路径被事件处理程序处理.与依赖属性一样,可以使用传统的事件方式使用路由事件.尽管路由事件的使用方式与传统的事件一样,但是理解其工作原理还是相当重要的. 二.路由事件的详细介绍 对于.NET中的事件,大家应该在熟悉不过了.事件指的在某个事情发生时,由对象发送用于通知代码的消息.WPF中的路由事件允许事件可以被

WPF事件转Command示例

using System.Windows; using System.Windows.Input; namespace TestLauncher.Tools { internal class DragDropHelper { public static readonly DependencyProperty DropCommandProperty; static DragDropHelper() { //Drop DropCommandProperty = DependencyProperty.

WPF事件(一)内置路由事件

原文:WPF事件(一)内置路由事件 Windows是消息驱动的操作系统,运行其上的程序也遵照这个机制运行,随着面向对象开发平台日趋成熟,微软把消息机制封装成了更容易让人理解的事件模型,一个事件包含3个关键点:事件的拥有者.事件的响应者.事件的订阅关系 为了降低由事件订阅带来的耦合度和代码量,WPF推出了路由事件机制,路由事件与传统的直接事件的区别在于,直接事件激发时发送者直接将消息通过事件订阅交送给事件响应者,事件响应者使用其事件处理器方法対事件的发生做出响应.驱动程序逻辑按客户需求运行,而路由

【WPF学习】第十五章 WPF事件

前两章学习了WPF事件的工作原理,现在分析一下在代码中可以处理的各类事件.尽管每个元素都提供了许多事件,但最重要的事件通常包括以下5类: 生命周期事件:在元素被初始化.加载或卸载时发生这些事件. 鼠标事件:这些事件是鼠标动作的结果. 键盘事件:这些事件是键盘动作(如按下键盘上的键)的结果. 手写笔事件:这些事件是使用类似钢笔的手写笔的结果.在平板电脑上用手写笔代替鼠标. 多点触控事件:这些事件是一根或多根手指在多点触控屏上触摸的结果.尽在Windows7中支持这些事件. 一.生命周期事件 当首次

WPF事件中的sender就是事件源

可以看到wpf中所有的事件都是这个格式: private void btnTest_Click(object sender, RoutedEventArgs e)  { Button btn = (Button)sender; btn.Content = "i am the soure contrl"; } 其中的sender 指的是事件源,也就是触发该事件的控件

WPF:事件委托对于不同界面间通信的应用

界面1内设定点击事件,生成Path用事件传出public partial class TemplateWindow : Window { internal delegate void ConfirmButtonClick(string  Path); /// <summary> /// 点击确定按钮事件 /// </summary> internal event ConfirmButtonClick OnConfirmButtonClick;                  pr

wpf事件绑定,比如一个控件的左键按下事件

<i:Interaction.Triggers> <i:EventTrigger EventName="MouseLeftButtonDown"> <i:InvokeCommandAction Command="{Binding Cmd,RelativeSource={RelativeSource AncestorType=Window}}" /> </i:EventTrigger> </i:Interactio

wpf 事件参数 绑定到viewmdoel

public sealed class EventCommand : TriggerAction<DependencyObject> { public static readonly DependencyProperty CommandParameterProperty = DependencyProperty.Register("CommandParameter", typeof(object), typeof(EventCommand), null); public s

WPF学习之事件(一)

就像属性系统在WPF中得到升级.进化为依赖属性一样,事件系统在WPF中也被升级,从而进化成为——路由事件(Routed Event),并在其基础上衍生出命令传递机制.就让我们一起来领略这些新消息机制的风采吧! 1.近观WPF的树形结构. 路由(Route)一词的大意为:起点和终点之间有若干个中转站,从起点出发后经过每个中转站时都要进行选择,最终以正确(比如最短或者最快)的路径到达终点.我们知道,WPF的UI是由布局组件和控件构成的属树形结构.因此,当这棵树上的某个节点激发出某个事件的时候,程序员