2

In my WinAPI application, I have a series of edit controls in a child window. I would like the user to be able to move between them by pressing on the tab key to go forward and shift-tab to go back, but I can't seem to figure out how to use WS_TABSTOP with child windows. What I intend to have happen is that when the user clicks the tab key, the subsequent edit control is selected. However, when I click the tab in the window of the following code the cursor simply disappears.

Here is a minimal reproducible example:

    //libraries
#pragma comment ("lib", "Comctl32.lib")
#pragma comment ("lib", "d2d1.lib")


#define WIN32_LEAN_AND_MEAN             // Exclude rarely-used stuff from Windows headers
// Windows Header Files
#include <windows.h>
#include <CommCtrl.h>
// C RunTime Header Files

#include <vector>
#include <string>


#define IDS_APP_TITLE           103
#define IDI_PRACTICE            107
#define IDI_SMALL               108
#define IDC_PRACTICE            109

#define MAX_LOADSTRING          100

// Global Variables:
HINSTANCE hInst;                                // current instance
WCHAR szTitle[MAX_LOADSTRING];                  // The title bar text
WCHAR szWindowClass[MAX_LOADSTRING];            // the main window class name

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
LRESULT CALLBACK WndProcChild(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);

ATOM MyRegisterClass(HINSTANCE hInstance);

HWND childHWND;

HWND InitInstance(HINSTANCE hInstance, int nCmdShow);

int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
    _In_opt_ HINSTANCE hPrevInstance,
    _In_ LPWSTR    lpCmdLine,
    _In_ int       nCmdShow)
{
    UNREFERENCED_PARAMETER(hPrevInstance);
    UNREFERENCED_PARAMETER(lpCmdLine);
    // Initialize global strings
    LoadStringW(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
    LoadStringW(hInstance, IDC_PRACTICE, szWindowClass, MAX_LOADSTRING);
    MyRegisterClass(hInstance);

    SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);

    // Perform application initialization:
    HWND hWnd = InitInstance(hInstance, nCmdShow);
    if(!hWnd)
    {
        return FALSE;
    }
    HACCEL hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_PRACTICE));
    MSG msg;
    // Main message loop:
    while (GetMessage(&msg, nullptr, 0, 0))
    {
        if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
        {
            if (!IsDialogMessage(hWnd, &msg))
            {
                TranslateMessage(&msg);
                DispatchMessage(&msg);
            }
        }
    }
    return (int)msg.wParam;
}


LRESULT CALLBACK WndProcChild(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    case WM_CREATE:
    {
        HWND edit1 = CreateWindow(WC_EDIT, L"", WS_CHILD | WS_BORDER | WS_VISIBLE | WS_TABSTOP, 100, 100, 100, 100, hWnd, (HMENU)1, hInst, NULL);
        HWND edit2 = CreateWindow(WC_EDIT, L"", WS_CHILD | WS_BORDER | WS_VISIBLE | WS_TABSTOP, 300, 100, 100, 100, hWnd, (HMENU)2, hInst, NULL);
        break;
    }
    }
    return DefWindowProc(hWnd, message, wParam, lParam);
}
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;

}

ATOM MyRegisterClass(HINSTANCE hInstance)
{
    WNDCLASSEXW wcex;
    wcex.cbSize = sizeof(WNDCLASSEX);
    wcex.style = CS_HREDRAW | CS_VREDRAW;
    wcex.lpfnWndProc = WndProc;
    wcex.cbClsExtra = 0;
    wcex.cbWndExtra = 0;
    wcex.hInstance = hInstance;
    wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_PRACTICE));
    wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
    wcex.hbrBackground = (HBRUSH)(GetStockObject(WHITE_BRUSH));
    wcex.lpszMenuName = MAKEINTRESOURCEW(IDC_PRACTICE);
    wcex.lpszClassName = L"Parent";
    wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));


    //Child wnd class
    WNDCLASSEXW wcexChild;
    wcexChild.cbSize = sizeof(WNDCLASSEX);
    wcexChild.style = CS_HREDRAW | CS_VREDRAW;
    wcexChild.lpfnWndProc = WndProcChild;
    wcexChild.cbClsExtra = 0;
    wcexChild.cbWndExtra = 0;
    wcexChild.hInstance = hInstance;
    wcexChild.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_PRACTICE));
    wcexChild.hCursor = LoadCursor(nullptr, IDC_ARROW);
    wcexChild.hbrBackground = (HBRUSH)(GetStockObject(WHITE_BRUSH));
    wcexChild.lpszMenuName = MAKEINTRESOURCEW(IDC_PRACTICE);
    wcexChild.lpszClassName = L"Child";
    wcexChild.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));
    return RegisterClassExW(&wcexChild) && RegisterClassExW(&wcex);
}


