7

i want to receive OnKeyPress events when the user presses the Tab key.

procedure TForm1.Edit1(Sender: TObject; var Key: Char);
begin
   case Key of
   #09:
      begin
         //Snip - Stuff i want to do
      end;
   end;
end;

i try subclassing the Edit box, and handle the WM_GETDLGCODE message:

procedure TfrmEnableVIPMode.AccountNumberWindowProc(var Message: TMessage);
begin
   case Message.Msg of
   WM_GETDLGCODE: Message.Result := DLGC_WANTTAB;
   else
      FOldAccountNumberWindowProc(Message);
   end;
end;

And i now receive Tab KeyPress events (as i hoped), but now pressing the Left or Right cursor keys causes focus to shift to the previous, or next, control in the tab order.

What is the correct way to recieve Tab Key Press events?

Bonus Reading

i tried doing what the MSDN documentation says:

wParam
The virtual key, pressed by the user, that prompted Windows to issue this notification. The handler must selectively handle these keys. For instance, the handler might accept and process VK_RETURN but delegate VK_TAB to the owner window. For a list of values, see Virtual-Key Codes.

lParam A pointer to an MSG structure (or NULL if the system is performing a query).

but wParam and wParam are both zero.

Update Two

i realized i have the same bug as this answer:

if Message.Msg = WM_GETDLGCODE then
   Message.Result:= Message.Result or DLGC_WANTTAB
else
   if Assigned(FOldWndProc) then FOldWndProc(Message);

when i should actually use concepts from the correct code listed elsewhere in the same answer:

if Assigned(FOldWndProc) then FOldWndProc(Message);
if Message.Msg = WM_GETDLGCODE then
   Message.Result:= Message.Result or DLGC_WANTTAB;

That helps to explain why my original code is wrong. Setting Message.Result to DLGC_WANTTAB is wrong:

procedure TfrmEnableVIPMode.AccountNumberWindowProc(var Message: TMessage);
begin
   case Message.Msg of
   WM_GETDLGCODE: Message.Result := DLGC_WANTTAB;
   else
      FOldAccountNumberWindowProc(Message);
   end;
end;

it is also wrong to try to bitwise or the flag DLGC_WANTTAB into Message.Result, because Message.Result doesn't have a value yet:

procedure TfrmEnableVIPMode.AccountNumberWindowProc(var Message: TMessage);
begin
   case Message.Msg of
   WM_GETDLGCODE: Message.Result := Message.Result or DLGC_WANTTAB;
   else
      FOldAccountNumberWindowProc(Message);
   end;
end;

i must first call the original window procedure, to get Windows' EDIT control set correct values of Message.Result. Then i can bitwise combine DLGC_WANTTAB:

procedure TfrmEnableVIPMode.AccountNumberWindowProc(var Message: TMessage);
begin
    FOldAccountNumberWindowProc(Message);

    case Message.Msg of
    WM_GETDLGCODE: Message.Result := Message.Result or DLGC_WANTTAB;
    end;
end;

To paraphrase the Raymond Chen blog entry, and adding emphasis as required:

After asking the original control what behavior it thinks it wants, we turn on the DLGC_WANTTAB flag

So this is better. Cursor keys continue to navigate text in the Edit control (rather than shifting focus), and i receive OnKeyPress (and OnKeyDown and OnKeyUp) events for the Tab key.

The remaining problem is that the user pressing Tab no longer shifts focus.

i tried to start to start manually hacking in focus changing myself:

procedure TfrmEnableVIPMode.edAccountNumberKeyPress(Sender: TObject; var Key: Char);
begin
   case Key of
   #09:
      begin
         //Snip - Stuff i want to do

         { 
            The DLGC_WANTTAB technique broke Windows focus change. 
            Keep throwing in hacks until it's no longer obviously broken
         }
         //Perform(CM_DialogKey, VK_TAB, 0); //doesn't work
         Self.ActiveControl := Self.FindNextControl(edAccountNumber, True, True, False);
      end;
   end;
end;

The above code works - if the user pressed the Tab key. But the code is broken, as Raymond Chen notes six year ago:

There are many things wrong with this approach. You can spend quite a lot of time nitpicking the little details, how this code fails to set focus in a dialog box properly, how it fails to take nested dialogs into account, how it fails to handle the Shift+Tab navigation key

In my case, i broke Shift+Tab. And who knows what else.


So, my question:

How to receive TAB key press in edit box?

i don't want to eat them, i just want to know that the user pressed the Tab key.

Bonus Chatter

