WPF入门到跪下 第七章 事件

WPF的事件包括生命周期事件、输入事件、路由事件和行为等方面。

生命周期事件

当WPF程序运行时候,编译完成后会生成并调用一个程序入口函数,即Main函数,Main函数中会实例化App对象,并调用App对象的初始化函数和Run函数。

关于Main函数,可以在编译后查看程序集目录下obj\Debug文件夹中的App.g.i.cs文件。

一、应用事件

应用程序的生命周期事件可以在App.xaml.cs文件中的APP类的构造函数中进行定义。(没构造函数可以自己定义)

Startup:在调用Application对象的Run方法时发生。

SessionEnding:在用户通过注销或关闭操作系统结束Windows会话时发生。

Activated:当应用程序中任意窗口成为前台应用程序时发生,也就是应用窗体获得焦点时。

Deactivated:当应用程序中所有窗口停止作为前台应用程序时发生, 也就是应用程序完全失去焦点时。

Exit:在应用程序关闭之前发生,无法取消。

public App()

{

Startup += MyStartUpFunc;

......

}

二、Browser类型的应用事件(Page开发)

Navigating:在应用程序中的导航请求新导航时发生。

LoadCompleted:在已经加载、分析并开始呈现应用程序中的导航器导航到的内容时发生。

Navigated:在已经找到应用程序中的导航器要导航的内容时发生,尽管此时可内容可能尚未完成加载。

NavigatedFailed:在应用程序中的导航器在导航到所请求内容时出现错误的情况下发生。

NavigationProgress:在由应用程序中的导航器管理的下载过程中定期发生,以提供导航进度信息。

NavigationStopped:在调用程序中的导航器的StopLoading方法时发生,或者当导航器在当前导航正在进行期间请求了一个新导航时发生。

以上Browser事件,如果是做窗口开发基本上都用不上,其中Navigating事件会在应用启动时调用一次,来加载确认有没有Page,需不需要进行导航。需要使用事件时同样可以在App构造函数中订阅。

public App()

{

......

Navigating += NavigatingFunc;

......

}

三、异常捕获事件

DispatcherUnhandledException:在应用程序UI线程引发异常但未进行处理时发生。

注意,这里说的是UI线程上的异常,此事件无法捕获UI线程外的异常。

AppDomain.CurrentDomain.UnhandledException:应用程序上所有线程引发异常但未处理时发生。

这个事件可以捕获除Task线程外的所有线程发生的异常,比DispatcherUnhandledException事件更加全面。

TaskScheduler.UnobservedTaskException:Task线程引发异常但没有进行处理时触发,专门捕获应用上的Task异常。

需要注意的是这个事件的触发时机,此事件并不会在异常发生时就触发,要在#C进行垃圾回收后才会触发。触发时间有一定的不确定性。(可以过GC.Collect()主动进行垃圾回收)

public App()

{

......

DispatcherUnhandledException += MyExceptionFunc;

......

}

窗体常见事件

窗体的生命周期事件可以在对应窗体的cs文件的窗体类型构造函数中进行定义。

SourceInitialized:操作系统给窗体分配句柄时触发。

说到这里,提及一下,与Winform不同,在WPF中,只有窗口具有句柄,窗口中的控件上是没有的。

ContentRendered:对应窗体内容渲染时触发,一般在窗口首次呈现时触发。

Loaded:窗体加载完成时触发。

Activated:当前窗口成为前台应用程序时发生,也就是当前窗体获得焦点时触发。

Deactivated:当前窗体失去焦点时触发。

Closing:窗体关闭时触发。

Closed:窗体关闭后触发。

输入事件

一、鼠标输入事件

WPF中的常用鼠标事件有:MouseEnter、MouseLeave、MouseDown、MouseUp、MouseMove、MouseLeftButtonDown、MouseLeftButtonUp、MouseRightButtonDown、MouseRightButtonUp、MouseDoubleClick

这些都是一些很常用的事件,使用上都差不多,没啥好说的。

MouseLeftButtonDown诡异事件

有一点需要注意的是,在使用时会发现MouseLeftButtonDown事件如果用在Button控件上是不会触发(目前只发现MouseLeftButtonDown事件这样而MouseLeftButtonDown不会触发也导致了MouseLeftButtonUp不会触发)的,原因有两点:

第一,WPF设计原则上的问题,控件在捕获了MouseLeftButtonDown事件后,会将该事件的Handled设置为True,这个属性是用在事件路由中的,当某个控件得到一个RoutedEvent,就会检测Handled是否为true,为true则忽略该事件。控件本身的Click事件,相当于将MouseLeftButtonDown事件抑制了,转换成了Click事件。

对此有三种解决方案:

第一:使用PreviewMouseLeftButtonDown事件来代替。(最简单了)第二:在路由中去进行对应的处理。第三:在初始化的函数里利用UIElement的AddHandler方法,显式的增加这个事件。

二、键盘输入事件

KeyDown、KeyUp:键盘按下和松开事件。

可以通过事件函数的KeyEventArgs参数对象来获取实际按下的键盘信息。System.Windows.Input命名控件下有枚举类型Keys,其中存放了与键盘上所有键对应的值。

