13

I want my form to handle the arrow keys, and I can do it -- as long as there is no button on the form. Why is this?

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
Jeff Bannon
  • 233
  • 1
  • 3
  • 11
  • 6
    Great question. Digging into the underlying complexities of things like this is what separates the experts from the tourists. Keep it up! – Warren P Dec 24 '11 at 20:40

5 Answers5

16

Key messages are processed by the controls themselves who receives these messages, that's why when you're on a button the form is not receiving the message. So normally you would have to subclass these controls, but the VCL is kind enough to ask the parenting form what to do if the form is interested:

type
  TForm1 = class(TForm)
    ..
  private
    procedure DialogKey(var Msg: TWMKey); message CM_DIALOGKEY;
    ..


procedure TForm1.DialogKey(var Msg: TWMKey); 
begin
  if not (Msg.CharCode in [VK_DOWN, VK_UP, VK_RIGHT, VK_LEFT]) then
    inherited;
end;

François editing: to answer the OP original question, you need to call onKeyDown somehow so that his event code would work (feel free to edit; was too long for a comment).

type
  TForm1 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    Button3: TButton;
    Button4: TButton;
    procedure FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
  private
    { Private declarations }
    procedure DialogKey(var Msg: TWMKey); message CM_DIALOGKEY;
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.DialogKey(var Msg: TWMKey);
begin
  case Msg.CharCode of
    VK_DOWN, VK_UP, VK_RIGHT, VK_LEFT:
      if Assigned(onKeyDown) then
        onKeyDown(Self, Msg.CharCode, KeyDataToShiftState(Msg.KeyData));
    else
      inherited
  end;
end;

procedure TForm1.FormKeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
begin
  case Key of
    VK_DOWN: Top := Top + 5;
    VK_UP: Top := Top - 5;
    VK_LEFT: Left := Left - 5;
    VK_RIGHT: Left := Left + 5;
  end;
end;
Francesca
  • 21,452
  • 4
  • 49
  • 90
