1. 前言

WPF 事件的路由环境是 UI 组件树,先来看看这棵树。

1.1 Logical Tree 和 Visual Tree

WPF 中的树有两种,一颗是逻辑树,另一颗也是逻辑树。

开玩笑,WPF 不是鲁迅,另一颗是可视元素树。

  • 逻辑树的每个节点不是布局组件就是控件;
  • 而可视化树把逻辑树延伸到 Template;
  • 一般都是和 Logical Tree 打交道,如果到 Visual Tree,多半还是程序设计不良;
  • Logical Tree 查找元素用 LogicalTreeHelper 类的 static 方法实现;
  • Visual Tree 查找元素用 VisualTreeHelper 类的 static 方法实现;

路由事件被激发后沿着 Visual Tree 传递,因此 Template 里的控件能把消息送出来。

2. 事件

事件隐藏消息机制的很多细节;

拥有者、响应者、订阅关系;

A 订阅了 B,本质是让 B.Event 与 A.EventHandler 关联起来;

事件激发: B.Event 被调用,与之关联的 A.EventHandler 就会被调用。

// 拥有者:myButton,事件:myButton.Click,事件的响应者:窗体本身,处理器:this.myButton_Click
this.myButton.Click += new System.EventHandler(this.myButton_Click); void myButton_Click(object sender, EventArgs e) {}

缺点:

  • 动态生成一组相同的控件,每个控件的同一事件都是用同一个处理器来相应,需要显式书写事件订阅代码;
  • 控件的内部事件不能被外部订阅,当 UI 层级很多时,如果想让外层的容器订阅深层控件的某个事件就需要为每一层组件定义用于暴露内部事件的事件。

3. 路由事件

3.1 跟直接事件的区别

直接事件:发送者直接将事件消息发送给事件响应者,事件响应者使用其事件处理器方法对事件的发生做出响应、驱动程序逻辑按客户需求运行;

路由事件:拥有者和事件响应者没有直接显式的订阅关系,拥有者只负责激发事件,事件由谁响应它不知道,事件的响应者则安装有事件监听器,针对某类事件进行侦听,当有此类事件传递至此事件响应者就使用事件处理器来响应事件并决定事件是否可以继续传递。

3.2 使用内置路由事件

// C# 添加路由事件处理器
this.gridRoot.AddHandler(Button.ClickEvent, new RoutedEventHandler(this.ButtonClicked)); void ButtonClicked(object sender, RoutedEventArgs e) {} <!--xaml 添加路由事件处理器-->
<Grid x:Name="gridRoot" ButtonBase.Click="ButtonClicked"></Grid>

3.3 自定义路由事件

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

2)为路由事件添加 CLR 事件包装;// 非必须

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

public abstract class ButtonBase : ContentControl, ICommandSource
{
public static readonly RoutedEvent ClickEvent;
static ButtonBase()
{
ClickEvent = EventManager.RegisterRoutedEvent("Click", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ButtonBase));
...
} public event RoutedEventHandler Click
{
add
{
AddHandler(ClickEvent, value);
}
remove
{
RemoveHandler(ClickEvent, value);
}
} protected virtual void OnClick()
{
RoutedEventArgs e = new RoutedEventArgs(ClickEvent, this);
RaiseEvent(e);
CommandHelpers.ExecuteCommandSource(this);
}
3.3.1 注册路由事件入参
  1. 第 1 个参数:与依赖属性类似,需要拿去生成用于注册路由事件的 hash Code,微软建议与 RoutedEvent 变量的前缀和 CLR 事件包装器的名称一致,如例子中的 "Click";
  2. 第 2 个参数:路由事件策略, RoutingStrategy.Bubble/Tunnel/Direct,Bubble 为从事件的激发者向上级容器一层一层路由,Tunnel 的方向是反过来,Direct 是类似于直接事件的方式,直接将事件消息传达给事件处理器;
  3. 第 3 个参数:指定事件处理器的类型,从源代码可以看出,为指定的路由事件添加路由事件处理程序时,函数的调用顺序是:UIElment.AddHandler->EventHandlersStore.AddRoutedEventHandler->RoutedEvent.IsLegalHandler(检查路由事件处理程序的类型是否为路由事件注册时指定的处理器类型 或 RoutedEventHandler,否则抛出 ArgumentException);
  4. 第 4 个参数:指定路由事件的宿主是哪个类型,跟依赖属性类似,用于生成 hash Code;

例子:

