7. WPF事件
路由事件
路由事件与直接事件(WinForm方式的事件)的区别在于:
直接事件激发时,发送者直接将消息通过事件订阅交给事件响应者,事件响应者通过处理方法做出响应。
路由事件的事件拥有者和响应者没有直接的订阅关系,事件拥有者只负责触发事件,事件的响应者则是安装事件监听器,针对某类事件进行侦听,当有此类事件传递到响应者就用事件处理方法来响应,并决定事件是否要继续向下传递。
WPF内置路由事件
案例:
<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原理相同。
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个步骤:
声明并注册路由事件,使用EventManager的RegisterRoutedEvent方法进行注册
为路由事件添加包装器,目的是把路由事件暴露的像一个传统直接事件,并仍然可以使用+=或者-=操作符。
创建可以激发事件的方法
案例:当点击按键时,报告事件发生的时间
//用于承载事件消息的时间参数 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}"); }
<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,意思为“已经处理完成”。
//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属性发生变化则激发一个路由事件,并用界面元素来捕捉。
<Grid x:Name="gird"> <Button x:Name="btn1" Content="OK" Click="Btn1_Click"/> </Grid>
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; } }
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); }
为附加事件增加包装器
//在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">