События ввода с клавиатуры

50

Когда пользователь нажимает клавишу, возникает целая серия событий. В таблице эти события перечислены в порядке их возникновения:

Хронология возникновения событий
Имя Тип маршрутизации Описание
PreviewKeyDown Туннелирование Возникает при нажатии клавиши.
KeyDown Пузырьковое распространение То же
PreviewTextInput Туннелирование Возникает, когда нажатие клавиши завершено, и элемент получает текстовый ввод. Это событие не возникает для тех клавиш, которые не "печатают" символы (например, оно не возникает при нажатии клавиш <Ctrl>, <Shift>, <Backspace>, клавиш управления курсором, функциональных клавиш и т.д.)
TextInput Пузырьковое распространение То же
PreviewKeyUp Туннелирование Возникает при отпускании клавиши
KeyUp Пузырьковое распространение То же

Обработка событий клавиатуры отнюдь не так легка, как это может показаться. Некоторые элементы управления могут блокировать часть этих событий, чтобы выполнять свою собственную обработку клавиатуры. Наиболее ярким примером является элемент TextBox, который блокирует событие TextInput, а также событие KeyDown для нажатия некоторых клавиш, таких как клавиши управления курсором. В подобных случаях обычно все-таки можно использовать туннелируемые события (PreviewTextlnput и PreviewKeyDown).

Элемент TextBox добавляет одно новое событие — TextChanged. Это событие возникает сразу после того, как нажатие клавиши приводит к изменению текста в текстовом поле. Однако в этот момент новый текст уже видим в текстовом поле, потому отменять нежелательное нажатие клавиши уже поздно.

Обработка нажатия клавиши

Понять, как работают и используются события клавиатуры, лучше всего на примере. Ниже представлен пример программы, которая отслеживает и протоколирует все возможные нажатия клавиш, когда в фокусе находится текстовое поле. В данном случае показан результат ввода заглавной буквы S.

Этот пример демонстрирует один важный момент. События PreviewKeyDown и KeyDown возникают при каждом нажатии клавиши. Однако событие TextInput возникает только тогда, когда в элементе был "введен" символ. На самом деле это может означать нажатие многих клавиш. В примере, нужно нажать две клавиши, чтобы получить заглавную букву S: сначала клавишу <Shift>, а затем клавишу <S>. В результате получаются по два события KeyDown и KeyUp, но только одно событие TextInput.

<Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"></RowDefinition>
            <RowDefinition Height="*"></RowDefinition>
            <RowDefinition Height="auto"></RowDefinition>
            <RowDefinition Height="auto"></RowDefinition>
        </Grid.RowDefinitions>
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="auto"></ColumnDefinition>
                <ColumnDefinition Width="*"></ColumnDefinition>
            </Grid.ColumnDefinitions>
            <Label Margin="3" Grid.Column="0">Введите букву: </Label>
            <TextBox MinWidth="100" HorizontalAlignment="Stretch" Grid.Column="1"
                     Margin="3" Padding="3" KeyDown="KeyEvents" KeyUp="KeyEvents"
                     PreviewTextInput="TextInputEvent" Name="txtContent"></TextBox>
        </Grid>
        <ListBox Grid.Row="1" Margin="3" BorderBrush="LightBlue"
                 BorderThickness="3" Padding="3" Name="lbxEvents"></ListBox>
        <CheckBox Grid.Row="2" Margin="3" Name="chkIgnoreRepeat">
            Игнорировать повторное нажатие символов</CheckBox>
        <Button Grid.Row="3" Margin="3" Padding="3" HorizontalAlignment="Right" Name="Clear"
                Click="Clear_Click">Очистить</Button>
</Grid>
public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void Clear_Click(object sender, RoutedEventArgs e)
        {
            lbxEvents.Items.Clear();
            txtContent.Clear();
            i = 0;
        }

        protected int i = 0;
        private void KeyEvents(object sender, KeyEventArgs e)
        {
            if ((bool)chkIgnoreRepeat.IsChecked && e.IsRepeat) return;
            i++;
            string s = "Event" + i + ": " + e.RoutedEvent + " Клавиша: " + e.Key;
            lbxEvents.Items.Add(s);
        }

        private void TextInputEvent(object sender, TextCompositionEventArgs e)
        {
            i++;
            string s = "Event" + i + ": " + e.RoutedEvent + " Клавиша: " + e.Text;
            lbxEvents.Items.Add(s);
        }
}
Наблюдение за клавиатурой

