0xF5T9
0xF5T9

Reputation: 21

Child Window Sends WM_COMMAND Message to Unexpected Parent on Enter Key Press While Focused

According to:

https://learn.microsoft.com/en-us/windows/win32/menurc/wm-command

What message causes a button to send a WM_COMMAND message

(And a lot of other posts)

Window controls send their WM_COMMAND message to their direct parent. But it wasn't the case in this scenario, and I can't figure out why. It only happened with the enter key (Return key).

/// Marcos:
#define IDC_BUTTON1 100;
#define IDC_BUTTON2 101;

/// Windows:
HWND g_hWnd;      // Main application window.
HWND g_Container; // Static window (Act like a container)
HWND g_Button1;   // Button 1
HWND g_Button2;   // Button 2
  1. In the WinMain entry, create the main window, nothing special:
g_hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
   CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, nullptr);
  1. In the WM_CREATE message:
// Button 1 is a direct child of the main window.
g_Button1 = CreateWindowExW(NULL, L"BUTTON", L"Button", WS_VISIBLE | WS_CHILD | WS_TABSTOP, 100, 100, 130, 40, g_hWnd, (HMENU)IDC_BUTTON1, NULL, NULL);
if (!g_Button1)
   exit(EXIT_FAILURE);

// Create the container window(static).
g_Container = CreateWindowExW(WS_EX_CONTROLPARENT, L"STATIC", L"", WS_VISIBLE | WS_CHILD | SS_NOPREFIX | WS_CLIPCHILDREN,
            300, 300, 500, 500, g_hWnd, NULL, NULL, NULL);
// Subclassing the container window.
if (!g_Container || !SetWindowSubclass(g_Container, WindowProcedure_Container, NULL, NULL))
   exit(EXIT_FAILURE);

// Button 2 is a direct child of the container window.
g_Button2 = CreateWindowExW(NULL, L"BUTTON", L"Button", WS_VISIBLE | WS_CHILD | WS_TABSTOP, 100, 100, 130, 40, g_Container, (HMENU)IDC_BUTTON2, NULL, NULL);
if (!g_Button2)
    exit(EXIT_FAILURE);
  1. Main window procedure and container window procedure definitions:
// Main window procedure:
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    case WM_COMMAND:
    {
       switch (LOWORD(wParam))
       {
       // The main window is the direct parent of button 1, so this message is expected (on click/enter or space key press on focus).
       case IDC_BUTTON1:
       MessageBox(NULL, L"Button 1 clicked", L"Main window", MB_OK);
       return 0;
       // I was not expecting this message to be sent here, but it is, and only when the enter key is pressed.
       // The space key or normal click with the mouse still sends the message to the container window as expected.
       case IDC_BUTTON2:
       MessageBox(NULL, L"Button 2 clicked", L"Main window", MB_OK);
       return 0;
       }
       break;
    }
     // Other unrelated messages handling ...
    default:
       return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
}
// Container window procedure:
LRESULT CALLBACK WindowProcedure_Container(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData)
{
    switch (uMsg)
    {
    case WM_COMMAND:
    {
        switch (LOWORD(wParam))
        {
        // Triggered normally on button click or space key press on focus but not the enter key,
        // The WM_COMMAND message triggered by the enter key is sent to the main window instead of its parent, and I don't know why.
        case IDC_BUTTON2:
        {
            MessageBox(NULL, L"Button 2 clicked", L"Container window", MB_OK);
            return 0;
        }
        default:
            break;
        }
        break;
    }
     // Remove the window subclass callback.
    case WM_NCDESTROY:
    {
        // Removes installed subclass callback from the window.
        if (!RemoveWindowSubclass(hWnd, &WindowProcedure_Container, uIdSubclass))
        {
            exit(EXIT_FAILURE);
        }

        return 0;
    }
    }

    return DefSubclassProc(hWnd, uMsg, wParam, lParam);
}
  1. Message loop