HWND InitInstance(HINSTANCE hInstance, int nCmdShow)
{
    hInst = hInstance; // Store instance handle in our global variable
    HWND hWnd = CreateWindowW(L"Parent", L"PARENT", WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, nullptr, nullptr, hInstance, nullptr);

    childHWND = CreateWindowW(L"Child", L"", WS_CHILD | WS_VISIBLE | WS_EX_CONTROLPARENT,
        0, 0, 700, 700, hWnd, nullptr, hInstance, nullptr);

    if (!hWnd)
    {
        return NULL;
    }
    ShowWindow(childHWND, nCmdShow);
    UpdateWindow(childHWND);

    ShowWindow(hWnd, nCmdShow);
    UpdateWindow(hWnd);
    return hWnd;
}
  • I believe I've seen (and had) this problem at some point. [This](https://stackoverflow.com/questions/26698257/c-dialog-box-tab-key-not-working) may be related. If I'm not mistaken, `IsDialogMessage` should be of use – IWonderWhatThisAPIDoes Apr 03 '21 at 17:24
  • Please show a [mcve], a description of the expected behavior, and a description of the observed behavior. – IInspectable Apr 03 '21 at 17:27
  • 1
    https://devblogs.microsoft.com/oldnewthing/20100930-00/?p=12683 – Hans Passant Apr 04 '21 at 00:35
  • 1
    https://devblogs.microsoft.com/oldnewthing/20031021-00/?p=42083 – Hans Passant Apr 04 '21 at 00:36
  • You still haven't explained what you expect your code to do, and what behavior you observe. Though as explained in the links in the previous comments, you need to instruct the dialog manager to do use keyboard navigation by calling `IsDialogMessage`. [Using the TAB key to navigate in non-dialogs, redux](https://devblogs.microsoft.com/oldnewthing/20131009-00/?p=2983) has more information. – IInspectable Apr 04 '21 at 14:27

1 Answers1

1

Problem here is that if (!IsDialogMessage(hWnd, &msg)) gets called on the wrong window.

Replacing the line with if (!IsDialogMessage(childHWND, &msg)) gets TAB navigation to work.

From the IsDialogMessage documentation:

Although the IsDialogMessage function is intended for modeless dialog boxes, you can use it with any window that contains controls, enabling the windows to provide the same keyboard selection as is used in a dialog box.

In the posted code, the "window that contains controls" is the direct parent of the edit controls, which is childHWND.


[ EDIT ]   One other problem pointed out in the comments (thanks @IInspectable) is that the extended style WS_EX_CONTROLPARENT is mistakenly passed with the style flags, instead of the extended style flags. To fix that, the call to childHWND = CreateWindowW(L"Child", L"", WS_CHILD | WS_VISIBLE | WS_EX_CONTROLPARENT, ... should be changed to childHWND = CreateWindowExW(WS_EX_CONTROLPARENT, L"Child", L"", WS_CHILD | WS_VISIBLE, ... instead.

dxiv
  • 16,984
  • 2
  • 27
  • 49
  • Thank you, and if I have multiple child windows that each contain controls with which I would like to use `WS_TABSTOP`, do I just call `IsDialogMessage` on each? – Juanita Lopez Apr 05 '21 at 02:24
  • @JuanitaLopez You *could* call `IsDialogMessage` for each. But only *one* of the child windows can have the input focus at any given time, so the more economical way would be to track which one is active and call `IsDialogMessage` for that one, only. See for example "*IsDialogMessage and multiple Modeless Dialogs*" on [this page](http://www.mvps.org/user32/modal.html) and/or the (now retired) article [How To Use One IsDialogMessage() Call for Many Modeless Dialogs](https://web.archive.org/web/20100926084558/http://support.microsoft.com/kb/71450). – dxiv Apr 05 '21 at 02:34
  • Actually, that's not required. Specifying the `WS_EX_CONTROLPARENT` [extended window style](https://learn.microsoft.com/en-us/windows/win32/winmsg/extended-window-styles) should be sufficient. The OP is specifying that style, but for the wrong argument (`dwStyle` as opposed to `dwExStyle`). Proper use of [CreateWindowEx](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-createwindowexw) fixes the issue. – IInspectable Apr 05 '21 at 07:44
  • @IInspectable Right, of course. I added a note about that, however, in this case TAB'ing works even without *any* `WS_EX_CONTROLPARENT` as long as `IsDialogMessage` gets called with the right `HWND`. Guess the extended style is positively required for nested controls that must participate in the same TAB sequence, though verifying that theory will have to wait for another day. – dxiv Apr 05 '21 at 08:42