TMemo vs. Esc key

If you have ever had a TMemo on a modal form with an OK and Cancel button, where the latter has its Cancel property set to true, you might have found that Esc does not cancel the dialog while the memo has got the focus.

According to David Heffernan that’s because the VCL tells it to use all keys but it then doesn’t handle Esc. David also provides a fix for this via an interposer class. While this works it means that you have to add this interposer to every form in your application.

If you are already using Andras Hausladen’s excellent VCL Fix Pack there is another option: Add David’s fix to the hook installed by InitContextMenuFix (works only for Delphi 6-2007) or add a special hook for the fix only (for later Delphi versions where the context menu bug has been fixed).

So far I have only done the first. Look for the code between the two “TMemo Esc Fix” comments

procedure TContextMenuFixWinControl.DefaultHandler(var Message);
type
  TDefHandler = procedure(Self: TControl; var Message);
begin
  if HandleAllocated then
  begin
    with TMessage(Message) do
    begin
      { Here was the WM_CONTEXTMENU Code that is not necessary because
        DefWndProc will send this message to the parent control. }

      { Keep the odd bahavior for grids because everybody seems to be used to it. }
      if (Msg = WM_CONTEXTMENU) and (Parent <> nil) and (Parent is TCustomGrid) then
      begin
        Result := Parent.Perform(Msg, WParam, LParam);
        if Result <> 0 then Exit;
      end;

      // Begin - TMemo Esc Fix
      if (Msg = WM_GETDLGCODE) and (Parent <> nil) and Self.InheritsFrom(TCustomMemo) then
      begin
        //inherited DefaultHandler(Message);
        TDefHandler(@TControl.DefaultHandler)(Self, Message);
        Result := Result and not DLGC_WANTALLKEYS;
        Exit;
      end;

      if (Msg = CM_WANTSPECIALKEY) and (Parent <> nil) and Self.InheritsFrom(TCustomMemo) then
      begin
        case TCMWantSpecialKey(Message).CharCode of
        VK_ESCAPE:
          begin
            Result := 0;
            Exit;
          end;
        VK_RETURN, VK_EXECUTE, VK_CANCEL:
          begin
            Result :=1;
            Exit;
          end;
        end;
      end;
      // End - TMemo Esc Fix

      case Msg of
        WM_CTLCOLORMSGBOX..WM_CTLCOLORSTATIC:
          Result := SendMessage(LParam, CN_BASE + Msg, WParam, LParam);
        CN_CTLCOLORMSGBOX..CN_CTLCOLORSTATIC:
          begin
            SetTextColor(WParam, ColorToRGB(Font.Color));
            SetBkColor(WParam, ColorToRGB(Brush.Color));
            Result := Brush.Handle;
          end;
      else
        if Msg = RM_GetObjectInstance then
          Result := LRESULT(Self)
        else
          Result := CallWindowProc(DefWndProc, Handle, Msg, WParam, LParam);
      end;
      if Msg = WM_SETTEXT then
        SendDockNotification(Msg, WParam, LParam);
    end;
  end
  else
    //inherited DefaultHandler(Message);
    TDefHandler(@TControl.DefaultHandler)(Self, Message);
end;

What it does is basically the same as David’s interposer class:

  1. It handles WM_GETDLGCODE by calling the inherited message handler (in this case: The original WindowProc) and removing the DLGC_WANTALLKEYS bit from the result.
  2. It handles CM_WANTSPECIALKEY, checks for VK_ESCAPE for which it sets Result to 0, meaning “I’m not interested in this key.”, and VK_RETURN, VK_EXECUTE, VK_CANCEL setting Result to 1 meaning “I’m interested in these keys.”.

Andy’s code hooks TWinControl.DefaultHandler so the code above gets called for all TWinControls, but we don’t want to meddle with the Esc key handling of other controls. There was a small problem with checking whether the control is actually a Memo. “Self is TMemo” did not work because TContextMenuFixWinControl.DefaultHandler is a class method, so the compiler thinks that Self is a class rather than a class instance and didn’t want to compile this code. Changing the condition to Self.InheritsFrom(TCustomMemo) did the trick.