Каждое из событий PreviewKeyDown, KeyDown, PreviewKey и KeyUp передает объекту KeyEventArgs одну и ту же информацию. Самой важной ее частью является свойство Key, которое возвращает значение из перечисления System.Windows.Input.Key и идентифицирует нажатую или отпущенную клавишу.

Значение Key не учитывает состояние любой другой клавиши — например, была ли прижата клавиша <Shift> в момент нажатия <S>; в любом случае вы получите одно и то же значение Key (Key.S).

Здесь присутствует одна сложность. В зависимости от настройки клавиатуры в Windows, удержание клавиши в прижатом состоянии приводит к повторам нажатия после короткого промежутка времени. Например, прижатие клавиши <S> приведет к вводу в текстовое поле целой серии символов S. Точно так же прижатие клавиши <Shift> приводит к повторам нажатия и возникновению серии событий KeyDown. В реальном примере при нажатии комбинации <Shift+S> текстовое поле сгенерирует серию событий KeyDown для клавиши <Shift>, потом событие KeyDown для клавиши <S>, событие TextInput (или событие TextChanged в случае текстового поля), а затем событие KeyUp для клавиш <Shift> и <S>. Если нужно игнорировать повторы нажатия клавиши <S>, то можно проверить, является ли нажатие результатом прижатия клавиши, с помощью свойства KeyEventArgs.IsRepeat.

События PreviewKeyDown, KeyDown, PreviewKey и KeyUp больше подходят для написания низкоуровневого кода обработки ввода с клавиатуры (что редко бывает нужно — разве что в пользовательских элементах управления) и обработки нажатий специальных клавиш (например, функциональных).

За событием KeyDown следует событие PreviewTextInput. (Событие TextInput не возникает, поскольку элемент TextBox блокирует его.) В этот момент текст еще не отображается в элементе управления.

Событие TextInput обеспечивает код объекта TextCompositionEventArgs. Этот объект содержит свойство Text, которое дает обработанный текст, подготовленный к передаче элементу управления.

В идеале событие PreviewTextInput можно было бы использовать для выполнения проверки в элементах управления наподобие TextBox. Например, если вы создаете текстовое поле для ввода только чисел, можно проверить, не была ли введена при текущем нажатии клавиши буква, и установить флаг Handled, если это так. Увы — событие PreviewTextIlnput не генерируется для некоторых клавиш, которые бывает нужно обрабатывать. Например, при нажатии клавиши пробела в текстовом поле событие PreviewTextInput вообще пропускается. Это означает, что придется обрабатывать также событие PreviewKeyDown.

К сожалению, трудно реализовать надежную логику проверки данных в обработчике события PreviewKeyDown, т.к. в наличии имеется только значение Key, а это слишком низкоуровневый фрагмент информации. Например, в перечислении Key различаются клавиши цифровой клавиатуры (блок, предназначенный для ввода только цифр) и обычной клавиатуры. Это означает, что в зависимости от того, где нажата клавиша с цифрой 9, вы получите или значение Key.D9, или значение Key.NumPad9. Проверка всех допустимых значений как минимум очень утомительна.

Одним из выходов является использование класса KeyConverter, который позволяет преобразовать значение Key в более полезную строку. Например, вызов функции KeyConverter.ConvertToString() с любым из значений Key.D9 и Key.NumPad9 возвращает строковый результат "9". Вызов преобразования Key.ToString() дает менее полезное имя перечисления (либо "D9", либо "NumPad9"):

KeyConverter converter = new KeyConverter();
   string key = converter.ConvertToString(e.Key);

Однако использовать KeyConverter тоже не очень удобно, поскольку приходится обрабатывать длинные строки (например, "Backspace") для тех нажатий клавиш, которые не приводят к вводу текста.

Наиболее подходящим вариантом является обработка события PreviewTextlnput (где выполняется большая часть проверки) в сочетании с событием PreviewKeyDown для нажатий тех клавиш, которые не генерируют событие PreviewTextInput в текстовом поле (например, пробела).

Пройди тесты
Лучший чат для C# программистов