路由事件
路由事件时具有更强传播能力的事件——它们可在元素树中向上冒泡和向下隧道传播,并且沿着传播路径被事件处理程序处理。路由事件允许事件在某个元素上被处理(如标签),即使该事件源自另一个元素(如标签内部的一幅图像)也是如此。与依赖项属性一样,可通过传统的方式使用路由事件——通过关联具有正确签名的事件处理程序——但为了使用路由事件的所有功能,需要理解其工作原理。
理解路由事件
每个.NET开发人员都熟悉“事件”的思想–当有意义的事情发生时,由对象(如WPF元素)发送的用于通知代码的消息。WPF通过事件路由(event routing)的概念增强了.NET事件模型。事件路由允许源自某个元素的事件由另一个元素引发。例如,使用事件路由,来自工具栏按钮的单击事件可在被代码处理之前上传到工具栏,然后上传到包含工具栏的窗口。
定义/注册和封装路由事件
WPF事件模型和WPF属性模型非常类似。与依赖项属性一样,路由事件由只读的静态字段表示,在静态构造函数中注册,并通过标准的.NET事件定义进行封装。
例如,WPF 的Button类提供了大家熟悉的Click事件,该事件继承自抽象的ButtonBase基类。下面的代码说明了该事件是如何被定义和注册的:
1 | public abstract class ButtonBase : ContentControl |
依赖项属性是使用DependencyProperty. Register()方法注册的,而路由事件是使用EventManager.RegisterRoutedEvent()方法注册的。
共享路由事件
与依赖项属性一样,可在类之间共享路由事件的定义。例如,UIElement(该类是所有普通WPF 元素的起点)和ContentElement(该类是所有内容元素的起点,内容元素是可以被放入流文档中的单独内容片段)这两个基类都使用了MouseUp事件。MouseUp事件是由SvstemWindows.Input,.Mouse类定义的。UElement 类和 ContentElement类只通过Routed- Event.AddOwner()方法重用MouseUp事件:
1 | UIElement.MouseUpEvent = Mouse.MouseUpEvent.AddOwner(typeof(UIElement)); |
引发路由事件
路由事件不是通过传统的.NET事件封装器引发的,而是使用RaiseEvent()方法引发事件,所有元素都从UIElement类继承了该方法。
1 | RoutedEventArgs e = new RoutedEventArgs(ButtonBase.ClickEvent,this); |
所有WPF事件都为事件签名使用熟悉的.NET约定。每个事件处理程序的第一个参数(sender参数)都提供引发事件的对象的引用。第二个参数时EventArgs对象,该独显与其他所有可能很重要的附加细节绑定在一起。例如,MouseUp事件提供了一个MouseEventArgs对象,用于指示但事件发生时按下了哪些鼠标键:
1 | private void img_MouseUp(object sender,MouseButtonEventArgs e) |
在WPF 中,如果事件不需要传递任何额外细节,可使用RoutedEventArgs类,该类包含了有关如何传递事件的一些细节。如果事件确实需要传递额外的信息,那么需要使用更特殊的继承自RoutedEventArgs的对象(如上面示例中的MouseButtonEventArgs)。因为每个WPF事件参数类都继承自RoutedEventArgs类,所以每个WPF事件处理程序都可访问与事件路由相关的信息。
处理路由事件
XAML标记
最常用的方法是为XAML标记天机事件特性。通常约定“元素名_事件名”的形式明明事件处理程序方法。
1 | <Image x:Name="img" Source="happyface.jpg" Stretch="None" MouseUp="img_MouseUp"/> |
代码连接
如果需要动态创建控件,并在窗口生命周期的某一时刻关联事件处理程序,代码方法是非常有用的。
1 | img.MouseUp += new MouseButtonEventHandler(img_Mouseup); // 完整写法 |
事件封装器
当使用这种方法时,始终需要创建合适的委托类型,而不是能隐式地创建委托对象。这是因为UIElement.AddHandler()方法支持所有WPF事件,并且他不知道您要使用的委托类型
1 | img.AddHandler(UIElement.MouseUpEvent,new MouseButtonEventHandler(img_MouseUp)); |
断开事件处理程序
1 | img.MouseUp -= img_MouseUp; |
事件路由
WPF中的许多空间都是内容控件,而内容控件可包含任何类型以及大量的嵌套内容,因此恰当的事件标记处理可以避免处理程序变得杂乱无章。
路由手机将实际上以下列三种方式出现:
- 直接路由事件(direct event):与普通.NET事件类似,它们源于一个元素,不传递给其他元素,例如,MouseEnter事件(当鼠标指针移到元素上时发生)
- 冒泡路由事件(bubbling event):在包含层次中向上传递的,
通常鼠标事件都是冒泡路由事件
,例如,MouseDown事件,该事件首先由被单击的元素引发,接下来被该元素的父元素引发,然后被父元素的父元素引发,以此类推,直到WPF到达元素树顶部为止 - 隧道路由事件(tunneling event):在包含层次中向下传递,
通常以Preview开头的事件
,隧道路由事件在事件到达恰当的控件之前为预览事件(甚至终止事件)提供了机会,例如,通过PreviewKeyDown事件可截获时候按下了某个键。首先在窗口级别上,然后时更具体的容器,直到到达但按下键时具有焦点的元素。
RoutedEventArgs类
在处理冒泡路由事件时,sender参数提供了对整个链条上最后那个链接的引用
名称 | 说明 |
---|---|
Source | 指示引发了事件的对象 |
OriginalSource | 指示最初时什么对象引发了事件 |
RoutedEvent | 通过事件处理程序为出发的事件提供RoutedEvent对象 |
Handled | 该属性允许终止事件的冒泡或隧道过程。如果控件将Handled属性设为true,那么事件就不会继续传递,也不会为其他任何元素引发该事件 |
处理挂起的事件
AddHandler()方法提供了一个重载版本,该版本可以接收一个Boolean值作为它的第三个参数。如果将该参数设置为true,那么即使设置了Handled标志,也将接收到事件:
1 | cmdClear.AddHandler(UIElement.MouseUpEvent,new MouseButtonEventHandler(cmdClear_MouseUp),true); |
这通常不是正确的设计决策。为防止可能造成的困惑,按钮被设计为会挂起MouseUp事件
附加事件
假设在StackPanel面板中封装了一堆按钮,并希望在一个事件处理程序中处理所有这些按钮的单击事件。。粗略的方法是将每个按钮的Click事件关联到同一个事件处理程序。但Click事件支持事件冒泡,从而提供了一种更好的选择。可通过处理更高层次元素的Click事件(如包含按钮的StackPanel面板)来处理所有按钮的Click事件。
1 | <StackPanel Button.Click = "DoSomething"> |
1 | private void DoSomething(object sender, RoutedEventArgs e) |
隧道路由事件
隧道路由事件的工作方式和冒泡路由事件相同,但方向相反。隧道路由事件易于识别,它们都以单词 Preview开头。而且,WPF通常成对地定义冒泡路由事件和隧道路由事件。这意味着如果发现冒泡的MouseUp事件,就还可以找到PreviewMouseUp 隧道事件。隧道路由事件总在冒泡路由事件之前被触发
如果准备将隧道路由事件标记为处理过,务必要谨慎从事。根据编写控件的方式,这有可能阻止控件处理自己的事件(相关的冒泡路由事件),从而阻止执行某些任务或阻止更新控件自身的状态。
WPF事件
尽管每个元素都提供了许多事件,但最重要的事件童话参观包括以下5类:
- 生命周期事件:在元素被初始化、加载或卸载时发生这些事件
- 鼠标事件:这些事件时鼠标动作的结果
- 键盘事件:这些事件时键盘动作(如按下键盘上的键)的结果
- 手写笔事件:这些事件时使用类似钢笔的手写笔的结果,在平板电脑上用手写笔代替鼠标
- 多点触控事件:这些事件时一根或多根手指在多点触控屏幕上触摸的结果。仅在Widdows7中支持这些事件
生命周期事件
但首次创建以及释放所有元素时都会引发事件,可使用这些事件初始化窗口。
名称 | 说明 |
---|---|
Initialized | 但元素被实例化,并已根据XAML标记设置了元素的属性之后发生。这是元素已经初始化,但窗口的其他部分可能尚未初始化。此外,尚未应用样式和数据绑定。这是,IsInitialized属性为true。Initialized事件时普通的.NET事件——并未路由事件 |
Loaded | 当整个窗口已经初始化并应用了样式和数据绑定时,该事件发生。这是在元素被呈现之前的最后一站。这是,IsLoaded属性为true |
Unloaded | 当元素被释放时,该事件发生,原因时包含元素的窗口被关闭或特定的元素被从窗口中删除 |
为了弄清Initialized 事件和Loaded 事件之间的关系,分析一下呈现过程是有帮助的。FrameworkElement 类实现了 ISupportInitialize 接口,该接口提供了两个用于控制初始化过程的方法。
- 第一个方法是 BeginInit(),在实例化元素后会立即调用该方法。调用 BeginInit( )方法后,XAML 解析器设置所有元素的属性(并添加内容)。
- 第二个方法是Endnit(),完成初始化后,将调用该方法,此时引发Initialized 事件。
名称 | 说明 |
---|---|
SourceInitialized | 当取得窗口的HwndSource属性时(当在窗口可见之前)发生。HwndSource时窗口句柄,如果调用Win32 API中的遗留函数,就可能需要使用该句柄 |
ContentRendered | 当窗口首次呈现后立即发生。对于执行任何可能会影响窗口可视外观的更改操作,这不是一个好位置,否则将会强制进行第二次呈现(改用Loaded事件)。然而,ContentRendered事件表明窗口已经完成可见,并且已经准备好接收输入 |
Activated | 当用户切换到该窗口时发生(例如,从应用程序的其他窗口或从其他应用程序切换到该窗口)。当窗口第一次加载时也会引发 Activated 事件。从概念上讲,窗口的 Activated 事件相当于控件的 GotFocus 事件 |
Deactivated | 当用户从该窗口切换到其他窗口时发生(例如,切换到应用程序的其他窗口或切换到其他应用程序)。当用户关闭窗口时也会发生该事件,该事件在Cosing事件之后,但在Closcd事件之前发生。从概念上讲,窗口的Deactivated 事件相当于控件的LostFocus事件 |
Closing | 当关闭窗口时发生,不管是用户关闭窗口还是通过代码调用Window.Close()或Application.Shutdown()方法关闭窗口。Closing 事件提供了取消操作并保持打开状态的机会,具体通过将CancelEventArgs.Cancel 属性设置为true 实现该目标。但是,如果是因为用户关闭或注销计算机而导致应用程序被关闭,就不能接收到Closing事件。为应对这种情况,需要处理Application.SessionEnding 事件 |
Closed | 当窗口已经关闭后发生。但是,此时仍可以访问元素对象,当然是在Unoaded.事件尚未发生之前。在此,可以执行一些清理工作,向永久存储位置(如配置文件或 Windows 注册表)写入设置信息等 |
也可以使用窗口构造函数进行初始化(在紧跟 ImitializeComponent()调用之后,添加自己的代码)。但使用Loaded 事件总是更好的选择
。这是因为如果在 Window 类的构造函数中发生异常,就会在 XAML解析器解析页面时抛出该异常。因此,该异常将与 InnerException 属性中的原始异常一起被封装到一个没有用处的XamlParseException 对象中。
输入事件
输入事件是当用户使用某些种类的外设硬件进行交互时发生的事件,例如鼠标、键盘、手写笔或多点触控屏。输入事件可通过继承自InputEventArgs 的自定义事件参数类传递额外的信息。下图显示了继承层次。
InputEventArgs 类只增加了两个属性:Timestamp和 Device。Timestamp 属性提供了一个整数,指示事件何时发生的毫秒数(它所代表的实际时间并不重要,但可比较不同的时间戳值以确定哪个事件先发生。时间戳值大的事件是在更近发生的)。Device 属性返回一个对象,该对象提供与触发事件的设备相关的更多信息,设备可以是鼠标、键盘或手写笔。这三种可能的设备由不同的类表示,所有这些类都继承自抽象类System.Windows.Input.InputDevice。
键盘输入
当用户按下键盘上的一个键时,就会发生一系列事件。下面根据它们发生的顺序列出了这些事件。
名称 | 路由类型 | 说明 |
---|---|---|
PreviewKeyDown | 隧道 | 当按下一个键时发生 |
KeyDown | 冒泡 | 当按下一个键时发生 |
PreviewTextInput | 隧道 | 当按键完成并且元素正在接收文本输入时发生。对于那些不会产生文本“输入”的按键(如 Ctr1键、Shif 键、Backspace 键、方向键和功能键等),不会引发该事件 |
TextInput | 冒泡 | 当按键完成并且元素正在接收文本输入时发生。对于那些不会产生文本的按键,不会引发该事件 |
PreviewKeyUp | 隧道 | 当释放一个按键时发生 |
KeyUp | 冒泡 | 当释放一个按键时发生 |
键盘处理永远不会像上面看到的这么简单。一些控件可能会挂起这些事件中的某些事件,从而可执行自己更特殊的键盘处理。最明显的例子是 TextBox 控件,它挂起了TextInput 事件。对于一些按键,TextBox 控件还挂起了KeyDown 事件,如方向键。对于此类情形,通常仍可使用隧道路由事件(PreviewTextInput和PreviewKeyDown 事件)。
TextBox 控件还添加了名为 TextChanged 的新事件。在按键导致文本框中的文本发生改变之后会立即引发该事件。这时,在文本框中已经可以看到新的文本,所以阻止不需要的按键已为时太晚。
获取键盘状态
名称 | 说明 |
---|---|
IsKeyDown() | 当事件发生时,通知时候按下了该键 |
IsKeyUp() | 当事件发生时,通知时候释放了该键 |
IsKeyToggled() | 当事件发生时,通知该键是否处于“打开”状态。该方法只对那些能够打开、关闭的键有意义,如 Caps Lock 键、Scroll Lock 键以及 Num Lock 键 |
GetKeyStates() | 返回一个或多个 KeyStates 枚举值,指明该键当前是否被释放了、按下了或处于切换状态。该方法本质上和为同一个键同时调用IsKeyDown()方法和IsKeyToggled()方法相同 |
使用KeyConverter类将Key值转换为更有用的字符串。例如,使用KeyConverter.ConverterToString( )方法,Key.D9 和 Dey.NumPad9 都返回字符串“9”。如果只使用 Key.ToString()方法,将得到不那么有用的枚举名称(D9 或 NumPad9)
鼠标输入
鼠标事件执行几个关联的任务。当鼠标移到某个元素上时,可通过最基本的鼠标事件进行响应。这些事件是 MouseEnter(当鼠标指针移到元素上时引发该事件)和 MoseLeave(当鼠标指针离开元素时引发该事件)。这两个事件都是直接事件,这意味着它们不使用冒泡和隧道过程,而是源自一个元素并且只被该元素引发。考虑到控件嵌入到 WPF窗口的方式,这是合理的。
UIElement 类还包含两个有用的属性,这两个属性能帮助进行鼠标命中测试。可使用IsMouseOver 属性确定当前鼠标是否位于某个元素及其子元素上面,还可以使用 IsMouseDirectlyOver属性检查鼠标是否位于某个元素上面,但未位于其子元素上面。通常不会在代码中读取和使用这些值,反而会使用它们构建样式触发器,从而当鼠标移到元素上时,自动修改元素。
鼠标单击
鼠标单击事件的引发方式和按键事件的引发方式有类似之处。区别是对于鼠标左键和鼠标右键引发不同的事件
名称 | 路由类型 | 说明 |
---|---|---|
PreviewMouseLeftButtonDown、PreviewMouseRightButtonDown | 隧道 | 当按下鼠标键时发生 |
MouseLeftButtonDown、MouseRightButtonDown | 冒泡 | 当按下鼠标键时发生 |
PreviewMouseLeftButtonUp、PreivewMouseRightButtonUp | 隧道 | 当释放鼠标键时发生 |
MouseLeftButtonUp、MouseRightButtonUp | 冒泡 | 当释放鼠标键时发生 |
某些元素添加了更高级的鼠标事件。例如,Control类添加了PreviewMouseDoubleClick事件和 MouseDoubleClick 事件,这两个事件代替了MouseLeftButtonUp 事件。与此类似,对于Button类,通过鼠标或键盘可触发 Click事件。
捕获鼠标
通常,元素每次接收到鼠标键“按下”事件后,不久后就会接收到对应的鼠标键“释放”事件。但情况不见得总是如此。例如,如果单击一个元素,保持按下鼠标键,然后移动鼠标指针离开该元素,这时该元素就不会接收到鼠标键释放事件。
某些情况下,可能希望通知鼠标键释放事件,即使鼠标键释放事件是在鼠标已经离开了原来的元素之后发生的。为此,需要调用Mouse.Capture()方法并传递恰当的元素以捕获鼠标。此后就会接收到鼠标键按下事件和释放事件,直到再次调用Mouse.Capture()方法并传递空引用为止。当鼠标被一个元素捕获后,其他元素就不会接收到鼠标事件。这意味着用户不能单击窗口中其他位置的按钮,不能单击文本框的内部。鼠标捕获有时用于可以被拖放并可以改变尺寸的元素。
当调用 Mouse.Capture()方法时,可传递可选的 CaptureMode 值作为第二个参数。通常,当调用 Mouse.Capture()方法时,使用 CaptureMode.Element值,这表示元素总是接收鼠标事件。然而如果使用 CaptureMode.SubTree,鼠标事件就可以经过已单击的元素(假定这个元素是执行捕获的元素的子元素)。如果在子元素中已经使用了事件冒泡或隧道特性来监视鼠标事件,这是非常合理的。
有些情况下,可能由于其他原因(不是您的错)丢失鼠标捕获。例如,如果需要显示系统对话框,Windows 可能会释放鼠标捕获。如果当鼠标键释放事件发生后没有释放鼠标,并且用户单击了另一个应用程序中的窗口,也可能丢失鼠标捕获。无论哪种情况,都可以通过处理元素的 LostMouseCapture
事件来响应鼠标捕获的丢失。
当鼠标被一个元素捕获时,就不能与其他元素进行交互(例如,不能单击窗口中的其他元鼠标捕获通常用于短时间的操作,如拖放。素)。
不是使用 Mouse.Capture( )方法,而是改用 UIElement类提供的两个方法:CaptureMouse()和 ReleaseMouseCapture()。只在合适的元素上调用这些方法。这种方法的唯一限制是不允许使
用 CaptureMode.SubTree 选项。
鼠标拖放
本质上,拖放操作通过以下三个步骤进行:
- 用户点击元素(或选择元素中的一块特定区域),并保持鼠标键为按下状态。这是,某些信息被搁置起来,并且拖放操作开始
- 用户键鼠标移到其他元素上。如果该元素可接受正在拖动的内容的类型,鼠标指针会变成拖放图标,否则鼠标指针会变成内部有一条线的圆形
- 当用户释放鼠标键时,元素接受信息并决定如何处理接收到的信息。在没有释放鼠标键时,可按下Esc键取消该操作
多点触控输入
多点触控(multi-touch)是通过触摸屏幕与应用程序进行交互的一种方式。多点触控输入和更传统的基于笔(pen-based)的输入的区别是多点触控识别手势(gesture)—用户可移动多根手指以执行常见操作的特殊方式。
要获得 Windows7 能够识别的标准多点触控手势列表,请参阅 多点触控
多点触控的输入层级
WPF提供了三个独立的层次:
- 原始触控(raw touch):这是最低级的支持,可访问用户执行的每个触控。缺点是由您的应用程序负责将单独的触控消息组合到一起,并对它们进行解释。如果不准备识别标准触摸手势,反而希望创建以独特方式响应多点触控输入的应用程序,使用原始触控是合理的。一个例子是绘图程序,例如 Windows7画图程序,该程序允许用户同时使用多根手指在触摸屏上绘图。
- 操作(manipulation):操作(manipulation):这是一个简便的抽象层,该层将原始的多点触控输入转换成更有意义的手势,与WPF控件将一系列MouseDown和MouseUp 事件解释为更高级的MouseDoubleClick事件很相似。WPF 支持的通用手势包括移动(pan)、缩放(zoom)、旋转(rotate)以及轻按(tap)。
- 内置的元素支持(built-in element support):内置的元素支持(built-in element support):有些元素已对多点触控事件提供了内置支持,从而不需要再编写代码。例如,可滚动的控件支持触控移动,如 ListBox、ListView、DataGrid、TextBox 以及 ScrollViewer.
原始触控
与基本的鼠标和键盘事件一样,触控事件被内置到低级的UIElement以及ContentElement类。
名称 | 路由类型 | 说明 |
---|---|---|
PreiviewTouchDown | 隧道 | 当用户触摸元素时发生 |
TouchDown | 冒泡 | 当用户触摸元素时发生 |
PreviewTouchMove | 隧道 | 当用户移动放到触摸屏上的手指时发生 |
TouchMove | 冒泡 | 当用户移动放到触摸屏上的手指时发生 |
PreiviewTouchUp | 隧道 | 当用户一开手指,结束触摸时发生 |
TouchUp | 冒泡 | 当用户一开手指,结束触摸时发生 |
TouchEnter | 无 | 当触点从元素外进入元素内时发生 |
TouchLeave | 无 | 当触点离开元素时发生 |
所有这些事件都提供了一个 TouchEventArgs对象,该对象提供了两个重要成员。
- 第一个是GetTouchPoint()方法,该方法返回触控事件发生位置的屏幕坐标(还有一些不怎么常用的数据例如触点的大小)。
- 第二个是 TouchDevice 属性,该属性返回一个 TouchDevice 对象。这里的技巧是将每个触点都视为单独设备。因此,如果用户在不同的位置按下了两根手指(同时按下或者先按下一根再按下另一根),WPF将它们作为两个触控设备,并为每个触控设备指定唯一的ID。当用户移动这些手指,并且触控事件发生时,代码可以通过 TouchDevice.Id 属性区分两个触点
操作
WPF为手势提供了更高级别的支持,称为触控操作(manipulation)。通过将元素的 IsManipulationEnabled 属性设为 True,将元素配置为接受触控操作。然后可响应4个操作事件:ManipulationStarting、ManipulationStartedManipulationDelta 以及ManipulationCompleted.
惯性
WPF 还有一层构建在基本操作支持之上的特性,称为惯性(intertia)。本质上,通过惯性可以更逼真、更流畅地操作元素。
只需处理 ManipulationInertiaStarting 事件。当用户结束手势并抬起手指释放元素时,触发 ManipulationInertiaStarting 事件。这时,可使用 ManipulationInertiaStartingEventsArgs 对象确定当前速度–当操作结束时元素的移动速度–并设置希望的减速度。下面的示例为移动、缩放以及旋转手势添加了惯性:
为使元素从障碍物自然地被弹回,需要在 ManipulationDelta 事件中检查是否将元素拖到了错误的位置。如果穿过了一条边界,那么由您负责通过调用ManipulationDeltaEventArgsReportBoundaryFeedback()方法进行报告。
当参阅 multitouch 上的 WPF Multi-Touch项目。该项目提供了两种方便的方式,通过这两种方式可以为容器添加操作支持,而不需要自己编写代码–使用会自动应用的