Как TMemo питается ключом спасения, когда TEdit нет?

Я пытаюсь остановить элемент TMemo (а также TRichEdit) от еды Escape.

Если пользователь сфокусирован на TEdit, нажатие Escape вызовет форму, чтобы сделать то, что делает форма, когда пользователь нажимает кнопку escape. Если пользователь сфокусирован на TMemo, нажатие кнопки escape происходит через TMemo.

Конечно, я мог бы взломать:

procedure TForm1.Memo1KeyPress(Sender: TObject; var Key: Char);
begin
    if Key = #27 then
    begin
       //figure out how to send a key to the form
    end;
end;

Но это не идеально (я должен обрабатывать ключ-побег, а не позволять форме обрабатывать его).

Конечно, я мог бы взломать:

Form1.KeyPreview := True;

procedure TForm1.FormKeyPress(Sender: TObject; var Key: Char);
begin
   if Key = #27 then
   begin
      //Figure out how to invoke what the form was going to do when the user presses escape
   end;
end;

Но это не идеально (я должен обрабатывать ключ-побег, а не позволять форме обрабатывать его).

Итак, мы ответим на вопрос, а не на проблему

Вместо этого мы воспользуемся возможностью, чтобы что-то узнать. Каким образом TMemo даже получает событие keyPress, связанное с ключом эвакуации, когда TEdit не работает:

procedure TForm1.Edit1KeyPress(Sender: TObject; var Key: Char);
begin
    if Key = #27 then
    begin
        //never happens
    end;
end;

TEdit и TMemo - это тот же общий EDIT общий контроль Windows.

Почему escape обходит форму KeyPreview

Если я включу форму KeyPreview, и пользователь нажимает Escape, когда он сфокусирован в поле TEdit, и свойство кнопки Cancel установлено, форма закрывается и:

  • Событие Edit1.KeyPress не запускается
  • событие Form1.KeyPress не запускается

Если создано действие, чей Shortcut - Esc, то не возникает событие KeyPress, независимо от того, какой контроль он использует.

tl; dr: Где свойство TMemo.WantEscape?

Ответ 1

Поведение, которое вы наблюдаете, контролируется обработкой сообщения WM_GETDLGCODE. Для напоминания, которое выглядит так:

procedure TCustomMemo.WMGetDlgCode(var Message: TWMGetDlgCode);
begin
  inherited;
  if FWantTabs then Message.Result := Message.Result or DLGC_WANTTAB
  else Message.Result := Message.Result and not DLGC_WANTTAB;
  if not FWantReturns then
    Message.Result := Message.Result and not DLGC_WANTALLKEYS;
end;

Для управления редактированием VCL не реализует специальную обработку для WM_GETDLGCODE, а основной элемент управления Windows обрабатывает ее.

В стандартном приложении Win32 диспетчер диалоговых окон Windows отправляет сообщения WM_GETDLGCODE. Но Delphi не построен поверх диспетчера диалогов, поэтому VCL отвечает за отправку WM_GETDLGCODE. Это делается в обработчике CN_KEYDOWN. Код выглядит следующим образом:

Mask := 0;
case CharCode of
  VK_TAB:
    Mask := DLGC_WANTTAB;
  VK_LEFT, VK_RIGHT, VK_UP, VK_DOWN:
    Mask := DLGC_WANTARROWS;
  VK_RETURN, VK_EXECUTE, VK_ESCAPE, VK_CANCEL:
    Mask := DLGC_WANTALLKEYS;
end;
if (Mask <> 0) and
  (Perform(CM_WANTSPECIALKEY, CharCode, 0) = 0) and
  (Perform(WM_GETDLGCODE, 0, 0) and Mask = 0) and
  (GetParentForm(Self).Perform(CM_DIALOGKEY,
  CharCode, KeyData) <> 0) then Exit;

Обратите внимание, что VK_RETURN, VK_EXECUTE, VK_ESCAPE и VK_CANCEL все сгруппированы. Это означает, что элемент управления VCL должен решить, обрабатывать ли эти ключи самостоятельно или позволить форме обрабатывать их в своем обработчике CM_DIALOGKEY.

Как вы можете видеть из TCustomMemo.WMGetDlgCode, вы можете повлиять на этот выбор с помощью свойства WantReturns. Итак, вы можете убедить VCL, чтобы форма дескриптора формы ESC просто установила WantReturns в записке на False. Но это также останавливает нажатие клавиши ENTER на заметку и делает довольно сложным для пользователя заметки ввод новых строк. Они должны сделать это с помощью CTRL + ENTER.

Фактически WantReturns действительно должен был называться WantReturnsAndEscapesAndExecutesAndCtrlBreaks. Дизайнеры VCL могли реализовать свойство WantEscapes, но его просто нет.

Таким образом, вы сами так или иначе обрабатываете его. Лично я делаю это с помощью собственного производного контроля над записью. Он переопределяет метод KeyDown и делает следующее:

procedure TMyMemo.KeyDown(var Key: Word; Shift: TShiftState);
var
  Form: TCustomForm;
  Message: TCMDialogKey;
begin
  inherited;
  if (Key=VK_ESCAPE) and (Shift*[ssShift..ssCtrl])=[]) then begin
    Form := GetParentForm(Self);
    if Assigned(Form) then begin
      // we need to dispatch this key press to the form so that it can 'press' 
      // any buttons with Cancel=True
      Message.Msg := CM_DIALOGKEY;
      Message.CharCode := VK_ESCAPE;
      Message.KeyData := 0;
      Message.Result := 0;
      Form.Dispatch(Message);
    end;
  end;
end;

Другой способ добиться этого - обработать CM_WANTSPECIALKEY и WM_GETDLGCODE. Здесь грубый мешанин, который иллюстрирует технику:

type
  TMemo = class(StdCtrls.TMemo)
  protected
    procedure CMWantSpecialKey(var Msg: TCMWantSpecialKey); message CM_WANTSPECIALKEY;
    procedure WMGetDlgCode(var Message: TWMGetDlgCode); message WM_GETDLGCODE;
  end;

procedure TMemo.CMWantSpecialKey(var Msg: TCMWantSpecialKey);
begin
  case Msg.CharCode of
  VK_ESCAPE:
    Msg.Result := 0;
  VK_RETURN, VK_EXECUTE, VK_CANCEL:
    Msg.Result := 1;
  else
    inherited;
  end;
end;

procedure TMemo.WMGetDlgCode(var Message: TWMGetDlgCode);
begin
  inherited;
  Message.Result := Message.Result and not DLGC_WANTALLKEYS;
end;