【问题标题】:Set full focus on a button (`SetFocus()` is not enough)将全部焦点设置在按钮上(`SetFocus()` 是不够的)
【发布时间】:2016-03-01 08:30:28
【问题描述】:

有没有办法在 WinAPI 中的按钮(button 窗口类)上设置真正的完全焦点?

SetFocus() 有点设置焦点(按钮获得内部虚线边框),但按钮实际上是部分焦点的,仍然无法使用Enter按下键,只有Spacebar 键有效。同时,如果我使用Tab 键将焦点移动到同级按钮,那么这个同级按钮(以及如果我随后使用Shift+Tab 将焦点返回到它的第一个按钮)将获得 true 焦点(视觉上,不只是 inner 点状焦点边框被添加到真正的焦点按钮,而是它的主要外边框变为 蓝色 [Windows 7]),现在它会按预期对Enter 做出反应。

如何使按钮以编程方式完全聚焦?

三个按钮状态的截图:

为了清晰起见的一些背景:有一个带有多行编辑框的窗口(使用WNDCLASSEX / RegisterClassEx / CreateWindowEx()WS_OVERLAPPEDWINDOW 作为其样式的常规组合创建)和一个多行编辑框(edit 窗口类ES_MULTILINE 风格)和几个按钮。为了使用Tab 键实现键盘导航,我在编辑框的过程中处理WM_KEYDOWN 事件(通过SetWindowLong() 子类化),否则我可以在按钮之间以及从按钮到编辑框之间导航,但不能从编辑中导航框到一个按钮。所有控件都有WS_TABSTOP 样式。当我在具有焦点和插入符号的编辑框上按下 Tab 键时,使用 SetFocus() 设置焦点时,会出现按钮焦点问题。

最少的相关 C++ 代码:

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
    );
}

谢谢。

【问题讨论】:

  • 我们看不到您的代码,但没有什么“全焦点”不存在。只有控件有焦点或没有焦点。
  • 谢谢,我不知道为什么状态二(部分专注,SetFocus() 的实际结果)与状态三(完全专注,预期/期望的结果)不同。就外观和行为而言,差异本身当然存在,这就是我在这里提出问题的原因。
  • 没有“部分专注”的东西。您看到的是使用 DrawFocusRect() 绘制的按钮,没有视觉样式。再次没有人看到你的代码!
  • @JonathanPotter 谢谢,但我有多个按钮,没有一个应该是默认的。或者您的意思是同时动态地使每个焦点按钮成为默认按钮?在发布问题之前,我认为这是一种可能的解决方法。或者这是在按钮之间切换时自然实现的方式?
  • 焦点和默认按钮齐头并进,但它们不是一回事。具有焦点的按钮将获得键盘输入 - 例如空格键将激活它。默认按钮是在您按下返回时激活的按钮。对话框本身管理默认按钮,这就是为什么如果要更改它,需要将DM_SETDEFID 发送到对话框的原因。这不是“解决方法”,而是您应该这样做的方式。

标签: c++ winapi


【解决方案1】:

这个问题并不清楚WS_OVERLAPPEDWINDOW 主窗口是对话框还是子类化以像对话框一样工作(即基于DefDlgProc)。 OP 和后续 cmets 中关于 TAB 导航和 DM_SETDEFID 的一些提示似乎表明它是一个对话框(样式)窗口。

对于对话框,在子控件之间移动输入焦点的正确方法是发送WM_NEXTDLGCTL message,而不是直接调用SetFocus。如文档中所述:

除了 SetFocus 函数 WM_NEXTDLGCTL 执行的操作之外,此消息执行额外的对话框管理操作,更新默认按钮边框,设置默认控件标识符,并自动选择编辑控件的文本(如果目标窗口是编辑控件) .

更多详情How to set focus in a dialog box,包括这部分:

正如对 DM_SETDEFID 函数注释的注释,不小心直接弄乱默认 ID 会导致奇怪的情况,例如带有两个默认按钮的对话框。幸运的是,您很少需要更改对话框的默认 ID。

更大的问题是使用 SetFocus 将焦点推到对话框周围。如果这样做,您将直接进入窗口管理器,绕过对话框管理器。这意味着您可以创建“不可能”的情况,例如将焦点放在按钮上,而该按钮不是默认按钮!

为避免此问题,请勿使用 SetFocus 更改对话框的焦点。而是使用 WM_NEXTDLGCTL 消息。


[编辑] OP 编辑​​后,主窗口变成了常规窗口,而不是对话框。然而,新发布的代码在主窗口被停用和重新激活后不记得焦点子(也不会删除/恢复默认按钮样式),将 VK_TAB 等键盘导航委托给子控件等。这些引入与默认的类似对话框的行为。

为了使主窗口的行为一个对话框(并且做得正确),WNDPROC 需要模仿 DefDlgProc 的相关部分,至少是那些与导航相关的部分。在Dialog Box Programming Considerations 中,需要对此类 WNDPROC 进行特殊处理的消息有:DM_GETDEFIDDM_SETDEFIDWM_ACTIVATEWM_NEXTDLGCTLWM_SHOWWINDOWWM_SYSCOMMAND。完成后,我的原始答案仍然适用。

【讨论】:

  • 谢谢,但看起来WM_NEXTDLGCTLPostMessage(mainWindow, WM_NEXTDLGCTL, TRUE, FALSE)SendMessage(...) 相同)在我的情况下没有任何作用。 Fwiw,我的窗口是用WNDCLASSEX/RegisterClassEx/CreateWindowEx()的常规组合创建的,WS_OVERLAPPEDWINDOW作为它的样式;我怀疑这不是对话。
  • @user5994649 查看文档,您可能需要PostMessage(mainWindow, WM_NEXTDLGCTL, hwndControlToReceiveFocus, TRUE)。至于主窗口,技术上可能(虽然不常见)它会处理特定于对话框的消息而不是对话框本身,但由于您没有显示任何代码,因此很难猜测发生了什么。请关注How to create a Minimal, Complete, and Verifiable example,然后发布必要的代码。
  • 也不起作用。我知道一个最小的测试用例是可取的,但是对于 C++/WinAPI,这有点问题。我特意详细描述了这个问题,以便可能从遇到完全相同问题的人那里获得反馈(我肯定不是世界上唯一遇到过它的人)。
  • @user5994649 你真的需要停止说“不起作用”而不发布代码。你得到的所有建议都是正确的。
  • 不提供minimal reproducible example 是不够的。在这里做起来很简单。描述你的代码会留下误解的空间。我们已经一次又一次地看到它并且对这个问题感到厌烦。如果您确实需要帮助,请发布 MCVE。
猜你喜欢
  • 2011-12-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-03-19
  • 2021-05-21
  • 1970-01-01
相关资源
最近更新 更多