Очень часто требуется сделать так, чтобы окно закрывалось по нажатию на клавишу Escape. Это действительно удобно. Более того, есть негласное правильно: интерфейсы ввода данных должны уметь работать и без мыши. Т.е. чтобы после ввода данных с клавиатуры можно было нажать Enter или Escape, а не тянуться за мышкой и потом целиться курсором в маленький крестик.
Возможно Вам покажется, что тема избитая и этот вопрос может волновать лишь новичков. Однако даже опытные программисты не всегда знают, как это делать правильно. В этой заметке я также расскажу, как правильно обрабатывать и другие диалоговые клавиши.
Итак. Приведу несколько вариантов.
Вариант первый.
Если у вас на форме есть кнопки (TButton), то можно у одной из кнопок выставить свойство: Cancel := True. Когда пользователь нажмёт на клавишу Escape, сработает обработчик OnClick этой кнопки, в котором можно просто вызвать метод Close формы.
Для модальной формы всё ещё проще: вместо обработчика OnClick достаточно указать свойство ModalResult := mrCancel. После попытки вызова OnClick кнопки, VCL смотрит это свойство, и если оно отлично от нуля (<> mrNone), то прописывает его в ModalResult формы, что приводит к закрытию модальной формы.
Этот вариант правильный, но он требует наличия дополнительных кнопок на форме, что не всегда удобно.
Вариант второй.
К этому варианту я отнесу все способы перехвата нажатия любой клавиши на уровне формы. Для этого надо у формы выставить свойство KeyPreview := True, и прописать обработчик OnKeyPress:
procedure TForm.FormKeyPress(Sender: TObject; var Key: Char); begin if Key = #27 then // VK_ESCAPE Close; end;
либо обаботчик OnKeyDown. Либо OnKeyUp.
Как видите, этот вариант довольно простой, и самый распространённый (который можно найти на просторах интернета). Но этот вариант не совсем корректный. Чтобы это показать, проделаем ещё несколько действий с формой.
Создайте на форме обычный комбобокс (TCombobox), заполните его Items произвольными значениями. Запустите приложение, откройте форму (с комбобоксом и обработчиком OnKeyPress). Теперь раскройте выпадающий список и нажмите Escape. Что произошло? Правильно, форма закрылась. Хотя я более чем уверен, что пользователь в этот момент времени ожидал другое поведение на нажатие Escape. А именно: по первому нажатию - закрытие комбобокса, а уже по второму нажатию - закрытие формы. (Замечу, что кроме комбобокса на форме могут оказаться и другие компоненты, которые по-своему обрабатывают клавишу Escape.)
Это происходит потому, что обработчик формы OnKeyPress отработал раньше, чем комбобокс получил событие о нажатии на Escape (помните, KeyPreview выставлен в True?). Если KeyPreview сбросить в False, то OnKeyPress формы вообще не обработается.
Так как же правильно обрабатывать клавишу Escape?
В этой заметке я несколько раз упомянул словосочетание "диалоговые клавиши". К этим клавишам относятся: Escape, Enter, Tab и стрелки (и ещё несколько других клавиш нестандартных клавиатур). Называются они так, потому что эти клавиши специальные. Они не предназначены для непосредственного ввода данных, а используются для управления окнами (комбобокс - это тоже окно).
Для обработки диалоговых клавиш в VCL используется сообщение CM_DIALOGKEY. Это сообщение сначала приходит текущему контролу (т.е. тому, который в данный момент находится в фокусе), а затем (до тех пор, пока оно не обработается, т.е. пока Result = 0) - родительскими контролами (от текущего до уровня формы). Если CM_DIALOGKEY не было обработано, то срабатывает OnKeyDown текущего контрола.
Во втором примере, чтобы отловить Escape на уровне формы, мы выставили KeyPreview. Это свойство ломает описанную логику: все сообщения от клавиатуры сначала обрабатываются формой, а затем уже приходят к котролам.
Кому интересно, может поизучать исходники VCL, я же приведу третий вариант.
Вариант третий. Универсальный.
TForm1 = class(TForm) .. private procedure CMDialogKey(var Message: TCMDialogKey); message CM_DIALOGKEY; .. procedure TForm1.CMDialogKey(var Message: TCMDialogKey); begin with Message do if (CharCode = VK_ESCAPE) and // была нажата клавиша Escape (KeyDataToShiftState(KeyData) = []) then // сдвиговые клавиши не тронуты begin // для модального окна - скажем Cancel if fsModal in FormState then begin ModalResult := mrCancel; Result := 1; end // для обычного - пошлём команду на закрытие else Result := Integer(PostMessage(Handle, WM_CLOSE, 0, 0)); if Result <> 0 then Exit; end; inherited; end;
CM_DIALOGKEY также следует обрабатывать и для остальных диалоговых клавиш. Приведу типичный пример: на форме есть поле ввода (SomeEdit: TEdit) и таблица. По нажатию на Enter в SomeEdit, пользователь ожидает некой реакции (например фильтрация данных в таблице). Однако, если форма модальная и на ней есть кнопка "OK" (у которой выставлено свойство Default := True и ModalResult := mrOk), то сообщение о нажатии на Enter до SomeEdit дойти не успеет (сработает Click кнопки и модальная форма закроется). В этом случае можно написать такой обработчик:
procedure TForm1.CMDialogKey(var Message: TCMDialogKey); begin // обработка клавиши Enter необходима на этом уровне. До контролов сообщения // о нажатии диалоговых клавиш могут не дойти // (если, например, на форме есть Default-кнопка) if (Message.CharCode = VK_RETURN) and SomeEdit.Focused then begin .. ApplyFilter .. Message.Result := 1; end else inherited; end;
Ну вобщем-то и всё…
9 коммент.:
Спасибо, очень интересная статья.
+1
А еще удобно окна закрывать по ALT+F4
Очень любопытно, не знал про CM_DIALOGKEY. Спасибо!
тоже раньше сталкивался с подобной проблемой (правильно отработать ESC и ENTER в падающих списках) и ковыряя VCL нарыл CM_DIALOGKEY. Ваша статья лишний раз подтверждает правильность действий) Спасибо)
Весьма пользительная информация.
В третьем варианте лучше использовать CMChildKey. В этом случае реакция на Escape будет даже в случае фокуса на TMemo, которое в свою очередь перехватывает события
у меня до сих пор есть такая проблема - есть модальное окно, которое должно закрыться ENTER-ом, но если поверх него открыть еще одно модальное окно, которое тоже должно закрываться ENTER-ом, то при закрытии второго окна, закроется и перное окно. Никак не смог "погасить" ENTER, после закрытия второго окна. Как это можно сделать? Так же с ESCAPE.
Message.Result := 1 в обработчике CM_DIALOGKEY как раз это и делает ("гасит" дальнейшую обработку нажатия клавиши)
Отправить комментарий