7.WPF事件

7.WPF事件

7. WPF事件

路由事件

路由事件与直接事件(WinForm方式的事件)的区别在于:

  • 直接事件激发时,发送者直接将消息通过事件订阅交给事件响应者,事件响应者通过处理方法做出响应。
  • 路由事件的事件拥有者和响应者没有直接的订阅关系,事件拥有者只负责触发事件,事件的响应者则是安装事件监听器,针对某类事件进行侦听,当有此类事件传递到响应者就用事件处理方法来响应,并决定事件是否要继续向下传递。

WPF内置路由事件

案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<Grid x:Name="gridRoot">
<Grid x:Name="gridA">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Canvas x:Name="canLeft" Grid.Column="0" >
<Button x:Name="btnLeft" Content="Left"/>
</Canvas>
<Canvas x:Name="canRight" Grid.Column="1" >
<Button x:Name="btnRight" Content="Right"/>
</Canvas>
</Grid>
</Grid>

逻辑树结构

当单击btnLeft时,Button.Click事件会沿着btnLeft-canLeft-gridA-gridRoot-Window路线传送,单击btnRight原理相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public MainWindow()
{
InitializeComponent();
//为gridRoot安装针对Button.Click事件的监听器
this.gridRoot.AddHandler(Button.ClickEvent, new RoutedEventHandler(this.ButtonClicked));
}

//事件处理程序
private void ButtonClicked(object sender, RoutedEventArgs e)
{
//路由事件是一层层传出的,最后到达gridRoot,并由gridRoot将事件消息交给事件处理程序
//所以sender是gridRoot,而不是btnLeft或者btnRight,这点和传统的直接事件不同
//e.OriginalSource可以查看事件的最初发起者
MessageBox.Show((e.OriginalSource as FrameworkElement).Name);
}

在XAML实现,<Grid x:Name="gridRoot" Button.Click="ButtonClicked">

自定义路由事件

自定义路由事件大致分为3个步骤:

  1. 声明并注册路由事件,使用EventManager的RegisterRoutedEvent方法进行注册
  2. 为路由事件添加包装器,目的是把路由事件暴露的像一个传统直接事件,并仍然可以使用+=或者-=操作符。
  3. 创建可以激发事件的方法

案例:当点击按键时,报告事件发生的时间

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
38
39
40
41
42
43
44
//用于承载事件消息的时间参数
class ReportTimeEventArgs : RoutedEventArgs
{
public ReportTimeEventArgs(RoutedEvent routedEvent, object source) : base(routedEvent, source)
{
}
public DateTime ClickTime { set; get; }
}
class TimeButton:Button
{
//声明和注册事件
//参数1:路由事件名称,和事件包装器的名称相同
//参数2:路由事件的策略,wpf路由事件的策略有3种
// Bubble:冒泡式,由激发者向上级容器一层一层传递直到UI树的根部,路径唯一
// Tunnel:隧道式,与Bubble策略相反,路径不唯一
// Direct: 直达式,直接将事件消息发送到事件处理方法
//参数3:事件类型
//参数4:事件拥有者
public static readonly RoutedEvent ReportTimeEvent = EventManager.RegisterRoutedEvent("ReportTime", RoutingStrategy.Bubble, typeof(EventHandler<ReportTimeEventArgs>), typeof(TimeButton));

//路由事件的包装器,固定写法
public event RoutedEventHandler ReportTime
{
add { this.AddHandler(ReportTimeEvent, value); }
remove { this.RemoveHandler(ReportTimeEvent, value); }
}

//激发路由事件,使用Click激发
protected override void OnClick()
{
base.OnClick();
ReportTimeEventArgs args = new ReportTimeEventArgs(ReportTimeEvent, this);
args.ClickTime = DateTime.Now;
this.RaiseEvent(args);
}
}

//ReportTimeEvent事件处理方法
private void ReportTimeHandle(object sender, ReportTimeEventArgs e)
{
FrameworkElement element = sender as FrameworkElement;
string timeStr = e.ClickTime.ToLongTimeString();
this.listBox.Items.Add($"{timeStr} 到达 {element.Name}");
}
1
2
3
4
5
6
7
8
9
10
<Grid x:Name="grid_1" local:TimeButton.ReportTime="ReportTimeHandle">
<Grid x:Name="grid_2" local:TimeButton.ReportTime="ReportTimeHandle">
<Grid x:Name="grid_3" local:TimeButton.ReportTime="ReportTimeHandle">
<StackPanel x:Name="stackPanel" local:TimeButton.ReportTime="ReportTimeHandle">
<ListBox x:Name="listBox"/>
<local:TimeButton x:Name="timeBtn" Content="报时" local:TimeButton.ReportTime="ReportTimeHandle"/>
</StackPanel>
</Grid>
</Grid>
</Grid>

