Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Multicolumn Autocompleting Combobox

0.00/5 (No votes)
13 Jan 2004 1  
A Combobox that will open up when a unique match is found and can contain more than one column

Introduction

A combo box that has the auto completing feature and also allows multiple columns. It was derived from Chris Maunder's CComboCompletion.

Version 1.2 fixes the following problems:

  • The size problem, courtesy of Magnus Egelberg and his CheckCombobox
  • The AddRow() routine is now overloaded to make it easier to add rows.
  • The SetColCount(UINT) is no longer needed.
  • DeleteString(), an overload of a CComboBox routine, has been superseded with DeleteRow().
  • The odd reversion to a substring is also fixed by removing the extra close-up on receipt of an "Enter" key.

This combobox will handle text strings that start with the same characters and only signal a match when it is unique. i.e.. Consider the box filled with the strings:

    0
    1
    100
    1234

On typing the "1", this is a unique match for the second item so the combobox will open up and highlight the one. If you continue with the "0" then the combobox will again open and display the 100 except that the last zero will be highlighted since that is where the next character is going to go. Typing in a "2" after the "1" will select the "1234" with the "34" highlighted. Multiple columns are supported so that the combobox has to be Owner Draw, Variable, with Strings. The combobox can be given the attributes of a dropdown or a dropdown list. In the latter, characters that are entered that don't match one of the first column strings will beep and be rejected. Creating the combobox is by a line in the dialog that is usually supplied by the MS Class Wizard for the class associated with the combobox when it is inserted in the dialog:

class CMyDialog : CDialog {
    ...
    CAutoCombobox m_myAutoCombobox;
    ...
};
CMyDialog::CMyDialog(...)
 : m_myAutoCombobox(ACB_DROP_LIST) { }

or use ACB_DROP_DOWN instead. In the following there is the following definition.

    typedef vector<CString> CStringsVec;
    typedef vector<int> IntVec;

The special purpose member functions are as follows:

CAutoComboBox::AddRow(const TCHAR* pString);
CAutoComboBox::AddRow(const CStringsVec& allCols);
CAutoCombobox::AddRow(const TCHAR* pString, CStringsVec& extraCols);

These routines are overloaded to add the strings in each row. The first one just adds the text for one column if a single column autocomplete is needed. The second allows the strings for all the columns to be inserted in a vector of strings. The first string in the vector will be added to the actual combobox itself and the rest will be held in an internal member strings vector. The third one is for backward compatibility and requires the separation of the first column from the strings for the extra columns. The first time a row is added, the number of columns is remembered and all subsequent additions must have exactly the same number of column strings. If a column is empty, use the empty string to fill the space (_T("")).

CAutoCombobox::DeleteRow(int iRow) 

This will delete the row and the corresponding extra text from the internal vector.

CAutoCombobox::GetSelectedRow() const 

The selected row can be retrieved via this function. If no row has been uniquely identified, the return will be -1.

CAutoCombobox::ResetSelection() 

The current selection is cancelled and we can start the match from scratch again

CAutoCombobox::ResetContent() 

All the rows are removed from the combobox and the internal arrays so you can start filling from scratch again.

CAutoCombobox::SetReverseEntry(bool bReverse = true) 

In my case, I had a requirement to allow for the entry of the strings from right to left instead of left to right. It turns out that for a list of numbers, the variability is usually more in the low order digits, so one in general, doesn't have to enter as many characters to get the unique match. It's odd though to enter the 100 as a "0" followed by a "0", followed by the "1".

CAutoCombobox::DeleteString(UINT uRow) 

This will delete the row and the corresponding extra text from the internal vector. This routine is deprecated an should be replaced by DeleteRow(int).

CAutoCombobox::SetColCount(int iCols) 

This routine is not needed any more so its use is deprecated. The column count is mow established from the first AddRow(...) call. If used, this routine must be called before any rows are added since it effectively sizes the extra strings vector.

The sample project loads the combobox with two columns and a set of numeric strings. It is an MDI app and has been tested with MS Visual Studio .NET 2003 and Windows XP. (There is a left over .dsw file from the VS6.0 implementation that also appears to work!) Selecting the menu item Test (^A shortcut) will open the dialog that has been filled with strings. Enter a 3 will open up the box and show the row with a "3" selected. Now continue to enter 453 and the box will open up again, now showing the selection as the row with 345337. Tabbing from this will keep the 345337 in the box. On pushing OK, the MDI app shows the selection in the main window via Item:nnnn.

I needed a match that was case insensitive so used the CString::CompareNoCase(...). If a case sensitive match is needed, change the two references to the comparison function to just use CString::Compare(...).

