作过.net开发的朋友对于事件应该都不陌生。追溯历史,事件(Event)首先应用在Com和VB上,它是对在MFC中使用的烦琐的消息机制的一个封装,而后.net又继承了这种事件驱动机制,这种事件也叫.net事件。正如WPF在简单的.net属性概念上添加了许多基础的东西同样,它也为.net事件添加了许多基础的东西。路由事件(RoutedEvent)是专门设计用于在元素树中使用的事件。当路由事件触发后,它能够向上或向下遍历逻辑树和可视树,用一种简单并且持久的方式在每一个元素上触发,而不须要使用任何定制代码。下面咱们就来学习下路由事件,本节主要包括如下几个方面内容:路由事件和WPF事件(包括键盘输入、鼠标输入和多点触控输入等)。windows
路由事件的实现和行为与依赖属性有不少相同的地方。咱们仍是先举个例子来讲明下路由事件的实现方式,而后再来说下路由事件的一些特性。拿最经常使用的Button的Click事件(继承自ButtonBase抽象类)来讲明:app
class MyButton:Button { //Add CLR Event wrapper for Routed Event public event RoutedEventHandler MyClick { add { this.AddHandler(MyClickEvent, value); } remove { this.RemoveHandler(MyClickEvent, value); } } //State and Register Routed Event public static readonly RoutedEvent MyClickEvent = EventManager.RegisterRoutedEvent("MyClick", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(MyButton)); //Trigger Method of Routed Event protected override void OnClick() { base.OnClick(); RoutedEventArgs newEvent = new RoutedEventArgs(MyClickEvent, this); this.RaiseEvent(newEvent); } }
我照着依赖属性的Code Snippet的样子也写个了路由事件的,名字无所谓,只要不冲突就好,代码以下:框架
<?xml version="1.0" encoding="utf-8"?> <CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet"> <CodeSnippet Format="1.0.0"> <Header> <Title>定义一个 Routed Event</Title> <Shortcut>propre</Shortcut> <Description>将 RoutedEvent 用做后备存储的路由事件的代码段</Description> <Author>Jello Chen</Author> <SnippetTypes> <SnippetType>Expansion</SnippetType> </SnippetTypes> </Header> <Snippet> <Declarations> <Literal> <ID>Click</ID> <ToolTip>事件类型</ToolTip> <Default>Click</Default> </Literal> <Literal> <ID>newEvent</ID> <ToolTip>路由事件参数</ToolTip> <Default>newEvent</Default> </Literal> </Declarations> <Code Language="csharp"> <![CDATA[ //Add CLR Event wrapper for Routed Event public event RoutedEventHandler $Click$ { add {this.AddHandler($Click$Event,value);} remove {this.RemoveHandler($Click$Event,value);} } //State and Register Routed Event public static readonly RoutedEvent $Click$Event = EventManager.RegisterRoutedEvent("$Click$",RoutingStrategy.Bubble,typeof(RoutedEventHandler),typeof(ButtonBase)); //Trigger Method of Routed Event protected virtual void On$Click$ () { RoutedEventArgs $newEvent$ = new RoutedEventArgs($Click$Event,this); this.RaiseEvent($newEvent$); } $end$]]> </Code> </Snippet> </CodeSnippet> </CodeSnippets>
看起来是否是和依赖属性的结构很像?首先是声明注册了一个RoutedEvent类型的MyClickEvent,也由static readonly修饰,也是经过一个静态方法来得到实例,方法多了第二个事件路由策略的参数;而后是为其添加CLR事件包装器MyClick,分别经过AddHandler和RemoveHandler来向路由事件添加和移除一个委托,这两个方法不是在DependencyObject中定义的,而是在更高层的UIElement类中定义的;最后定义了一个路由事件的触发方法,实例化一个该路由事件相关的路由事件参数,将其做为参数传入RaiseEvent激发事件方法中,这个方法也是定义在UIElement类中的。ide
咱们先来使用上面的代码:布局
Xaml代码:学习
<local:MyButton x:Name="btnTest" Content="Press me" Width="80" Height="30"/>
C#代码:ui
public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); this.btnTest.MyClick +=new RoutedEventHandler(btnTest_MyClick); } private void btnTest_MyClick(object sender, RoutedEventArgs e) { MessageBox.Show("I am Pressed!"); } }
这里,咱们在构造器中使用过程式代码来进行路由事件的订阅,固然也能够在Xaml中使用,由VS来自动生成这个订阅关系。看起来和咱们在Webform和Winform中使用的没什么差异,这应该归功于事件包装器(Event Wrapper)。下面来详细说下路由事件的注册方法EventManager.RegisterRoutedEvent(string name, RoutingStrategy routingStrategy, Type handlerType, Type ownerType):this
再来看下路由事件处理程序的签名,它与.net事件处理程序签名相匹配。第一个参数是Object类型,指该处理程序被添加到的元素;第二个参数是RoutedEventArgs类型,它是EventArgs的子类,它提供了四个属性:spa
Source属性:逻辑树中一开始触发该事件的元素。.net
OriginalSource属性:可视树中一开四触发该事件的元素。
Handled属性:将事件标记为是否已处理,true时,Tunnel隧道方式和Bubble冒泡方式将再也不继续,不然继续。
RoutedEvent属性:指真正的路由事件对象,主要用于当一个事件处理程序同时被多个路由事件。
来看个冒泡事件的例子:
Xaml代码:
<Window x:Class="RoutedEventDemo.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window1" Height="300" Width="300" MouseDown="Image_MouseDown" x:Name="window1"> <Grid MouseDown="Image_MouseDown" x:Name="grid1"> <StackPanel x:Name="sp1" MouseDown="Image_MouseDown"> <TextBlock x:Name="tb1" MouseDown="Image_MouseDown" Width="60" Height="60"> <Image x:Name="img1" Source="Images/photo.png" MouseDown="Image_MouseDown"/> </TextBlock> <CheckBox Content="Handler Setting" x:Name="cb"/> </StackPanel> </Grid> </Window>
这里的CheckBox是用来设置Handler属性。
过程式代码:
private int count = 0; private void Image_MouseDown(object sender, MouseButtonEventArgs e) { count++; string msg = "#" + count.ToString() + ":\r\n" + "sender:" + sender.ToString() + "\r\n" + "Source:" + e.Source + "\r\n" + "OriginalSource:" + e.OriginalSource + "\r\n"; e.Handled = (bool)this.cb.IsChecked; MessageBox.Show(msg); }
CheckBox未选中时,会触发5次,依次为Image--TextBlock--StackPanel--Grid--Window;选中时只会冒泡一次到Image。须要注意的是:Button继承自UIElement的MouseDown事件(真正定义在Mouse类)在类内部已经处理(事件被挂起),通常不会触发附加到Mouse类的其它实例的事件处理,了解详细点击MSDN,里面提到了两种解决办法,更推荐使用相对应的Preview隧道事件来处理。
附加事件的思想,其实和附加属性是同样,某一元素没有某一种事件,须要将该事件附加在该元素上以实现相应的事件监听处理功能。先来看这样的场景:
Xaml代码:
<Window x:Class="RoutedEventDemo.Window2" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window2" Height="300" Width="300"> <Grid> <StackPanel> <Button Content="1" /> <Button Content="2" /> <Button Content="3" /> </StackPanel> </Grid> </Window>
如今须要用一个事件处理程序来处理StackPanel中的全部按钮的单击事件。天然而然,咱们会想到监听StackPanel的Click事件。然而,Click事件是Button特有的事件(继承自ButtonBase),这时候就须要使用附加事件来解决了。写成这样<StackPanel ButtonBase.Click="StackPanel_Click">......</StackPanel>。这里没写成Button.Click是为了智能提示的方便,固然在写出Button.Click后Xaml已经能识别出这是Button的Click事件。固然,这两种是由区别的,Button.Click只能监听Button类型的单击事件,而ButttonBase.Click能监听ButtonBase类型的单击事件,例如Button、RadioButton和CheckBox等。
过程式代码:
this.sp.AddHandler(Button.ClickEvent, new RoutedEventHandler(StackPanel_Click));
UIElement.AddHandler还有一个重载版本:public void AddHandler(RoutedEvent routedEvent, Delegate handler, bool handledEventsToo);须要注意的是第三个参数,若是为 true,则将按如下方式注册处理程序:即便路由事件在其事件数据中标记为已处理,也会调用该处理程序;若是为 false,则使用默认条件注册处理程序,即当路由事件被标记为已处理时,将不调用处理程序。默认值为false。前面说过这不是一种好的方式,由于事件应在第一时间被处理,而应该尽量地避免处理已经处理过的事件。由这也能看出来,当将事件标记为已处理时,隧道传递和冒泡仍然会继续,只不过在默认状况下事件处理程序只处理未处理的事件。
在WPF框架中,已经为咱们封装了许多的事件,主要分为这么几类:
当首次建立或释放元素时都会触发一些事件,这些事件就是生命周期事件。
来看下这些事件的执行时机。在FrameworkElement中实现了ISupportInitialize接口,该接口中有两个方法BeginInit()和EndInit(),前者是用信号通知对象初始化即将开始,当元素被实例化后调用该方法,而后开始属性设置;后者是用信号通知对象初始化已完成,当初始化完成后调用该方法,而后触发Initialized事件。实现这个接口的目的是保证初始化工做的原子性。当初始化窗口时,是从下到上进行的,也就是从叶子节点开始的,这保证了当某一元素须要内容时其内容都已初始化。当全部的元素都已初始化完成后,而后开始布局、应用样式及可能的数据绑定等。Initialized事件后,开始触发Loaded事件,Load的顺序和Initialized的顺序相反,是从上到下进行的,当全部的元素都Load完成后,窗口显示出来。
对于Window窗口还有一些特有事件:
当但愿在窗口首次加载时作一些额外的初始化工做时就能够经过在其Loaded事件的处理程序中完成。正常状况下,也能够在窗口构造器中的InitializeComponent()方法后来处理。
用户经过一些外设如鼠标、键盘、手写笔和多点触控屏等来进行输入操做时触发的事件,就是输入事件。输入事件能够经过继承自InputEventArgs自定义事件参数类来附加额外的信息,看下继承关系图:
InputEventArgs事件参数类在RoutedEventArgs类基础上只增长了Timestamp和Device两个属性,Timestamp属性表示事件什么时候发生的毫秒数,用于用于比较各事件之间的发生顺序,值越大越是最近发生;Device属性表示触发事件的设备的对象,设备对象是继承自抽象类System.Windows.Input.InputDevice的子类的实例。
当用户按下一个键,就会触发一系列的事件,这里按顺序依次列出公共的事件:
上面是一些公共的事件,不一样控件可能还有一些本身特有的事件,为了避免冲突,还会将上面的致使冲突的事件挂起。例如TextBox控件拥有TextChanged事件,而挂起了TextInpute事件。
PreviewKeyDown事件、KeyDown事件、PreviewKeyUp事件和KeyUp事件都是经过KeyEventArgs对象来提供相应的信息。该对象有一个Key属性,是一个System.Windows.Input.Key的枚举类型。当咱们在检查文本框的输入的内容时,一般须要监听PreviewTextInput事件(能够接受文本输入元素)和PreviewKeyDown事件(不能够接受文本输入元素)。看下面的例子:
Xaml代码:
<StackPanel UIElement.PreviewTextInput="StackPanel_PreviewTextInput" UIElement.PreviewKeyDown="StackPanel_PreviewKeyDown"> <TextBox /> <TextBox /> <TextBox /> </StackPanel>
cs代码:
//PreviewTextInput事件处理 private void StackPanel_PreviewTextInput(object sender, TextCompositionEventArgs e) { long num = 0; if (!long.TryParse(e.Text, out num)) { MessageBox.Show(e.Text + "键不是数字"); e.Handled = true; } } //PreviewKeyDown事件处理 private void StackPanel_PreviewKeyDown(object sender, KeyEventArgs e) { if (e.Key == Key.Space) { KeyConverter cvt = new KeyConverter();//这里只是为了演示KeyConverter的用法,此处并不合适 MessageBox.Show(cvt.ConvertToString(e.Key) + "键不是数字"); e.Handled = true; } /* //获取键盘对象来判断事件发生时是否打开的大小写键 if (e.KeyboardDevice.IsKeyToggled(Key.CapsLock)) MessageBox.Show("CapsLock is opened"); else MessageBox.Show("CapsLock is closed"); */ /* //事件触发时键盘状态和实际的键盘状态可能会不一致 //经过Keyboard.IsKeyToggled()静态方法来实时获取键盘状态 if (Keyboard.IsKeyToggled(key: Key.CapsLock)) { MessageBox.Show("CapsLock is opened"); } else { MessageBox.Show("CapsLock is closed"); } */ }
运行效果:
在上面例子中,经过PreviewTextInput事件处理哪些能够触发PreviewTextInput事件的按键,经过PreviewKeyDown事件处理剩下的哪些不能触发PreviewTextInput事件的按键(上面的空格键),从而让输入只能为数字。
鼠标操做会触发一系列相关的事件触发。主要有移入移出事件、单击双击事件、捕获鼠标事件和鼠标拖放事件。
最基本的移入移出事件是MouseEnter和MouseLeave事件,这两个事件是直接事件(Direct Event),也就是说不会冒泡会隧道传输。还有几个冒泡和隧道事件为PreviewMouseMove/MouseMove。
private void Window_MouseMove(object sender, MouseEventArgs e) { Point p = e.GetPosition(this);//获取鼠标相对于窗口的坐标 MessageBox.Show("Relative to Window,x is " + p.X + ",y is " + p.Y); Point p1 = PointToScreen(p);//获取鼠标相对于屏幕的坐标 MessageBox.Show("Relative to Screen,x is " + p1.X + ",y is " + p1.Y); }
能够经过MouseEventArgs的GetPosition(IInputElement element)方法来获取触发事件时鼠标相对于控件的坐标,而后能够经过Visual的PointToScreen(Point p)方法将相对坐标转化为相对于屏幕的坐标,这个在一些交互中常常用到,固然也能够经过P/Invoke方法调用Win32 API来获取相对于屏幕坐标。
鼠标单击事件和键盘按键事件相似,区别是鼠标单击分左右键。下面按顺序列出事件:
这些事件都提供了MouseButtonEventArgs对象,它继承自MouseEventArgs(有判断鼠标是按下仍是释放的MouseButtonState属性,有获取相对位置的GetPosition方法),MouseButtonEventArgs对象自身又增长了两个属性,一个是MouseButton属性,用于判断事件是由鼠标那个键触发的;另外一个是ClickCount,用于判断点击次数,能够用来判断单击双击。
某些元素又添加了更高级的鼠标事件,例如ButtonBase添加了Click事件,Control类添加了PreviewMouseDoubleClick事件和MouseDoubleClick事件。
另外,还提供了PreviewMouseWheel和MouseWheel鼠标滚轮滚动事件,它们提供了MouseWheelEventArgs对象,这个对象有个Delta的属性,用于获取指示鼠标滚轮变动量的值,若是鼠标滚轮朝上旋转(背离用户的方向),则该值为正;若是鼠标滚轮朝下旋转(朝着用户的方向),则该值为负。
能够经过Mouse类来实时地获取鼠标的相关信息。
一般状况下,按下事件和释放事件是成对被触发的,可是也有另外,如单击某个元素,保持按下状态,而后移动鼠标指针离开该元素,这种状况就不会触发释放的事件。当咱们想要在鼠标离开了元素后触发仍然触发释放事件执行其它操做,就要先判断该元素是否可用(IsEnabled="true",禁用的元素是没法获取捕获鼠标的),而后经过UIElement.CaptureMouse()方法来捕获鼠标,成功捕获返回true,不然返回false,若是返回true,就会触发GotMouseCapture和IsMouseCaptureChanged事件,并将事件数据中的RoutedEventArgs.Source报告为调用 CaptureMouse 方法的元素。 若是强制执行捕获,则可能会干扰现有捕获,特别是与鼠标拖放有关的捕获。若要从全部元素中清除鼠标捕获,请用值为 null 的 element 参数调用Mouse.Capture()方法。在UIElment和Mouse类中都有GotMouseCapture事件,它们存在这样的关系:当UIElement做为基元素继承时,此事件会为该类的Mouse.GotMouseCapture附加事件建立一个别名,以便 GotMouseCapture 包含在该类的成员列表中。 附加到 GotMouseCapture 事件的事件处理程序将附加到基础Mouse.GotMouseCapture附加事件上,并接收同一事件数据实例。UIElement.LostMouseCapture事件也相似。
鼠标拖放是做为一种快捷方便的方式来使用的,例如将垃圾文件拖放到回收站,将一个doc文档拖放到打开的Word窗口来打开该doc文档等。
通常,拖放操做分为三个步骤:
1)用户单击某个元素,并保持鼠标键按下状态,这时某些信息被搁置,拖放操做开始。
2)用户将鼠标将鼠标移动到其它元素上,若是该元素能够接受拖放的内容,则鼠标指针变为拖放图标,不然变为拒绝操做图标。
3)用户释放鼠标键时,该元素接受信息。按ESC可取消操做。
某些控件内部已经内置了拖放逻辑,例如TextBox,你能够选中TextBox中的文本,将其拖放到另外一个TextBox中。
对于那些没有内置拖放逻辑的控件来讲,想要实现拖放,实现咱们来处理控件的拖放事件。下面来举个例子:
Xaml代码:
<Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="138*" /> <ColumnDefinition Width="140*" /> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="99*" /> <RowDefinition Height="162*" /> </Grid.RowDefinitions> <TextBox Height="30" /> <TextBox Grid.Column="1" Height="30" /> <StackPanel x:Name="sp" Grid.Row="1" Button.PreviewMouseDown="StackPanel_PreviewMouseDown"> <Button Content="1" Background="Green"/> <Button Content="2" Background="Red" /> <Button Content="3" Background="Orange" /> <TextBlock Text="4" Background="AliceBlue" /> </StackPanel> <StackPanel x:Name="sp1" Grid.Row="1" Grid.Column="1" Background="Gray" AllowDrop="True" Drop="StackPanel_Drop"> </StackPanel> </Grid>
cs代码:
//在PreviewMouseDown事件中调用DragDrop.DoDragDrop方法初始化拖放操做(建立源) private void StackPanel_PreviewMouseDown(object sender, MouseButtonEventArgs e) { Button button = e.Source as Button; if (button != null) DragDrop.DoDragDrop(button, button, DragDropEffects.Copy); } //将目标的AllowDrop设为true,监听其Drop事件 private void StackPanel_Drop(object sender, DragEventArgs e) { Button button = e.Data.GetData(typeof(Button)) as Button; this.sp.Children.Remove(button);//因为拖放的Button已是sp的Child,故须要断开与原容器的链接 this.sp1.Children.Add(button); }
效果以下:
重点是DragEventArgs对象的Data属性,它是IDataObject接口类型,用于封装拖放对象相关的信息,里面有一些常常要的方法,如GetData/SetData方法,GetDataPresent方法。
若是想要达到过滤拖放内容的目标,应该使用DragEnter事件来判断,最后交给Drop事件处理。
若是拖放操做时是在应用程序间进行的,并且源是复杂的对象,一种作法是经过序列化反序列化方式,另外一种是经过使用XamlWriter方法将WPF对象转化为Xaml,而后经过XamlReader方法再转化为WPF对象,实质也是第一种。
手写笔是一种在平板电脑或其它触屏设备上使用的相似笔的设备,它的行为很像鼠标,能够触发MosueMove、MouseDown和MouseUp等事件,同时它还具备对应的事件StylusMove、StylusDown和StylusUp事件及它们的Preview事件,另外它还有一些特有的事件,例如:StylusInAirMove、StylusInRange、StylusOutOfRange和StylusSystemGesture事件,这些事件是手写笔特有的事件。在InkCanvas中应用手写笔事件,常常能够实现不错的效果。关于这些事件的详细信息,可查看MSDN。
多点触控和手写笔不一样的是,多点触控支持同时多个手指操做甚至手势(Gesture),win7上标准的手势可查看MSDN,当前支持触控的硬件列表可查看MSDN。
正如鼠标事件有高低层次同样(Click及MouseDoubleClick等事件属于高层次事件,而MouseDown及MouseUp等事件属于低层次事件),多点触控也有高低层次事件的区分。
1)原始触控(raw touch):这是触控的低级支持,都是一些单独的触控事件,不支持手势。
2)操做(manipulation):这是一个对原始触控的抽象层,支持手势。WPF支持的通用的手势包括移动(pan)、缩放(zoom)、旋转(rotate)和轻按(tap)。
3)内置的元素支持(built-in element support):有些控件已经对多点触控提供了内置支持,例如可滚动的控件支持触控移动,如ListBox、ListView、DataGrid、TextBox和ScrollViewer。
原始触控事件也像低级的鼠标键盘事件同样,被封装在UIElement和ContentElement之中。以下图所示:
上面这些事件都提供了TouchEventArgs对象,这个对象有两个重要的成员,一个GetTouchPoint方法(获取触控事件发生时触控点的坐标);一个是TouchDevice属性。内部是将每一个触点都当作是单独的设备,会为每一个触点分配惟一的设备ID,根据TouchDevice.Id来区分触点(手指)。
对于那些直接简明的触控,使用原始触控就足够了。可是,若是要方便地支持触控手势,例如旋转,则须要触控操做。经过将元素的IsManipulationEnabled属性(定义在UIElement中)设为true,则该元素可使用触控操做,而后能够响应四个事件:ManipulationStarting、ManipulationStarted、ManipulationDelta和ManipulationCompleted。它们都提供了不一样的事件参数对象,每一个对象都有本身独特的属性和方法。具体能够查看MSDN。
WPF中的惯性(Inertia)也是构建在低级事件之上的,它使得用户体验更好。例如,当用手指在划动一张图片的时候,正常状况下,当手指离开的时候图片会当即中止。而当启用了惯性属性后,手指离开时图片还会减速划动一段距离,并且,当图片到边界时还能够产生一个反弹的效果。这须要监听ManipulationInertiaStarting事件,这个事件提供了ManipulationInertiaStartingEventArgs对象,这个对象提供了延伸惯性行为ExpansionBehavior、线性惯性行为TranslationBehavior和旋转惯性行为RotationBehavior,经过设置相应Behavior的InitialVelocity初始速率和DesiredDeceleration预期减速度来实现相应惯性效果。另外,为了使其接触边界时,可以天然弹回,须要在ManipulationDelta事件中调用ReportBoundaryFeedback()方法,将会触发UIElement.ManipulationBoundaryFeedback事件,在该事件中,使应用程序或组件可以在对象到达边界时提供可视反馈。还能够调用事件参数的Cancel方法来取消操做,将再也不引起操做事件而且对于触控将会触发鼠标事件。
本节主要讲了WPF的路由事件及附加事件,而后描述了WPF框架中的生命周期事件、鼠标事件、键盘事件、手写笔事件和多点触控事件,细节不少,但都有迹可循。