Reputation: 21
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
g_hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, nullptr);
// 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);
// 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);
}
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
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