// 自定义路由事件的数据,继承自 RoutedEventArgs(包含与路由事件相关联的状态信息和事件数据)
class ReportedEventArgs : RoutedEventArgs
{
public ReportedEventArgs(RoutedEvent routedEvent, object source) : base (routedEvent, source) {} public DateTime ClickTime { get; set; }
} class TimeButton : Button
{
public static readonly RoutedEvent ReportTimeEvent =
EventManager.RegisterRoutedEvent("ReportTime", RoutingStrategy.Bubble, typeof(EventHandler<ReportTimeEventArgs>), typeof(TimeButton)); public event RoutedEventHandler ReportTime
{
add { this.AddHandler(ReportTimeEvent, value); }
remove { this.RemoveHandler(ReportTimeEvent, value); }
} protected override void OnClick()
{
base.OnClick(); ReportTimeEventArgs args = new ReportTimeEventArgs(ReportTimeEvent, this);
args.ClickTime = DateTime.Now;
this.RaiseEvent(args);
}
} <StackPanel x:Name="stackPanel_1" local:TimeButton.ReportTime="ReportTimeHandler">
<ListDox x:Name""listBox" />
<local:TimeButton x:Name-"timeButton" Width="50" Height-"80" Content="报时" local:TimeButton.ReportTime="ReportTimeHandler"/>
</StackPanel> private void ReportTimeHandler(object sender, ReportTimeEventArgs e) { }
<StackPanel x:Name="stackPanel_1" local:TimeButton.ReportTime="ReportTimeHandler">
翻译成 C#:
Delegate dg = Delegate.CreateDelegate(typeof(EventHandler<ReportTimeEventArgs>), this, "ReportTimeHandler", false, true);
this.stackPanel_1.AddHandler(TimeButton.ReportTimeEvent, dg);

RoutedEventArgs 类具有一个 bool 类属性 Handled,一旦设置为 ture,就不会再往下传递,需要停止的时候就让它停止。

TextBox 的 TextChanged 事件、Binding 类的 SourceUpdated 事件也是路由事件。

4. RoutedEventArgs 的 Source 和 OriginalSource

Source 是 LogicalTree 上的消息源头;

OriginalSource 是 VisualTree 上的源头。

比如 一个 Window 包括一个 UserControl,UserControl 里面有个 Button,在 Window 中 UserControl 是 LogicalTree 的末端结点,e.Source 是那个 userControl,而窗体的 VisualTree 能看到内部结果,e.OriginalSource 是那个 button

5. 附加事件

5.1 非 UIElement 子类使用路由事件

class Student
{
public static readonly RoutedEvent NameChangedEvent =
EventManager.RegisterRoutedEvent("NameChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(Student)); public string Name { get; set; }
} public partial class EventWindow : Window
{
public EventWindow()
{
InitializeComponent(); this.grid1.AddHandler(Student.NameChangedEvent, new RoutedEventHandler(StudentNameChangedHandler));
} private void StudentNameChangedHandler(object sender, RoutedEventArgs e)
{
MessageBox.Show(string.Format("名称改成{0}", (e.OriginalSource as Student).Name));
} private void ButtonClick(object sender, RoutedEventArgs e)
{
Student stu = new Student() { Name = "OldName" };
stu.Name = "NewName"; RoutedEventArgs args = new RoutedEventArgs(Student.NameChangedEvent, stu);
this.button.RaiseEvent(args);
}
}

grid 监听 Student 的路由事件 NameChangedEvent;

Student 没有继承自 UIElement,没有 RaiseEvent 方法,因此借用其他 UIElement 元素调用 RaiseEvent 方法;

上面只是举例,一般我们用附加事件在 Binding、Mouse、Keyboard 这种全局的 Helper 类中。

5.2 例子 Binding

<Grid x:Name="grid1">
<Slider x:Name="slider" Width="300"/>
<TextBox x:Name="tb"
Text="{Binding Path=Value, ElementName=slider, Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged,
NotifyOnSourceUpdated=True,
NotifyOnTargetUpdated=True}"/>
</Grid> this.grid1.AddHandler(Binding.TargetUpdatedEvent, new RoutedEventHandler(TargetChangedHandler));
this.grid1.AddHandler(Binding.SourceUpdatedEvent, new RoutedEventHandler(SourceChangedHandler)); private void TargetChangedHandler(object sender, RoutedEventArgs e) { }
private void SourceChangedHandler(object sender, RoutedEventArgs e) { }

5.3 例子 Keyboard

public EventWindow()
{
InitializeComponent(); Keyboard.AddKeyDownHandler(this, new KeyEventHandler(KeyDownHandler)); this.AddHandler(UIElement.KeyDownEvent, new RoutedEventHandler(KeyDownHandler2));
} private void KeyDownHandler(object sender, KeyEventArgs e)
{
if ((e.KeyboardDevice.IsKeyDown(Key.LeftCtrl) || e.KeyboardDevice.IsKeyDown(Key.RightCtrl)) && e.KeyboardDevice.IsKeyDown(Key.C))
{
}
} private void KeyDownHandler2(object sender, RoutedEventArgs e)
{
KeyEventArgs args = e as KeyEventArgs;
if ((args.KeyboardDevice.IsKeyDown(Key.LeftCtrl) || args.KeyboardDevice.IsKeyDown(Key.RightCtrl)) && args.KeyboardDevice.IsKeyDown(Key.C))
{
}
}

