Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C

Win32 SDK C Autocomplete Combobox Made Easy

4.84/5 (39 votes)
28 Jun 2012CPOL4 min read 3   6K  
This article describes formatting a non-MFC based combobox.

Sample Image

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:

  1. The ComboBox should auto-complete
  2. The dropdown should self-search
  3. 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.

C++
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);


    // Sub Class and customize the ComboBox
    MakeAutocompleteCombo(hcbDemo);

    // and the ComboBoxEx
    MakeAutocompleteCombo(hcbExDemo);

    // Populate the combobox with choices (they will sort in alphabetical order)
    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"));

    // Populate the ComboBoxEx with choices and images
    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; //Focus to default control
}

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().

C++
void MakeAutocompleteCombo(HWND hComboBox)
{
    // SubClass the combo's Edit control
    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);

    // Set the text limit to standard size
    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.

C++
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:    //Unsubclass the edit control
            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().

C++
static void DoAutoComplete(HWND hwnd, TCHAR ch)
{
    // Note: If user presses VK_RETURN or VK_TAB then
    //  the ComboBox Notification = CBN_SELENDCANCEL and
    //  a call to ComboBox_GetCurSel() will return the canceled index.
    //  If the user presses any other key that causes a selection
    //  and closure of the dropdown then
    //  the ComboBox Notification = CBN_SELCHANGE

    static TCHAR buf[DEFAULT_TXT_LIM];
    static TCHAR toFind[DEFAULT_TXT_LIM];
    static BOOL fMatched = TRUE;
    int index = 0;

    // Handle keyboard input
    if (VK_RETURN == ch)
    {
        ComboBox_ShowDropdown(hwnd, FALSE);
        Combo_SetEditSel(hwnd, 0, -1); //selects the entire item
    }
    else if (VK_BACK == ch)
    {
        if(fMatched)// 27Jan11 - added
        {
            //Backspace normally erases highlighted match
            //  we only want to move the highlighter back a step
            index = ComboBox_GetCurSel(hwnd);
            int bs = LOWORD(ComboBox_GetEditSel(hwnd)) - 1;

            // keep current selection selected
            ComboBox_SetCurSel(hwnd, index);

            // Move cursor back one space to the insertion point for new text
            // and hilight the remainder of the selected match or near match
            Combo_SetEditSel(hwnd, bs, -1);
        }
        else// 27Jan11 - added
        {
            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)) // keep focus on edit box
            SetFocus(ComboBoxEx_GetEditControl(hwnd));

        // Get the substring from 0 to start of selection
        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);

        // Find the first item in the combo box that matches ToFind
        index = ComboBox_FindStringExact(hwnd, -1, toFind);

        if (CB_ERR == index) //no match
        {
            // Find the first item in the combo box that starts with ToFind
            index = Combo_FindString(hwnd, -1, toFind);
        }
        if (CB_ERR != index)
        {
            // Else for match
            fMatched = TRUE;
            ComboBox_SetCurSel(hwnd, index);
            Combo_SetEditSel(hwnd, _tcslen(toFind), -1);
        }
        else // 27Jan11 - Display text that is not in the selected list 
        {
            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:

C++
#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)
{
    // Note: ComboBox_FindString does not work with ComboBoxEx and so it is necessary
    //  to furnish our own version of the function.  We will use this version for
    //  both types of comboBoxes.

    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. 

 

C++
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:  // Item selected from list but moused away
            {
               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:  //Enter pressed or tab
            {
               //
               // Add new item to combolist
               //
               int iLen = ComboBox_GetTextLength(hwndCtl) + 1;

               if (1 < iLen)   // not an empty string
               {
                  TCHAR tofind[iLen];
                  _tmemset(tofind, (TCHAR)0, iLen);
                  ComboBox_GetText(hwndCtl, tofind, iLen);
                  if (CB_ERR == ComboBox_FindStringExact(hwndCtl, -1, tofind))   //No add it
                  {
                     //No it hasn't so add it
                     if (id == IDC_CBDEMO)
                        ComboBox_AddString(hwndCtl, tofind);
                     else
                        ComboboxEx_AddItem(hwndCtl, 0, tofind);

                     ComboBox_SetCurSel(hwndCtl, ComboBox_FindStringExact(hwndCtl, -1, tofind));
                  }

                  //
                  // Update Selected
                  //
                  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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)