Is there a way to have a KeyPreview-like functionality when working with Frames?

2.6k views Asked by At

I would like to have a KeyPreview functionality within Frames, I mean, that when the input (say, one of the controls of the frame is selected, or the mouse is inside) is in a frame (which would have several panels and other controls) then the keys pressed by the user are first processed by the frame.

Is there a way to do this? I haven't found a property similar to KeyPreview in TFrame.

I'm using version XE5 of RAD Studio, altough I mostly work with C++Builder.

3

There are 3 answers

6
NGLN On BEST ANSWER

Thanks to my recent "When does a ShortCut fire"-investigation, I have worked out a stand alone solution for your Frame.

In short: all key messages enter in TWinControl.CNKeyDwon of the active control. That method calls TWinControl.IsMenuKey which traverses all parents while determining whether the message is a ShortCut. Is does so by calling its GetPopupMenu.IsShortCut method. I have overridden the Frame's GetPopupMenu method by creating one if it is not present. Note that at all time you still can add a PopupMenu to the Frame yourself. By subclassing TPopupMenu and overriding the IsShortCut method, the Frame's KeyDown method is called, which serves as the KeyPreview functionality you require. (I could also have assigned the OnKeyDdown event handler).

unit Unit2;

interface

uses
  Winapi.Messages, System.Classes, Vcl.Controls, Vcl.Forms, Vcl.Menus,
  Vcl.StdCtrls;

type
  TPopupMenu = class(Vcl.Menus.TPopupMenu)
  public
    function IsShortCut(var Message: TWMKey): Boolean; override;
  end;

  TFrame2 = class(TFrame)
    Label1: TLabel;
    Edit1: TEdit;
  private
    FPreviewPopup: TPopupMenu;
  protected
    function GetPopupMenu: Vcl.Menus.TPopupMenu; override;
    procedure KeyDown(var Key: Word; Shift: TShiftState); override;
  end;

implementation

{$R *.dfm}

{ TPopupMenu }

function TPopupMenu.IsShortCut(var Message: TWMKey): Boolean;
var
  ShiftState: TShiftState;
begin
  ShiftState := KeyDataToShiftState(Message.KeyData);
  TFrame2(Owner).KeyDown(Message.CharCode, ShiftState);
  Result := Message.CharCode = 0;
  if not Result then
    Result := inherited IsShortCut(Message);
end;

{ TFrame2 }

function TFrame2.GetPopupMenu: Vcl.Menus.TPopupMenu;
begin
  Result := inherited GetPopUpMenu;
  if Result = nil then
  begin
    if FPreviewPopup = nil then
      FPreviewPopup := TPopupMenu.Create(Self);
    Result := FPreviewPopup;
  end;
end;

procedure TFrame2.KeyDown(var Key: Word; Shift: TShiftState);
begin
  if (Key = Ord('X')) and (ssCtrl in Shift) then
  begin
    Label1.Caption := 'OH NO, DON''T DO THAT!';
    Key := 0;
  end;
end;

end.
3
SilverWarior On

If you only have one frame on the form at the time you could make use of forms KeyPreview ability and forward the necessary information to the frame.

If you are only forwarding the information you don't need to make any changes to original VCL code just make a modified TFrame class. So there is no wory that you might break whole VCL doing it.

Here is a quick code example:

MainForm code:

unit Unit2;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Unit3, Vcl.ExtCtrls, Vcl.StdCtrls;