如果传递到某个节点不再继续向下传递事件可以将RoutedEventArgs中的Handled属性设置为true,意思为“已经处理完成”。

1
2
3
4
5
6
7
8
9
10
11
12
//ReportTimeEvent事件处理方法
private void ReportTimeHandle(object sender, ReportTimeEventArgs e)
{
FrameworkElement element = sender as FrameworkElement;
string timeStr = e.ClickTime.ToLongTimeString();
this.listBox.Items.Add($"{timeStr} 到达 {element.Name}");

if (element == this.grid_2)
{
e.Handled = true;
}
}

Source和OriginalSource

我们常说的WPF树形结构通常指的是LogicalTree,而事件则是沿着VisualTree传递的,他俩的区别在于:LogicalTree的叶子结点构成了用户界面,而VisualTree要连控件中的细微结构也算上。如一个ListBox控件的细微结构由Border、ScrollViewer、Grid等等组成。

  • Source代表着LogicalTree的事件起点
  • OriginalSource代表着VisualTree上的事件起点

附加事件

常见的附加事件

  • Binding类:SourceUpdated事件,TargetUpdated事件
  • Mouse类:MouseEnter事件、MouseLeave事件等
  • Keyboard类:KeyDown事件、KeyUp事件等

可以看出,路由事件的宿主都是拥有可视化实体的界面元素,而附加事件不具备显示在用户界面上的能力。

案例:设计一个Student类,如果其中的Name属性发生变化则激发一个路由事件,并用界面元素来捕捉。

1
2
3
<Grid x:Name="gird">
<Button x:Name="btn1" Content="OK" Click="Btn1_Click"/>
</Grid>
1
2
3
4
5
6
7
8
class Student
{
//声明并定义路由事件
public static readonly RoutedEvent NameChangedEvent = EventManager.RegisterRoutedEvent("NamgeChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(Student));

public int Id { set; get; }
public string Name { set; get; }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public MainWindow()
{
InitializeComponent();

//为gird添加路由事件监听器
this.gird.AddHandler(Student.NameChangedEvent, new RoutedEventHandler(this.StudentNameChangedHandler));
}

//Grid的事件处理方法
private void StudentNameChangedHandler(object sender, RoutedEventArgs e)
{
MessageBox.Show((e.OriginalSource as Student).Id.ToString());
}

//非UIElement类没有RaiseEvent方法,所以要借用一个Button
private void Btn1_Click(object sender, RoutedEventArgs e)
{
Student student = new Student() { Id = 100, Name = "Tim" };
student.Name = "Tom";
//准备事件消息并发送路由事件
RoutedEventArgs arg = new RoutedEventArgs(Student.NameChangedEvent, student);
this.btn1.RaiseEvent(arg);
}

为附加事件增加包装器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//在Student类中增加
//为目标UI元素增加事件监听器的包装器
//参数1:事件监听者
//参数2:事件处理函数
public static void AddNameChangedHandler(DependencyObject d, RoutedEventHandler h)
{
UIElement e = d as UIElement;
if (e != null)
{
e.AddHandler(Student.NameChangedEvent, h);
}
}
//为目标UI元素移除事件监听器的包装器
public static void RemoveNameChangedHandler(DependencyObject d, RoutedEventHandler h)
{
UIElement e = d as UIElement;
if (e != null)
{
e.RemoveHandler(Student.NameChangedEvent, h);
}
}

这样可以将上面
this.gird.AddHandler(Student.NameChangedEvent, new RoutedEventHandler(this.StudentNameChangedHandler));
改为
Student.AddNameChangedHandler(this.gird, new RoutedEventHandler(this.StudentNameChangedHandler));
或者删除上句将XMAL改为
<Grid x:Name="gird" local:Student.NameChanged="StudentNameChangedHandler">

作者

步步为营

发布于

2024-05-08

更新于

2025-03-15

许可协议