事件和委托参见C#事件和委托
路由事件是WPF中特有的,独特的事件,允许事件在元素树中进行路由。
传统的事件模式,事件的触发者和调用者是同一个,因为订阅者实际上也只是添加了自己的方法到发布者身上。而路由事件,发布者也就是事件的拥有者只管发布,事件由谁处理不归它管,和订阅者之间没有显式的订阅关系
路由:“起点与终点间有若干个中转站,从起点出发后经过每个中转站时要做出选择,最终以正确(比如最短或者最快)的路径到达终点。” 路由描述的就是这样的一个过程。
路由事件,是指事件的拥有者和响应者不必建立订阅关系,拥有者只管激发事件,响应者通过在自身设置事件监听器去监听对应的事件,并可以决定事件是否继续传播,如果说原始事件是两个人窃窃私语的话,那路由事件就是一队人挨个传话。当事件响应者通过事件监听器监听到某个事件的发生,通过事件携带的参数可以获取到事件的来源,从而做出判断该事件是否是自己关心的某个控件激发的,如果是,可以处理并停止事件的传播,如果不是,则放行不予理睬。
例如,可以将图像放在 Button的内部,这会有效地扩展按钮的可视化树。 但是,添加的图像不能破坏按钮的命中测试行为,它需要在用户单击图像像素时做出响应。
事件类型
WPF支持三种路由事件类型:冒泡(Bubbling)、隧道(Tunneling) 和 直接(Direct)。
-
冒泡事件(Bubbling Events):事件从事件源开始向父元素传播,直到根元素或被处理。(最常见的,也是默认的,如果没有显式声明事件被处理,还是会继续传播)
例如,Button
的Click
事件是一个冒泡事件。当按钮被点击时,事件会沿着元素树从按钮向上传播,直到到达根元素或被处理。<Button name="Button1" Click="Button_Click">Click Me</Button>
等效于
Button1.AddHandler(ButtonBase.ClickEvent, new RoutedEventHandler(Button_Click)); //或者 如果实现了CLR包装器 //Button1.Click += Button_Click
private void Button_Click(object sender, RoutedEventArgs e) { MessageBox.Show("Button clicked!"); }
-
隧道事件(Tunneling Events):事件从根元素开始向子元素传播,直到事件源。隧道事件通常以“Preview”前缀命名,例如PreviewMouseDown。
例如,PreviewKeyDown
事件是一个隧道事件,当按下键盘按键时,事件会从根元素开始向下传播到事件源。<Window PreviewKeyDown="Window_PreviewKeyDown"> <Grid> <Button>Click Me</Button> </Grid> </Window>
private void Window_PreviewKeyDown(object sender, KeyEventArgs e) { MessageBox.Show("Key pressed!"); }
很多事件即有隧道事件的版本,又有冒泡事件的版本,例如PreviewKeyDown 和KeyDown,称为一对事件,这对事件共享相同的事件参数,如KeyEventArgs,路由传播过程是先隧道传播,从父节点一直到子节点,然后再冒泡从子节点传播回父事件。如果在隧道传播时,就已经声明为已处理,不仅隧道传播会停止,冒泡传播也不会生效。
直接事件(Direct Events):事件只在事件源元素上引发,不进行路由传播,类似于传统的.NET事件
为什么要这样设计
1.支持更合理的事件处理控制,不需要在子控件上单独处理事件,可以在父控件中处理子控件的事件。
比如常规窗体上的最大化,最小化,关闭按钮,如果使用常规的事件处理,需要在按钮上处理事件,但是通过路由事件,我们可以在父控件窗体上处理,提高代码的可维护性
2.可以配置传播路径,支持在某处进行处理或拦截事件,如果不处理可以一直传播,比较灵活,这样运行支持复杂的UI结构和自定义结构
3.同一类事件可以统一处理
<Border Height="30" Width="200" BorderBrush="Gray" BorderThickness="1">
<StackPanel Background="LightBlue" Orientation="Horizontal" Button.Click="YesNoCancelButton_Click">
<Button Name="YesButton">Yes</Button>
<Button Name="NoButton">No</Button>
<Button Name="CancelButton">Cancel</Button>
</StackPanel>
</Border>
这三个按钮中的每一个都是潜在的 Click 事件源。 单击其中一个按钮时,它会引发 Click
事件,从按钮浮升到根元素。 Button和 Border元素没有附加事件处理程序,但 StackPanel有。
如果相关父元素上没有默认实现该事件,那么必须显式指定事件,比如上面的StackPanel 本身没有实现按钮的点击事件 ,所以要加Button.Click="YesNoCancelButton_Click"
private void YesNoCancelButton_Click(object sender, RoutedEventArgs e)
{
FrameworkElement sourceFrameworkElement = e.Source as FrameworkElement;
switch (sourceFrameworkElement.Name)
{
case "YesButton":
// YesButton logic.
break;
case "NoButton":
// NoButton logic.
break;
case "CancelButton":
// CancelButton logic.
break;
}
e.Handled = true;
}
最初引发路由事件的元素在事件处理程序参数中被标识为 RoutedEventArgs.Source(也就是上面的button)。
事件侦听器是附加和调用了事件处理程序的元素,它在事件处理程序参数中标识为 sender(上面的StackPanel)
路由事件支持在事件路由路线上的元素之间交换事件信息,因为每个侦听器都可以访问事件数据的同一实例。 如果事件数据中的某个元素更改了某些内容,则该更改对事件路由中的后续元素可见。
4.支持EventTrigger和EventSetter
自定义路由事件
- 注册路由事件 ,使用 EventManager.RegisterRoutedEvent
- 添加事件包装器,注意包装器名称一般和路由事件一致
- 添加触发
public class CustomButton : Button
{
// Register a custom routed event using the Bubble routing strategy.
public static readonly RoutedEvent ConditionalClickEvent = EventManager.RegisterRoutedEvent(
name: "ConditionalClick",
routingStrategy: RoutingStrategy.Bubble,
handlerType: typeof(RoutedEventHandler),
ownerType: typeof(CustomButton));
// Provide CLR accessors for assigning an event handler.
public event RoutedEventHandler ConditionalClick
{
add { AddHandler(ConditionalClickEvent, value); }
remove { RemoveHandler(ConditionalClickEvent, value); }
}
void RaiseCustomRoutedEvent()
{
// Create a RoutedEventArgs instance.
RoutedEventArgs routedEventArgs = new(routedEvent: ConditionalClickEvent);
// Raise the event, which will bubble up through the element tree.
RaiseEvent(routedEventArgs);
}
// For demo purposes, we use the Click event as a trigger.
protected override void OnClick()
{
// Some condition combined with the Click event will trigger the ConditionalClick event.
if (DateTime.Now > new DateTime())
RaiseCustomRoutedEvent();
// Call the base class OnClick() method so Click event subscribers are notified.
base.OnClick();
}
}
在XAML中使用自定义控件并监听事件。
<Window x:Class="CodeSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:custom="clr-namespace:WpfControl;assembly=WpfControlLibrary"
Title="How to create a custom routed event" Height="100" Width="300">
<StackPanel Name="StackPanel1" custom:CustomButton.ConditionalClick="Handler_ConditionalClick">
<custom:CustomButton
Name="customButton"
ConditionalClick="Handler_ConditionalClick"
Content="Click to trigger a custom routed event"
Background="LightGray">
</custom:CustomButton>
</StackPanel>
</Window>
在对应cs代码中处理事件。
// The ConditionalClick event handler.
private void Handler_ConditionalClick(object sender, RoutedEventArgs e)
{
string senderName = ((FrameworkElement)sender).Name;
string sourceName = ((FrameworkElement)e.Source).Name;
Debug.WriteLine($"Routed event handler attached to {senderName}, " +
$"triggered by the ConditionalClick routed event raised on {sourceName}.");
}
// Debug output when CustomButton is clicked:
// Routed event handler attached to CustomButton,
// triggered by the ConditionalClick routed event raised on CustomButton.
// Routed event handler attached to StackPanel1,
// triggered by the ConditionalClick routed event raised on CustomButton.
RoutedEventArgs
是事件数据的基类,用于传递事件的相关信息。它包含事件的路由信息和其他数据
RaiseEvent
方法用于触发事件的实际传播。它会使用传递的 RoutedEventArgs
实例,并且根据 RoutedEventArgs
中的 RoutedEvent
来决定是哪个事件被触发。RaiseEvent
方法会根据事件的路由策略(在这里是 RoutingStrategy.Bubble
)沿着控件树传播事件。
常见的路由事件
-
Mouse Events:如
MouseDown
、MouseUp
、MouseMove
、PreviewMouseDown
、PreviewMouseUp
、PreviewMouseMove
。 -
Keyboard Events:如
KeyDown
、KeyUp
、PreviewKeyDown
、PreviewKeyUp
。 -
Touch Events:如
TouchDown
、TouchUp
、TouchMove
、PreviewTouchDown
、PreviewTouchUp
、PreviewTouchMove
。
事件传播
将事件声明为已处理来停止事件传播
你可以在事件处理程序中将RoutedEventArgs
的Handled
属性设置为true
,来停止事件的进一步传播。
private void Button_Click(object sender, RoutedEventArgs e)
{
e.Handled = true;
MessageBox.Show("Button clicked!");
}
如果想要处理已经处理的事件(一般情况下不会),那么可以使用 AddHandler(RoutedEvent, Delegate, Boolean)重载后台代码,将 handledEventsToo
参数设置为 true
。
底层原理
路由事件的底层实现基于 WPF 的 RoutedEvent 系统,这是一种能够在控件层级之间传递的事件。每个控件(如 UIElement)都支持注册、触发和处理路由事件。
路由事件的传播通过事件管理器 EventManager 进行处理,事件会沿着视觉树(VisualTree)或者逻辑树(LogicalTree)传播,因此支持父子元素之间的事件传递。
注册路由事件类似于注册依赖属性,会设置一个哈希表存储事件的信息,存储起来,使得 WPF 知道哪些事件是路由事件、如何传播它们、以及它们的路由策略(冒泡或隧道)。
事件的传播是由 WPF 的事件系统在控件树中自动管理的,当子控件触发一个路由事件(比如点击按钮时的 Button.Click 事件),WPF 的事件系统会将该事件注册到事件源(子控件)上,并开始执行事件路由的过程。
事件从事件源控件(例如按钮)开始传播。UIElement 的 RaiseEvent 方法会检测事件的路由策略,并决定如何沿着控件树传播事件。
事件会沿着控件的路径向上或者向下传播。每个控件在传播过程中会调用 OnEvent 方法,以便处理该事件。
如果某个父控件或中间控件的 OnEvent 方法处理了事件(例如调用了 AddHandler),该控件的事件处理程序会被执行。事件传播会在找到第一个处理程序后停止,除非事件设置为继续传播。
附加事件
附加事件(Attached Event)是一种特殊的路由事件,它允许事件由没有直接处理该事件的元素来声明和处理。这种事件类型通常用于使得子元素或关联的元素能够处理事件,即使它们并未在事件的原始触发元素的对象树中。
附加事件类似于附加属性。附加属性允许你在不是该属性定义类的对象上设置该属性值。附加事件则是让一个对象能够处理由其他对象引发的事件,或者说一个事件可以由没有直接关系的元素处理。
例如,WPF 中的 Button
类定义了一个 Click
事件,通常只能由 Button
自己或者其继承类处理。然而,附加事件的机制允许其他类(例如 Grid
或 StackPanel
)声明自己对 Button
的 Click
事件感兴趣。
2. 使用场景
附加事件通常用于以下场景:
-
事件冒泡和隧道:当你想让父级容器或者祖先元素处理某个子元素的事件时,附加事件特别有用。举个例子,如果你有一个父级容器需要对其所有子按钮的点击做出反应,你可以在父级容器中处理
Button.Click
事件。 - 在XAML中声明事件处理:附加事件可以在 XAML 中声明,允许你在布局中直接定义事件处理逻辑。
3. 创建和使用附加事件
创建附加事件时通常需要两部分:
-
注册事件:使用
EventManager.RegisterRoutedEvent
方法注册事件。(这里和路由事件一致) - 声明包装器:附加事件不需要继承特点基类,因为WPF知道可以在附加事件的UIElement实例上调用UIElement.RemoveHandler或者UIElement.AddHandler方法(这里包装器是两个静态方法,和路由事件不一致)
// 在一个类中定义附加事件
public static class MyElement
{
// 定义一个路由事件
public static readonly RoutedEvent MyAttachedEvent =
EventManager.RegisterRoutedEvent(
"MyAttached",
RoutingStrategy.Bubble,
typeof(RoutedEventHandler),
typeof(MyElement)
);
// 提供添加事件处理器的方法
public static void AddMyAttachedHandler(DependencyObject d, RoutedEventHandler handler)
{
if (d is UIElement element)
{
element.AddHandler(MyAttachedEvent, handler);
}
}
// 提供移除事件处理器的方法
public static void RemoveMyAttachedHandler(DependencyObject d, RoutedEventHandler handler)
{
if (d is UIElement element)
{
element.RemoveHandler(MyAttachedEvent, handler);
}
}
// 一个方法,用于触发事件,需要在别的
public static void RaiseMyAttachedEvent(UIElement element)
{
element.RaiseEvent(new RoutedEventArgs(MyAttachedEvent));
}
}
在XAML中使用:
<Window x:Class="WpfApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApp"
Title="MainWindow" Height="350" Width="525">
<Grid local:MyElement.MyAttached="Grid_MyAttached">
<!-- 其他子元素 -->
</Grid>
</Window>
在 C# 代码中,你可以处理事件:
private void Grid_MyAttached(object sender, RoutedEventArgs e)
{
MessageBox.Show("附加事件触发!");
}
public class Student
{
public int Id { get; set; }
public string Name { get; set; }
// 声明和注册路由事件(由于 Student 类 UIElment 类的派生类,因此不具备 AddHandler 和 RemoveHandler 方法,需要手动实现)
public static readonly RoutedEvent NameChanedEvent = EventManager.RegisterRoutedEvent("NameChaned",
RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(Student));
// 为界面元素添加路由事件
public static void AddNameChangedHandler(DependencyObject sender, RoutedEventHandler e)
{
UIElement ui= sender as UIElement;
if (ui != null)
{
ui.AddHandler(Student.NameChanedEvent,e);
}
}
// 为界面元素移除路由事件
public static void RemoveNameChangedHandler(DependencyObject sender, RoutedEventHandler e)
{
UIElement ui = sender as UIElement;
if (ui != null)
{
ui.RemoveHandler(Student.NameChanedEvent, e);
}
}
}
// 声明一个事件处理器
private void NameChanged(object sender, RoutedEventArgs e)
{
var s = (e.OriginalSource as Student);
MessageBox.Show($"{s.Id},{s.Name}");
}
<Grid local:Student.NameChanged="NameChanged">
<Button x:Name="Button1" Click="ButtonBase_OnClick"></Button>
</Grid>
// 触发 NameChanged 事件
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
Student s = new Student() {Name = "Tim", Id = 0,};
RoutedEventArgs reArgs = new RoutedEventArgs(Student.NameChanedEvent, s);
this.Button1.RaiseEvent(reArgs);
}