WPF 中的路由事件 (Routed Events) 是专门为树形 UI 结构设计的事件传递机制。在传统的 C# 开发(如 WinForms)中,事件是"点对点"的(即谁触发、谁响应);而在 WPF 中,所有的 UI 控件都是像树枝一样层层嵌套的。路由事件允许一个事件在 UI 树中上下穿梭,直到被某个节点处理为止。
一、 核心概念:为什么需要"路由"?
假设你的界面结构是这样的:<Window> -> <Grid> -> <Button> -> <Image>。 如果用户点击了最里面的 <Image>,传统 C# 事件只会触发 Image 自己的 Click 事件。但实际上,用户不仅是点击了图片,同时也点击了包含图片的 Button,点击了 Grid,甚至点击了整个 Window。
路由事件的意义就在于,它能让这个"点击动作"按照特定的路线,自动通知整个家族树上的相关成员,而不需要你手动去挨个绑定。
二、 路由的三大策略 (Routing Strategies)
WPF 的事件路由引擎提供了三种完全不同的"旅行路线":
1. 冒泡路由 (Bubbling) ------ 【自下而上】
-
路线:像水里的泡泡一样,从触发事件的最底层元素开始,一级一级往上冒,直到最顶层的 Window。
-
应用场景 :绝大多数普通的交互动作(如
Click、MouseDown、MouseUp)。 -
触发顺序:
-
<Image>触发 ➡<Button>触发 ➡<Grid>触发 ➡<Window>触发。
2. 隧道路由 (Tunneling) ------ 【自上而下】
-
路线:像钻隧道一样,从最顶层的根节点 (Window) 开始,一级一级往下钻,直到找到真正触发事件的那个底层元素。
-
命名规范 :WPF 中所有的隧道事件都严格以 Preview 开头(如
PreviewMouseDown、PreviewKeyDown)。 -
触发顺序 :
<Window>触发 ➡<Grid>触发 ➡<Button>触发 ➡<Image>触发。 -
核心作用:给上层父容器一个"提前拦截"或"预处理"的机会。
3. 直达路由 (Direct) ------ 【点对点】
-
路线:不冒泡也不钻隧道,只有触发事件的元素本身能收到通知,行为和传统的 .NET CLR 事件一模一样。
-
应用场景 :纯粹的局部状态变化(如
MouseEnter、MouseLeave)。因为鼠标进入某个按钮,并不代表鼠标也进入了按钮内部包裹的图片,这种事件不需要通知父级或子级。
三、 核心机制:事件的拦截与中断 (e.Handled)
当一个事件在树形结构中游走时(无论是冒泡还是隧道),任何一个节点只要觉得"这个事件我已经处理完毕,不需要其他人再插手了",就可以强行踩下刹车。
在事件处理函数的参数 RoutedEventArgs e 中,有一个极其重要的布尔属性:e.Handled。
private void Button_MouseDown(object sender, MouseButtonEventArgs e)
{
// 1. 执行你的业务逻辑
Console.WriteLine("按钮处理了点击事件");
// 2. 踩下刹车!告诉 WPF 路由引擎:事件到此为止,不要再往上冒泡了。
e.Handled = true;
}
继承链路剖析
FrameworkElement (框架元素):
定义了控件在屏幕上渲染所需的最基础属性,比如
Width、Height、Margin 以及数据绑定上下文 (DataContext)。但它本身不知道什么是"外观"。
Control (基础控件):
它是所有 UI (User Interface,用户界面) 控件的基类。它引入了 Template (控件模板) 属性,允许控件拥有自定义的"皮肤" (ControlTemplate)。但是,并不是所有的控件都有"内容"(比如 ProgressBar 只有进度条,没有文字)。

ContentControl (内容控件)
------ 【Content 属性的诞生地】 :微软发现,有很多控件都需要在内部装载"一个东西"(比如一段文字、一张图片)。于是单独抽离出了 ContentControl。它定义了极其核心的 Content 属性(类型为 System.Object),这意味着它不仅能装字符串,还能装任何复杂的 UI 元素。
ButtonBase (按钮基类) :它继承自 ContentControl,因此顺理成章地拥有了装载内容的 Content 属性。它专门负责处理鼠标按下、抬起等点击语义,并定义了 Click 路由事件。
Button (按钮实体) :最终的具体实现类。它拥有了祖先提供的所有能力:能设置大小(来自 FrameworkElement),能换皮肤(来自 Control),能装载任意内容(来自 ContentControl),能被点击(来自 ButtonBase)。
ContentControl vs ItemsControl :WPF 将控件严格分为了两派。只能装一个元素的,叫 ContentControl (如 Button, Window, Label, CheckBox);可以装一堆元素的,叫 ItemsControl (如 ListBox, ComboBox, TreeView),其核心属性不再是 Content,而是 Items 和 ItemsSource。
WPF内容模型WPF Content Model
里面分两大类控件:
ContentControl 内容控件
代表Button Label CheckBox Window
拥有Content属性单个对象,仅支持一个直接子元素。不能直接写多个标签。
Panel面板 (布局面板,继承自Panel)
代表:Grid StackPanel Canvas DockPanel WrapPanel
拥有Children(UIElementCollection,集合)可以放任意数量子控件。
RadioButton方法
private void RadioButton_Checked(Object sender,RouteEventArgs e)
1、sender:
是在XAML里,写了这个事件绑定的那个控件;简单的来讲:是谁挂了这个事件,谁就是sender。
<RadioButton Checked="RadioButton_Checked"/>
<RadioButton Checked="RadioButton_Checked"/>
两个单选框,全部绑定同一个RadioButton_Checked
触发的时候,sender就是当前点击的这个RadioButton,这种简单场景直接写
if(sender is RadioButton rb) 即可
2、e.OriginalSource(原始源)
WPF是路由事件(RoutedEvent)
控件内部是由很多更小的UI元素拼出来的,比如RadioButton里面包含圆形小圆圈+TextBlock文字
当你点击RadioButton的内部小圆点:
点击最先发生在"小圆点"这个最底层的元素,这个真正被送鼠标点击到的最底层UI对象,就是OriginalSource,然后事件向上冒泡,一路往上跑,跑到最外面的RadioButton,在跑到Grid,Window。
OriginSource=物理上真正产生这个事件的最原始元素。
什么时候sender!=e.OriginalSource
当把事件挂在外层父容器(比如StackPanel)上面!
<StackPanel Checked="spSex_Checked">
<RadioButton Content="男"/>
<RadioButton Content="女"/>
</StackPanel>
事件不是写在RadioButton身上!事件是写在StackPanel(SpSex)上面!
这是函数里面:
·sender永远等于StackPanel(因为是StackPanel挂载的方法,不会变)
sender拿不到到底里面哪一个RadioButton被点了!
那我们去哪里找真正触发动作的控件?就去读e.OriginalSource!
private void spSex_Checked(object sender,RoutedEventArgs e)
{
//sender 这里固定是StackPanel//
真正产生事件的控件,放在e.OriginalSource
if(e.OriginalSource is RadioButton rb)
{
rb就是被选中的那个单选按钮;
}
}
e.Source
还有一个属性叫e.Source(不带Original)
Source:路由冒泡过程中,当前这一跳的源。事件每往上走一层,Source就会被修改。
OriginalSource从头到尾永远不变,保存最开始那一个。
总结
OriginalSource :出生点,一辈子不变
Source:旅途上,当前站;冒泡每上一层就更新
sender:终点站,我们给它注册函数的哪一个对象。