SUMMARY
An application enters an infinite loop when you use the TAB or arrow keys
to navigate through dialog box controls. The problem occurs only when the
dialog box has another dialog box as a child. When you use the TAB key, the
problem occurs only when the child dialog box has the WS_MAXIMIZEBOX or
WS_TABSTOPS style.
This article describes the problem in detail together with two possible
solutions. DlgTab.exe demonstrates the problem and one of the workarounds.
Problem 1
When arrow keys are used to maneuver through a dialog box's last control
(the last control in the parent dialog box), the focus is passed to the
next child (in this case, the child dialog box) and the application goes
into an infinite loop.
Problem 2
When the TAB key is used to maneuver through the dialog box's last control,
the focus is then passed to the next child (again the child dialog box) and
the application goes into an infinite loop.
The infinite loop is clearly visible when spying on the child dialog box
window. The child dialog box window continually receives WM_GETDLGCODE
messages.
The dialog box manager confuses the child dialog box window with a child
control.
When maneuvering through controls in a dialog box, the dialog box
manager looks through its children and checks for the next child to pass
focus to. In order to do that, the dialog box manager sets the focus to
the child control and generates messages to complete the process.
Using the Arrow Keys to Navigate Through Controls
WS_GROUP defines the start of a group within a dialog box. All controls
defined between two WS_GROUP styles are members of a group. Arrow keys can
then be used to navigate between members of a group.
Normally a dialog box does not have WS_GROUP for a style; therefore, the
child dialog box does not have a WS_GROUP but a previous control does have
one. Based on the definition of a group, the child dialog box is considered
a member of the last group by the dialog box manager. When the arrow key is
used on the last control, the dialog box manager checks for the next child
control in that group to pass the focus to, in this case the child dialog
box window. The dialog box manager then tries to set focus to the window
and generates necessary messages to complete the process. These generated
messages are meaningful to a control but not to a window. One of these
generated messages is WM_GETDLGCODE, which causes the infinite loop.
Using the TAB Key to Navigate Through Controls
WS_TABSTOPS defines which control has a tab stop. When you tab through
controls, the dialog box manager looks for the next child that has the
WS_TABSTOPS style and changes the focus to that child.
Normally a dialog box does not use WS_TABSTOPS for a style. It is quite
normal, however, to use WS_MAXIMIZEBOX for a dialog box style. The child
dialog box in this case has WS_MAXIMIZEBOX. The values for WS_TABSTOPS and
WS_MAXIMIZEBOX are the same in WINDOWS.H. Because the dialog box manager
expects a child control and not another window, the WS_MAXIMIZEBOX is
interpreted as WS_TABSTOPS. So, when you tab on the last control of the
parent dialog box, the dialog box manager tries to set the focus to the
next child with WS_TABSTOPS, in this case the child dialog box. Then the
dialog box manager generates the necessary messages to complete the
process. These generated messages are meaningful to a control but not to a
window. One of these messages is WM_GETDLGCODE, which causes the infinite
loop.
Workarounds are described in the MORE INFORMATION section below.
MORE INFORMATION
- Do not include the WS_MAXIMIZEBOX style for the child dialog box to
resolve the TAB key problem, and add WS_GROUP to the child dialog box to
resolve the arrow key problem.
- Subclass the last control and handle the change of focus in the dialog
box. This method is outlined below:
- Subclass the last control in the parent dialog box.
- In the subclass procedure for the control, process the WM_GETDLGCODE
message by:
- Passing the WM_GETDLGCODE to the button window procedure.
- Or'ing the return value of (a.) with DLGC_WANTMESSAGE and
returning it.
The following is sample code demonstrating the WM_GETDLGCODE message:
lpButtonProcOrg is a pointer to the button window
procedure. This is a global and was set before setting
the pointer to the subclass procedure for the button.
To do this call:
lpButtonProgOrg = (FARPROC)
GetWindowLong(GetDlgItem(hCancelBtn,GWL_WNDPROC);
:
:
case WM_GETDLGCODE:
lRet = CallWindowProc(lpButtonProcOrg,hWnd,iMessage,
wParam,lParam);
if (lParam)
{
lpmsg= (LPMSG) lParam;
if (lpmsg->message == WM_KEYDOWN)
{
if ( (lpmsg->wParam == VK_TAB) ||
(lpmsg->wParam == VK_DOWN) ||
(lpmsg->wParam == VK_UP) ||
(lpmsg->wParam == VK_RIGHT) ||
(lpmsg->wParam == VK_LEFT) )
lRet |= DLGC_WANTMESSAGE;
}
}
return (lRet);
For more information on WM_GETDLGCODE, please see the following
article in the Microsoft Knowledge Base:
83302
Using the WM_GETDLGCODE Message
- In the subclass procedure, process the WM_KEYDOWN message by:
- Set the focus to the first control.
- If the first control is not a push button, then the process is
complete; otherwise, the dark border needs to be removed from
whichever button that has it, and placed on the first button, as
follows:
- First send a WM_GETDLGCODE.
- If the return value is DLGC_DEFPUSHBUTTON, then send a
BM_SETSTYLE message to the button that has the dark border,
with BS_PUSHBUTTON for wParam. This will cause the dark border
to be removed.
- Now the dark border needs to be placed on the button that has
the focus (that is, the first button). To do this, send
BM_SETSTYLE to the first button, with BS_DEFPUSHBUTTON for
wParam.
Sample Code Demonstrating the WM_KEYDOWN Message
case WM_KEYDOWN:
// Check for keys that need this processing such as the
// TAB and arrow keys.
if ( (wParam == VK_TAB) ||
(wParam == VK_DOWN) || (wParam == VK_UP) ||
(wParam == VK_RIGHT) || (wParam == VK_LEFT) )
{
// Set the focus to the first control, Button #1
// in this case.
hwndBtn1 = GetDlgItem(hParentDlg,IDBUTTON1);
SetFocus(hwndBtn1);
// Loop through all the controls and remove the
// dark border that the previous default push
// button has.
hwndCtrls = GetWindow(hParentDlg, GW_CHILD);
while (hwndCtrls)
{
wRet = (WORD)(DWORD)SendMessage(hwndCtrls,
WM_GETDLGCODE, 0, 0L);
if (wRet & DLGC_DEFPUSHBUTTON)
SendMessage(hwndCtrls, BM_SETSTYLE,
(WPARAM)BS_PUSHBUTTON, TRUE);
hwndCtrls = GetWindow(hwndCtrls, GW_HWNDNEXT);
}
// Give the hwndBtn1 button the default push button
// border.
SendMessage(hwndBtn1, BM_SETSTYLE,
(WPARAM)BS_DEFPUSHBUTTON, TRUE);
return(0L);
}
break;