Community
  • 1
  • 1
Ian Boyd
  • 246,734
  • 253
  • 869
  • 1,219
  • You might get inspiration in [`this post`](http://stackoverflow.com/q/2363456/960757). – TLama Jul 23 '13 at 16:18
  • @TLama It did help me find a (common) bug in the approach i am using. i might be using the completely wrong approach, but at least i fixed the buggy wrong approach! – Ian Boyd Jul 23 '13 at 17:54
  • 1
    First you say "I want the edit control to get the TAB key instead of moving focus", and then you complain "The edit control gets the TAB key instead of moving focus!" Which do you want? – Raymond Chen Jul 24 '13 at 01:17
  • +1 elaborate, but worth reading it all through. – Jeroen Wiert Pluimers Jul 24 '13 at 07:25
  • @RaymondChen i didn't say that. You're thinking of [user1651105](http://stackoverflow.com/users/1651105/user1651105) in the [question i *linked* to](http://stackoverflow.com/questions/2363456/how-do-i-catch-a-vk-tab-key-in-my-tedit-control-and-not-let-it-lose-the-focus/2364305#2364305). There he says, *"How do I catch a VK_TAB key in my TEdit control and not let it lose the focus?"*. I, on the other hand, want to catch a VK_TAB key in my TEdit control. – Ian Boyd Jul 24 '13 at 13:27
  • Okay, now I see that you merely want to know that the TAB key was pressed without interfering with it. Why not just listen for the focus loss? That also catches the case where the user leaves the control by means other thnn TAB. (Fairness to mouse users.) If you really care about the TAB key only, then you need to pick it off in the modal loop. (This is not really how dialog boxes are designed to be used, so it's not surprising that it's messy.) – Raymond Chen Jul 24 '13 at 13:51
  • @RaymondChen i've thought about that; and some places in my application *do* that. Except from a user point of view it's not very desirable. Pressing TAB causes a database fetch, and the contents of the entire screen can change. The user was happy with the dialog contents, and chooses to press `OK`. Without them noticing, the contents of the entire dialog change an instant before vanishing. Then they later return to that form and nothing is what they typed in ("Stupid computers can't do anything.") i want the action that will *autofill* the form to be explicit (By pressing `Enter` or `Tab`). – Ian Boyd Jul 24 '13 at 14:22
  • Okay, if you say so. The other options are to sniff the message passed to `WM_GETDLGCODE` (you claim it is null, which I don't believe to be true for keyboard queries, but if you say so), or modify your modal loop to sniff the keypress. (Perhaps a message filter will help.) – Raymond Chen Jul 24 '13 at 14:31
  • 1
    @RaymondChen These are *Delphi* forms; not actual Windows *"dialogs"*. Maybe that has something to do with the unexpected behavior. – Ian Boyd Jul 24 '13 at 15:56
  • Okay, I assumed that Delphi forms were wrappers around Windows dialogs, since they are using a message normally used only by Windows dialogs (`WM_GETDLGCODE`). Maybe they are repurposing the message. – Raymond Chen Jul 24 '13 at 20:04
  • @RaymondChen i'd have to delve back into the guts of it, but i'm certain they use a series of `CreateWindow`, each child window (Button, Edit, etc) parented to the containing window, which is also created with `CreateWindow`. – Ian Boyd Jul 24 '13 at 20:08

2 Answers2

7

You can handle the CN_KEYDOWN message:

procedure TfrmEnableVIPMode.AccountNumberWindowProc(var Message: TMessage);
begin
   case Message.Msg of
   CN_KEYDOWN:
      if TWMKey(Message).CharCode = VK_TAB then
         ....
   end;
   FOldAccountNumberWindowProc(Message);
end;


It is also possible to detect the key down message at the form level, without subclassing the edit:

procedure TfrmEnableVIPMode.CMDialogKey(var Message: TCMDialogKey);
begin
  if (Message.CharCode = VK_TAB) and (ActiveControl = edAccountNumber) then
    ...

  inherited;
end;
Sertac Akyuz
  • 54,131
  • 4
  • 102
  • 169
5

You need to call the previous WndProc first so the Message.Result gets a default value for the key codes that TEdit natively wants, then append your DLGC_WANTTAB flag to that result, eg:

procedure TfrmEnableVIPMode.AccountNumberWindowProc(var Message: TMessage);
begin
  FOldAccountNumberWindowProc(Message);
  if Message.Msg = WM_GETDLGCODE then
    Message.Result := Message.Result or DLGC_WANTTAB;
end;
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770