WPF/MVVM系列(4)——事件

学习WPF的路由事件,我认为最重要是以下两点:

  1. 相比传统的消息驱动和事件驱动模型,路由事件的优势是什么。

  2. 基于WPF的两种树形结构,直观理解路由事件的运行规律。

关于如何自定义路由事件以及附加事件,通常不是开发者的重点,如有必要可再深入探究。

消息驱动和事件驱动

消息驱动

Windows是消息驱动的操作系统,在Windows下进行GUI程序开发,最底层的方式就是Windows API。以下是一个窗口消息处理函数,整体是switch条件判断结构,不同的消息流向不同的分支被处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_COMMAND:
{
int wmId = LOWORD(wParam);
// 分析菜单选择:
switch (wmId)
{
case IDM_ABOUT:
DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About);
break;
case IDM_EXIT:
DestroyWindow(hWnd);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
}
break;
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
// TODO: 在此处添加使用 hdc 的任何绘图代码...
EndPaint(hWnd, &ps);
}
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}

注册窗体类时,将以上消息处理函数的地址赋值给窗体类的lpfnWndProc字段,就完成了消息处理函数的绑定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ATOM MyRegisterClass(HINSTANCE hInstance)
{
WNDCLASSEXW wcex;
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = WndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = hInstance;
wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_PROJECT1));
wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wcex.lpszMenuName = MAKEINTRESOURCEW(IDC_PROJECT1);
wcex.lpszClassName = szWindowClass;
wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));
return RegisterClassExW(&wcex);
}

事件驱动

基于消息驱动的GUI程序开发与底层Windows API打交道较多,开发门槛较高。微软采用面向对象的方式对其进一步封装,就得到了事件驱动模型,最为典型的就是WinForms技术。

(1)在WinFroms窗体上添加一个按钮“按钮1”:

(2)双击界面设计器的按钮,跳转到按钮单击事件处理函数中:

(3)界面设计器会自动把按钮单击事件处理函数绑定到按钮的单击事件上:

以上例子将事件处理函数(Form1的button1_Click函数)显式地绑定到按钮的单击事件上(button1.Click)。

总结一下事件驱动的关键特点:不管是窗体设计器自动绑定还是人为绑定,肯定存在“显式绑定”这一操作;且在绑定时,控件的事件、事件处理函数必须可见。

对于复杂软件系统而言,事件驱动的这一“显式绑定”要求会增加模块之间的耦合度,伤害到软件的可维护性,为此WPF路由事件应运而生!

WPF的两种树

在正式学习WPF路由事件之前,先了解WPF中的两种树形结构——逻辑树、可视化树,这是理解路由事件的基础。

逻辑树(Logical Tree):由控件和布局组件组成。

可视化树(Visual Tree):将控件和布局组件延伸至模板级别,就是可视化树。

以下面这个WPF窗体为例,直观展示WPF这两种属性结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<Window x:Class="TestWPF01.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:TestWPF01"
mc:Ignorable="d"
Title="MainWindow" SizeToContent="WidthAndHeight">
<Grid HorizontalAlignment="Center">
<StackPanel Orientation="Vertical">
<StackPanel Orientation="Horizontal">
<Button Margin="5" Content="按钮1" Width="150" Height="30" Click="Button_Click"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<Button Margin="5" Content="按钮2" Width="150" Height="30" Click="Button_Click_1"/>
</StackPanel>
</StackPanel>
</Grid>
</Window>

使用Snoop工具查看窗体的逻辑树:

使用Snoop工具查看窗体的可视化树:

在VS中调试WPF窗体程序时,也可以查看可视化树,与Snoop工具展示的结果一致:

注意:路由事件在可视化树上传递。

路由事件

现在,我们开始探索WPF路由事件是如何避免事件和事件处理器的显式绑定的。

使用步骤

当某个控件产生某种事件时,事件会沿着窗体的可视化树从底向上传播,如果途中节点设置了相应的事件侦听器,那么就会自动做出响应。

因此,使用路由事件非常简单,只需要在控件的某个父类节点上设置事件侦听器。

对于上节的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<Window x:Class="TestWPF01.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:TestWPF01"
mc:Ignorable="d"
Title="MainWindow" SizeToContent="WidthAndHeight">
<Grid HorizontalAlignment="Center" x:Name="grid01">
<StackPanel Orientation="Vertical" x:Name="sp1">
<StackPanel Orientation="Horizontal" x:Name="sp2">
<Button Margin="5" Content="按钮1" Width="150" Height="30"/>
</StackPanel>
<StackPanel Orientation="Horizontal" x:Name="sp3">
<Button Margin="5" Content="按钮2" Width="150" Height="30"/>
</StackPanel>
</StackPanel>
</Grid>
</Window>

在窗体的构造函数中设置路由事件侦听器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using System.Windows;
using System.Windows.Controls;

namespace TestWPF01 {
public partial class MainWindow : Window {
public MainWindow() {
InitializeComponent();
this.grid01.AddHandler(Button.ClickEvent, new RoutedEventHandler(this.ButtonClicked));
}

private void ButtonClicked(object sender, RoutedEventArgs e) {
MessageBox.Show("有按钮被单击了!");
}
}
}

也可以在XAML中设置侦听器:

1
2
<Grid HorizontalAlignment="Center" x:Name="grid01" Button.Click="ButtonClicked">
</Grid>

这时,无论单击哪个按钮,单击事件都会被grid01的侦听器监测到,都会弹出提示框。

获取路由事件的源头

通过路由事件处理器的事件参数可以获得事件源头,其中Source返回逻辑树上的事件源头,OriginalSource返回可视化树上的事件源头。

1
2
3
4
private void ButtonClicked(object sender, RoutedEventArgs e) {
MessageBox.Show($"{(e.Source as FrameworkElement).Name} 被单击了!");
MessageBox.Show($"{(e.OriginalSource as FrameworkElement).Name} 被单击了!");
}

评论