MSG message = { 0 };
while (GetMessageW(&message, NULL, 0, 0))
{
    if (!IsDialogMessageW(g_hWnd, &message))
    {
        TranslateMessage(&message);
        DispatchMessageW(&message);
    }
}

I tried search engines and experiments with Spy++. My first guess was that something blocked the enter key and its message never reached the button, but the Spy++ result says otherwise. It indeed received the key message(KEYDOWN, KEYUP, DLGCODE,..); it just sent the WM_COMMAND message to the main window.

I hit the wall and have no idea what to do next. I endup try to subclass the button and "suppress" everything and implement my own key handling for the button (it worked) But I felt like this issue could be solved easier if I knew the root of the problem.

Anyone know what's causing this behavior? Thank you.

EDIT: Attached single file compile-able code.

Replicate steps: -> Open application -> Use the Tab key to navigate between buttons. Navigate to the second button and press the Enter key. It'll trigger a dialog that shouldn't happen.

Visual Studio 2022: Empty Project Linker Subsystem: Windows C++17

#include <Windows.h>  // Windows API: Winapi header.
#include <Windowsx.h> // Windows API: Winapi marcos.
#include <Uxtheme.h>  // Windows API: Visual themes and styles.
#include <commctrl.h> // Windows API: Common controls.
#include <string>

// Directive: Enable visual style .
#pragma comment(linker,"\"/manifestdependency:type='win32' \
name='Microsoft.Windows.Common-Controls' version='6.0.0.0' \
processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"")

// Directive: Include necessary libs:
#pragma comment(lib, "UxTheme.lib")
#pragma comment(lib, "Comctl32.lib")

// Marcos:
#define IDC_BUTTON1 100
#define IDC_BUTTON2 101

// Global variables:
HINSTANCE g_hInstance; // Global instance.
HWND g_hWnd;           // Main application window.
HWND g_Container;      // Static window (Act like a container)
HWND g_Button1;        // Button 1
HWND g_Button2;        // Button 2

