1.1 逻辑树与可视树
如果把一片树叶放在显微镜下观察,你会发现这片叶子也像一棵树----有自己的基部并向上生长出多级分叉。在WPF的Logic Tree上,扮演叶子的一般都是控件。如果我们把WPF中的控件也放在显微镜下观察,你会发现WPF控件本身也是一棵由更细微级别的组件(他们不是控件,而是一些可视化组件,派生至Visual类)组成的树。
在WPF中有两种树:逻辑树(Logical Tree)和可视树(Visual Tree),XAML是表达WPF的一棵树。逻辑树完全是由布局组件和控件构成。如果我们把逻辑树延伸至Template组件级别,我们就得到了可视树,所以可视树把树分的更细致。
1.2 事件的来龙去脉
微软把消息机制封装成了更容易让人理解的事件模型。
事件模型隐藏了消息机制的很多细节,让程序开发变的简单。繁琐的消息驱动机制在事件模型中被简化为了3个关键点:
事件的拥有者:即消息的发送者。事件的宿主可以在某些条件下激发它拥有的事件,事件被触发则消息被发送。
事件的响应者:即消息的接收者、处理者。事件接收者使用其事件处理器(EventHandler)对事件做出响应。
事件的订阅关系:事件的拥有者可以随时激发事件,但事件发生后会不会得到响应要看有没有事件响应者,或者说要看这个事件是否被关注。如果对象A关注对象B的某个事件是否发生,则称A订阅了B的某个事件。更进一步讲,事件实际上是一个使用Event关键字修饰的委托类型的成员变量,事件处理器则是一个函数,说A订阅了B的某个事件,本质就是让B.Event和A.EventHandler关联起来。所谓事件激发就是B.Event被调用,这时,与其关联的A.EventHandler就会被调用。
在这种模型里,事件的响应者通过订阅关系直接关联在事件拥有者的事件上,为了与WPF路由事件模型分开,我们把这种事件模型称为直接事件模型或CLR事件模型。每条消息是“发送---响应”关系,必须显示的建立点对点订阅关系。
1.3 初试路由事件
为了降低由事件订阅带来的耦合度和代码量,WPF推出了路由事件机制。路由事件和直接事件的区别在于,直接事件激发时,发送者直接将消息通过事件订阅交给事件的响应者,事件响应者使用其事件处理器方法对事件的发生做出响应驱动逻辑程序按客户需求运行。路由事件的拥有者和事件响应者之间则没有直接显示的订阅关系,事件的拥有者只负责激发事件,事件将由谁响应它并不知道,事件的响应者则安装有事件侦听器,针对某类事件进行侦听。当有某类事件传递至此时事件响应者就使用事件处理器来响应事件并决定事件是否可以继续传递。
举个例子,在Visual Tree上有一个button控件,当它被单击的时候就相当于自己喊了一声“我被单击了”,这样一个button.Click开始在Visual Tree上开始传播。当事件经过某个节点的时候如果这个节点没有安装用于侦听button.Click事件的“耳朵”,那么它会无视这个事件,让它继续畅通无阻的继续传播,如果某个节点安装了针对button.Click的侦听器,它的事件处理器就会被调用,在事件处理器内部,程序员可以查看路由事件原始的出发点是哪个控件,上一站是哪里,还可以决定事件传递到此为止还是可以继续往下传递----路由事件就是这样依靠“口耳相传”的办法将消息传递给“关心”它的控件的。虽然WPF推出了路由事件机制,但它仍然支持传统的直接事件模型。
下面通过一个例子初试一下路由事件:
<Window x:Class="WpfApplication8.wnd831" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="200" Width="200"> <Grid x:Name="_gridRoot" Background="Lime"> <Grid x:Name="_gridA" Background="Blue" Margin="10"> <Grid.ColumnDefinitions> <ColumnDefinition></ColumnDefinition> <ColumnDefinition></ColumnDefinition> </Grid.ColumnDefinitions> <Canvas x:Name="_canvasLeft" Background="Red" Margin="10" Grid.Column="0"> <Button x:Name="_buttonLeft" Content="Left" Margin="10" Width="45" Height="110"/> </Canvas> <Canvas x:Name="_canvasRight" Background="LightSlateGray" Margin="10" Grid.Column="1"> <Button x:Name="_buttonRight" Content="Right" Margin="10" Width="45" Height="110"/> </Canvas> </Grid> </Grid> </Window>
我们点击按钮时,无论是_buttonLeft还是_buttonRight单击都能显示按钮的名称。两个按钮到顶部的window有唯一条路,左边的按钮对应的路:_buttonLeft->_canvasLeft->_gridA->_GridRoot->_Window,右边按钮对应的路:_buttonRight->_canvasRight->_gridA->_GridRoot->_Window。如果GridRoot订阅两个处理器,那么处理器应该是相同的。后台代码为:
/// <summary> /// MainWindow.xaml 的交互逻辑 /// </summary> public partial class wnd831 : Window { public wnd831() { InitializeComponent(); // 为指定的路由事件添加路由事件处理程序 _gridRoot.AddHandler(Button.ClickEvent, new RoutedEventHandler(ButtonClicked)); } public void ButtonClicked(object sender, RoutedEventArgs e) { MessageBox.Show((e.OriginalSource as FrameworkElement).Name); } }
下面先解释一下路由事件是怎么沿着可视树来传播的,当Button被点击,Button就开始发送消息了,可视树上的元素如果订阅了Button的点击事件,那么才会根据消息来作出相应的反应,如果没有订阅的话,就无视它发出的消息,当然我们还可以控制它的消息的传播方式,是从树根到树叶传播,还是树叶向树根传播以及是直接到达目的传播,不仅如此,还能控制消息传到某个元素时,停止传播。具体的会在后面记录到。
其次是this._GridRoot.AddHandler(Button.ClickEvent,new RoutedEventHandler(this.ButtonClicked));订阅事件时,第一个参数是路由事件类型,在这里用的是Button的ClickEvent,就像依赖属性一样,类名加上依赖属性,这里是类名加上路由事件。另外一个是e.OriginalSource与e.Source的区别,由于消息每传一站,都要把消息交到下一个控件(此控件成为了消息的发送地点),e.Source为逻辑树上的源头,要想获取原始发消息的控件(可视树的源头)要用e.OriginalSource。
1.4 自定义路由事件
创建自定义路由事件大体分为3个步骤:
声明并注册路由事件。
为路由事件添加CLR事件包装。
创建可以激发路由事件的方法。
声明路由事件参数:
/// Event Handler delegate void ReportTimeRouteEventHandler(object sender, ReportTimeRoutedEventArgs e); /// <summary> /// 包含时间的路由事件参数 /// </summary> public class ReportTimeRoutedEventArgs : RoutedEventArgs { public ReportTimeRoutedEventArgs(RoutedEvent routedEvent, object source) : base(routedEvent, source){} public DateTime ClickTime { get; set; } }
创建路由事件:
/// <summary> /// 继承Button /// </summary> public class TimeButton : Button { /// <summary> /// 声明和注册路由事件 /// </summary> public static readonly RoutedEvent ReportTimeEvent = EventManager.RegisterRoutedEvent( "ReportTime", RoutingStrategy.Bubble, typeof(ReportTimeRouteEventHandler)/*typeof(EventHandler<ReportTimeRoutedEventArgs>)*/, typeof(TimeButton)); /// <summary> /// CLR事件包装器 /// </summary> public event RoutedEventHandler ReportTime { add { this.AddHandler(ReportTimeEvent, value); } remove { this.RemoveHandler(ReportTimeEvent, value); } } /// <summary> /// 激发路由事件,借用Click事件激发 /// </summary> protected override void OnClick() { base.OnClick(); ReportTimeRoutedEventArgs args = new ReportTimeRoutedEventArgs(ReportTimeEvent, this); args.ClickTime = DateTime.Now; this.RaiseEvent(args); } }
注册路由事件时注意:
第一个参数是一个String类型,被称为路由事件的名称,按微软的建议,这个字符串应该与RountEvent变量的前缀和CLR事件包装器名称一致。本例中,路由事件的名称是ReportTimeEvent,则此字符串是ReportTime,CLR事件名亦为ReportTime。
第二个参数为路由事件的策略。WPF路由事件有三种路由策略:
Bubble,冒泡式:路由事件由事件激发者出发向它的上一层容器一层一层路由,直至最外层的容器(Windows或Page)。因为是由树的底部想树的顶部移动,而且从事件激发元素到UI树的树根只有确定的一条路径,所以这种策略被形象的命名为“冒泡式”。
Tunnel,隧道式:事件的路由刚好和冒泡式相反,是由树的树根向事件激发者移动,这就想当于在树根和目标控件之间挖了一条隧道,事件只能沿着隧道移动,所以称为“隧道式”。
Direct,直达式:模仿CLR直接事件,直接将事件消息送达事件处理器。
第三个参数用于指定事件处理器的类型。事件处理器的返回值类型和参数列表必须与此参数指定的委托保持一致,不然会导致在编译的时候报异常。
第四个参数用于指定路由事件的宿主(拥有者)是哪个类型。
XAML:让控件都监听类型为ReportTime的路由事件:
<Window x:Class="WpfApplication8.wnd832" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:WpfApplication8" Title="wnd832" Height="300" Width="300"> <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="dock1" local:TimeButton.ReportTime="ReportTimeHandle"> <ListBox x:Name="listBox1" local:TimeButton.ReportTime="ReportTimeHandle"/> <local:TimeButton x:Name="btn1" Content="报时" Width="80" Height="80" local:TimeButton.ReportTime="ReportTimeHandle"/> </StackPanel> </Grid> </Grid> </Grid> </Window>
事件处理器:
public void ReportTimeHandle(object sender, ReportTimeRoutedEventArgs e) { FrameworkElement ele = sender as FrameworkElement; string content = string.Format("{0}到达{1}", e.ClickTime.ToLongTimeString(), ele.Name); listBox1.Items.Add(content); //当事件传递到grid_2就停止了 if (ele == grid_2) { e.Handled = true; } }
参考《深入浅出WPF》