Search code examples
cwinapidialogchildwindowcreatewindow

Embedding modeless dialogs as child window with Windows API


I know, there must be a way to embed an (modeless) dialog as child of a window created with CreateWindow. In my case I want to embed them into an scroll-able container window, whereby this container windows it self is a child of the main window (see picture).

Embedded Dialogs

The first problem that I encounter is, that I still want be able to use TAB keys and other dialog specific navigation. But how?

My message loop:

while (GetMessage(&msg, NULL, 0, 0)) {
    if (IsDialogMessage(msg.hwnd, &msg)) continue;
    if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
}

Edit: For testing purpose I modified the loop:

while (GetMessage(&msg, NULL, 0, 0)) {
    if (IsEmbeddedDialogWindow(msg.hwnd)) {
        if (IsDialogMessage(msg.hwnd, &msg)) continue;
    }
    if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
}

while (GetMessage(&msg, NULL, 0, 0)) {
    if (IsScrollableContainerWindow(msg.hwnd)) {
        if (IsDialogMessage(msg.hwnd, &msg)) continue;
    }
    if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
}

More information how to make it the right way can be found here: Using the TAB key to navigate in non-dialogs, redux

With this message loop, nothing happens if I want to enter some dialog texts as if the message is not handled. If IsDialogMessage is removed, I can enter some text into an edit control in one of the embedded dialogs, however dialog navigation doesn't work as intended. Of course, WS_TABSTOP style is set for dialog child controls.

The scroll-able container is created with CreateWindowEx with styles WS_CHILD, WS_VISIBLE, WS_VSCROLL, WS_TABSTOP, WS_EX_CONTROLPARENT and dialogs are created as children of this container.

    HWND hWndContainer = GroupBarPanelCreate(WS_CHILD | WS_VISIBLE | WS_VSCROLL | WS_TABSTOP, WS_EX_CONTROLPARENT, hWndMain, 0, 0, 400, 400);
    GROUPBAR_PANEL* GroupBarPanel = (GROUPBAR_PANEL*) GetWindowLongPtr(hWndContainer, 0);
    // Test embedding dialogs
    for (unsigned int i = 0; i < 10; i++) {
        HWND hWndDlg = CreateDialogParam(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWndContainer, About, 0);
        GroupBarPanelInternalAddLast(GroupBarPanel, hWndChild, hWndDlg, nullptr);
    }

My GroupBarPanelInternalAddLast modify modeless dialog styles by removing caption and borders, and ensures that WS_CHILD, WS_VISIBLE and WS_TABSTOP is set (SetWindowLong(hWndDlg, GWL_STYLE, ...)). (Also tested WS_EX_CONTROLPARENT style) With SetParent(hWndDlg, hWndContainer) modeless dialogs parent is changed.

Working Demo

So what am I missing here? As I found out, neither the container window procedure nor the embedded (for testing purpose subclassed) dialog procedure almost never gets WM_SETFOCUS or WM_KILLFOCUS messages for example, but why that?

Solution: Calling IsDialogMessage for the top level window.

while (GetMessage(&msg, NULL, 0, 0)) {
    if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg)) {
        if (IsDialogMessage(hWndTopLevel, &msg)) {
            continue;
        }
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
}

Solution

  • You are not using IsDialogMessage() correctly, and you said you learned from some bad tutorials. I don't know which tutorials you saw, so I can't tell you what they got wrong; all I can do is say how to use it correctly.

    IsDialogMessage() takes two parameters: the message itself, and the window handle of the toplevel window. This bit in bold is the important part: the IsDialogMessage() function needs to know which dialog to work with in the event of tab navigation or Enter/Esc handling.

    You don't want to pass msg.hwnd; that's the control itself.

    And in the case of nested child dialogs like you have here, you don't want to pass in the child dialog's handle; that would confine IsDialogMessage() to that dialog.

    So in your screenshot, you want to pass in the handle of the main window, that is, the window called Win32ApiDemo1.

    Also make sure all the child dialogs and your custom expander control have WS_EX_CONTROLPARENT so tab navigation can recurse.