Sertac Akyuz
  • 54,131
  • 4
  • 102
  • 169
  • I think you need a bit more explanation as to what is special about `CM_DIALOGKEY`. This is certainly the slick way to change behaviour. Feel free to overlap with my answer and I'll delete it since a comprehensive answer with this code in beats what I have done. – David Heffernan Dec 23 '11 at 22:06
  • @David - I'm afraid you won't be able to delete your post :). There's not much I can explain about CM_DIALOGKEY, it's a VCL made up message used to broadcast key messages. This is what my help file says about it: *"This is constant Controls.CM_DIALOGKEY"* :). Update: The [documentation](http://docwiki.embarcadero.com/VCL/2010/en/Controls.CM_DIALOGKEY) has been improved: *"CM_DIALOGKEY represents a control message and is used internally by the VCL framework."* – Sertac Akyuz Dec 23 '11 at 22:16
  • Hmm, I thought it sounded as though it was related to `TApplication.IsDlgMsg`. In fact is `TApplication.IsDlgMsg` even relevant here or does the VCL handle these navigation keys rather than Windows dialogs? – David Heffernan Dec 23 '11 at 22:24
  • @David - It seems it is used to differentiate whether the vcl should process a message, if it is intended for a non VCL window, ProcessMessage does not dispatch the message. That's what I understood.. – Sertac Akyuz Dec 23 '11 at 22:34
  • @David - Peter explains in the link you gave, it is indeed for API dialogs, not really relevant for forms. – Sertac Akyuz Dec 23 '11 at 22:50
  • @SertacAkyuz Yup, I'm caught up now. Thanks. – David Heffernan Dec 23 '11 at 23:00
  • 1
    Using the code from Francois and Sertac, I was able to solve the problem. At first, when PgUp (vk_prior) and PgDn (vk_next) were not trapped, I thought I was going in circles. However, when I turned the form's KeyPreview back to true, all was right with world. Thanks for the help. – Jeff Bannon Dec 28 '11 at 15:43
8

Arrow keys are used to navigate between buttons on a form. This is standard Windows behaviour. Although you can disable this standard behaviour you should think twice before going against the platform standard. Arrow keys are meant for navigation.

If you want to get the full low down on how a key press finds its way through the message loop I recommend reading A Key's Odyssey. If you want to intercept the key press before it becomes a navigation key, you need to do so in IsKeyMsg or earlier. For example, Sertac's answer gives one such possibility.

Community
  • 1
  • 1
David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • 1
    Peter Below (author of that Key's Odyssey) is an important Delphi community guy (TeamB), and a great JVCL contributor. Nice article. – Warren P Dec 24 '11 at 20:37
5

Only the object that has the focus can receive a keyboard event.

To let the form have access to the arrow keys event, declare a MsgHandler in the public part of the form. In the form create constructor, assign the Application.OnMessage to this MsgHandler.

The code below intercepts the arrow keys only if they are coming from a TButton descendant. More controls can be added as needed.

procedure TForm1.FormCreate(Sender: TObject);
begin
  Application.OnMessage := Self.MsgHandler;
end;

procedure TForm1.MsgHandler(var Msg: TMsg; var Handled: Boolean);
var
  ActiveControl: TWinControl;
  key : word;
begin
  if (Msg.message = WM_KEYDOWN) then
    begin
      ActiveControl := Screen.ActiveControl;
      // if the active control inherits from TButton, intercept the key.
      // add other controls as fit your needs 
      if not ActiveControl.InheritsFrom(TButton)
        then Exit;

      key := Msg.wParam;
      Handled := true;
      case Key of // intercept the wanted keys
        VK_DOWN : ; // doStuff
        VK_UP : ; // doStuff
        VK_LEFT : ; // doStuff
        VK_RIGHT : ; // doStuff
        else Handled := false;
      end;
   end;
end;
LU RD
  • 34,438
  • 5
  • 88
  • 296
3

Because they are preempted to deal with setting the focus on the next available WinControl.
(I'm pretty sure that if you put an Edit instead of a Button you see the same thing).

If you want to handle them yourself, you can provide the Application with an OnMessage event that will filter those before they are processed and handle them yourself there.

Francesca
  • 21,452
  • 4
  • 49
  • 90
0
var
KBHook: HHook; {this intercepts keyboard input}

implementation

{$R *.dfm}

function KeyboardHookProc(Code: Integer; WordParam: Word; LongParam: LongInt): LongInt; stdcall;
 begin
 case WordParam of
   vk_Space: ShowMessage ('space')  ;
   vk_Right:ShowMessage ('rgt') ;
   vk_Left:ShowMessage ('lft') ;
   vk_Up: ShowMessage ('up') ;
   vk_Down: ShowMessage ('down') ;
  end; {case}
 end;

procedure TForm4.FormCreate(Sender: TObject);
begin
KBHook:=SetWindowsHookEx(WH_KEYBOARD,@KeyboardHookProc,HInstance,GetCurrentThreadId());
end;

This code will work even when a control is focused (buttons , listboxes), so be careful some controls may loose their keyboard events (Read David haffernans answer) .

keyboard events with Focused controls

eg: If you are having textbox in your app and want to recive text(if focused) also , then

add an applicationevent1

procedure TForm4.ApplicationEvents1Message(var Msg: tagMSG;var Handled: Boolean);
begin
if Msg.message = WM_KEYFIRST then
  KBHook:=SetWindowsHookEx(WH_KEYBOARD,@KeyboardHookProc,HInstance,GetCurrentThreadId());
end;

add the following code at the bottom of the function KeyboardHookProc

UnhookWindowsHookEx(KBHook);

and remove

KBHook:=SetWindowsHookEx(WH_KEYBOARD,@KeyboardHookProc, HInstance, 
GetCurrentThreadId());

from oncreate event.

Vibeeshan Mahadeva
  • 7,147
  • 8
  • 52
  • 102