среда, 13 октября 2010 г.

Как правильно закрывать форму по Escape. Обработка диалоговых клавиш

Очень часто требуется сделать так, чтобы окно закрывалось по нажатию на клавишу 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. Ваша статья лишний раз подтверждает правильность действий) Спасибо)

vsmihaylovsky комментирует...

Весьма пользительная информация.

Анонимный комментирует...

В третьем варианте лучше использовать CMChildKey. В этом случае реакция на Escape будет даже в случае фокуса на TMemo, которое в свою очередь перехватывает события

Unknown комментирует...

у меня до сих пор есть такая проблема - есть модальное окно, которое должно закрыться ENTER-ом, но если поверх него открыть еще одно модальное окно, которое тоже должно закрываться ENTER-ом, то при закрытии второго окна, закроется и перное окно. Никак не смог "погасить" ENTER, после закрытия второго окна. Как это можно сделать? Так же с ESCAPE.

Николай Зверев комментирует...

Message.Result := 1 в обработчике CM_DIALOGKEY как раз это и делает ("гасит" дальнейшую обработку нажатия клавиши)

Отправить комментарий

.

.