23W
23W

Reputation: 1540

Invalid selection in Combo Box with several identical items

I have combo-box created with style CBS_DROPDOWN. This combo box contains several items with names, for example:

As you see the second and third items have identical names. And its required by task. When user opens combo's list box and select third item, its name is copied to edit part of combo box and my class is received CBN_SELCHANGE notification. I send message CB_GETCURSEL and receive selected item's index equal to "2" (zero based numeration). And in this stage everything is good.

But, when user opens combo's list box second time, Combo box shows as selected second item (with index "1")! My code hasn't received any notification about item selection changes, so why combo shows incorrect selection?

If I changed combo box style from CBS_DROPDOWN to CBS_DROPDOWNLIST, it will work fine. But I'm need to work with CBS_DROPDOWN.

How to fix it?

Upvotes: 1

Views: 273

Answers (2)

23W
23W

Reputation: 1540

Great thanks to Zett42. His idea about combo's listbox subclassing is excellent. And it works. This is my implementation of his method in ATL.

template<typename TBase>
class CComboDDownBox
    : public TBase
    , protected ATL::CMessageMap
{
public:

    CComboDDownBox() 
        : m_lb(_T(""), this, m_lbMapId)
        , m_parent(_T(""), this, m_parentMapId)
        , m_blockSelection(false)
    {};

    virtual ~CComboDDownBox() {};

public:

    bool InitLB()
    {
        COMBOBOXINFO info = { sizeof(COMBOBOXINFO), 0 };
        bool res = ::GetComboBoxInfo(TBase::operator HWND(), &info) != FALSE;
        if (res)
        {
            res = (::GetWindowLong(TBase::operator HWND(), GWL_STYLE) & CBS_DROPDOWN) == CBS_DROPDOWN;
            if (res)
            {
                res = m_lb.SubclassWindow(info.hwndList) != FALSE;
                if (res)
                {
                    res = m_parent.SubclassWindow(::GetParent(TBase::operator HWND())) != FALSE;
                }
            }
        }

        return res;
    }

protected:

    BEGIN_MSG_MAP(CComboDDownBox<TBase>)
    ALT_MSG_MAP(m_lbMapId)
        MESSAGE_HANDLER(LB_SETCURSEL, OnLbSetCurSel)
    ALT_MSG_MAP(m_parentMapId)
        COMMAND_CODE_HANDLER(CBN_DROPDOWN, OnCbDropDown)
        COMMAND_CODE_HANDLER(CBN_CLOSEUP, OnCbCloseUpOrSelectionChanged)
        COMMAND_CODE_HANDLER(CBN_SELCHANGE, OnCbCloseUpOrSelectionChanged)
    END_MSG_MAP()

    LRESULT OnLbSetCurSel(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& bHandled)
    {
        if (!m_blockSelection)
        {
            bHandled = FALSE;
        }

        return LB_ERR;
    }

    LRESULT OnCbDropDown(WORD /*wNotifyCode*/, WORD /*wID*/, HWND hWndCtl, BOOL& bHandled)
    {
        if (hWndCtl == TBase::operator HWND())
        {
            m_blockSelection = true;
        }

        bHandled = FALSE;
        return 0;
    }

    LRESULT OnCbCloseUpOrSelectionChanged(WORD /*wNotifyCode*/, WORD /*wID*/, HWND hWndCtl, BOOL& bHandled)
    {
        if (hWndCtl == TBase::operator HWND())
        {
            m_blockSelection = false;
        }

        bHandled = FALSE;
        return 0;
    }

private:

    static const DWORD      m_lbMapId = 1;
    static const DWORD      m_parentMapId = 2;

    ATL::CContainedWindow   m_lb;
    ATL::CContainedWindow   m_parent;

    bool                    m_blockSelection;
};

You can use this template in your dialogues, for example in MFC:

class CMyDialog 
    : public CDialog
{
public:

    // .... Other methods ...

    virtual BOOL OnInitDialog() 
    {
        CDialog::OnInitDialog();

        m_combo.InitLB();

        return TRUE;
    }

private:
    CComboDDownBox<CComboBox> m_combo;
};

Upvotes: 1

zett42
zett42

Reputation: 27776

When you open the listbox, the control selects the first listbox item that begins with the text shown in the edit control. After all, the text could have been entered by the user. How could the control know which of the two listbox items are meant by the user?

Simple solution - add a suffix to the duplicate items like "Item B (1)", "Item B (2)" and so on to make them unique.

If this is not possible, you could subclass the combo's listbox and prevent it from handling selection change requests of the combobox.

To do that, put the following code in the CBN_DROPDOWN notification handler for the combobox:

COMBOBOXINFO info{ sizeof(info) };
GetComboBoxInfo( hwndOfComboBox, &info );
SetWindowSubclass( info.hwndList, ComboLBoxProc, 0, 0 );

ComboLBoxProc is a callback function that could look like this:

LRESULT CALLBACK ComboLBoxProc( HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData )
{
    switch( uMsg )
    {
        case LB_SETCURSEL:
            return LB_ERR;  // to prevent selection change
    }

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

With the above code, the user would still be able to change selection in the listbox, but the text entered in the edit control will no longer be selected in the listbox. If you want to keep this feature, you could handle the CBN_EDITCHANGE notification of the combobox and set a flag that you would check in ComboLBoxProc. In this case you would allow the default processing of LB_SETCURSEL. After a selection has been made in the listbox (CBN_SELCHANGE), reset this flag.

This is a bit of a hack so I would propably go with the "simple solution" of adding suffixes to the duplicates.

Upvotes: 2

Related Questions