private void Button_KeyDown(object sender, KeyEventArgs e)

{

if (e.KeyCode == Keys.Enter)

{

......

}

}

需要注意的是,WPF中,Window控件可以正常触发到PreviewKeyDown或者KeyDown事件,但是UserControl无法直接捕获到这两个事件,原因是UserConrtol默认是无法获取对焦的,因此如果希望在UserControl中触发这两个事件,触发之前先要给UserControl获取焦点。

public partial class ComponentConfigDialog : UserControl

{

public ComponentConfigDialog()

{

InitializeComponent();

//为了实现键盘的KeyDown事件,UserControl无法自动获取焦点,所以在这里获取

Focusable = true;

Focus();

}

}

TextInput:文本框输入事件,当文本输入时触发,但使用后发现跟MouseLeftButtonDown类似,无法触发。可以使用PreviewTextInput事件来代替。

除上述键盘输入事件外,常用的还有KeyPressEvent事件,一次键盘完整的按下松开时触发,需要注意的是此事件函数接收的不是KeyEventArgs对象而是KeyPressEventArgs对象,两者在用法上略有不同。

此外,在WPF中,如果在C#编写过程中希望获取当前键盘上按着的修饰键是什么,可以使用Keyboard.Modifiers。

Keyboard.Modifiers:获取或设置当前正在按着的修饰键(例如Alt、Shift等),其有效值可以通过ModifierKeys枚举获取。

if (Keyboard.Modifiers == ModifierKeys.Alt)

{

......

}

三、拖拽事件

拖拽接收事件

Drop:在输入系统报告出现将此元素作为放置目标的基础放置事件时发生。其实也就是将其他控件拖拽到当前控件时,当前控件的Drop事件就会触发。

前提条件是要将当前控件设置为允许接收拖放,设置 AllowDrop=True。作为收纳容器的控件,Background不能为null,例如Canvas默认就是null的,这时要设置Background="Transparent",否则拖拽的控件会放置到上一层有Background实例的控件上。

拖拽方法

DragDrop.DoDragDrop(DependencyObject dragSource, object data, DragDropEffects allowedEffects):启动控件的拖拽操作。

dragSource:拖拽的控件对象。data:传递给收纳控件的Drop事件的处理函数的数据,为object类型。注意,不能为null,否则报错。allowedEffects:拖拽的数据模式,为枚举类型。感觉没啥用,就是鼠标的显示略有不同,一般使用Copy或者Move就可以了。

拖拽接收事件的事件参数类型

DragEventArgs:拖拽事件的参数类型。

Source:接收拖放的容器控件对象。

Data:获取IDataObject数据对象。

GetPosition(IInputElement relativeTo):获取当前拖放位置与指定控件对象的相对坐标。

也就是获取放开鼠标左键时,鼠标与relativeTo的相对位置,一般可以使用上述中的Source对象。

GetData(Type format):IDataObject对象的实例方法(注意这里是IDataObject的实例方法),获取传输数据中,指定数据类型的对象。

完整示例

xaml代码

后台代码

public partial class MainWindow : Window

