【WPF学习】第十七章 鼠标输入
鼠标事件执行几个关联的任务。当鼠标移到某个元素上时,可通过最基本的鼠标事件进行响应。这些事件是MouseEnter(当鼠标指针移到元素上时引发该事件)和MouseLeave(当鼠标指针离开元素时引发该事件)。这两个事件都是直接事件,这意味着他们不使用冒泡和隧道过程,而是源自一个元素并且只被该元素引发。考虑到控件嵌入到WPF窗口的方式,这是合理的。
例如,如果有一个包含按钮的StackPanel面板,并将鼠标指针移到按钮上,那么首先会为这个StackPanel引发MouseEnter事件(当鼠标指针进入StackPanel面板的边界时),然后为StackPanel面板引发MouseLeave事件。
还可响应PreviewMouseMove事件(隧道路由事件)和MouseMove事件(冒泡路由事件),只要移动鼠标就会引发这两个事件。所有这些事件都为代码提供了相同的信息:MouseEventArgs对象。MouseEventArgs对象包含当事件发生时标识鼠标键状态的属性,以及GetPosition()方法,该方法返回相对于所选元素的鼠标坐标。下面列举一个示例,该例以设备无关的像素显示鼠标指针在窗口中的位置:
private void MouseMoved(object sender, MouseEventArgs e) { Point pt = e.GetPosition(this); lblInfo.Text = string.Format("You are at ({0},{1}) in window coordinates", pt.X, pt.Y); }
在该例中,从客户区(标题栏的下面)的左上角开始测量坐标。下图显示了上述代码的运行情况。
XAML代码如下所示:
<Window x:Class="MouseEvents.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="300" Width="300"> <Grid Margin="5"> <Grid.RowDefinitions> <RowDefinition Height="*"></RowDefinition> <RowDefinition Height="Auto"></RowDefinition> </Grid.RowDefinitions> <Rectangle Grid.Row="0" Name="rect" MouseMove="MouseMoved" Fill="LightBlue"></Rectangle> <TextBlock Name="lblInfo" Grid.Row="1"></TextBlock> </Grid> </Window>
一、鼠标单击
鼠标单击事件的引发方式和按键事件的引发方式有类似之处。区别是对于鼠标左键和鼠标右键引发不同的事件。下表根据他们的发生顺序列出了这些事件。除这些事件外,还有两个响应鼠标滚动动作的事件:PreviewMouseWheel和MouseWheel。
表 所有元素的鼠标单击事件(按顺序排列)
所有鼠标事件提供MouseButtonEventArgs对象。MouseButtonEventArgs类继承自MouseEventArgs类(这意味着该类包含相同的坐标和按钮状态信息),并添加了几个成员。这些成员中相对不重要的是MouseButton(该成员用于通知是哪个鼠标键引发的事件)和ButtonState(该成员用于通知当事件发生时鼠标键时处于按下状态还是释放状态)。ClickCount属性更有趣,该属性用于通知鼠标键被单击了多少次,从而可以区分是单击(ClickCount的值是1)还是双击(ClickCount的值为2)。
某些元素添加了更高级的鼠标事件。例如,Control类添加了PreviewMouseDoubleClick事件和MouseDoubleClick事件,这两个事件代替了MouseLeftButtonUp事件。以此类推,对于Button类,通过鼠标或键盘可触发Click事件。
二、捕获鼠标
通常,元素每次接收到鼠标键“按下”事件后,不久后就会接受到对应的鼠标键“释放”事件。但情况不见的总是如此。例如,如果单击一个元素,保持按下鼠标键,然后移动鼠标指针离开该元素,这时该元素就不会接收到鼠标键释放事件。
某些情况下,可能希望通过鼠标键释放事件,即使鼠标键释放事件是在鼠标已经离开了原来的元素之后发生的。为此,需要调用Mouse.Capture()方法并传递恰当的元素以捕获鼠标。此后,就会接受到鼠标键按下事件和释放事件,直到再次调用Mouse.Capture()方法传递空引用为止。当鼠标被一个元素捕获后,其他元素就不会接收到鼠标事件。这意味着用户不能单击窗口中其他位置的按钮,不能单击文本框的内部。鼠标捕获有时用于可以被拖放并可以改变尺寸的元素。
有些情况下,可能由于其他原因(不是你的错)丢失鼠标捕获。例如,如果需要显示系统对话框,Windows可能会释放鼠标捕获。如果当鼠标键释放事件发生后没有释放鼠标,并且用户单击了另一个应用程序的窗口,也可能丢失鼠标捕获。无论哪种情况,都可以通过处理元素的LostMouseCapture事件来响应鼠标捕获的丢失。
当鼠标被一个元素捕获时,就不能与其他元素进行交互(例如,不能单击窗口中的其他元素)。鼠标捕获通常用于短事件的操作,如拖放。
对前面一个示例进行修改,如下图所示:
完整代码如下所示:
<Window x:Class="MouseEvents.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="300" Width="300"> <Grid Margin="5"> <Grid.RowDefinitions> <RowDefinition Height="*"></RowDefinition> <RowDefinition Height="Auto"></RowDefinition> <RowDefinition Height="Auto"></RowDefinition> </Grid.RowDefinitions> <Rectangle Grid.Row="0" Name="rect" MouseMove="MouseMoved" Fill="LightBlue"></Rectangle> <Button Grid.Row="1" Name="cmdCapture" Click="cmdCapture_Click">Capture the Mouse</Button> <TextBlock Name="lblInfo" Grid.Row="2"></TextBlock> </Grid> </Window>
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; namespace MouseEvents { /// <summary> /// MainWindow.xaml 的交互逻辑 /// </summary> public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } private void MouseMoved(object sender, MouseEventArgs e) { Point pt = e.GetPosition(this); lblInfo.Text = string.Format("You are at ({0},{1}) in window coordinates", pt.X, pt.Y); } private void cmdCapture_Click(object sender, RoutedEventArgs e) { this.AddHandler(Mouse.LostMouseCaptureEvent, new RoutedEventHandler(LostMouseCapture)); Mouse.Capture(rect); cmdCapture.Content = "[ Mouse is now captured ... ]"; } protected void LostMouseCapture(object sender, RoutedEventArgs e) { MessageBox.Show("Lost capture"); cmdCapture.Content = "Capture the Mouse"; } } }
三、鼠标拖放
拖放操作(是一种拖动信息使其离开窗口中的某个位置,然后将其放到其他位置的技术)和前几年相比现在不是非常普遍。编程人员已经逐渐地使用方法复制信息,从人不在需要按住鼠标键(许多用户发现这一技术比较困难)。支持鼠标拖放功能的程序通常将它用作为高级用户提供的一种快捷方式,而不是一种标准的工作方式。
本质上,拖放操作通过以下三个步骤进行:
(1)用户单击元素(或选择元素中的一块特定区域),并保持鼠标键为按下状态。这是,某些信息被搁置起来,并且拖放操作开始。
(2)用户将鼠标移到其他元素上。如果该元素可接受正在拖动的内容的类型(例如一幅位图或一块文本),鼠标指针会变成拖放图标,否则鼠标指针会变成内部有一条信息的图像。
(3)当用户释放鼠标键时,元素接收信息并决定如何处理接收信息。在没有释放鼠标键时,可按下Esc键取消该操作。
可在窗口中添加两个文本框来尝试拖放操作支持的工作方式。因为TextBox控件提供了支持拖放的内置逻辑。如果选中文本框中的一些文本,就可以将这些文本拖动到另一个文本框中。当释放鼠标键时,这些文本将移动位置。同一技术在两个应用程序之间也可以工作——例如,可从Word文本中拖动一些文本,并放入到WPF应用程序的TextBox对象中,也可将文本从WPF应用程序的TextBox对象拖动到Word文档中。
有时,可能希望在两个未提供内置拖放功能的元素之间进行拖放。例如,可能希望允许用户将内容从文本框拖放到标签中;或者可能希望创建如下图所示的示例,该例允许用户从Label对象或TextBox对象拖动文本,并放到另一个标签中。对于这种情况,需要处理拖放事件。
拖放操作有两个方面:源和目标。为了创建拖放源,需要在某个位置调用DragDrop.DoDragDrop()方法来初始化拖放操作。此时确定拖放操作的源,搁置希望拖动的内容,并指明允许什么样的拖放效果(复制、移动等)。
通常,在响应MouseDown或PreivewMouseDown事件时调用DoDragDrop()方法。下面是一个示例,当单击标签时该例初始化拖放操作。标签中的文本内容用于拖放操作:
private void lblSource_MouseDown(object sender, MouseButtonEventArgs e) { Label lbl = (Label)sender; DragDrop.DoDragDrop(lbl, lbl.Content, DragDropEffects.Copy); }
接收数据的元素需要将它的AllowDrop属性设置为true。此外,它还需要通过处理Drop事件来处理数据:
<Label Grid.Row="1" Grid.ColumnSpan="2" Background="LightGoldenrodYellow" VerticalAlignment="Center" HorizontalAlignment="Center" Padding="20" AllowDrop="True" Drop="lblTarget_Drop" DragEnter="lblTarget_DragEnter">To this Label</Label>
将AllowDrop属性设置为true时,就将元素配置为允许任何类型的信息。如果希望有选择地接收内容,可处理DragEnter事件。这时,可以检查正在拖动的内容的数据类型,然后确定所允许的操作类型。下面的示例只允许文本内容——如果拖动的内容不能转换成文本,就不能允许执行拖动操作,鼠标指针会变成内部具有一条线的图像光标,表示禁止操作:
private void lblTarget_DragEnter(object sender, DragEventArgs e) { if (e.Data.GetDataPresent(DataFormats.Text)) e.Effects = DragDropEffects.Copy; else e.Effects = DragDropEffects.None; }
最后,当完成操作后就可以检索并处理数据了。下面的代码将拖放的文本插入标签中:
private void lblTarget_Drop(object sender, DragEventArgs e) { ((Label)sender).Content = e.Data.GetData(DataFormats.Text); }
可通过拖放操作交换任意类型的对象。然而,如果需要和其他应用程序通信,这种自由的方法尽管很完美,却是不明智的。如果希望将内容拖放到其他应用程序中,应当使用基本数据类型(如字符串、整型等),或者使用实现了ISerializable或IDataObject接口的对象(这两个接口允许.NET将对象转换成字节流,并在另一个应用程序域中重新构造对象)。一个有趣的技巧就是将WPF对象转换成XAML,并在其他地方重新构成该WPF对象。所需要的所有对象就是XamlWriter和XamlReader对象。
拖放功能的完整代码如下所示:
<Window x:Class="MouseEvents.DragAndDrop" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="DragAndDrop" Height="300" Width="300"> <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" DragEnter="lblTarget_DragEnter">To this Label</Label> </Grid> </Window>
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Shapes; namespace MouseEvents { /// <summary> /// DragAndDrop.xaml 的交互逻辑 /// </summary> public partial class DragAndDrop : 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; } } }