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;
class CAutoComboBox : public CComboBox {
public:
enum AutoComboBoxType {
ACB_DROP_DOWN,
ACB_DROP_LIST
};
CAutoComboBox(AutoComboBoxType acbt = ACB_DROP_LIST);
virtual ~CAutoComboBox();
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();
int DeleteString(UINT uRow);
void SetColCount(int iCols);
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;
bool m_bAutoComplete;
bool m_bReverseEntry;
bool m_bEditHeightSet;
bool m_bClosing;
AutoComboBoxType m_autoComboBoxType;
IntVec m_vecEndPixels;
vector<CStringsVec> m_vecRows;
};
Listing take from
autocbox.cpp
...
void
CAutoComboBox::OnEditUpdate()
{
CString line;
CString sMatchedText;
GetWindowText(line);
int iHiLightStart = line.GetLength();
if(line.GetLength() == 0) {
ShowDropDown(FALSE);
SetWindowText(_T(""));
m_iSelectedRow = -1;
m_bAutoComplete = true;
return;
}
if(!m_bAutoComplete) {
m_bAutoComplete = true;
if(m_bReverseEntry) {
SetEditSel(0, 0);
}
return;
}
m_iSelectedRow = FindUniqueMatchedItem(line, sMatchedText);
if(m_iSelectedRow >= 0) {
ShowDropDown(TRUE);
PostMessage(CB_SETCURSEL, m_iSelectedRow, 0);
int iStartChar = 0;
int iEndChar = -1;
if(!m_bReverseEntry) {
iStartChar = line.GetLength();
}
else {
iEndChar = sMatchedText.GetLength() - line.GetLength();
}
PostMessage(CB_SETEDITSEL, 0, MAKELPARAM(iStartChar, iEndChar));
line = sMatchedText;
}
else if(sMatchedText.IsEmpty() && m_autoComboBoxType == ACB_DROP_LIST) {
MessageBeep(MB_ICONEXCLAMATION);
if(!m_bReverseEntry) {
line.Delete(line.GetLength() - 1);
}
else {
line.Delete(0);
}
assert(iHiLightStart > 0);
iHiLightStart -= 1;
SetWindowText(line);
}
else {
ShowDropDown(FALSE);
SetWindowText(line);
}
if(!m_bReverseEntry) {
SetEditSel(iHiLightStart, -1);
}
else {
SetEditSel(0, 0);
}
}