WPF新手村教程(三)路由事件如何为?

摘要:WPF个人文档(三)—— 路由事件 一.路由事件 先来说一下分类,参考某位博主以及博主参考的对应资料分类,先留个印象,然后我们逐步讲解 路由事件:冒泡事件、隧道事件(预览事件)、直接事件(直达事件) 事件(从作用角度划分):生命周期事件、输
WPF个人文档(三)—— 路由事件 一.路由事件 先来说一下分类,参考某位博主以及博主参考的对应资料分类,先留个印象,然后我们逐步讲解 路由事件:冒泡事件、隧道事件(预览事件)、直接事件(直达事件) 事件(从作用角度划分):生命周期事件、输入事件(鼠标事件、键盘输入事件、触控事件) 1.路由 —— 在既定结构中,按照规则传播信号的路径 路径 → 静态结构 规则 → 传播策略 信号 → 事件 路由:网络工程术语,指分组从源到目的地时,决定端到端路径的网络范围的进程 路由的本质:信息从哪里来,要往哪里去,经过哪些节点 1.最原始的路由:网络 在互联网里,一个数据包从电脑中发出去,不是直线飞到服务器 它要经过多个路由器,每个路由器根据规则决定“下一跳” 不是简单传递,而是“根据结构和规则决定传播路径” 2.抽象到程序中 当一个事件发生时,它不一定只通知一个对象,它可能沿着某种结构传播 而这整条“传播路径”就是我们所说的路由 # 比如 UI 是一棵树 Window └── Grid  └── Button 当 Button 被点击时,事件可以: 只在 Button 内处理(直达) 往上通知 Grid → Window(冒泡) 或者从 Window 先往下检查(隧道) 2.路由事件 前面我们已经提到过路由事件的分类,现在我们对其进行较为详细的 吐槽 讲解 路由事件:冒泡事件、隧道事件(预览事件)、直接事件(直达事件) 1.🌱冒泡事件 → 由里往外 传播方向:子 → 父 典型例子:MouseDown、KeyDown、Click 使用场景:子控件触发行为,但由父容器统一处理 统一日志 权限判断 容器级行为控制 MVVM 中命令转发 代码示例: // 只要窗口里任何 Button 被点击,Window 都能收到 this.AddHandler(Button.ClickEvent, new RoutedEventHandler(Window_Click)); 优点:解耦 缺点:调试时要小心,可能在半路被 e.Handled = true 截断 2.🌱隧道事件 → 从外往里 传播方向:父 → 子 事件先从 Window 往下走到最底层控件,然后才进入冒泡阶段 即:父级可以“预先审查”事件 典型事件格式:PreviewXXX 比如:PreviewMouseDown、PreviewKeyDown 经典场景:全局快捷键、输入过滤、行为拦截 代码示例: // 行为拦截:子控件接收不到按键 F5 private void Window_PreviewKeyDown(object sender, KeyEventArgs e) { if (e.Key == Key.F5) e.Handled = true; // e.Handled = true 表示这个按键的路由到此结束 // 这个事件已经被处理完了,不需要继续传播 // 1.阻止事件继续沿当前路由传播 // 2.阻止对应的后续阶段执行 } 3.🌱直达事件 → 不传播 传播方向:只在当前控件触发,不向上也不向下 典型例子:MouseEnter、MouseLeave、Loaded [!IMPORTANT] 为什么它只在当前控件触发,不向上不向下? 因为语义上不适合传播 鼠标进入 Button,不等于进入它父 Grid 如果冒泡,会产生逻辑污染 直达事件适合: 局部状态改变 单控件行为 不需要结构参与的逻辑 总结 🌱冒泡事件:我干了件事,要向上级汇报。 🌱隧道事件:我要干件事,谁敢不同意的? 🌱直达事件:我自己的事,谁也管不了我! 类型 传播方向 典型事件 核心作用 适用场景 优点 风险点 🌱冒泡事件 子 → 父 MouseDown KeyDown Click 子控件触发,上层统一处理 1.统一日志 2.权限判断 3,命令转发 4.容器控制 解耦、集中管理 可能被 e.Handled 中途截断,调试链路复杂 🌱隧道事件 父 → 子 PreviewMouseDown PreviewKeyDown 事件发生前的“预审查”机制 1.全局快捷键 2.输入过滤 3.行为拦截 可提前控制 滥用会造成隐藏拦截逻辑 🌱直达事件 不传播 MouseEnter MouseLeave Loaded 仅当前控件自身处理 1.局部状态变化 2.生命周期控制 语义清晰 不能参与结构级控制 二.普通事件 回顾之前的分类 事件(从作用角度划分):生命周期事件、输入事件(鼠标事件、键盘输入事件、触控事件) 换句话说,是根据 事件在干什么 分类的 1.生命周期事件 → 控件自己的阶段变化 要理解生命周期事件,需要考虑一件事情:这个控件现在处于什么阶段?,它描述的是“状态变化” 常见生命周期事件:Initialized、Loaded、Unloaded、SizeChanged 代码示例:通常为直达事件 // 控件初始化 public MyControl() { InitializeComponent(); this.Initialized += (s, e) => { Debug.WriteLine("控件初始化完成"); }; } // 控件在视觉树中的加载 private void UserControl_Loaded(object sender, RoutedEventArgs e) { // 这里可以安全访问 ActualWidth Debug.WriteLine(this.ActualWidth); } // 控件离开视觉树 private void UserControl_Unloaded(object sender, RoutedEventArgs e) { // 释放资源 timer.Stop(); } // 布局尺寸发生变化 private void Grid_SizeChanged(object sender, SizeChangedEventArgs e) { Debug.WriteLine($"新尺寸: {e.NewSize}"); } 2.输入事件 → 用户或外部行为驱动 要理解输入事件,需要考虑一件事情:用户做了什么?,来源是操作系统输入系统,本质是用户行为推动系统状态变化 常见输入事件:MouseDown / MouseMove、KeyDown / KeyUp 代码示例: // 键盘输入 private void Window_KeyDown(object sender, KeyEventArgs e) { if (e.Key == Key.Enter) { Submit(); } } // 隧道路由 -> 数据拦截 private void Window_PreviewKeyDown(object sender, KeyEventArgs e) { if (e.Key == Key.F5) { e.Handled = true; // 阻断传播 } } // 鼠标输入 private void Button_MouseDown(object sender, MouseButtonEventArgs e) { Debug.WriteLine("鼠标按下"); } 维度 生命周期事件 输入事件 核心问题 控件现在处于什么阶段? 用户做了什么? 时间属性 内部时间 外部时间 触发来源 控件状态变化 操作系统输入 是否依赖用户 否 是 典型事件 Loaded / Unloaded / SizeChanged MouseDown / KeyDown / TextInput 路由类型 多为 Direct 多为 Bubble + Tunnel 常见用途 初始化、释放、布局响应 行为驱动、交互控制 滥用风险 状态逻辑与业务耦合 传播链复杂、拦截难追踪 三.路由事件 VS 普通事件 普通事件 是 对象之间的通信 普通事件 强调 对象之间的关系 点对点:A → B 路由事件 是 结构中的信号传播 路由事件 强调 层级结构的控制权 A → 父 → 祖父 → … 或者 祖父 → 父 → A 路径由“结构”决定,而不是写代码时手动指定 维度 普通事件 路由事件 传播方式 点对点 沿视觉树传播 是否有传播路径 没有 有(冒泡 / 隧道 / 直达) 是否可拦截 不支持 支持 e.Handled 结构参与决策 不参与 由 UI 树结构决定 典型场景 业务逻辑通知 输入系统、控件行为管理 复杂度 简单 较复杂 四.UI与树状结构 —— 以 “空间和归属” 思考UI结构 在路由事件中,我们可以将UI 的可视元素(Window、Grid、Button、TextBox……)看作一棵树(树状结构) Window ├─ Grid │ ├─ Button │ └─ TextBox └─ StatusBar 这棵树告诉我们: 父级和子级的关系 控件层级和布局关系 事件传播路径(冒泡/隧道) 树结构决定了结构相关的行为: 路由事件传播 父控件统一拦截子控件行为 布局和绘制顺序 [!NOTE] 如何在Visual Studio中查看WPF中UI的树状结构? 只需要打开文档大纲即可,文档大纲在项目中默认显示在左侧 如果不小心关闭,使用快捷键 Ctrl+Alt+T 来快速打开文档大纲视图 随笔参考:25.第5章_路由概念及路由事件_哔哩哔哩_bilibili