I removed the close up of the dropdown list when an "Enter" key was seen. The intent of this was to allow a single key stroke to both close up the dropdown list and also trigger the default button of the containing dialog. The normal behavior for a combobox in a dialog is that the first "Enter" closes up the list and a second "Enter" triggers the default action, two key strokes in all. If this extra close-up is implemented on a Win95/98/ME OS, then a problem shows up. Sometimes the string will revert to a previously matched substring. This is due to a bug in the Microsoft implementation of the ComboBox object. There is no problem with NT or XP. The reversion only happens sometimes and always when the string has first matched a short string and has followed up matching a longer string. In the above list of columns, entering a "1" will select the "1" row. Now entering a "0", will select the "100" row. Clicking outside the combobox will close up the dropdown but the selection will go back to the "1" for some unknown reason. Funnily enough, if the "1" is followed by the "0" to highlight the "100" and then the up arrow/down arrow is used to select the next row and back to the "100", the change of focus will leave the "100" in place without the reversion to the "1". It doesn't happen with the "3" followed by "2" which selects the "323513" as shown. Changing the focus away from the "323513" selected leaves this in place even though there is a "3" by itself in the list. The Microsoft incident number that documents this bug is SRX020705601306. According to Microsoft there is no work around so for this reason the close up has been removed in this new version!

The meat of the control is in the OnEditUpdate() routine shown below. When a unique match is found the combobox is dropped down and the item in question is shown highlighted.

The PreTranslateMsg(...) has to test for a backspace or a delete and cancel the autocomplete for the next character. If this isn't done, it's not possible to correct a mistake as the system keeps on filling in the next characters. This is described better in Chris Maunder's CComboCompletion example.

The listing needs the header file to show the variable definitions listed next and then the OnEditUpdate() code follows. The user entered text line is obtained via the GetWindowText() function and is modified when a match is found through SetWindowText() and SetEditSel().

Listing taken from autocbox.h

using std::vector;
typedef vector<Int> IntVec;
typedef vector<CString> CStringsVec;

////////////////////////////////////////////////////////////////////////

//

// CAutoComboBox window

//

class CAutoComboBox : public CComboBox {

public:
    // define the two types to emulate

    enum AutoComboBoxType {
        ACB_DROP_DOWN,
        ACB_DROP_LIST
    };
    CAutoComboBox(AutoComboBoxType acbt = ACB_DROP_LIST);
    virtual ~CAutoComboBox();

    // ClassWizard generated virtual function overrides

private:
    virtual BOOL PreTranslateMessage(MSG* pMsg);
    virtual void DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct);
    virtual void MeasureItem(LPMEASUREITEMSTRUCT lpMeasureItemStruct);

public:
    int GetSelectedRow();
    int AddRow(const TCHAR* pString, const CStringsVec& extraCols);
    int AddRow(const CStringsVec& cols);
    int AddRow(const TCHAR* pString);
    int DeleteRow(int iRow);
    void SetReverseEntry(bool bReverse) { m_bReverseEntry = bReverse; }
    void GetLBText(int nIndex, CString& rString) const;
    void ResetSelection();
    void ResetContent();
    // the following routines are deprecated!!

    int DeleteString(UINT uRow);        // use DeleteRow(...)

    void SetColCount(int iCols);        // not needed any more


protected:
    afx_msg void OnEditUpdate();
    afx_msg void OnSelChange();
    afx_msg void OnCbnDropdown();

    DECLARE_MESSAGE_MAP()
