2
votes

I've got a problem with an invalidly drawn ComboBox using WinAPI. When you minimize the application and resume it after the selection of the ComboBox control is not hidden, it looks like this:

enter image description here

As you can see the OK button has the focus but yet the ComboBox's selection is still not hidden. The normal behavior of the ComboBox hides the selection when the control loses the input focus.

Code:

#define WIN32_MEAN_AND_LEAN

#include <SDKDDKVer.h>
#include <Windows.h>
#include <Windowsx.h>
#include <CommCtrl.h>
#include <assert.h>

struct window_context {
    HINSTANCE _instance;
    HWND _window;

    HWND _combo_box2;
    HWND _ok_button;

    window_context(HINSTANCE instance) noexcept : _instance{ instance }
    {
    }
};

static BOOL on_create(HWND hwnd, LPCREATESTRUCT lpCreateStruct) noexcept
{
    auto context = reinterpret_cast<window_context*>(lpCreateStruct->lpCreateParams);

    context->_combo_box2 = CreateWindowW(WC_COMBOBOXW,
                                        L"",
                                        CBS_DROPDOWN | CBS_AUTOHSCROLL | WS_TABSTOP | WS_VSCROLL | WS_VISIBLE | WS_CHILD,
                                        0, 0, 0, 0,
                                        hwnd,
                                        (HMENU)44,
                                        nullptr,
                                        nullptr);
    ComboBox_AddString(context->_combo_box2, L"select me");

    context->_ok_button = CreateWindowW(WC_BUTTONW,
                                        L"OK",
                                        BS_DEFPUSHBUTTON | BS_NOTIFY | WS_TABSTOP | WS_VISIBLE | WS_CHILD,
                                        0, 0, 0, 0,
                                        hwnd,
                                        nullptr,
                                        nullptr,
                                        nullptr);

    return true;
}

static void on_destroy(HWND hwnd) noexcept
{
    PostQuitMessage(0);
}

static void on_size(HWND hwnd, UINT state, int cx, int cy) noexcept
{
    auto context = reinterpret_cast<window_context*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));

    MoveWindow(context->_combo_box2,
            10,
            10,
            100,
            100,
            true); // causes the weird behaviour to occur
                   // using SetWindowPos doesn't help
                   // using SetWindowPos with SWP_NOSIZE prevents the bug from occuring
                   // but also locks it into size which is not acceptable
                   // since I use this to deal with DPI scaling changes

    MoveWindow(context->_ok_button,
            10,
            110,
            100,
            50,
            true);
}

static void on_activate(HWND hwnd, UINT state, HWND hwndActDeact, BOOL fMinimized) noexcept
{
    auto context = reinterpret_cast<window_context*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));

    if (state)
        SetFocus(context->_ok_button);
}

static BOOL on_nc_create(HWND hwnd, LPCREATESTRUCT lpCreateStruct) noexcept
{
    auto context = reinterpret_cast<window_context*>(lpCreateStruct->lpCreateParams);

    context->_window = hwnd;
    SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(context));

    return FORWARD_WM_NCCREATE(hwnd, lpCreateStruct, DefWindowProcW);
}

static LRESULT CALLBACK wnd_proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) noexcept
{
    switch (uMsg) {
        HANDLE_MSG(hwnd, WM_CREATE, on_create);
        HANDLE_MSG(hwnd, WM_DESTROY, on_destroy);
        HANDLE_MSG(hwnd, WM_SIZE, on_size);
        HANDLE_MSG(hwnd, WM_ACTIVATE, on_activate);
        HANDLE_MSG(hwnd, WM_NCCREATE, on_nc_create);
    }

    return DefWindowProcW(hwnd, uMsg, wParam, lParam);
}

static bool register_class(HINSTANCE hInstance) noexcept
{
    WNDCLASSEXW wcex;
    wcex.cbSize = sizeof(wcex);
    wcex.style = CS_HREDRAW | CS_VREDRAW;
    wcex.lpfnWndProc = &wnd_proc;
    wcex.cbClsExtra = 0;
    wcex.cbWndExtra = 0;
    wcex.hInstance = hInstance;
    wcex.hIcon = nullptr;
    wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
    wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
    wcex.lpszMenuName = nullptr;
    wcex.lpszClassName = L"test";
    wcex.hIconSm = nullptr;

    return RegisterClassExW(&wcex) != 0;
}

