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 ItemsControlWPF 将控件严格分为了两派。只能装一个元素的叫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拥有ChildrenUIElementCollection集合可以放任意数量子控件。RadioButton方法private void RadioButton_Checked(Object sender,RouteEventArgs e)1、sender:是在XAML里写了这个事件绑定的那个控件简单的来讲是谁挂了这个事件谁就是sender。RadioButton CheckedRadioButton_Checked/RadioButton CheckedRadioButton_Checked/两个单选框全部绑定同一个RadioButton_Checked触发的时候sender就是当前点击的这个RadioButton这种简单场景直接写if(sender is RadioButton rb 即可2、e.OriginalSource(原始源WPF是路由事件RoutedEvent控件内部是由很多更小的UI元素拼出来的比如RadioButton里面包含圆形小圆圈TextBlock文字当你点击RadioButton的内部小圆点点击最先发生在“小圆点”这个最底层的元素这个真正被送鼠标点击到的最底层UI对象就是OriginalSource然后事件向上冒泡一路往上跑跑到最外面的RadioButton在跑到GridWindow。OriginSource物理上真正产生这个事件的最原始元素。什么时候sendere.OriginalSource当把事件挂在外层父容器比如StackPanel上面StackPanel CheckedspSex_CheckedRadioButton 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.OriginalSourceif(e.OriginalSource is RadioButton rb){rb就是被选中的那个单选按钮}}e.Source还有一个属性叫e.Source(不带Original)Source:路由冒泡过程中当前这一跳的源。事件每往上走一层Source就会被修改。OriginalSource从头到尾永远不变保存最开始那一个。总结OriginalSource 出生点一辈子不变Source旅途上当前站冒泡每上一层就更新sender终点站我们给它注册函数的哪一个对象。