Introduction
I like to use combo boxes in my applications, but let's face it: the out-of-the-box ComboBox leaves something to be desired. Our friends in the VB.NET community have provided several articles dealing with ComboBox customizations, but there is not much out there for those of us using plain old C.
I wanted to enhance the functionality of a combobox in one of my applications. It needed to meet the following criteria:
- The ComboBox
should auto-complete - The dropdown
should self-search - Hitting Enter should set the selection or add a new item to the list
The initial version of the code accomplished all of this, but only for the ComboBox
and not for ComboBoxEx
. In July of 2007, I had some time to revisit this and, after doing some research and some experimentation, I came up with a version that worked equally well for both types of Combo Boxes.
In 2010 I made some improvements to the code and recently updated it again to support adding items to the list.
Usage
A look at AutoCombo.h yields only one method prototype: MakeAutocompleteCombo()
. I use it when handling the WM_INITDIALOG
message. This single method is used to turn a ComboBox
or a ComboBoxEx
into an auto-completing self-searcher.
BOOL FormMain_OnInitDialog(HWND hwnd, HWND hwndFocus, LPARAM lParam)
{
hlblSelected=GetDlgItem(hwnd,LBL_SELECTEDTXT);
hcbDemo=GetDlgItem(hwnd,IDC_CBDEMO);
hlblSelected1=GetDlgItem(hwnd,LBL_SELECTEDTXT1);
hcbExDemo=GetDlgItem(hwnd,IDC_CBEXDEMO);
MakeAutocompleteCombo(hcbDemo);
MakeAutocompleteCombo(hcbExDemo);
ComboBox_AddString(hcbDemo,_T("Andrew"));
ComboBox_AddString(hcbDemo,_T("Angela"));
ComboBox_AddString(hcbDemo,_T("Bill"));
ComboBox_AddString(hcbDemo,_T("Bob"));
ComboBox_AddString(hcbDemo,_T("Jack"));
ComboBox_AddString(hcbDemo,_T("Jill"));
ComboBox_AddString(hcbDemo,_T("Vickie"));
HIMAGELIST hList = ImageList_Create(16,16,ILC_COLOR|ILC_MASK,1,1);
int iImage = ImageList_AddIcon(hList,LoadIcon(ghInstance,
MAKEINTRESOURCE(IDR_ICO_MAIN)));
ComboBoxEx_SetImageList(hcbExDemo,hList);
ComboboxEx_AddItem(hcbExDemo,iImage,_T("Andrew"));
ComboboxEx_AddItem(hcbExDemo,iImage,_T("Angela"));
ComboboxEx_AddItem(hcbExDemo,iImage,_T("Bill"));
ComboboxEx_AddItem(hcbExDemo,iImage,_T("Bob"));
ComboboxEx_AddItem(hcbExDemo,iImage,_T("Jack"));
ComboboxEx_AddItem(hcbExDemo,iImage,_T("Jill"));
ComboboxEx_AddItem(hcbExDemo,iImage,_T("Vickie"));
return TRUE; }
That's all there is to it! Now I ask you: how much easier can it get?
Points of Interest
All of the customization is taking place in AutoCombo.c. Let's take a little tour, starting with MakeAutocompleteCombo()
.
void MakeAutocompleteCombo(HWND hComboBox)
{
HWND hEdit = IsExtended(hComboBox) ?
ComboBoxEx_GetEditControl(hComboBox) :
FindWindowEx(hComboBox, NULL, WC_EDIT, NULL);
SetProp(hEdit, TEXT("Wprc"), (HANDLE)GetWindowLongPtr(hEdit, GWLP_WNDPROC));
SubclassWindow(hEdit, ComboBox_Proc);
ComboBox_LimitText(hComboBox, DEFAULT_TXT_LIM);
}
Combo boxes are composite controls consisting of an edit control and a list box control packaged together. This makes sub-classing a bit tricky because keyboard messages for the child components are routed to each child and may not be exposed outside the package. I found that I did not have access to the WM_CHAR
messages of the edit control, so it was necessary to sub-class that component, but it turned out that it was not necessary to subclass the parent ComboBox.
The next challenge consisted of getting the handle of the component edit control. ComboBoxEx
has a message/macro for this: ComboBoxEx_GetEditControl()
. Unfortunately, the simple ComboBox
doesn't have anything like this. We know that the edit control is in there, but where?
FindWindowEx()
to the rescue! This handy function will search the children of a given window and return a match. In this case, it matches the class name WC_EDIT
.
Now, let's move on to the callback procedure.
static LRESULT CALLBACK ComboBox_Proc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
static HWND hCombo;
switch (msg)
{
case WM_GETDLGCODE:
return VK_TAB == wParam ? FALSE: DLGC_WANTALLKEYS;
case WM_CHAR:
hCombo = GetParent(GetParent(hwnd));
if (!IsExtended(hCombo))
hCombo = GetParent(hwnd);
DoAutoComplete(hCombo, (TCHAR)wParam);
break;
case WM_DESTROY: SetWindowLongPtr(hwnd, GWLP_WNDPROC, (DWORD)GetProp(hwnd, TEXT("Wprc")));
RemoveProp(hwnd, TEXT("Wprc"));
break;
default:
return CallWindowProc((WNDPROC)GetProp(hwnd, TEXT("Wprc")), hwnd, msg, wParam, lParam);
}
return FALSE;
}
All messages from the sub-classed controls are handled here. In this case, that means the edit components of our ComboBox
or ComboBoxEx
. I am interested in WM_CHAR
messages primarily, but what about WM_GETDLGCODE
? In times past, I did not understand this message, and it would seem that there was an error in the early documentation for it. More recent documentation on MSDN states that the wParam
of the message contains the virtual key pressed by the user that initiates the message. Armed with this knowledge, I was able to request all keys, which gave me a WM_CHAR
for the return key, yet I could detect the tab key and return FALSE
, leaving that key unhandled so that the default behavior would result.
At this point, the control will send WM_CHAR
messages in response to all keyboard input that I might be interested in handling. Each time a key is pressed, I call DoAutoComplete()
.
static void DoAutoComplete(HWND hwnd, TCHAR ch)
{
static TCHAR buf[DEFAULT_TXT_LIM];
static TCHAR toFind[DEFAULT_TXT_LIM];
static BOOL fMatched = TRUE;
int index = 0;
if (VK_RETURN == ch)
{
ComboBox_ShowDropdown(hwnd, FALSE);
Combo_SetEditSel(hwnd, 0, -1); }
else if (VK_BACK == ch)
{
if(fMatched) {
index = ComboBox_GetCurSel(hwnd);
int bs = LOWORD(ComboBox_GetEditSel(hwnd)) - 1;
ComboBox_SetCurSel(hwnd, index);
Combo_SetEditSel(hwnd, bs, -1);
}
else {
toFind[_tcslen(toFind) -1] = 0;
ComboBox_SetText(hwnd, toFind);
Combo_SetEditSel(hwnd, -1, -1);
FORWARD_WM_KEYDOWN(hwnd, VK_END, 0, 0, SNDMSG);
}
}
else if (!_istcntrl(ch))
{
BOOL status = GetWindowLongPtr(hwnd, GWL_STYLE) & CBS_DROPDOWN;
if (status)
ComboBox_ShowDropdown(hwnd, TRUE);
if (IsExtended(hwnd)) SetFocus(ComboBoxEx_GetEditControl(hwnd));
ComboBox_GetText(hwnd, buf, NELEMS(buf));
buf[LOWORD(ComboBox_GetEditSel(hwnd))] = 0;
_stprintf(toFind, NELEMS(toFind),
#ifdef _UNICODE
_T("%ls%lc"),
#else
_T("%s%c"),
#endif
buf, ch);
index = ComboBox_FindStringExact(hwnd, -1, toFind);
if (CB_ERR == index) {
index = Combo_FindString(hwnd, -1, toFind);
}
if (CB_ERR != index)
{
fMatched = TRUE;
ComboBox_SetCurSel(hwnd, index);
Combo_SetEditSel(hwnd, _tcslen(toFind), -1);
}
else {
fMatched = FALSE;
ComboBox_SetText(hwnd, toFind);
Combo_SetEditSel(hwnd, _tcslen(toFind), -1);
FORWARD_WM_KEYDOWN(hwnd, VK_END, 0, 0, SNDMSG);
}
}
}
Here I use the ComboBox
messaging macros to respond to keyboard input. There are some subtle adaptations to this code in order to support ComboBoxEx
. This was the result of quite a bit of trial and error. Notice the line following the call to ComboBox_ShowDropdown()
. It is necessary to reset focus within the ComboBoxEx
control to the edit box sub-component! Without this step, focus tends to shift to the dropdown and we loose keyboard input, causing some strange and unexpected behavior.
In order to support both types of Combo Boxes while keeping the code simple, I employed the following helper functions and macros:
#define Combo_SetEditSel(hwndCtl, ichStart, ichEnd) IsExtended(hwndCtl) ? \
(Edit_SetSel(ComboBoxEx_GetEditControl(hwndCtl),ichStart,ichEnd),0) : \
(ComboBox_SetEditSel(hwndCtl,ichStart,ichEnd),0)
static BOOL IsExtended(HWND hwndCtl)
{
static TCHAR buf[MAX_PATH];
GetClassName(hwndCtl, buf, MAX_PATH);
return 0 == _tcsicmp(buf, WC_COMBOBOXEX);
}
static int Combo_FindString(HWND hwndCtl, INT indexStart, LPTSTR lpszFind)
{
TCHAR lpszBuffer[DEFAULT_TXT_LIM];
TCHAR tmp[DEFAULT_TXT_LIM];
int ln = _tcslen(lpszFind) + 1;
if (ln == 1 || indexStart > ComboBox_GetCount(hwndCtl))
return CB_ERR;
for (int i = indexStart == -1 ? 0 : indexStart; i < ComboBox_GetCount(hwndCtl); i++)
{
ComboBox_GetLBText(hwndCtl, i, lpszBuffer);
lstrcpyn(tmp, lpszBuffer, ln);
if (!_tcsicmp(lpszFind, tmp))
return i;
}
return CB_ERR;
}
Below is a snippet from the demo where I handle the ComboBox's notifications in order to add and select new items or update selections to existing selected items.
void FormMain_OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify)
{
switch (id)
{
case IDC_CBDEMO:
case IDC_CBEXDEMO:
switch (codeNotify)
{
case CBN_SELCHANGE:
case CBN_SELENDOK: {
TCHAR buf[MAX_PATH] = { 0 };
int idx = ComboBox_GetCurSel(hwndCtl);
if (-1 != idx)
ComboBox_GetLBText(hwndCtl, idx, buf);
if (hcbDemo == hwndCtl)
Static_SetText(hlblSelected, buf);
else
Static_SetText(hlblSelected1, buf);
}
break;
case CBN_SELENDCANCEL: {
int iLen = ComboBox_GetTextLength(hwndCtl) + 1;
if (1 < iLen) {
TCHAR tofind[iLen];
_tmemset(tofind, (TCHAR)0, iLen);
ComboBox_GetText(hwndCtl, tofind, iLen);
if (CB_ERR == ComboBox_FindStringExact(hwndCtl, -1, tofind)) {
if (id == IDC_CBDEMO)
ComboBox_AddString(hwndCtl, tofind);
else
ComboboxEx_AddItem(hwndCtl, 0, tofind);
ComboBox_SetCurSel(hwndCtl, ComboBox_FindStringExact(hwndCtl, -1, tofind));
}
if (hcbDemo == hwndCtl)
Static_SetText(hlblSelected, tofind);
else
Static_SetText(hlblSelected1, tofind);
}
}
}
}
}
Final Comments
I documented this source with Doxygen [^] comments for those who might find it helpful or useful. Your feedback is appreciated.
History
- Initial release: January 5, 2007 - Version 1.0.0.0.
- Update: May 14, 2007 - Version 2.0.0.0 -- Now supports
ComboBoxEx
! - Update: May 18, 2010 - Version 3.0.0.0 -- Refactored to support Win64 and Unicode, updated article text.
- Update: June 28, 2012 - Version 4.0.0.0 -- Reworked the demo to reduce buggy behavior, added the ability to enter a new item into the comboboxes.