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
