【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.";
}

 

posted @ 2020-01-28 17:12  Peter.Luo  阅读(2783)  评论(0编辑  收藏  举报