3
votes

I have a VCL forms application where I'm intercepting keys pressed in the OnKeyPress and OnKeyDown handlers. This works fine for regular characters and keys like VK_ESC for example.

However, when trying to catch the arrow keys (VK_UP and VK_DOWN), it does not work as they seem only to be changing the focus of the controls on the main form, and are not triggering the main forms OnKeyDown handler.

How does one catch/process those key presses?

Update: Trying the following, based on answer below, but still no success.

void __fastcall TMain::WndProc(TMessage& Message)
{
  switch (Message.Msg)
  {
    case WM_GETDLGCODE:
        Log(lInfo) << "Got WM_GETDLGCODE";
    break;
    case CM_WANTSPECIALKEY:
        Log(lInfo) << "Got WM_WANTSPECIALKEY";
    break;

None of the above cases are triggered by using the up/down arrows in my application.

I have made the following observation. If creating an empty vcl form, the above cases are triggered, CM_WANSPECIALKEY is triggered first, followed by WM_GETDLGCODE.

However, as soon as a TButton is placed on the form, the cases are not triggered anymore.

I'm using C++ Builder XE3.

2

2 Answers

2
votes

Arrow keys are reserved by Windows for navigation purposes. If a window wants to handle arrow key messages, it must respond to the WM_GETDLGCODE message and include the DLGC_WANTARROWS (or DLGC_WANTALLKEYS) flag in its return value.

Most VCL UI components (including TForm) return 0 for WM_GETDLGCODE. Only a handful of standard VCL components respond with DLGC_WANTARROWS (or DLGC_WANTALLKEYS):

TColorGrid, TToolBar, TCustomGrid, TMediaPlayer, TCustomRibbon, TRibbonSpinButton, TSpinButton, TTabbedNotebook, TCustomCombo, and TTabSet

In your TForm class, override its virtual WndProc() method, or declare a message handler, to handle WM_GETDLGCODE, then you can return whatever flag(s) you need.

void __fastcall TMyForm::WndProc(TMessage &Message)
{
    TForm::WndProc(Message);
    if (Message.Msg == WM_GETDLGCODE)
        Message.Result |= DLGC_WANTARROWS;
}

Update: If WM_GETDLGCODE doesn't work, try responding to the CM_WANTSPECIALKEY message instead:

void __fastcall TMyForm::WndProc(TMessage &Message)
{
    TForm::WndProc(Message);
    if (Message.Msg == CM_WANTSPECIALKEY)
    {
        switch (reinterpret_cast<TCMWantSpecialKey&>(Message).CharCode)
        {
            case VK_LEFT:
            case VK_RIGHT:
            case VK_UP:
            case VK_DOWN:
                Message.Result = 1;
                break;
        }
    }
}

Update: you are trying to process key events in the Form itself, which will only work when:

  • the Form window itself has input focus.

  • a child VCL window receives key messages, and the Form's KeyPreview property is true.

When a windowed child control, like a button, has the input focus, key messages go to that window, not to the Form window. In order for the KeyPreview property to work for arrow keys, the focused child must respond to the WM_GETDLGCODE message asking the OS to send arrow key messages. Only then can the child forward the messages to the Form for processing.

A button doesn't ask for arrow key messages by default, so you would have to subclass the button to handle the WM_GETDLGCODE message manually, eg:

class TMain : public TForm
{
__published:
    TButton *Button1;
    void __fastcall FormKeyDown(TObject *Sender, Word &Key, TShiftState Shift);
    void __fastcall FormKeyPress(TObject *Sender, Char &Key);
private:
    TWndMethod PrevBtnWndProc;
    void __fastcall BtnWndProc(TMessage &Message);
public:
    __fastcall TMain(TComponent *Owner);
};

__fastcall TMain::TMain(TComponent *Owner)
    : TForm(Owner)
{
    PrevBtnWndProc = Button1->WindowProc;
    Button1->WindowProc = &BtnWndProc;
}

void __fastcall TMain::BtnWndProc(TMessage &Message
{
    PrevBtnWndProc(Message);
    if (Message.Msg == WM_GETDLGCODE)
        Message.Result |= DLGC_WANTARROWS;
}

void __fastcall TMain::FormKeyDown(TObject *Sender, Word &Key, TShiftState Shift)
{
    // works now!
}

void __fastcall TForm7::FormKeyPress(TObject *Sender, Char &Key)
{
    // works now!
}

That is fine if you have only a few controls to subclass, but if you have a lot of controls, an alternative solution would be to have your Form class handle the CM_DIALOGKEY message instead:

void __fastcall TMyForm::WndProc(TMessage &Message)
{
    TForm::WndProc(Message);
    if (Message.Msg == CM_DIALOGKEY)
    {
        switch (reinterpret_cast<TCMDialogKey&>(Message).CharCode)
        {
            case VK_LEFT:
            case VK_RIGHT:
            case VK_UP:
            case VK_DOWN:
                // process key as needed...
                // set Message.Result to suppress the message further...
                Message.Result = 1;
                break;
        }
    }
}

You should read the following article on EDN, it has a good explanation of how the VCL processes key messages, and how the VCL implements various hooks you can use to intercept key messages:

A Key’s Odyssey

1
votes

I am doing this a bit differently (no WinAPI needed). If your VCL app has no focus on any sub component (like TButton,TEdit,TMemo...) then Main form keyboard events (OnKeyDown and OnKeyUp) will fire on arrow keys use. There are 2 approaches for this I know of:

  1. do not use components with focus

    it may sound silly but you can have TSpeedButton instead of TButton. but essentially you can place all focusable components in some sub-window or page and make it invisible if not needed (like setup window).

  2. disable focus when arrows are needed

    Simply I make an unfocus function like this:

    void main_unfocus()
        {
        Main->bt_unfocus->Visible=true;
        Main->bt_unfocus->SetFocus();
        Main->bt_unfocus->Visible=false;
        }
    

    where bt_unfocus is invisible 2x2 TButton placed in top left corner of main window Main. Now whenever I need to capture events like mouse wheel, arrow keys etc in the main form events then I just call the main_unfocus();

    I usually call it on mouse click on some viewing area or mouse move over it etc and Also I call this on hitting escape on any such component like TButton,TMemo,TEdit as they usually share the same event handlers in my Apps. All depends on what functionality you need/want.

PS hope you set the KeyPreview property to true.