WPF学习(6)路由事件
做过.net开发的朋友对于事件应该都不陌生。追溯历史,事件(Event)首先应用在Com和VB上,它是对在MFC中使用的烦琐的消息机制的一个封装,然后.net又继承了这种事件驱动机制,这种事件也叫.net事件。正如WPF在简单的.net属性概念上添加了许多基础的东西一样,它也为.net事件添加了许多基础的东西。路由事件(RoutedEvent)是专门设计用于在元素树中使用的事件。当路由事件触发后,它可以向上或向下遍历逻辑树和可视树,用一种简单而且持久的方式在每个元素上触发,而不需要使用任何定制代码。下面我们就来学习下路由事件,本节主要包括以下几个方面内容:路由事件和WPF事件(包括键盘输入、鼠标输入和多点触控输入等)。
1.路由事件
1.1自定义路由事件
路由事件的实现和行为与依赖属性有很多相同的地方。我们还是先举个例子来说明下路由事件的实现方式,然后再来讲下路由事件的一些特性。拿最常用的Button的Click事件(继承自ButtonBase抽象类)来说明:
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类中的。
我们先来使用上面的代码:
Xaml代码:
<local:MyButton x:Name="btnTest" Content="Press me" Width="80" Height="30"/>
C#代码:
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):
- 参数name:路由事件的名称。该名称在所有者类型中必须是唯一的,并且不能为 null 或空字符串。
- 参数routingStrategy:作为枚举值的事件的路由策略。有三种路由策略:
- Tunnel隧道方式:路由事件使用隧道策略,以便事件实例通过树向下路由(从根到源元素)。
- Bubble冒泡方式:路由事件使用冒泡策略,以便事件实例通过树向上路由(从事件元素到根)。
- Direct直接方式:路由事件不通过元素树路由,仅在源元素发生,与.net事件类似,但其支持其他路由事件功能,例如类处理、System.Windows.EventTrigger 或 System.Windows.EventSetter。
- 参数handlerType: 事件处理程序的类型。该类型必须为委托类型,并且不能为 null。
- 参数ownerType:路由事件的所有者类类型。该类型不能为 null。
再来看下路由事件处理程序的签名,它与.net事件处理程序签名相匹配。第一个参数是Object类型,指该处理程序被添加到的元素;第二个参数是RoutedEventArgs类型,它是EventArgs的子类,它提供了四个属性:
Source属性:逻辑树中一开始触发该事件的元素。
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隧道事件来处理。
1.2附加事件
附加事件的思想,其实和附加属性是一样,某一元素没有某一种事件,需要将该事件附加在该元素上以实现相应的事件监听处理功能。先来看这样的场景:
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。前面说过这不是一种好的方式,因为事件应在第一时间被处理,而应该尽可能地避免处理已经处理过的事件。由这也能看出来,当将事件标记为已处理时,隧道传递和冒泡仍然会继续,只不过在默认情况下事件处理程序只处理未处理的事件。
2.WPF事件
在WPF框架中,已经为我们封装了许多的事件,主要分为这么几类:
- 生命周期事件:在元素被初始化、加载和卸载时发生。
- 输入事件:
- 鼠标事件
- 键盘事件
- 手写笔事件
- 多点触控事件
2.1生命周期事件
当首次创建或释放元素时都会触发一些事件,这些事件就是生命周期事件。
来看下这些事件的执行时机。在FrameworkElement中实现了ISupportInitialize接口,该接口中有两个方法BeginInit()和EndInit(),前者是用信号通知对象初始化即将开始,当元素被实例化后调用该方法,然后开始属性设置;后者是用信号通知对象初始化已完成,当初始化完成后调用该方法,然后触发Initialized事件。实现这个接口的目的是保证初始化工作的原子性。当初始化窗口时,是从下到上进行的,也就是从叶子节点开始的,这保证了当某一元素需要内容时其内容都已初始化。当所有的元素都已初始化完成后,然后开始布局、应用样式及可能的数据绑定等。Initialized事件后,开始触发Loaded事件,Load的顺序和Initialized的顺序相反,是从上到下进行的,当所有的元素都Load完成后,窗口显示出来。
对于Window窗口还有一些特有事件:
当希望在窗口首次加载时做一些额外的初始化工作时就可以通过在其Loaded事件的处理程序中完成。正常情况下,也可以在窗口构造器中的InitializeComponent()方法后来处理。
2.2输入事件
用户通过一些外设如鼠标、键盘、手写笔和多点触控屏等来进行输入操作时触发的事件,就是输入事件。输入事件可以通过继承自InputEventArgs自定义事件参数类来附加额外的信息,看下继承关系图:
InputEventArgs事件参数类在RoutedEventArgs类基础上只增加了Timestamp和Device两个属性,Timestamp属性表示事件何时发生的毫秒数,用于用于比较各事件之间的发生顺序,值越大越是最近发生;Device属性表示触发事件的设备的对象,设备对象是继承自抽象类System.Windows.Input.InputDevice的子类的实例。
2.2.1键盘事件
当用户按下一个键,就会触发一系列的事件,这里按顺序依次列出公共的事件:
- PreviewKeyDown事件:隧道事件,按下一个键触发
- KeyDown事件:冒泡事件,按下一个键触发
- PreviewTextInput事件:隧道事件,当按键完成并且元素正在接受文本输入时发生。对于一些像Ctrl、Shift等不会产生文本输入的键不会触发该事件
- TextInput事件:冒泡事件,当按键完成并且元素正在接受文本输入时发生。对于一些像Ctrl、Shift等不会产生文本输入的键不会触发该事件
- PreviewKeyUp事件:隧道事件,当释放一个键时发生
- KeyUp事件:冒泡事件,当释放一个键时发生
上面是一些公共的事件,不同控件可能还有一些自己特有的事件,为了不冲突,还会将上面的导致冲突的事件挂起。例如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事件的按键(上面的空格键),从而让输入只能为数字。
2.2.2鼠标事件
鼠标操作会触发一系列相关的事件触发。主要有移入移出事件、单击双击事件、捕获鼠标事件和鼠标拖放事件。
2.2.2.1移入移出事件
最基本的移入移出事件是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来获取相对于屏幕坐标。
2.2.2.2单击双击事件
鼠标单击事件和键盘按键事件类似,区别是鼠标单击分左右键。下面按顺序列出事件:
这些事件都提供了MouseButtonEventArgs对象,它继承自MouseEventArgs(有判断鼠标是按下还是释放的MouseButtonState属性,有获取相对位置的GetPosition方法),MouseButtonEventArgs对象自身又增加了两个属性,一个是MouseButton属性,用于判断事件是由鼠标那个键触发的;另一个是ClickCount,用于判断点击次数,可以用来判断单击双击。
某些元素又添加了更高级的鼠标事件,例如ButtonBase添加了Click事件,Control类添加了PreviewMouseDoubleClick事件和MouseDoubleClick事件。
另外,还提供了PreviewMouseWheel和MouseWheel鼠标滚轮滚动事件,它们提供了MouseWheelEventArgs对象,这个对象有个Delta的属性,用于获取指示鼠标滚轮变更量的值,如果鼠标滚轮朝上旋转(背离用户的方向),则该值为正;如果鼠标滚轮朝下旋转(朝着用户的方向),则该值为负。
可以通过Mouse类来实时地获取鼠标的相关信息。
2.2.2.3捕获鼠标事件
通常情况下,按下事件和释放事件是成对被触发的,但是也有另外,如单击某个元素,保持按下状态,然后移动鼠标指针离开该元素,这种情况就不会触发释放的事件。当我们想要在鼠标离开了元素后触发仍然触发释放事件执行其它操作,就要先判断该元素是否可用(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事件也类似。
2.2.2.4鼠标拖放事件
鼠标拖放是作为一种快捷方便的方式来使用的,例如将垃圾文件拖放到回收站,将一个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对象,实质也是第一种。
2.2.3手写笔事件
手写笔是一种在平板电脑或其它触屏设备上使用的类似笔的设备,它的行为很像鼠标,可以触发MosueMove、MouseDown和MouseUp等事件,同时它还具有对应的事件StylusMove、StylusDown和StylusUp事件及它们的Preview事件,另外它还有一些特有的事件,例如:StylusInAirMove、StylusInRange、StylusOutOfRange和StylusSystemGesture事件,这些事件是手写笔特有的事件。在InkCanvas中应用手写笔事件,经常可以实现不错的效果。关于这些事件的详细信息,可查看MSDN。
2.2.4多点触控事件
多点触控和手写笔不同的是,多点触控支持同时多个手指操作甚至手势(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。
2.2.4.1原始触控
原始触控事件也像低级的鼠标键盘事件一样,被封装在UIElement和ContentElement之中。如下图所示:
上面这些事件都提供了TouchEventArgs对象,这个对象有两个重要的成员,一个GetTouchPoint方法(获取触控事件发生时触控点的坐标);一个是TouchDevice属性。内部是将每个触点都看成是单独的设备,会为每个触点分配唯一的设备ID,根据TouchDevice.Id来区分触点(手指)。
2.2.4.2操作
对于那些直接简明的触控,使用原始触控就足够了。但是,如果要方便地支持触控手势,例如旋转,则需要触控操作。通过将元素的IsManipulationEnabled属性(定义在UIElement中)设为true,则该元素可以使用触控操作,然后可以响应四个事件:ManipulationStarting、ManipulationStarted、ManipulationDelta和ManipulationCompleted。它们都提供了不同的事件参数对象,每个对象都有自己独特的属性和方法。具体可以查看MSDN。
2.2.4.3惯性
WPF中的惯性(Inertia)也是构建在低级事件之上的,它使得用户体验更好。例如,当用手指在划动一张图片的时候,正常情况下,当手指离开的时候图片会立即停止。而当启用了惯性属性后,手指离开时图片还会减速划动一段距离,而且,当图片到边界时还可以产生一个反弹的效果。这需要监听ManipulationInertiaStarting事件,这个事件提供了ManipulationInertiaStartingEventArgs对象,这个对象提供了延伸惯性行为ExpansionBehavior、线性惯性行为TranslationBehavior和旋转惯性行为RotationBehavior,通过设置相应Behavior的InitialVelocity初始速率和DesiredDeceleration预期减速度来实现相应惯性效果。另外,为了使其接触边界时,能够自然弹回,需要在ManipulationDelta事件中调用ReportBoundaryFeedback()方法,将会触发UIElement.ManipulationBoundaryFeedback事件,在该事件中,使应用程序或组件能够在对象到达边界时提供可视反馈。还可以调用事件参数的Cancel方法来取消操作,将不再引发操作事件并且对于触控将会触发鼠标事件。
3总结
本节主要讲了WPF的路由事件及附加事件,然后描述了WPF框架中的生命周期事件、鼠标事件、键盘事件、手写笔事件和多点触控事件,细节很多,但都有迹可循。