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;
......
}
Navigating
:在应用程序中的导航请求新导航时发生。
LoadCompleted
:在已经加载、分析并开始呈现应用程序中的导航器导航到的内容时发生。
Navigated
:在已经找到应用程序中的导航器要导航的内容时发生,尽管此时可内容可能尚未完成加载。
NavigatedFailed
:在应用程序中的导航器在导航到所请求内容时出现错误的情况下发生。
NavigationProgress
:在由应用程序中的导航器管理的下载过程中定期发生,以提供导航进度信息。
NavigationStopped
:在调用程序中的导航器的StopLoading
方法时发生,或者当导航器在当前导航正在进行期间请求了一个新导航时发生。
以上Browser
事件,如果是做窗口开发基本上都用不上,其中Navigating
事件会在应用启动时调用一次,来加载确认有没有Page
,需不需要进行导航。需要使用事件时同样可以在App
构造函数中订阅。
public App()
{
......
Navigating += NavigatingFunc;
......
}
DispatcherUnhandledException
:在应用程序UI线程引发异常但未进行处理时发生。
AppDomain.CurrentDomain.UnhandledException
:应用程序上所有线程引发异常但未处理时发生。
Task
线程外的所有线程发生的异常,比DispatcherUnhandledException
事件更加全面。TaskScheduler.UnobservedTaskException
:Task
线程引发异常但没有进行处理时触发,专门捕获应用上的Task
异常。
GC.Collect()
主动进行垃圾回收)public App()
{
......
DispatcherUnhandledException += MyExceptionFunc;
......
}
窗体的生命周期事件可以在对应窗体的cs文件的窗体类型构造函数中进行定义。
SourceInitialized
:操作系统给窗体分配句柄时触发。
ContentRendered
:对应窗体内容渲染时触发,一般在窗口首次呈现时触发。
Loaded
:窗体加载完成时触发。
Activated
:当前窗口成为前台应用程序时发生,也就是当前窗体获得焦点时触发。
Deactivated
:当前窗体失去焦点时触发。
Closing
:窗体关闭时触发。
Closed
:窗体关闭后触发。
WPF中的常用鼠标事件有:MouseEnter
、MouseLeave
、MouseDown
、MouseUp
、MouseMove
、MouseLeftButtonDown
、MouseLeftButtonUp
、MouseRightButtonDown
、MouseRightButtonUp
、MouseDoubleClick
这些都是一些很常用的事件,使用上都差不多,没啥好说的。
MouseLeftButtonDown诡异事件
有一点需要注意的是,在使用时会发现MouseLeftButtonDown
事件如果用在Button
控件上是不会触发(目前只发现MouseLeftButtonDown
事件这样而MouseLeftButtonDown
不会触发也导致了MouseLeftButtonUp
不会触发)的,原因有两点:
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)
:启动控件的拖拽操作。
Drop
事件的处理函数的数据,为object
类型。注意,不能为null
,否则报错。Copy
或者Move
就可以了。拖拽接收事件的事件参数类型
DragEventArgs
:拖拽事件的参数类型。
Source
:接收拖放的容器控件对象。
Data
:获取IDataObject
数据对象。
GetPosition(IInputElement relativeTo)
:获取当前拖放位置与指定控件对象的相对坐标。
relativeTo
的相对位置,一般可以使用上述中的Source
对象。GetData(Type format)
:IDataObject
对象的实例方法(注意这里是IDataObject
的实例方法),获取传输数据中,指定数据类型的对象。
完整示例
xaml代码
<Grid ShowGridLines="True">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Border Height="80" Width="100" Background="Blue" MouseDown="Border_MouseDown"/>
<Canvas Grid.Column="1" AllowDrop="True" Drop="Canvas_Drop" Background="Transparent"/>
</Grid>
后台代码
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<T>
的使用,创建行为类型必须继承Behavior<T>
。
Behavior<T>
中有几个必须用到的成员,分别为OnAttached()
函数、OnDetaching()
函数和AssociatedObject
属性。
AssociatedObject
:Behavior
的属性成员,为当前使用行为的控件对象。
OnAttached()
:当WPF应用程序挂载使用了当前行为的控件对象时调用,一般用来给对应控件对象进行事件的订阅。
OnDetaching()
:当对应的控件对象销毁时调用,一般用来给控件对象取消事件的订阅,以减小对资源的占用。
public class BorderMoveBehavior : Behavior<Border>
{
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<T>
,然后重写OnAttached()
函数、OnDetaching()
函数。在函数中使用AssociatedObject
属性给控件对象进行事件订阅和取消订阅。
在本例中,逻辑编写过程中有一点需要注意的是鼠标的锁定问题:
当鼠标按下时必须将鼠标锁定到当前控件对象,否则当鼠标移动过快或者与碰撞物相接时,控件对象容易失去鼠标焦点。
当鼠标松开时,必须将鼠标解锁。
Mouse.Capture(obj)
:将鼠标对象锁定到指定的对象上,等价于obj.CaptureMouse()
。
Mouse.Capture(null)
:将鼠标对象从当前锁定对象上解除,等价于obj.CaptureMouse()
。
具体代码
public class BorderMoveBehavior : Behavior<Border>
{
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中使用行为
<Window ......
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
......>
<Grid>
<Canvas>
<Border Background="Orange" Width="100" Height="100">
<i:Interaction.Behaviors>
<local:BorderMoveBehavior/>
</i:Interaction.Behaviors>
</Border>
<Border Background="Green" Width="100" Height="100" Canvas.Left="100" Canvas.Top="100">
<i:Interaction.Behaviors>
<local:BorderMoveBehavior/>
</i:Interaction.Behaviors>
</Border>
</Canvas>
</Grid>
</Window>
逻辑树
简单的说,逻辑树就是我们在XAML中进行开发时,那些具有“实体”的控件元素所组成的逻辑层次。由开发过程中所关注的那些界面布局或控件元素组成。
<Window ......>
<Grid>
<DockPanel>
<Button/>
</DockPanel>
</Grid>
</Window>
如上述xaml代码所示,Window
、Grid
、DockPanel
、Button
这几个元素一起组成了逻辑树。
可视树
可视树是由界面上可见的元素构成的,这些元素主要是由从Visual
或者Visual3D
类中派生出来的类。例如上面的代码中、这些组成逻辑树的元素本身还包含了一些由Visual
或者Visual3D
类派生出的一些可视树的元素。这些组成逻辑树的元素以及其所包含的可视元素全部一起组成了界面的可视树。
遍历逻辑树与可视树
LogicalTreeHelper
:逻辑树的工具类,可以通过这个静态类对逻辑树进行操作。
VisualTreeHelper
:可视树的工具类,可以通过这个静态类对可视树进行操作。
创建类型定义遍历逻辑
public class WpfTreeHelper
{
static string GetTypeDescription(object obj)
{
return obj.GetType().FullName;
}
/// <summary>
/// 获取逻辑树
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public static TreeViewItem GetLogicTree(DependencyObject obj)
{
if (obj == null)
{
return null;
}
//创建逻辑树的节点
TreeViewItem treeItem = new TreeViewItem { Header = GetTypeDescription(obj), IsExpanded = true };
//循环遍历,获取逻辑树的所有子节点
foreach (var child in LogicalTreeHelper.GetChildren(obj))
{
//递归调用
var item = GetLogicTree(child as DependencyObject);
if (item != null)
{
treeItem.Items.Add(item);
}
}
return treeItem;
}
/// <summary>
/// 获取可视树
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public static TreeViewItem GetVisualTree(DependencyObject obj)
{
if (obj == null)
{
return null;
}
TreeViewItem treeItem = new TreeViewItem() { Header = GetTypeDescription(obj), IsExpanded = true };
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
{
var child = VisualTreeHelper.GetChild(obj, i);
var item = GetVisualTree(child);
if (item != null)
{
treeItem.Items.Add(item);
}
}
return treeItem;
}
}
xaml界面设计
<Window x:Class="WpfApp5.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<DockPanel>
<Button DockPanel.Dock="Top" Click="Button_Click" Content="获取逻辑树和可视树"></Button>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<DockPanel Grid.Column="0">
<TextBlock DockPanel.Dock="Top" Text="逻辑树"></TextBlock>
<TreeView Name="tvLogicTree"></TreeView>
</DockPanel>
<DockPanel Grid.Column="1">
<TextBlock DockPanel.Dock="Top" Text="可视树"></TextBlock>
<TreeView Name="tvVisualTree"></TreeView>
</DockPanel>
</Grid>
</DockPanel>
</Grid>
</Window>
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系统。
在这里延申出了两个概念:冒泡与隧道。
<Window
......
ButtonBase.Click="Window_Click">
<Grid Background="Yellow" MouseLeftButtonDown="Grid_MouseLeftButtonDown" PreviewMouseLeftButtonDown="Grid_PreviewMouseLeftButtonDown">
<StackPanel Background="Red" Height="300" Width=" 300" ButtonBase.Click="StackPanel_Click">
<Border Margin="30" Background="Blue" Height="100" Width="100" MouseLeftButtonDown="Border_MouseLeftButtonDown"/>
<Button Margin="30" Background="LightBlue" Height="100" Width="100" Click="Button_Click"/>
</StackPanel>
</Grid>
</Window>
点击Button
点击Border
冒泡与隧道
查看上述代码,在WPF中,当点击Button
时,一个完整的事件传递过程应该是从1-7的。
其中1-4是从Window
接收到鼠标点击消息后逐层向下传递到目标控件对象Button
上,这个过程称为隧道。
当Button
触发事件后、会将事件消息原路向上传递,即4-7的过程,这个过程称之为冒泡。
事件优先级
从Debug输出台中打印出来的信息中可以知道,在事件消息传递的过程中,有两点需要注意:
Preview
事件可以无视第一点,在隧道中只要遇到对应事件,就可以先触发,并且不影响其他控件的事件。基本上,Preview
事件是在隧道中发生的,称之为预览事件(隧道事件);其他路由事件是在冒泡中触发的,称为冒泡事件。
注意,上面的示例中,如果Button没有设置Background
属性,那么在鼠标点击时除了自身控件会触发点击事件,父类容器也会触发对应事件。
在开发过程中,如果希望事件消息在某个事件触发之后,路径上的对应事件不再触发,可以在节点事件函数中,将Handled
属性设置为true
。
WPF中,所有的路由事件都共享一个公共事件基类RoutedEventArgs
,该类中定义了有Handled
属性,Handle
属性用于告诉消息系统,这个事件是否已经被处理,如果是,则后续事件不需要再对该消息进行处理。
private void Button_Click(object sender, RoutedEventArgs e)
{
Debug.WriteLine("Button_Click");
e.Handled = true;
}
需要注意的是,仅仅是后续事件不再触发,消息依然在传递,这一点下面会讲到。
如果再将Handled
属性设置为true
之后,又希望在后面的传播路径上的某个控件的对应事件在接收到消息后依然能够触发,可以通过显示添加事件的方式进行设置。
AddHandler(RoutedEvent routedEvent, Delegate handler, bool handledEventsToo)
:为调用对象添加路由事件,该方法为UIElement
定义的方法,而几乎所有的控件都继承了UIElement
,因此都可以通过该方法进行路由事件的添加。
RoutedEventHandler
对象来传入。Handled
)public MainWindow()
{
InitializeComponent();
Stack.AddHandler(Button.ClickEvent, new RoutedEventHandler(Force_StackPanel_Click), true);
}
可以看到,虽然在Button
的Click
事件中已经将Handled
设置为true
,StackPanel
的事件还是照样触发了。(这也说明了事件消息是正常传递的,只不过默认情况下遇到消息中的Handle
为true
时,事件就不触发了)
在WPF中,事件的定义需要两个要素:事件注册和事件属性。
主要函数
事件注册通过EventManager
类的静态方法RegisterRoutedEvent
来实现。
RoutedEvent RegisterRoutedEvent(string name, RoutingStrategy routingStrategy, Type handlerType, Type ownerType)
:注册路由事件。
RoutingStrategy
枚举类型。
EventTriger
、EventSetter
。typeof(RoutedEventHandler)
。typeof(MyEventControl)
。实现路由事件的定义
新建WPF用户控件,然后在后台代码文件中进行事件的注册及属性的定义
public partial class NewControl : UserControl
{
public NewControl()
{
InitializeComponent();
}
//注册事件 冒泡事件一般以Event作为后缀、隧道事件一般以Preview作为前缀并且以Event作为后缀
public static readonly RoutedEvent TapEvent = EventManager.RegisterRoutedEvent("Tap", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(NewControl));
//定义事件属性,关乎Tap事件的 订阅及取消订阅
public event RoutedEventHandler Tap
{
add { AddHandler(TapEvent, value); }
remove { RemoveHandler(TapEvent, value); }
}
}
事件的注册完成后,就可以在xaml进行事件的订阅了,但是怎么去触发这个事件呢?
常见的操作是,使用原有的事件来引发我们自定义的事件。
做法相对简单,只要在自定义的用户控件中使用原生事件的处理函数中通过RaiseEvent
函数来引发自定义的事件就可以了。
使用原生事件引发自定义事件的操作必须在自定义的用户控件中进行。否则无法生效。
主要函数
RaiseEvent(RoutedEventArgs e)
:引发指定的路由事件。
RoutedEventArgs(RoutedEvent routedEvent)
:根据指定的路由事件,创建RoutedEventArgs
对象。
自定义路由事件的触发实现
在上文中注册事件的用户控件中,添加一个触发原生事件的控件
<UserControl ......>
<Grid>
<Button Click="Button_Click" Content="用户控件"/>
</Grid>
</UserControl>
在用户控件的后台代码中通过原生事件的处理函数来引发自定义事件
public partial class NewControl : UserControl
{
......
private void Button_Click(object sender, RoutedEventArgs e)
{
Debug.WriteLine("进入用户控件的原生Click事件,接下来引发自定义事件");
RoutedEventArgs newEventArgs = new RoutedEventArgs(TapEvent);
RaiseEvent(newEventArgs);
}
}
在主窗体xaml中使用控件及事件
<Window ......>
<Grid local:NewControl.Tap="Grid_Tap">
<StackPanel>
<local:NewControl/>
</StackPanel>
</Grid>
</Window>
public partial class MainWindow : Window
{
......
private void Grid_Tap(object sender, RoutedEventArgs e)
{
Debug.WriteLine("Grid_Tap:这里是自定义事件,冒泡过程中触发了");
}
}
至此,自定义事件的触发就完成了。如果自定义的事件是隧道事件,触发的实现也是一样的,只不过使用原生的Preview
事件来进行自定义事件的引发罢了。