type
  TForm2 = class(TForm)
    Panel1: TPanel;
    ModifiedFrame: TModifiedFrame;
    Edit1: TEdit;
    procedure FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
    procedure FormCreate(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form2: TForm2;

implementation

{$R *.dfm}

procedure TForm2.FormCreate(Sender: TObject);
begin
  //This is required since I'm asigning frames OnKeyDown event method manually
  ModifiedFrame.OnKeyDown := ModifiedFrame.FrameKeyDown;
end;

procedure TForm2.FormKeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
begin
  //Forward key down information to ModifiedFrame
  ModifiedFrame.DoKeyDown(Sender, Key, Shift);
  if Key = 0 then
    MessageDlg('Key was handled by the modified frame!',mtInformation,[mbOK],0)
  else
    MessageDlg('Key was not handled!',mtInformation,[mbOK],0);
end;

end.

ModifiedFrame code:

unit Unit3;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,
  Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;

type
  TModifiedFrame = class(TFrame)
    Edit1: TEdit;
    //Normally this method would be added by the Delphi IDE when you set the
    //OnKeyDown event but here I created this manually in order to avoid crating
    //design package with modified frame
    procedure FrameKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
  private
    { Private declarations }
    FOnKeyDown: TKeyEvent;
  public
    { Public declarations }
    //This is used to recieve forwarded key down information from the Form
    procedure DoKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
  published
    //Property to alow setting the OnKeyDown event at design-time
    //NOTE: In order for this to work properly you have to put this modified
    //frame class into separate unti and register it as new design time component
    property OnKeyDown: TKeyEvent read FOnKeyDown write FOnKeyDown;
  end;

implementation

{$R *.dfm}

procedure TModifiedFrame.DoKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
  //Check to see if OnKeyDownEvent has been assigned. If it is foward the key down
  //information to the event procedure
  if Assigned(FOnKeyDown) then FOnKeyDown(Self, Key, Shift);
end;

procedure TModifiedFrame.FrameKeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
begin
  //Do something
  if Key = VK_RETURN then
  begin
    MessageBeep(0);
    Key := 0;
  end;
end;

end.

Using simillar approach you can forward other key events.

9
Dalija Prasnikar On

It is doable, if you are willing to change VCL code.

KeyPreview is handled in TWinControl.DoKeyDown method. As it can be seen from the code control that has focus will lookup its parent form and invoke its DoKeyDown method if KeyPreview is turned on.

function TWinControl.DoKeyDown(var Message: TWMKey): Boolean;
var
  ShiftState: TShiftState;
  Form, FormParent: TCustomForm;
  LCharCode: Word;
begin
  Result := True;

 // Insert modification here

  { First give the immediate parent form a try at the Message }
  Form := GetParentForm(Self, False);
  if (Form <> nil) and (Form <> Self) then
  begin
    if Form.KeyPreview and TWinControl(Form).DoKeyDown(Message) then
      Exit;
    { If that didn't work, see if that Form has a parent (ie: it is docked) }
    if Form.Parent <> nil then
    begin
      FormParent := GetParentForm(Form);
      if (FormParent <> nil) and (FormParent <> Form) and
      FormParent.KeyPreview and TWinControl(FormParent).DoKeyDown(Message) then
        Exit;
    end;
  end;
  with Message do
  begin
    ShiftState := KeyDataToShiftState(KeyData);
    if not (csNoStdEvents in ControlStyle) then
    begin
      LCharCode := CharCode;
      KeyDown(LCharCode, ShiftState);
      CharCode := LCharCode;
      if LCharCode = 0 then Exit;
    end;
  end;
  Result := False;
end;

To change that behavior, you would need to either change TWinControl.DoKeyDown code to scan for frames too or intercept WM_KEYDOWN and WM_SYSKEYDOWN for every TWinControl descendant you want to use, and finally add KeyPreview field to base Frame class.

Probably best option would be to declare IKeyPreview interface and when scanning for parent forms/frames test if parent implements that interface. If none found, you can fall back to original code. That would contain VCL code changes only to TWinControl.DoKeyDown method, and you can easily implement interface in Frames where needed.

Note: On Windows control that has focus receives key events. So above modifications would be able to find frame only if some of its controls has focus. Whether or not mouse would be over the frame or not would not have any influence on functionality.

More detailed code would look like this:

Interface definition that Frame would have to implement:

  IKeyPreview = interface
    ['{D7318B16-04FF-43BE-8E99-6BE8663827EE}']
    function GetKeyPreview: boolean;
    property KeyPreview: boolean read GetKeyPreview;
  end;

Function for finding parent frame that implements IKeyPreview interface, should be put somewhere in Vcl.Controls implementation section:

function GetParentKeyPreview(Control: TWinControl): IKeyPreview;
var
  Parent: TWinControl;
begin
  Result := nil;
  Parent := Control.Parent;
  while Assigned(Parent) do
    begin
      if Parent is TCustomForm then Parent := nil
      else
      if Supports(Parent, IKeyPreview, Result) then Parent := nil
      else Parent := Parent.Parent;
    end;
end;

TWinControl.DoKeyDown modification (insert in above original code):

var
  PreviewParent: IKeyPreview;

  PreviewParent := GetParentKeyPreview(Self);
  if PreviewParent <> nil then
    begin
      if PreviewParent.KeyPreview and TWinControl(PreviewParent).DoKeyDown(Message) then
        Exit;
    end;