0
votes

Is there a way to set true full focus on a push button (button window class) in WinAPI?

SetFocus() somewhat sets focus (the button gets an inner dotted border), but the button is actually partially focused and still cannot be pressed with the Enter key, only the Spacebar key works. At the same time, if I move focus to a sibling button with the Tab key, then this sibling button (as well as the first button if I then return focus to it using Shift+Tab) gets a true focus (visually, not just inner dotted focus border is added to the really focused button, but its main outer border becomes blue [Windows 7]), and now it reacts to Enter as intended.

How to make a button such fully focused programmatically?

Screenshot of the three button states:

Screenshot of the three button states

Some background for clarity: there is a window (created with the regular combination of WNDCLASSEX / RegisterClassEx / CreateWindowEx() with WS_OVERLAPPEDWINDOW as its style) with a multiline edit box (edit window class with ES_MULTILINE style) and several push buttons. To implement keyboard navigation using the Tab key, I process the WM_KEYDOWN event in the edit box's procedure (subclassed via SetWindowLong()), otherwise I could navigate between buttons and from buttons to the edit box, but not from the edit box to a button. All the controls have WS_TABSTOP style. The issue with the button focus takes place when I set focus using SetFocus() when the Tab key is pressed on the edit box having focus and caret.

Minimal relevant C++ code:

HWND mainWindow;
WNDPROC defaultEditCallback = NULL;

int WINAPI WinMain(HINSTANCE instance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
    WNDCLASSEXW wc;

    wchar_t windowClass[] = L"testcase";

    wc.cbSize        = sizeof(WNDCLASSEXW);
    wc.style         = 0;
    wc.lpfnWndProc   = mainWindowCallback;
    wc.cbClsExtra    = 0;
    wc.cbWndExtra    = 0;
    wc.hInstance     = instance;
    wc.hIcon         = NULL;
    wc.hCursor       = LoadCursorW(NULL, IDC_ARROW);
    wc.hbrBackground = (HBRUSH) (COLOR_BTNFACE + 1);
    wc.lpszMenuName  = NULL;
    wc.lpszClassName = (LPCWSTR)windowClass;
    wc.hIconSm       = NULL;

    RegisterClassExW(&wc);

    mainWindow = CreateWindowW(
        (LPCWSTR)windowClass, (LPCWSTR)windowClass, WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT, 400, 200, NULL, NULL, instance, NULL
    );

    HWND edit = CreateWindowExW(
        WS_EX_CLIENTEDGE, (LPCWSTR)L"edit", NULL,
        WS_CHILD | WS_VISIBLE | WS_VSCROLL | ES_LEFT | ES_MULTILINE | WS_TABSTOP,
        0, 0, 0, 0, mainWindow, (HMENU) 10,
        (HINSTANCE) GetWindowLongPtrW(mainWindow, GWLP_HINSTANCE),
        NULL
    );

    defaultEditCallback = (WNDPROC)SetWindowLongPtrW(edit, GWLP_WNDPROC, (LONG)editCallback);

    HWND firstButton  = createButton(mainWindow, 20, L"First",  buttonWidth, buttonHeight);
    HWND secondButton = createButton(mainWindow, 30, L"Second", buttonWidth, buttonHeight);
    HWND thirdButton  = createButton(mainWindow, 40, L"Third",  buttonWidth, buttonHeight);

    // [Skipped] Sizing and positioning controls.

    ShowWindow(mainWindow, nCmdShow);
    UpdateWindow(mainWindow);

    MSG msg;

    while (GetMessageW(&msg, NULL, 0, 0) > 0) {
        if (!IsDialogMessage(mainWindow, &msg)) {
            TranslateMessage(&msg);
            DispatchMessageW(&msg);
        }
    }

    return (int)msg.wParam;
}

LRESULT CALLBACK mainWindowCallback(HWND window, UINT msg, WPARAM wParam, LPARAM lParam) {
    if (WM_DESTROY == msg) {
        PostQuitMessage(0);
    }

    return DefWindowProcW(window, msg, wParam, lParam);
}

LRESULT CALLBACK editCallback(HWND control, UINT msg, WPARAM wParam, LPARAM lParam) {
    if (WM_KEYDOWN == msg && VK_TAB == wParam) {
        HWND next = GetNextDlgTabItem(mainWindow, control, (int)(GetKeyState(VK_SHIFT) & 0x8000));
        SetFocus(next);
        return 0;
    }

    return CallWindowProc(defaultEditCallback, control, msg, wParam, lParam);
}


HWND createButton(HWND parentWindow, int id, wchar_t* caption, int width, int height) {
    return CreateWindowW(
        (LPCWSTR)L"button", (LPCWSTR)caption, WS_VISIBLE | WS_CHILD | WS_TABSTOP,
        0, 0, width, height, parentWindow, (HMENU)id, NULL, NULL
    );
}

Thanks.

1
We cannot see your code, but nothing as "full focus" not exists. Only control has focus or not.user2120666
Thanks, I have no idea why the state two (partially focused, the actual result of SetFocus()) is different from the state three (fully focused, the expected/desired result). The difference itself certainly exists, both in terms of apperarance and in terms of behavior, that's why I've asked the question here.user5994649
Nothing as "partially focused" not exists. What you see is button painted with DrawFocusRect() without visual style. Again nobody see your code!user2120666
@JonathanPotter Thanks, but I have multiple buttons, no one of them should be default. Or do you mean to dynamically make each focused button the default button at the same time? I thought of this as a possible workaround before posting the question. Or is this the way it's naturally implemented when tabbing between buttons?user5994649
Focus and default button go hand in hand but they are not the same thing. The button with focus will get keyboard input - e.g. the space bar will activate it. The default button is the one that's activated when you push return. The dialog itself manages the default button, which is why you need to send DM_SETDEFID to the dialog if you want to change it. This isn't a "workaround", it's how you are supposed to do it.Jonathan Potter

1 Answers

4
votes

The question does not make it clear whether the WS_OVERLAPPEDWINDOW main window is either a dialog, or subclassed to work like a dialog (i.e. based on DefDlgProc). Some hints about TAB navigation and DM_SETDEFID in the OP and following comments appear to indicate that it's a dialog(-styled) window.

For dialogs, the correct way to move the input focus between child controls is by sending the WM_NEXTDLGCTL message, rather than calling SetFocus directly. As noted in the docs:

This message performs additional dialog box management operations beyond those performed by the SetFocus function WM_NEXTDLGCTL updates the default pushbutton border, sets the default control identifier, and automatically selects the text of an edit control (if the target window is an edit control).

More details at How to set focus in a dialog box, including this part:

As the remarks to the DM_SETDEFID function note, messing directly with the default ID carelessly can lead to odd cases like a dialog box with two default buttons. Fortunately, you rarely need to change the default ID for a dialog.

A bigger problem is using SetFocus to shove focus around a dialog. If you do this, you are going directly to the window manager, bypassing the dialog manager. This means that you can create “impossible” situations like having focus on a pushbutton without that button being the default!

To avoid this problem, don’t use SetFocus to change focus on a dialog. Instead, use the WM_NEXTDLGCTL message.


In order to make the main window behave like a dialog (and do it right), the WNDPROC would need to mimic the relevant parts of DefDlgProc, at least those that pertain to navigation. From Dialog Box Programming Considerations, among the messages such a WNDPROC would need special handling for are DM_GETDEFID, DM_SETDEFID, WM_ACTIVATE, WM_NEXTDLGCTL, WM_SHOWWINDOW, WM_SYSCOMMAND. Once that's done, my original answer still applies.