private:
    int FindUniqueMatchedItem(const CString& line, CString& matchedLine);
    int SetDroppedWidthForTextLengths();
    bool LineInString(const CString& rsLine, const CString& rsString) const;
    int m_iSelectedRow;
    int m_iHorizontalExtent;    // list box width for all columns

    bool m_bAutoComplete;       // false after delete or backspace

    bool m_bReverseEntry;       // enter characters in reverse

    bool m_bEditHeightSet;      // true when we've set the height of the box

    bool m_bClosing;            // stops error in selection on enter

    AutoComboBoxType m_autoComboBoxType;
    IntVec m_vecEndPixels;      // ending positions for each column

    vector<CStringsVec> m_vecRows;// row text for each col after the first

};
Listing take from autocbox.cpp
...
void
CAutoComboBox::OnEditUpdate() 
{
    CString line;               // partial line entered by user

    CString sMatchedText;       // holds full line from list


    // get the text from the user input

    GetWindowText(line);
    int iHiLightStart = line.GetLength();

    // ?? remove

    // TRACEFN(_T("Entering OnEditUpdate line:%s\n"), (LPCTSTR) line);


    // if the line is empty

    if(line.GetLength() == 0) {
        // make sure the dropdown list is closed when back to

        // zero characters in the edit box

        ShowDropDown(FALSE);

        // empty the selection

        SetWindowText(_T(""));

        // cancel any previous selection

        m_iSelectedRow = -1;

        // turn on autocomplete again turned off by deletes

        m_bAutoComplete = true;
        return;
    }
    // if we have seen a delete or back key we leave the text alone so

    // that the user can continue to go backwards to correct an entry

    if(!m_bAutoComplete) {
        // but only for one character

        m_bAutoComplete = true;

        // if reversing entry, must keep insertion point on right

        if(m_bReverseEntry) {
            SetEditSel(0, 0);
        }
        return;
    }
    // if a single match, we can display that and identify the mismatch

    m_iSelectedRow = FindUniqueMatchedItem(line, sMatchedText);

    // if we found a unique matching row

    if(m_iSelectedRow >= 0) {

        // drop down the list part of the combo box

        ShowDropDown(TRUE);

        // ?? remove

        // TRACE(_T("OnEditUpdate: ShowDropDown(true)\n"));


        // set the dropdown to show the item as well

        // ?? int iCurSel = SetCurSel(m_iSelectedRow);

        // assert(iCurSel >= 0);

        // we try this so that the selection occurs AFTER the dropdown

        // box has been filled

        PostMessage(CB_SETCURSEL, m_iSelectedRow, 0);

        // ?? remove

        // TRACE(_T("OnEditUpdate-Post: SetCurSel(%d)\n"), m_iSelectedRow);


        // now we have to also remove the selection from the edit box

        // since we may want to continue to add to the string and

        // set out own highlight on the text we have added from the match

        //

        int iStartChar = 0;
        int iEndChar = -1;

        if(!m_bReverseEntry) {
            // straight entry, we want to highlight the added text from

            // the end of the input line text to the end of the field

            iStartChar = line.GetLength();
        }
        else {
            // reverse entry, we want to highlight the added text

            // on the left - from char 0 to match less line length

            iEndChar = sMatchedText.GetLength() - line.GetLength();
        }
        PostMessage(CB_SETEDITSEL, 0, MAKELPARAM(iStartChar, iEndChar));

        // ?? remove

        // TRACE(_T("OnEditUpdate-Post: SetEditSel(%d, %d)\n")

        //  , iStartChar, iEndChar);


        // set the window text to the match

        line = sMatchedText;
    }
    // if this text won't match any string and we are emulating

    // a drop list so the selection is forced

    else if(sMatchedText.IsEmpty() && m_autoComboBoxType == ACB_DROP_LIST) {

        // alert the user to no match

        MessageBeep(MB_ICONEXCLAMATION);

        // if we are not entering text backwards

        if(!m_bReverseEntry) {
            // remove the last character typed in error. at the end

            line.Delete(line.GetLength() - 1);
        }
        else {
            // remove the last character typed in error (at the beginning)

            // which will be in the firt position

            line.Delete(0);
        }
        assert(iHiLightStart > 0);
        iHiLightStart -= 1;

        // restore the line since the closeup cleared the edit box

        SetWindowText(line);

        // ?? remove

        // TRACE(_T("OnEditUpdate: SetWindowText(%s)\n"), (LPCSTR) line);

    }
    else {
        // the list box will be closed to show that there is no match

        ShowDropDown(FALSE);

        // ?? remove

        // TRACE(_T("OnEditUpdate: ShowDropDown(false)\n"));


        // restore the line since the closeup cleared the edit box

        SetWindowText(line);

        // ?? remove

        // TRACE(_T("OnEditUpdate: SetWindowText(%s)\n"), (LPCSTR) line);

    }
    // if we are not entering text backwards

    if(!m_bReverseEntry) {
        // now set the selection to beyond the last character which

        // moves the caret to the end

        SetEditSel(iHiLightStart, -1);

        // ?? remove

        // TRACE(_T("OnEditUpdate: SetEditSel(%d, -1)\n"), iHiLightStart);

    }
    else {
        // now set the insertion caret to the beginning so we

        // build up the text from the right

        SetEditSel(0, 0);

        // ?? remove

        // TRACE(_T("OnEditUpdate: SetEditSel(0, 0)\n"));

    }
    // ?? remove

    // TRACE(_T("EditUpdate SelectedRow: %d Line: %s Matched Text: %s\n")

    // , m_iSelectedRow, (LPCTSTR) line, (LPCTSTR) sMatchedText);

}

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here