WPF 基础 - 事件的更多相关文章

  1. WPF基础到企业应用系列6——布局全接触

    本文转自:http://knightswarrior.blog.51cto.com/1792698/365351 一. 摘要 首先很高兴这个系列能得到大家的关注和支持,这段时间一直在研究Windows ...

  2. WPF基础知识、界面布局及控件Binding(转)

    WPF是和WinForm对应的,而其核心是数据驱动事件,在开发中显示的是UI界面和逻辑关系相分离的一种开放语言.UI界面是在XAML语言环境下开发人员可以进行一些自主设计的前台界面,逻辑关系还是基于c ...

  3. WPF基础知识、界面布局及控件Binding

    WPF是和WinForm对应的,而其核心是数据驱动事件,在开发中显示的是UI界面和逻辑关系相分离的一种开放语言.UI界面是在XAML语言环境下开发人员可以进行一些自主设计的前台界面,逻辑关系还是基于c ...

  4. WPF 基础到企业应用系列索引

    转自:http://www.cnblogs.com/zenghongliang/archive/2010/07/09/1774141.html WPF 基础到企业应用系列索引 WPF 基础到企业应用系 ...

  5. WPF笔记(1.1 WPF基础)——Hello,WPF!

    原文:WPF笔记(1.1 WPF基础)--Hello,WPF! Example 1-1. Minimal C# WPF application// MyApp.csusing System;using ...

  6. 【深入浅出Linux网络编程】 “基础 -- 事件触发机制”

    回顾一下“"开篇 -- 知其然,知其所以然"”中的两段代码,第一段虽然只使用1个线程但却也只能处理一个socket,第二段虽然能处理成百上千个socket但却需要创建同等数量的线程 ...

  7. WPF 在事件中绑定命令(不可以在模版中绑定命令)

    其实这也不属于MVVMLight系列中的东东了,没兴趣的朋友可以跳过这篇文章,本文主要介绍如何在WPF中实现将命令绑定到事件中. 上一篇中我们介绍了MVVMLight中的命令的用法,那么仅仅知道命令是 ...

  8. WPF自学入门(三)WPF路由事件之内置路由事件

    有没有想过在.NET中已经有了事件机制,为什么在WPF中不直接使用.NET事件要加入路由事件来取代事件呢?最直观的原因就是典型的WPF应用程序使用很多元素关联和组合起来,是否还记得在WPF自学入门(一 ...

  9. WPF路由事件二:路由事件的三种策略

    一.什么是路由事件 路由事件是一种可以针对元素树中的多个侦听器而不是仅仅针对引发该事件的对象调用处理程序的事件.路由事件是一个CLR事件. 路由事件与一般事件的区别在于:路由事件是一种用于元素树的事件 ...

随机推荐

  1. leetcode 122 123 309 188 714 股票买卖 动态规划

    这类问题有一个通法 https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iii/solution/yi-ge-tong-y ...

  2. Leetcode(884)-索引处的解码字符串

    给定一个编码字符串 S.为了找出解码字符串并将其写入磁带,从编码字符串中每次读取一个字符,并采取以下步骤: 如果所读的字符是字母,则将该字母写在磁带上. 如果所读的字符是数字(例如 d),则整个当前磁 ...

  3. Leetcode(19)-删除链表的倒数第N个节点

    给定一个链表,删除链表的倒数第 n 个节点,并且返回链表的头结点. 示例: 给定一个链表: 1->2->3->4->5, 和 n = 2. 当删除了倒数第二个节点后,链表变为 ...

  4. Gym 101170F Free Weights(二分)题解

    题意:给出两行,每一行都有n个数组,一共有2 * n个,大小为1~n,每个有两个.现在可以进行操作:拿出一个物品i,然后放到一个空格,花费i.可以任意平移物品,平移没有花费.每一行空间无限.要求你把一 ...

  5. msf 信息收集

    MSF信息收集 转载自天堂空气 一 MSF主机发现 0x1:搜索arp 0x2:使用use auxiliary/scanner/discovery/arp_sweep 模块,然后show option ...

  6. 如何快速定位 Redis 热 key?

    背景 在 Redis 中,热 key 指的是那些在一段时间内访问频次比较高的键值,具体到业务上,商品的限时抢购.瞬时的新闻热点或某个全局性的资源,都极有可能产生热点 key. 热点 key 的出现可能 ...

  7. iPhone 12 Pro 屏幕时间设置的密码锁出现弹窗 UI 错位重大 Bug

    iPhone 12 Pro 屏幕时间设置的密码锁出现弹窗 UI 错位重大 Bug iOS 14.1 Bug 弹窗 UI 非常丑 弹窗屏占太高了 屏幕使用时间 https://support.apple ...

  8. Next.js & SSR & CSR & SG

    Next.js & SSR & CSR & SG getStaticPaths, getStaticProps, getServerSideProps getStaticPro ...

  9. RT-Thread学习笔记3-线程间通信 & 定时器

    目录 1. 事件集的使用 1.1 事件集控制块 1.2 事件集操作 2. 邮箱的使用 2.1 邮箱控制块 2.2 邮箱的操作 3. 消息队列 3.1 消息队列控制块 3.2 消息队列的操作 4. 软件 ...

  10. VAST助推NGK公链热度升温,日活超过以太坊!

    在区块链市场,如果说过去是比特币和以太坊的时代,那么现在和未来绝对是NGK的时代. NGK公链的出现,让区块链市场看到了新的希望.它不仅仅是开放的和可编程的,而且是低Gas燃耗的,以及创新共识机制的. ...