// Forward declarations:
LRESULT CALLBACK WindowProcedure(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
LRESULT CALLBACK WindowProcedure_Container(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData);

// WinMain Entry:
int WINAPI wWinMain(
    _In_ HINSTANCE hInstance,
    _In_opt_ HINSTANCE hPrevInstance,
    _In_ LPWSTR lpCmdLine,
    _In_ int nShowCmd)
{
    // Create my custom window class.
    WNDCLASSW my_class = { 0 };
    my_class.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
    my_class.hCursor = LoadCursorW(NULL, IDC_ARROW);
    my_class.hIcon = LoadIconW(NULL, IDI_APPLICATION);
    my_class.hInstance = hInstance;
    my_class.lpszClassName = L"MyWindowClass";
    my_class.lpfnWndProc = WindowProcedure;
    g_hInstance = hInstance;

    // Register my window class.
    if (!RegisterClassW(&my_class))
    {
        MessageBoxW(NULL, L"Failed to register the window class.", L"", MB_OK | MB_ICONERROR);
        return -1;
    }

    g_hWnd = CreateWindowExW(NULL, L"MyWindowClass", L"Window title", WS_OVERLAPPEDWINDOW,
        200, 200, 400, 350, nullptr, nullptr, hInstance, nullptr);
    if (!g_hWnd)
    {
        MessageBoxW(NULL, L"Failed to create the main window.", L"", MB_OK | MB_ICONERROR);
        return -1;
    }

    ShowWindow(g_hWnd, SW_SHOWDEFAULT);

    // Enter the message loop.
    MSG message = { 0 };
    while (GetMessageW(&message, NULL, 0, 0))
    {
        if (!IsDialogMessageW(g_hWnd, &message))
        {
            TranslateMessage(&message);
            DispatchMessageW(&message);
        }
    }

    return 0;
}

// Procedure definitions:
LRESULT CALLBACK WindowProcedure(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    case WM_CREATE:
    {
        g_Button1 = CreateWindowExW(NULL, WC_BUTTON, L"Button 1", WS_VISIBLE | WS_CHILD | WS_TABSTOP, 5, 5, 130, 40, hWnd, (HMENU)IDC_BUTTON1, NULL, NULL);
        if (!g_Button1)
            return -1;

        // Create the container window(static).
        g_Container = CreateWindowExW(WS_EX_CONTROLPARENT, WC_STATIC, L"", WS_BORDER | WS_VISIBLE | WS_CHILD | SS_NOPREFIX | WS_CLIPCHILDREN,
            5, 55, 300, 200, hWnd, NULL, NULL, NULL);
        // Subclassing the container window.
        if (!g_Container || !SetWindowSubclass(g_Container, WindowProcedure_Container, NULL, NULL))
            return -1;

        // Button 2 is a direct child of the container window.
        g_Button2 = CreateWindowExW(NULL, L"BUTTON", L"Button", WS_VISIBLE | WS_CHILD | WS_TABSTOP, 5, 5, 130, 40, g_Container, (HMENU)IDC_BUTTON2, NULL, NULL);
        if (!g_Button2)
            return -1;

        return 0;
    }
    case WM_COMMAND:
    {
        switch (LOWORD(wParam))
        {
            // The main window is the direct parent of button 1, so this message is expected (on click/enter or space key press on focus).
        case IDC_BUTTON1:
            MessageBox(NULL, L"Button 1 clicked (Expected message)", L"Main window", MB_OK);
            return 0;
            // I was not expecting this message to be sent here, but it is, and only when the enter key is pressed.
            // The space key or normal click with the mouse still sends the message to the container window as expected.
        case IDC_BUTTON2:
            MessageBox(NULL, L"Button 2 clicked (Unexpected message, main window is not direct parent of button 2 but it send the message here?)", L"Main window", MB_OK);
            return 0;

        default:
            break;
        }

        break;
    }
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    default:
        break;
    }

    return DefWindowProcW(hWnd, message, wParam, lParam);
}
LRESULT CALLBACK WindowProcedure_Container(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData)
{
    switch (uMsg)
    {
    case WM_COMMAND:
    {
        switch (LOWORD(wParam))
        {
            // Triggered normally on button click or space key press on focus but not the enter key,
            // The WM_COMMAND message triggered by the enter key is sent to the main window instead of its parent, and I don't know why.
        case IDC_BUTTON2:
        {
            MessageBox(NULL, L"Button 2 clicked (Expected message)", L"Container window", MB_OK);
            return 0;
        }
        default:
            break;
        }
        break;
    }
    // Remove the window subclass callback.
    case WM_NCDESTROY:
    {
        // Removes installed subclass callback from the window.
        if (!RemoveWindowSubclass(hWnd, &WindowProcedure_Container, uIdSubclass))
        {
            exit(EXIT_FAILURE);
        }

        return 0;
    }
    }

    return DefSubclassProc(hWnd, uMsg, wParam, lParam);
}

Upvotes: 1

Views: 167

Answers (1)

0xF5T9
0xF5T9

Reputation: 21

I was calling IsDialogMessageW on the wrong window. Blindly passing the main window to IsDialogMessageW will not achieve the behavior I want and will result in some kind of "message tangling" (I think) that causes unexpected behaviors.

I modified the message loop to catch the key message and make sure it passed the correct window to 'IsDialogMessageW, and now it works fine.

MSG message = { 0 };
while (GetMessageW(&message, NULL, 0, 0))
{
    if (message.message == WM_KEYDOWN)
    {
        switch (message.wParam)
        {
        case VK_TAB:
        {
            if (IsDialogMessageW(g_hWnd, &message))
                continue;

            break;
        }
        case VK_RETURN:
        {
            HWND parent = GetParent(message.hwnd);
            if (IsDialogMessageW((parent == g_hWnd ? g_hWnd : parent), &message))
                continue;

            break;
        }
        default:
            break;
        }
    }

    TranslateMessage(&message);
    DispatchMessageW(&message);
}

Read more: WS_TABSTOP in winapi on edit controls of child windows (marked answer)

Upvotes: 1

Related Questions