Customized form navigation

by Kent Reisdorph

Microsoft has defined a set of rules for how a Windows program should look and act. This includes rules for keyboard navigation of windows. This is true whether a particular window is a main window or a dialog box. The most basic keyboard handling rules are fairly simple to state:

 

 

Further, some controls have built-in keyboard handling. Pressing the space bar when a check box has focus, for example, will mark or clear the check box. Another obvious example is the case of a multi-line edit control where the control can be navigated using the arrow keys, the Enter key, and, if enabled, the Tab key.

For the most part I highly recommend that you follow Microsoft’s rules for keyboard navigation of forms. However, there are some specialty applications that require special window navigation features. This article will explain how you can implement customized keystroke handling for your applications.

 

Keystroke handling events

The VCL defines two events for keystroke handling: OnKeyDown and OnKeyPress. The OnKeyPress event corresponds to the Windows WM_CHAR message. In simple terms, this message is generated whenever a displayable key is pressed (such as one of the letter or number keys). It is also generated when the Enter key is pressed.

The OnKeyDown event, on the other hand, is fired both when displayable characters are pressed and when special keys are pressed. Special keys include function keys, arrow keys, page movement keys (Insert, Home, Delete, End, Page Up, and Page Down) and so on. The OnKeyDown event will be fired for just about every key on the keyboard. Exceptions are special Windows key combinations such as Ctrl-Alt-Delete and Alt-Tab. Windows grabs these key combinations and doesn’t pass them on to applications.

It is important to understand that pressing an alphanumeric key will result in both an OnKeyDown and an OnKeyPress event being fired. This article will focus on non-alphanumeric keys and will thus be using the OnKeyDown event for examining keystrokes.

Both of the keystroke events contain a parameter called Key. This parameter can be used to determine the key that was pressed. Windows defines a number of constants that map to keys on the keyboard. These virtual key constants all begin with VK_ and are defined in WINUSER.H. For example, the virtual key constant that represents the Enter key is VK_RETURN, the constant that represents the Shift key is VK_SHIFT, the constants that represent the arrow keys are VK_LEFT, VK_RIGHT, VK_UP, and VK_DOWN, and so on. You can compare the value of the Key parameter to one of these constants to determine which key was pressed.

 

The KeyPreview property

Custom keyboard navigation requires that you grab keystrokes as Windows and the VCL process those keystrokes. There are several ways to go about intercepting keystrokes. One way is to catch the OnKeyDown and/or OnKeyPress events for each component. Another way is to have the form receive and act on all keyboard events. This can be accomplished by setting the form’s KeyPreview property to true.

When KeyPreview is true, the form’s OnKeyDown and OnKeyPress events will be fired when a key is pressed and a component has focus. The event for the form will be fired before the corresponding event in the component is fired (hence the “preview”).

All of this assumes that a particular component is capable of catching keystrokes. Group boxes, for example, handle keystrokes internally and, as such, cannot pass keystrokes on to the form.

Using the KeyPreview has one obvious advantage: keystroke handling for all components on the form can be centralized. Rather than having handlers for each component’s keystroke events, you can have one handler that previews all keystrokes.

One disadvantage to using KeyPreview is that the form’s OnKeyDown and OnKeyPress event handlers can be complex. Whether you use KeyPreview for your forms depends on the specific needs of that form. The remainder of this article will assume a form with KeyPreview set to true.

 

Enter as tab

One of the more common needs is to have the Enter key act as the Tab key when an edit control has focus. This may be advantageous for forms that have a lot of data entry fields. Users can quickly enter data and simply hit the Enter key to move to the next field. This is particularly appealing in applications that require a large amount of numerical data entry. For example, an experienced 10-key operator is accustomed to using the Enter key on the numeric keypad after entering data. By making the Enter key act as the Tab key, users of this type of application can be much more efficient at data entry.

Changing focus when the Enter key is pressed is a relatively simple process. First, make sure that all components have their TabStop property set to true and that the tab order for the form has been properly set. After you have done that, the event handler for the form’s OnKeyDown event might look like this:

void __fastcall TForm1::FormKeyDown(
  TObject *Sender, WORD &Key,
  TShiftState Shift)
{ 
  if (Key == VK_RETURN) {
    TWinControl* ctrl = FindNextControl(
      ActiveControl, true, true, false);
    if (ctrl)
      ctrl->SetFocus();
  }
}

This code first examines the Key parameter to see if the Enter key was pressed. If so, the FindNextControl() method of TForm is called to obtain the control that comes next in the tab order. After obtaining a pointer to the next control in the tab order, the SetFocus() method is called for that control.

This code is simplified in that it doesn’t check to see what type of control currently has focus. You will implement some mechanism to determine whether the focused control is an edit control before switching the focus to the next control in the tab order. You can do this in one of two ways. One way is to use dynamic_cast to ensure that the focused control is an edit control before changing focus. For example:

if (dynamic_cast<TEdit*>(ActiveControl))
{
  // it's a special navigation component
}

Another way is to use the Tag property to designate the components that allow the Enter key to act as the Tab key. For example, you could set the Tag property to 1 for these controls. In that case the code might look like this:

if (ActiveControl->Tag == 1)
{
  // it's a TEdit
}

This method is a bit more flexible in that it allows you to specify exactly the components that allow Enter as Tab.

 

Using the arrow keys

For some applications you may wish to allow the user to press the arrow keys to change focus from one control to the next. The code to implement this is a variation on the code in the previous section. Here is an example:

TWinControl* ctrl;
switch (Key) {
  case VK_UP : {
    ctrl = FindNextControl(
      ActiveControl,false, true, false);
    if (ctrl)
      ctrl->SetFocus();
    break;
  }
  case VK_DOWN : {
    ctrl = FindNextControl(
      ActiveControl, true, true, false);
    if (ctrl)
      ctrl->SetFocus();
    break;
  }
}

The key element to this code is the second parameter passed to the FindNextControl() method. When true is passed for this parameter, a pointer to the next component in the tab order is returned. When false is passed, a pointer to the previous component in the tab order is returned. The result is that focus moves back in the tab order when the up arrow key is pressed, and forward in the tab order when the down arrow key is pressed.

This is fine when all of the controls on a form are edit controls. However, if a ListBox, ComboBox, Memo, or RichEdit component has focus, then that component’s default arrow key handling will be interrupted. You can easily write code around this possibility, but you must be aware of the types of components your form contains.

Naturally, you can add handling for the left and right arrow keys as well. However, you need to again carefully consider the components that are on your form. The user expects the right and left arrow keys to move the editing caret in an edit control from character to character. You don’t want to replace such obvious editing behavior unless you have a very good reason to do so.

Overall, getting notification that an arrow key was pressed is easy. From there on you will likely have to write code to determine the type of component that has focus and determine how to handle the arrow keys for each component type as needed.

Finally, let me say that you cannot necessarily handle arrow keys for every component type. The RadioGroup and GroupBox components, for example, handle the arrow keys internally. They do not surface those keystrokes to the form.

 

Conclusion

As I said at the beginning of this article, you should follow Microsoft’s guidelines regarding keyboard navigation whenever possible. If, however, you have an application that needs custom keyboard handling then you can use the techniques described in this article to implement that kind of support.