理解路由事件
当有意义的事情发生时,有对象(WPF的元素)发送的用于通知代码的消息,就是事件的核心思想。WPF通过事件路由的概念增强了.NET事件模型。事件由允许源自某个元素的事件由另一个元素引发。例如:使用路由事件,来自工具栏按钮的单击事件可在被代码处理前上传到工具栏,然后上传到包含工具栏的窗口。
事件路由为在最合适的位置编写紧凑的、组织良好的用于处理事件的代码提供了灵活性。要使用WPF内容模型,事件路由是必须的,内容模型允许使用许多不同的元素构建简单元素,并且这些元素都拥有自己独立的事件集合。
定义、注册和封装路由事件
路由事件由只读的静态字段表示,在静态构造函数中注册,并通过标准的.NET事件定义进行封装。当注册路由事件时,需要指定事件的名称、路由类型、定义事件处理程序语法的委托以及拥有事件的类。通常,路由事件通过普通的.NET事件进行封装,从而使左右的.NET语言都能够访问她们,事件封装器可使用AddHandler、RemoveHandler添加和删除已注册的调用程序,这两个方法定义在FrameworkElement基类中,可以被每个WPF元素继承。例如:
// 声明并注册路由事件
public static readonly RoutedEvent PointPositionChangedEvent =
EventManager.RegisterRoutedEvent("PointPosition", RoutingStrategy.Bubble,
typeof(EventHandler<PointChangedEvnetArgs>), typeof(TopoGraphyAnalysisItemControl));
public static readonly RoutedEvent LinePositionChangedEvent =
EventManager.RegisterRoutedEvent("LinePosition", RoutingStrategy.Bubble,
typeof(EventHandler<PointChangedEvnetArgs>), typeof(TopoGraphyAnalysisItemControl));
// 为路由事件添加CLR事件包装器
public event RoutedEventHandler PointPositionChanged
{
add { this.AddHandler(PointPositionChangedEvent, value); }
remove { this.RemoveHandler(PointPositionChangedEvent, value); }
}
public event RoutedEventHandler LinePositionChanged
{
add { this.AddHandler(LinePositionChangedEvent, value); }
remove { this.RemoveHandler(LinePositionChangedEvent, value); }
}
共享路由事件
和依赖属性一样,可在类之间共享路由事件的定义。例如,UIElement(所有普通WPF元素的起点)和ContentElement(所有内容元素的起点)这两个基类都使用了MouseUp事件。MouseUp事件是由System.Window.Input.Mouse类定义的。这两个类知识通过RoutedEvent.AddOwner方法重用了MouseUp事件。
// 声明并注册路由事件
public static readonly RoutedEvent PointPositionChangedEvent =
TopoGraphyAnalysisItemControl.PointPositionChangedEvent.AddOwner(typeof(TopoGraphyAnalysisDiffControl));
public static readonly RoutedEvent LinePositionChangedEvent =
TopoGraphyAnalysisItemControl.LinePositionChangedEvent.AddOwner(typeof(TopoGraphyAnalysisDiffControl));
引发路由事件
路由事件不是通过传统的.NET事件封装器引发的,而是使用RaiseEvent方法引发事件,所有元素都从UIElement类继承了该方法。
PointChangedEvnetArgs evnetArgs =
new PointChangedEvnetArgs(PointPositionChangedEvent,
ctrl, ctrl.AnalysisResult?.Result?.Id, offset);
ctrl.RaiseEvent(evnetArgs);
RaiseEvent()方法负责每个已经通过Addhandler()方法注册的调用程序引发事件。因为AddHandler()方法是公有的,所以调用程序可访问该方法(直接调用AddHandler()方法注册他们自己,也可以使用事件封装器),当调用RaiseEvent()方法时都会通知他们。
所有WPF事件都为事件签名使用熟悉的.NET约定。每个事件处理程序的第一个参数(sender)都提供引发该事件的对象的引用。第二个参数是EventArgs对象,该对象与其他所有可能很重要的附加细节绑定在一起。
//添加窗体接收事件,接收控件发出的点变化消息,通过参数更新所有界面中的点数据
this.AddHandler(TopoGraphyAnalysisItemControl.PointPositionChangedEvent, new EventHandler<PointChangedEvnetArgs>(
(object sender, PointChangedEvnetArgs e) =>
{
}));
如果时间不需要传递任何额外的细节,可使用RoutedEventArgs类,如果需要传递额外的信息,可以继承自RoutedEventArgs类的对象。
处理路由事件
事件的处理方式有很多种,最常用的方法是XAML标记添加事件特性。也可以使用代码连接事件,效果等同于XAML标记
public CustomWindow()
{
this.SizeChanged += CustomWindow_SizeChanged;
}
事件路由
路由事件在实际中以3种方式出现:
- 与普通.NET事件类似的直接路由事件。他们源于一个元素,不传递给其他元素。例如,MouseEnter事件是直接路由事件(鼠标移动到元素上时发生)
- 在包含层次中向上传递的冒泡路由事件。例如MouseDown事件,该事件首先由被单击的元素引发,接下来该元素的父元素引发,然后沿着元素树传递到顶部位置
- 在包含层次中向下传递的隧道路由事件。隧道路由事件在事件到达恰当的控件之前为预览事件/终止事件提供了机会。例如,通过PreviewKeyDown事件可截获是否按下某个键。首先在窗口级别上,然后是更具体的容器,直至到达按下键时具有焦点的元素。
使用EventManager注册路由事件时,需要传递一个枚举值RoutingStrategy,该值用于指示希望应用于事件的事件行为。
RoutedEventArgs类
在处理冒泡路由事件是,sender参数提供了对整个链条上最后那个链接的引用。有些情况下,可能希望确定事件最初发生的位置。可从RoutedEventArgs类的属性获得这些信息。
名称 | 说明 |
---|---|
Source | 引发事件的对象。对于键盘事件,是当事件发生时(比如按下键盘上的键)具有焦点的控件。对于鼠标事件,是当事件发生时(如单击鼠标按钮)鼠标指针下面所有元素中最靠上的元素 |
OriginalSource | 指示最初是什么对象引发了事件。通常和Source属性值相同。在某些情况下,本属性指向对象树中更深的层次,以获得作为更高一级元素一部分的后台元素。比如,如果单击窗口边框上的关闭按钮,事件源是Window对象,但事件最原始的源是Border对象。这是因为Window对象是由多个单独的更小的部分组成 |
RoutedEvent | 通过事件处理程序为触发的事件提供RoutedEvent对象。如果用同一个事件处理程序处理不同的事件,这一信息就非常有用了 |
Handled | 改属性允许终止事件的冒泡或者隧道过程。如果控件将Handle属性设置为true,那么事件就不会继续传递,也不会再为其他任何元素所看到 |
冒泡路由事件
创建测试窗口,将元素层析结构中的图像以及它上面的每个元素都关联到同一个事件处理程序中
<Window x:Class="RoutedEvents.BubbledLabelClick"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="BubbledLabelClick" Height="359" Width="329"
MouseUp="SomethingClicked"
>
<Grid Margin="3" MouseUp="SomethingClicked">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<Label Margin="5" Background="AliceBlue" BorderBrush="Black" BorderThickness="1" MouseUp="SomethingClicked" HorizontalAlignment="Left" >
<StackPanel MouseUp="SomethingClicked" >
<TextBlock Margin="3" MouseUp="SomethingClicked" >
Image and picture label</TextBlock>
<Image Source="happyface.jpg" Stretch="None"
MouseUp="SomethingClicked" />
<TextBlock Margin="3"
MouseUp="SomethingClicked" >
Courtesy of the StackPanel</TextBlock>
</StackPanel>
</Label>
<ListBox Margin="5" Name="lstMessages" Grid.Row="1"></ListBox>
<CheckBox Margin="5" Grid.Row="2" Name="chkHandle">Handle first event</CheckBox>
<Button Click="cmdClear_Click" Grid.Row="3" HorizontalAlignment="Right" Margin="5" Padding="3">Clear List</Button>
</Grid>
</Window>
处理事件:
private void SomethingClicked(object sender, RoutedEventArgs e)
{
eventCounter++;
string message = "#" + eventCounter.ToString() + ":\r\n" +
" Sender: " + sender.ToString() + "\r\n" +
" Source: " + e.Source + "\r\n" +
" Original Source: " + e.OriginalSource;
lstMessages.Items.Add(message);
e.Handled = (bool)chkHandle.IsChecked;
}
private void cmdClear_Click(object sender, RoutedEventArgs e)
{
eventCounter = 0;
lstMessages.Items.Clear();
}
因为SomethingClicked()方法处理由Window对象引发的MouseUp事件,所以也能截获在列表框和窗口表面空白处的鼠标单击事件。但当单击Clear按钮时,不会引发MouseUp事件,这是因为按钮包含了一些有趣的代码,这些代码会挂起MouseUp事件,并引发更高级的Click事件。同时,Handled标志被设置为true,从而会阻止MouseUp事件继续传递。大多数WPF元素没有提供Click事件,而是提供MouseDown和MouseUp事件,Click事件专门用于按钮的控件。
处理挂起路由事件
有一种办法可以接收被标记为处理过的事件。使用AddHandler()方法,使用其重载方法,第三个参数传递true,即使设置了Handled标志,也将接收到事件。
cmd.AddHandler(Button.MouseUpEvent, new RoutedEventHandler(Backdoor), true);
附加事件
并不是所有的元素都支持MouseUp事件。按钮就是一个例子,它添加了Click事件,而其他任何基类都没有定义该事件。假设在StackPanel面板中封装一堆按钮,并希望在一个事件处理程序中处理所有这些按钮的单击事件。最简单粗暴的形式是每个按钮都添加事件,并关联到同一个处理程序。但是Click事件支持冒泡,从而可以使用一个更加巧妙的办法,在更高层次的元素中处理Click事件。又因为StackPanel没有Click事件,所以采用附加事件的形式实现,这样事件处理程序就可以被StackPanel面板接收了。
<Grid Margin="3" Button.Click="cmdClear_Click">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
Click事件实际是ButtonBase类中定义的,而Button类继承了该事件。如果为ButtonBase.Click事件关联处理程序,那么当单击任何继承自ButtonBase的控件时(Button类,RadioButton类以及CheckBox类)时,都会调用该事件处理程序。
隧道路由事件
隧道路由事件的工作方式和冒泡路由事件相同,但是方向相反。隧道路由事件易于识别,都是以Preview开头。而且,WPF通常成对定义冒泡事件和隧道路由事件。隧道路由事件总是在冒泡路由事件之前被触发。如果将隧道路由事件标记为已处理过,那么就不发生冒泡路由事件(他们两个共享同一个RoutedEventArgs类的实例)。
如果需要执行一些预处理(根据键盘上特定的键执行动作或者过滤掉特定的鼠标动作),隧道路由事件是非常有用的。
WPF事件
生命周期事件
当首次创建以及释放所有元素时都会引发事件,可使用这些事件初始化窗口。他们都是在FrameworkElement类中定义的。
名称 | 说明 |
---|---|
Initialized | 当元素被实例化,并已根据XAML标记设置了元素的属性之后发生。这时元素已经初始化,但窗口的其他部分可能尚未初始化。此外,尚未应用样式和数据绑定。这时,IsInitialized属性为true。不是路由事件 |
Loaded | 当整个窗口已经初始化并应用了样式和数据绑定时,该事件发生。这是在元素被呈现之前的最后一站。这时,IsLoaded属性为true |
Unloaded | 当元素被释放时,该事件发生。原因是包含元素的窗口被关闭或者特定的元素被从窗口中删除 |
为了弄清Initialized事件和Loaded事件之间的关系,分析呈现过程是有帮助的。FrameworkElement类实现了ISupportInitialized接口,改接口提供了两个控制初始化过程的方法。第一个方法是BeginInit(),在实例化元素后立即调用该方法,然后XAML解析器设置所有元素的属性(并添加内容)。第二个方法是EndInit(),完成初始化后,将调用该方法,此时引发Initialized事件。
当创建窗口时,会自下而上地初始化每个元素分支。这意味着,位于深层的嵌套元素在他的容器之前被初始化。当引发初始化事件时,可确保元素树中当前元素以下的元素已经全部完成了初始化。但是,包含当前元素的元素可能还没有初始化,并且不能假定窗口的任何其他部分已经初始化。(可以理解为,初始化只是代表自己和子元素全部初始化完成,其余的都是不确定)。
在每个元素都初始化完成之后,还需要在他们的容器中进行布局、应用样式。如果需要的话,还会绑定到数据源。当引发窗口的Initialized事件后,就可以进入下一个阶段了。
一旦完成了初始化过程,就会引发Loaded事件。包含所有元素的窗口最先引发Loaded事件,然后才是更深层的嵌套元素。为所有元素都引发Loaded事件之后,窗口就可见了,并且元素都已经被呈现。
Window类的生命周期事件
名称 | 说明 |
---|---|
SourceInitialized | 当获取窗口的HwndSource属性时(在窗口可见之前)发生。HwndSource是窗口句柄,调用Win32 API时会用到 |
ContentRendered | 在窗口首次呈现后立即发生。该事件表明窗口已经完全可见,并且已经准备好接收输入 |
Activated | 当用户切换到该窗口时发生(例如,从应用程序的其他窗口或者从其他应用程序切换到该窗口时),当窗口第一次加载时也会引发该事件。 |
Deactivated | 当用户从该窗口切换到其他窗口时发生(例如,切换到应用程序的其他窗口或者切换到其他应用程序),当用户关闭窗口时也会发生该事件,该事件在Closing事件之后,但在Closed事件之前发生。 |
Closing | 当关闭窗口时发生,不管是用户关闭创建偶还是通过代码调用Window.Close()或者Application.Shutdown()方法关闭窗口,closing事件提供了取消操作并保持打开状态的机会,具体通过将CancelEventArgs.Cancel属性设置为true实现改目标。但是,如果是因为用户关闭或者注销计算机导致应用程序被关闭,就不能收到Closing事件。为应对这种情况,需要处理Application.SessionEnding事件 |
Closed | 当窗口已经关闭后发生。但是,此时认可访问元素对象,当然是在Unloaded事件尚未发生前。在此处,可以执行一些清理工作,向永久存储位置写入设置信息等。 |
输入事件
输入事件是用户通过某些种类的外设硬件进行交互时发生的事件,例如鼠标、键盘、手写笔、多点触摸屏。输入事件通过继承自InputEventAegs的自定义事件参数传递额外的信息。InputEventArgs类只增加了两个属性:Timestamp和Device。Timestamp属性是一个整数,指事件发生时的毫秒数。Device属性返回一个对象,该对象提供与触发事件的设备相关的更多信息。
键盘输入
当用户按下键盘上的一个键时,就会发生一系列事情。按照发生的顺序是:
名称 | 路由类型 | 说明 |
---|---|---|
PreviewKeyDown | 隧道 | 当按下一个键时发生 |
KeyDown | 冒泡 | 当按下一个键时发生 |
PreviewTextInput | 隧道 | 当按键完成并且元素正在接收文本输入时发生。对于那些不会产生输入的按键(Ctrl,Shift,Backspace,方向键,功能键等)不会引发该事件 |
TextInput | 冒泡 | 同上 |
PreviewKeyUp | 隧道 | 当释放一个按键式发生 |
KeyUp | 冒泡 | 同上 |
键盘处理是一个复杂的过程。一些空间可能会挂起这些事件中的某些事件,从而可执行自己更特殊的键盘处理。最明显的例子就是TextBox控件,它挂起了TextInput事件。对于一些键,TextBox控件还挂起了KeyDown事件(方向键)。对于这些情况,可以使用隧道路由事件。
TextBox控件还添加了TextChanged事件,在按键导致文本框文本发生变化后立即引发该事件。这时,在文本框中已经可以看到新的文本了,所以阻止不需要的按键已经晚了,可以使用隧道事件进行处理。
处理按键事件
按键输入例子:
<Window x:Class="RoutedEvents.KeyPressEvents"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="KeyPressEvents" Height="387" Width="368"
>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<DockPanel Margin="5">
<TextBlock Margin="3" >
Type here:
</TextBlock>
<TextBox PreviewKeyDown="KeyEvent" KeyDown="KeyEvent"
PreviewKeyUp="KeyEvent" KeyUp="KeyEvent"
PreviewTextInput="TextInput"
TextChanged="TextChanged"></TextBox>
</DockPanel>
<ListBox Margin="5" Name="lstMessages" Grid.Row="1"></ListBox>
<CheckBox Margin="5" Name="chkIgnoreRepeat" Grid.Row="2">Ignore Repeated Keys</CheckBox>
<Button Click="cmdClear_Click" Grid.Row="3" HorizontalAlignment="Right" Margin="5" Padding="3">Clear List</Button>
</Grid>
</Window>
public partial class KeyPressEvents : System.Windows.Window
{
public KeyPressEvents()
{
InitializeComponent();
}
private void KeyEvent(object sender, KeyEventArgs e)
{
if ((bool)chkIgnoreRepeat.IsChecked && e.IsRepeat) return;
string message = //"At: " + e.Timestamp.ToString() +
"Event: " + e.RoutedEvent + " " +
" Key: " + e.Key;
lstMessages.Items.Add(message);
}
private void TextInput(object sender, TextCompositionEventArgs e)
{
string message = //"At: " + e.Timestamp.ToString() +
"Event: " + e.RoutedEvent + " " +
" Text: " + e.Text;
lstMessages.Items.Add(message);
}
private void TextChanged(object sender, TextChangedEventArgs e)
{
string message =
"Event: " + e.RoutedEvent;
lstMessages.Items.Add(message);
}
private void cmdClear_Click(object sender, RoutedEventArgs e)
{
lstMessages.Items.Clear();
}
}
通过上面的例子,每次按下一个键时,都会触发PreviewKeyDown和PreviewKeyUp事件。但是只有当字符可以“输入”到元素中时,才会触发TextInput事件。当输入大写字母S时,需要按下两个按键,先按下Shift键,再按下S键,因此可以看到两个KeyDown和KeyUp事件,但是只有一个TextInput事件。
PreviewKeyDown、KeyDown、PreviewKeyUp、KeyUp事件都是通过KeyEventArgs对象提供了相同的信息。最重要的信息是Key属性。该属性返回一个System.Windows.Input.Key枚举值,该枚举值标志了按下和释放的键。
Key值没有考虑任何其他键的状态。例如当按下S键事不必关心当前是否按下Shift键,不管是否按下Shift键,都会得到相同的Key值(Key.S)
根据Windows键盘的设置,持续按下一个键一段时间,会重复引发按键事件。例如,保持按下S键,显然会在文本框中输入一系列S字符。同样,按下Shift键一段时间也会的到多个按键和一系列KeDown事件。按下Shift+S键进行测试的真实情况是,文本框实际上会为Shift键引发一系列KeyDown事件,然后为S键引发KeyDown事件,随后是TextInput时间(对于文本框,是TextChanged事件),最后是为Shift键和S键引发KeyUp事件。如果希望忽略这些重复的Shift键,可以通过检查KeyEventArgs.IsRepeat属性,确定按键是不是因为按住键导致的结果。
理想情况下,可在控件(TextBox控件中)使用PreviewTextInput事件执行验证工作(比如智能输入数字的文本框),可确保当前按键不是字母,如果是就设置Handled标志为true。有些键可能不会触发PreviewTextInput事件,例如在文本框按下空格键,这时还需要处理PreviewKeyDown事件。最好的办法是两个事件都要进行处理,PreviewTextInput事件负责大多数验证,PreviewKeyDown用于那些在文本框中不会引发PreviewTextInput事件的按键。可将这些事件管理到单个文本框,或者更高层次的容器。
焦点
在windows中,用户每次只能使用一个控件。当前接收用户按键的是具有焦点的控件。为让控件能够接受焦点,必须将Focusable属性设置为true,默认值就是true。可以通过Tab键来切换焦点的位置,如果使用了IsTabStop属性并设置了false,则阻止控件被包含在Tab键焦点范围内。
获取键盘状态
当发生按键事件时,经常需要知道按下的哪个键,而且需要确定其他键是否被按下,也很重要。对于键盘事件(PreviewKeyDown、KeyDown,PreviewKeyUpKeyUp),获取这些信息比较容易。首先KeyEventArgs对象包含了KeyStatus属性,该属性反应触发事件的键的属性。更有用的是,keyboardDevice属性为键盘上的所有键提供了相同的信息(包含当前元素是否具有焦点,以及当前事件发生时按下了哪些修饰键,并且可以使用位逻辑来检查他们的状态。KeyboardDevice属性的方法:
名称 | 说明 |
---|---|
IsKeyDown() | 当事件发生时,通知是否按下该键 |
IsKeyUP() | 当事件发生时,通知是否释放该键 |
IsKeyToggled() | 当事件发生时,通知该键是否处于打开状态,该方法只对那些能够打开、关闭的键有意义,CapsLock、ScroolLock,NumLock |
GetkeyStatues() | 返回一个或者多个KeySttues枚举值,指明该键当前是否被释放了,按下了,或者处于切换状态。该方法本质上和为同一个键同时调用IsKeyDown()方法和IsKeyToggled()方法相同 |
鼠标输入
鼠标事件执行几个关联任务。MouseEnter(当鼠标指针移到元素上时引发该事件)、MouseLeave(当鼠标离开元素时引发该事件)。这两个事件是直接事件,不使用冒泡或者隧道过程,而是源自一个元素且只被该元素引发。
例如:在一个StackPanel面板上放置一个按钮Button,并将鼠标指针移到按钮上,那么首先会为这个StackPanel面板引发MouseEnter事件(当鼠标指针进入StackPanel面板边界时),然后为按钮引发MouseEnter事件(当鼠标指针移到按钮上时),将鼠标指针移开时,首先为按钮引发MouseLeave事件,然后是按钮。还可以响应PreviewMouseMove事件(隧道事件)和MouseMove事件(冒泡事件),只要移动鼠标就会引发这两个事件。所有这些事件都提供了MouseEventArgs对象,此对象包含事件引发时标识鼠标键状态的属性,以及GetPosition()方法返回相对所选元素的鼠标坐标。
鼠标单击
鼠标单击事件的引发方式和按键事件的引发方式有类似之处。区别是对于鼠标左键和鼠标右键引发不同的事件。根据引发顺序列出事件,其余还有鼠标滚轮事件:PreviewMouseWheel和MouseWheel
名称 | 路由类型 | 说明 |
---|---|---|
PreviewMouseLeftButtonDown PreviewMouseRightButtonDown |
隧道 | 当按下鼠标键时发生 |
MouseLeftButtonDown MouseRightButtonDown |
冒泡 | 当按下鼠标键时发生 |
PreviewMouseLeftButtonUp PreviewMouseRightButtonUp |
隧道 | 当释放鼠标键时发生 |
MouseLeftButtonUp MouseRightButtonUp |
冒泡 | 当释放鼠标键时发生 |
所有鼠标事件都提供MouseButtonEventArgs对象,继承自MouseEventArgs类(包含相同的坐标和按钮状态信息),还包含MouseButton(用于通知是哪个鼠标键引发的事件),ButtonState(通知当事件发生时,鼠标键是处于按下状态还是释放状态),ClickCount(用于通知鼠标键被单击了几次,可以区分是单击还是双击)
通常的做法是,单击鼠标时,Windows程序对鼠标键的释放事件进行响应(Up事件,而不响应down事件)。
某些元素添加了高级的鼠标事件,Control类添加了PreviewMouseDoubleClick事件和MouseDoubleClick事件,这两个事件代替了MouseLeftButtonUp事件。类似处理,对于Button类,通过鼠标和键盘可触发Click事件。
与键盘按键事件一样,当发生鼠标事件时,这些事件提供了有关鼠标位置和哪个鼠标键被按下的信息。为获得当前鼠标位置和按键状态,可使用Mouse类的静态成员,他们和MouseButtonEventArgs类的成员类似。
捕获鼠标
有一种情况,如果单击一个元素,保持按下鼠标键,然后移动鼠标指针离开该元素,这时这个元素就不会接收鼠标键释放事件。这种情况下如果需要通知鼠标释放事件,就需要调用Mouse.Capture()方法并传递恰当的元素以捕获鼠标。此后,就会接收到鼠标按键按下事件和释放事件,直到再次调用Mouse.Capture()方法并传递空引用位置。当鼠标被一个元素捕获后,其他元素就不会接收到鼠标事件。这意味着永不能单击窗口中的其他位置按钮,不能单击文本框的内部。鼠标捕获有时用于被拖放并可以改变尺寸的元素时。
在有些情况下,可能会丢失鼠标捕获,如需要显示系统对话框,Windows可能会释放鼠标捕获,就可以通过处理LostMouseCapture事件来响应鼠标不会的丢失。
当鼠标被一个元素捕获时,就不能与其他元素进行交互。鼠标捕获通常用于短时间的操作(拖放,滑动等)。一般不使用Mouse.Capture()方法,而是使用UIElement类提供的两个方法CaptureMouse()和ReleaseMouseCapture()。
鼠标拖放
拖放操作一般是:拖动信息使其离开窗口中的某个位置,然后将其放到其他位置的技术。本质上,需要3个步骤:
- 用户单击元素(或者选择元素中的一块特定区域),并保持鼠标键为按下状态。这时,某些信息被搁置起来,并且拖放操作开始。
- 用户将鼠标移动到其他元素上。如果该元素可接受正在拖动的内容的类型,鼠标指针会变成拖放图标,否则鼠标指针会变成内部有一条线的圆形
- 当用户释放鼠标键时,元素接收信息并决定如何处理接收到的信息。在没有释放鼠标键时,可按下Esc键取消该操作。
可在窗口中添加两个文本框来尝试拖放操作支持的工作方式,因为TextBox控件提供了支持拖放的内部逻辑。如果选中文本框中的一些文本,就可以将这些文本拖动到两一个文本框中。当释放鼠标键时,这些文本将移动位置。同一技术在两个应用程序之间也可以操作(可以将在word文档中拖动一些文本,并放入到WPF的TextBox对象中,也可以将文本从WPF的TextBox对象拖动到word文档中。
有时,希望在两个未提供内置拖放功能的元素之间进行拖放。例如,用户将内容从文本框拖放到标签中,或者从Lable对象或者TextBox对象拖动文本,放到另一个标签中。用于拖放操作的方法和事件都在System.Windows.DragDrop类中。
<Window x:Class="RoutedEvents.DragAndDrop"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="DragAndDrop" Height="257.6" Width="392.8"
>
<Grid Margin="5">
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
</Grid.ColumnDefinitions>
<TextBox Padding="10" VerticalAlignment="Center" HorizontalAlignment="Center">Drag from this TextBox</TextBox>
<Label Grid.Column="1" Padding="20" Background="LightGoldenrodYellow"
VerticalAlignment="Center" HorizontalAlignment="Center"
MouseDown="lblSource_MouseDown">Or this Label</Label>
<Label Grid.Row="1" Grid.ColumnSpan="2" Background="LightGoldenrodYellow"
VerticalAlignment="Center" HorizontalAlignment="Center" Padding="20"
AllowDrop="True" Drop="lblTarget_Drop">To this Label</Label>
</Grid>
</Window>
public partial class DragAndDrop : System.Windows.Window
{
public DragAndDrop()
{
InitializeComponent();
}
private void lblSource_MouseDown(object sender, MouseButtonEventArgs e)
{
Label lbl = (Label)sender;
DragDrop.DoDragDrop(lbl, lbl.Content, DragDropEffects.Copy);
}
private void lblTarget_Drop(object sender, DragEventArgs e)
{
((Label)sender).Content = e.Data.GetData(DataFormats.Text);
}
private void lblTarget_DragEnter(object sender, DragEventArgs e)
{
if (e.Data.GetDataPresent(DataFormats.Text))
e.Effects = DragDropEffects.Copy;
else
e.Effects = DragDropEffects.None;
}
}
操作拖放有源和目标两方面。为了创建拖放源,需要在某个位置调用DragDrop.DoDragDrop()方法来初始化拖放操作。然后搁置希望拖放的内容,并指明允许什么样的拖放效果(复制、移动等)。通常在响应MouseDown或者PreviewDropDown事件时调用DragDrop.DoDragDrop方法,上面的例子是在单击标签时初始化拖放操作,标签中的文本内容用于拖放动作。
接收数据的元素需要将他的AllowDrop属性设置为true(允许任何类型的信息),还需要处理Drop事件来处理数据。如果希望选择性地接收内容,可以处理DrapEnter事件。可以在DrapEnter事件中过滤需要处理的类型。
最后,当完成操作后就可以检索并处理数据了。将拖放的文本插入标签中。
明确一点,拖放操作可以交换任意类型的对象。如果希望和其他应用程序通信,应使用基本数据类型(字符串或者整形等),或者使用实现了Iserializable或者IdataObject接口的对象(这两个接口将对象转换成字节流,并在另一个应用程序中重构对象)如果希望在两个应用程序之间传递数据,那么务必检查System.Windows.Clipboard类,用于在Windows剪切板中放置数据,并以各种不同的格式检索剪切板中的数据。