static bool create_window(window_context& context) noexcept
{
    return CreateWindowExW(0,
                        L"test",
                        L"Win32 Program",
                        WS_OVERLAPPEDWINDOW,
                        CW_USEDEFAULT,
                        CW_USEDEFAULT,
                        800,
                        600,
                        nullptr,
                        nullptr,
                        context._instance,
                        &context);
}

int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
                    _In_opt_ HINSTANCE hPrevInstance,
                    _In_ LPWSTR    lpCmdLine,
                    _In_ int       nCmdShow)
{
    if (!register_class(hInstance))
        return false;

    window_context context{ hInstance };

    if (!create_window(context))
        return false;

    ShowWindow(context._window, nCmdShow);

    MSG msg;
    BOOL got_message;

    while ((got_message = GetMessageW(&msg, nullptr, 0, 0)) && got_message != -1) {
        if (IsDialogMessageW(context._window, &msg))
            continue;

        TranslateMessage(&msg);
        DispatchMessageW(&msg);
    }

    return static_cast<int>(msg.wParam);
}

Steps to reproduce the problem:

  • select select me from the ComboBox
  • minimize the window
  • restore the window

The ComboBox should now appear selected even though it is not.

Steps taken to fix the problem:

  • Invalidating the ComboBox's area does not help, redrawing the combobox still doesn't fix the issue
  • The issue does not appear when you just create the ComboBox at a specified place and never move its position or change its size
  • The issue does not appear when you just move the ComboBox's position using SetWindowPos
  • As soon as you change the size of the ComboBox, this weird behaviour appears.
  • Dialogs somehow handle this properly
  • WinForms handles this properly aswell but I failed to find what they do to prevent it
  • The bug only appears if you selected an item from dropdown list, when you type in custom text the bug disappears

Surely enough this is a highly exotic problem, I'm very thankful for any help.

1
Doesn't that just show that the button is the default button ? Does the button handler get called if you push the space bar, or do you have to hit Enter ? - Sid S
The button is completely fine, it is drawn as it should be with the default button style. And yes handlers get called although in this example I omitted them because they don't contribute to the point. Enter causes IDOK to be sent to WM_COMMAND and space will press the focused button, but my issue is the painting of the combobox which appears selected even though it is not :( When I traverse through the controls it gets fixed though but yeah never the first time. - hl3mukkel
@Sid: Yes, the thick blue outline indicates that the button is the default button. But the XOR-styled focus rectangle indicates that it also is the focused control (so yes, spacebar will also activate it). - Andreas Rejbrand
Why do you think you need to override WM_ACTIVATE at all? Let Windows take care of restoring the focus. For that you should propably subclass the dialog window class (WC_DIALOG or "#32770") whose default WM_ACTIVATE should do the right thing. Last, using SetFocus() in a dialog is almost always wrong, because it doesn't care about default button state. Use WM_NEXTDLGCTL instead. - zett42

1 Answers

1
votes

You can fix this problem by calling ComboBox_SetEditSel(context->_combo_box2, -1, -1); (see CB_SETEDITSEL) Or find the selected characters in combo box's edit control before resize, then restore those values after resize.

static void on_size(HWND hwnd, UINT, int, int) noexcept
{
    auto context=reinterpret_cast<window_context*>(GetWindowLongPtr(hwnd,GWLP_USERDATA));

    DWORD range = ComboBox_GetEditSel(context->_combo_box2);
    DWORD start = LOWORD(range);
    DWORD end = HIWORD(range);

    MoveWindow(context->_combo_box2, 10, 10, 150, 100, true);
    MoveWindow(context->_ok_button, 10, 110, 100, 50, true);

    ComboBox_SetEditSel(context->_combo_box2, start, end);
}

However you may find that you will run in to similar problems with other controls, it will be difficult to find a fix for everything. Consider using DialogBox instead.