{

public MainWindow()

{

InitializeComponent();

}

private void Border_MouseDown(object sender, MouseButtonEventArgs e)

{

DragDrop.DoDragDrop(sender as DependencyObject, sender, DragDropEffects.Move

}

private void Canvas_Drop(object sender, DragEventArgs e)

{

var receiver = e.Source as Canvas;

var point = e.GetPosition(e.Source as IInputElement);

var border = (Border)e.Data.GetData(typeof(Border));

var newBorder = new Border();

newBorder.Width = border.Width;

newBorder.Height = border.Height;

newBorder.Background = border.Background;

receiver.Children.Add(newBorder);

Canvas.SetTop(newBorder, point.Y - newBorder.Height/2);

Canvas.SetLeft(newBorder, point.X - newBorder.Width/2);

}

}

这里可能会产生疑问,就是能不能直接将拖拽的控件作为数据传给收纳控件的Drop事件函数,然后将其直接作为子类添加到收纳控件?看起来像是可以,然而实际上因为一个控件对象不能同时存在于两个控件容器中,所以不能这么处理,一般的做法是根据传递给收纳控件的数据,通过反射来创建一个新的控件实例然后添加到收纳控件中,或者根据现有的数据集合,给集合添加数据。

行为

行为可以看作是对一系列事件的封装,可以在行为类型中,对使用了行为的控件进行指定事件的订阅以及事件处理函数的定义。

行为并不是WPF中的核心部分,是Expression Blend的设计特性,可以用触发器来取代行为。当然了,行为使用起来还是比较方便的。

想要使用行为,首先要通过Nuget下载对应的库Microsoft.Xaml.Behaviors。

一、Behavior

行为的使用其实就是对Behavior的使用,创建行为类型必须继承Behavior

Behavior中有几个必须用到的成员,分别为OnAttached()函数、OnDetaching()函数和AssociatedObject属性。

AssociatedObject:Behavior的属性成员,为当前使用行为的控件对象。

OnAttached():当WPF应用程序挂载使用了当前行为的控件对象时调用,一般用来给对应控件对象进行事件的订阅。

OnDetaching():当对应的控件对象销毁时调用,一般用来给控件对象取消事件的订阅,以减小对资源的占用。

public class BorderMoveBehavior : Behavior

{

protected override void OnAttached()

{

base.OnAttached();

//给控件对象进行多个事件的订阅,这里随便写一个意思一下

AssociatedObject.MouseDown += Method;

}

private void Method(object sender, MouseButtonEventArgs e)

{

//随便干点啥

}

protected override void OnDetaching()

{

base.OnDetaching();

//给订阅的事件全部取消订阅

AssociatedObject.MouseDown -= Method;

}

}

二、行为实例

这里以Border控件的拖动行为为例进行学习。

创建行为类型

创建类型继承Behavior,然后重写OnAttached()函数、OnDetaching()函数。在函数中使用AssociatedObject属性给控件对象进行事件订阅和取消订阅。

在本例中,逻辑编写过程中有一点需要注意的是鼠标的锁定问题:

当鼠标按下时必须将鼠标锁定到当前控件对象,否则当鼠标移动过快或者与碰撞物相接时,控件对象容易失去鼠标焦点。

当鼠标松开时,必须将鼠标解锁。

Mouse.Capture(IInputElement element[, CaptureMode captureMode]):将鼠标对象锁定到指定的对象上,等价于obj.CaptureMouse()。

captureMode:捕获模式,默认为CaptureMode.Element。

CaptureMode.Element1鼠标捕获应用于单个元素。 鼠标输入转至已捕获的元素。CaptureMode.SubTree2鼠标捕获应用于元素的子树。 如果鼠标悬停在具有捕获的元素的子级上,则会将鼠标输入发送至该子元素。 否则,会将鼠标输入发送至具有鼠标捕获的元素。CaptureMode.None0没有鼠标捕获。 鼠标输入转至鼠标下的元素。

Mouse.Capture(null):将鼠标对象从当前锁定对象上解除,等价于obj.ReleaseMouseCapture()。

具体代码

public class BorderMoveBehavior : Behavior

{

protected override void OnAttached()

{

base.OnAttached();

//给控件对象进行多个事件的订阅,这里随便写一个意思一下

AssociatedObject.MouseLeftButtonDown += MouseDownMethod;

AssociatedObject.MouseLeftButtonUp += MouseUpMethod;

AssociatedObject.MouseMove += MouseMoveMethod;

}

//父类的Canvas容器对象

private Canvas parentCanvas = null;

private bool isDragging = false;

private Point mouseCurrentPoint;

private void MouseMoveMethod(object sender, MouseEventArgs e)

{

if (isDragging)

{

// 相对于Canvas的坐标

Point point = e.GetPosition(parentCanvas);

// 设置最新坐标

AssociatedObject.SetValue(Canvas.TopProperty, point.Y - mouseCurrentPoint.Y);

AssociatedObject.SetValue(Canvas.LeftProperty, point.X - mouseCurrentPoint.X);

}

}

private void MouseUpMethod(object sender, MouseButtonEventArgs e)

{

if (isDragging)

{

isDragging = false;

//锁定鼠标到当前控件对象

//AssociatedObject.CaptureMouse();

Mouse.Capture(null);

}

}

private void MouseDownMethod(object sender, MouseButtonEventArgs e)

{

isDragging = true;

// Canvas

if (parentCanvas == null)

parentCanvas = (Canvas)VisualTreeHelper.GetParent(sender as Border);

// 当前鼠标坐标

mouseCurrentPoint = e.GetPosition(sender as Border);

// 鼠标锁定

//AssociatedObject.CaptureMouse();

Mouse.Capture(AssociatedObject);

}

protected override void OnDetaching()

{

base.OnDetaching();

//给订阅的事件全部取消订阅

AssociatedObject.MouseLeftButtonDown -= MouseDownMethod;

AssociatedObject.MouseLeftButtonUp -= MouseUpMethod;

AssociatedObject.MouseMove -= MouseMoveMethod;

}

}

在xaml中使用行为

xmlns:i="http://schemas.microsoft.com/xaml/behaviors"

......>

路由事件

一、逻辑树与可视树

逻辑树

简单的说,逻辑树就是我们在XAML中进行开发时,那些具有“实体”的控件元素所组成的逻辑层次。由开发过程中所关注的那些界面布局或控件元素组成。

Button事件代码

public partial class MainWindow : Window

{

public MainWindow()

{

InitializeComponent();

}

private void Button_Click(object sender, RoutedEventArgs e)

{

tvLogicTree.Items.Add(WpfTreeHelper.GetLogicTree(this));

tvVisualTree.Items.Add(WpfTreeHelper.GetVisualTree(this));

}

}

二、冒泡与隧道

WPF的事件都是由Window对象接收并然后在视觉树上逐层传递到对应控件上的。

Windows系统的消息都是通过句柄来传递的,而WPF中,只有窗口对象拥有句柄,这也是为什么会从Window对象开始隧道再冒泡返回,最后响应Windows系统。

在这里延申出了两个概念:冒泡与隧道。

......

ButtonBase.Click="Window_Click">