【WPF学习】第十六章 键盘输入
当用户按下键盘上的一个键时,就会发生一系列事件。下表根据他们的发生顺序列出了这些事件:
表 所有元素的键盘事件(按顺序)
键盘处理永远不会像上面看到的这么简单。一些控件可能会挂起这些事件中的某些事件,从而可执行自己更特殊的键盘处理。最明显的例子是TextBox控件,它挂起了TextInput事件。对于一些按键,TextBox控件还挂起了KeyDown事件,如方向键。对于此类情形,通常仍可使用隧道路由事件(PreviewTextInput和PreviewKeyDown事件).
TextBox控件还添加了名为TextChanged的新事件。在按键导致文本框中的文本发生改变之后立即引发该事件。这时,在文本框中已经可以看到新的文本,所以阻止不需要的按键已为时太晚。
一、处理按键事件
理解键盘事件的最好方式是使用简单的示例程序,如下图所示。该例在一个文本框中监视所有可能的键盘事件,并在发生时给出报告。下图显示了文本框中输入大写A键时结果。
上面示例的完整代码如下所示:
<Window x:Class="KeyEvents.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="KeyPressEvents" Height="350" Width="468.421"> <Grid Margin="3"> <Grid.RowDefinitions> <RowDefinition Height="Auto"></RowDefinition> <RowDefinition Height="*"></RowDefinition> <RowDefinition Height="Auto"></RowDefinition> <RowDefinition Height="Auto"></RowDefinition> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"></ColumnDefinition> <ColumnDefinition Width="*"></ColumnDefinition> </Grid.ColumnDefinitions> <Label Grid.Row="0" Grid.Column="0">Type Here:</Label> <TextBox Grid.Row="0" Grid.Column="1" PreviewKeyDown="KeyEvent" KeyDown="KeyEvent" PreviewKeyUp="KeyEvent" KeyUp="KeyEvent" PreviewTextInput="TextInput" TextInput="TextInput"></TextBox> <ListBox Grid.ColumnSpan="2" Grid.Row="1" Grid.Column="0" Margin="5" Name="lstMessages"></ListBox> <CheckBox Name="chkHandle" Margin="5" Grid.ColumnSpan="2" Grid.Row="2">Ignore Keys Events</CheckBox> <Button Grid.Row="3" Margin="5" Padding="3" HorizontalAlignment="Right" Grid.ColumnSpan="2" Name="cmdClear" Click="cmdClear_Click">Clear list</Button> </Grid> </Window>
KeyEvents.XAML
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 KeyEvents { /// <summary> /// MainWindow.xaml 的交互逻辑 /// </summary> public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } private void KeyEvent(object sender, KeyEventArgs e) { if ((bool)chkHandle.IsChecked && e.IsRepeat) return; string message = "Event:" + e.RoutedEvent + " Key:" + e.Key; this.lstMessages.Items.Add(message); } private void TextInput(object sender, TextCompositionEventArgs e) { string message = "Event:" + e.RoutedEvent + " Text:" + e.Text; this.lstMessages.Items.Add(message); } private void cmdClear_Click(object sender, RoutedEventArgs e) { this.lstMessages.Items.Clear(); } } }
KeyEvents.cs
该例演示了非常重要的一点。每次按下一个键时,都会触发PreviewKeyDown和PreviewKeyUp事件。但只有当字符可以“输入”到元素中时,才会触发TextInput事件。这一动作实际上可能涉及多个按键操作。从上图可知,为得到大写字母A,需要按下两个键。首先,按下Shift键,按着按下A键。因此,分别看到两个KeyDown和KeyUp事件,但只有一个TextInput事件。
PreviewKeyDown、KeyDown、PreviewKeyUp和KeyUp事件都通过KeyEventArgs对象提供了相同的信息。最重要的信息是Key属性,该属性返回一个System.Windows.Input.Key枚举值,该枚举值标识了按下或释放的键。下面是上图处理键盘事件的事件处理程序:
private void KeyEvent(object sender, KeyEventArgs e) { if ((bool)chkHandle.IsChecked && e.IsRepeat) return; string message = "Event:" + e.RoutedEvent + " Key:" + e.Key; this.lstMessages.Items.Add(message); }
Key值没有考虑任何其他键的状态。例如,当按下A键时不必关心当前是否按下了Shift键,不管是否按下了Shift键都会得到相同的Key值(Key.A).
这里还存在一个问题。根据Windows键盘的设置,持续按下一个键一段时间,会重复引发按键事件。例如,保持按下A键,显然会在文本框中输入一系列A字符。同样,按下Shift键一段时间也会得到多个按键和一系列KeyDown事件。按下Shift+A键进行测试的真实情况是,文本框实际上会为Shift键引发一系列KeyDown事件,然后为A键引发KeyDown事件,随后是TextInput事件(对于文本框,是TextChanged事件),最后是为Shift键和A键引发KeyUp事件。如果希望忽略这些重复的Shift键,可以通过检查KeyEventArgs.IsRepeat属性,确定按键是不是因为按住键导致的结果,如下所示:
if ((bool)chkHandle.IsChecked && e.IsRepeat) return;
KeyDown事件发生后,接着发生PreviewTextInput事件(因为TextBox控件挂起了TextInput事件,所以不会发生TextInput事件)。此时,文本尚未出现在控件中。
TextInput事件使用TextCompositionEventArgs对象提供代码。该对象包含Text属性,该属性提供了处理过的文本,它们是控件即将接受到得文本。下面的代码将这些文本添加到上图所示的列表中:
private void TextInput(object sender, TextCompositionEventArgs e) { string message = "Event:" + e.RoutedEvent + " Text:" + e.Text; this.lstMessages.Items.Add(message); }
理想情况下,可在控件(如TextBox控件)中使用PreviewTextInput事件执行验证工作。例如,如果构建只能输入数字的文本框,可确保当前按键不是字母,如果是就设置Handled标志。可惜,对于某些可能希望处理的键不会触发PreviewTextInput事件。例如,如果在文本框中按下了空格键,将直接绕过PreviewTextInput事件,这意味着还需要处理PreviewKeyDown事件。
但在PreviewKeyDown事件处理程序中编写出可靠的验证逻辑是比较困难的,因为在此只知道Key值,这是级别很低的信息。例如,Key枚举区分数字键盘和普通键盘字母以上的数字键。这意味着根据按下数字9的方式,可能得到的值Key.D9或Key.NumPad9.验证所有这些允许使用的键值至少可以说是非常枯燥的。
一种选择是使用KeyConverter类将Key值转换为更有用的字符串。例如,使用KeyConverter.ConverterToString()方法,Key.D9和Key.NumPad9都返回字符串“9”。如果只使用Key.ToString()方法,将得到不那么有用的枚举名称(D9或NumPad9):
KeyConverter converter=new KeyConverter(); string key=converter.ConverterToString(e.key);
然而,即使使用KeyConverter类也存在缺陷,因为对于不会产生文本输入的按键,会得到更长一点的文本(如Backspace).
最好同事处理PreviewTextInput事件(该事件负责大多数验证)和PreviewKeyDown事件,PreviewKeyDown用于那些在文本框中不会引发PreviewTextInput事件的按钮(例如空格键)。下面是完成这一工作的简单解决方案:
private void pnl_PreviewTextInput(object sender,TextCompositionEventArgs e) { short val; if(!Int16.TryParse(e.Text,out val)) { //Disallow non-numeric key presses. e.Handled=true; } } private void pnl_PreviewKeyDown(object sender,KeyEventArgs e) { if(e.Key==Key.Space) { // Disallow the space key,which doesn\'t raise a PreviewTextInput event. e.Handled=true; } }
可将这些事件处理程序关联到单个文本框,或在更高层次的容器(例如,包含几个只允许输入数字的文本框的StackPanel面板)中关联他们,这样做效率更高。
二、焦点
在Windows世界中,用户每次只能使用一个控件。当前接受用户按键的控件时具有焦点控件。有时,有焦点的控件的外观不同。例如,WPF按钮使用蓝色阴影显示它具有焦点。
为让控件能接受焦点,必须将Focusable属性设置为true,这是所有控件的默认值。
有趣的是,Focusable属性是在UIElement类中定义的,这意味着其他非控件元素也可以获得焦点。通常,对于非控件类,Focusable属性默认设置为false,但也可以设置为true。例如,使用布局容器(如StackPanel面板)测试这一点——当它获得焦点时,会在面板边缘的周围显示一条点划线边框。
为将焦点从一个元素移到另一个元素,用户可单击鼠标或使用Tab键和方向键。以前的开发框架强制编程人员确保Tab键以合理方式移动焦点(通常是从左项右,然后从上到下),并且确保在窗口第一次显示时正确的控件获得焦点。在WPF中,不必在完成这些额外工作,因为WPF使用层次结构的元素布局实现了Tab键切换焦点的顺序。本质上,按下Tab键会将焦点移到当前元素的第一个子元素,如果当前元素没有子元素,会将焦点移到同级的下一个子元素。例如,如果在具有两个StackPanel面板容器的窗口中使用Tab键转移焦点,焦点首先会通过第一个StackPanel面板中的所有控件,然后通过第二个StackPanel面板中的所有控件。
如果希望获得控制使用Tab键转移焦点顺序的功能,可按数字顺序设置每个控件的TabIndex属性。Tablndex属性为0的控件首先获得焦点,然后是次高的TabIndex值(例如首先是1,然后是2、3、4…等等)。如果多个元素具有相同的TabIndex值,WPF就使用自动Tab顺序,这意味着会跳过随后最靠近的元素。
TabIndex属性是在Control类中定义的,在该类中还定义了IsTabStop属性。可通过将IsTabStop属性设置为false来阻止控件被包含进Tab键焦点顺序。IsTabStop属性和Focusable属性之间的区别在于,如果控件的IsTabStop属性被设置为false,控件仍可通过其他方式获得焦点——通过编程(使用代码调用Focus()方法)或通过鼠标单击。
不可见或禁用的控件(“变灰的控件”)通常会忽略Tab键焦点顺序,并且不能被激活,不管TabIndex属性、IsTabStop属性以及Focusable属性如何设置。为了隐藏或禁用某个控件,可分别设置Visibility属性和IsEnabled属性。
三、获取键盘状态
当发生按键事件时,经常需要知道更多信息,而不仅要知道按下的是那个键。而且确定其他键是否同事被按下了也非常重要。这意味着可能需要检查其他键的状态,特别是Shift、Ctrl和Alt等修饰键。
对于键盘事件(PreviewKeyDown、KeyDown、PreviewKeyUp和KeyUp),获取这些信息比较容易。首先,KeyEventArgs对象包含KeyStates属性,该属性反映触发事件的键的属性。更有用的是,KeyboardDevice属性为键盘上的所有键提供了相同的信息。
自然,KeyboardDevice属性提供了KeyboardDevice类的一个实例。它的属性包含当前是哪个元素具有焦点(FocusedElement)以及当事件发生时按下了哪些修饰键。修饰键包括Shift、Ctrl和Alt键,并且可使用位逻辑来检查他们的状态。如下所示:
if((e.KeyboardDevice.Modifiers&ModifiersKeys.Control)==ModifierKeys.Control) { lblInfo.Text="You held the Control Key."; }
KeyboardDevice属性还提供了几个简便方法,这些方法在下表中列出。对于这些方法中的每个方法,需要传递一个Key枚举值。
表 KeyboardDevice属性提供的方法
当使用KeyEventArgs.KeyboardDevice属性时,代码获取虚拟键状态(virtual key state)。这意味着获取在事件发生时键盘的状态,这些状态和键盘的当前状态未必相同。例如,分析一下当用户输入速度超出代码执行速度时会发生什么情况?每次引发KeyPress事件时,都将访问触发事件的按键,而不是刚输入的字符。这几乎总是想得到的行为。
然而,没有限制在键盘事件中获取键的信息,也可以随时获取键盘状态信息。技巧是使用Keyboard类,该类和KeyboardDevice类非常类似,只是Keyboard类由静态成员构成。下面的例子使用Keyboard类检查左边Shift键的当前状态:
if(Keyboard.IsKeyDown(Key.LeftShift)) { lblInfo.Text="The left Shift is held down."; }