Click here to Skip to main content
Click here to Skip to main content

Win32 SDK C Autocomplete Combobox Made Easy

By , 28 Jun 2012
 

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.

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

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.

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

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:

#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. 

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)

About the Author

David MacDermot
United States United States
Member
No Biography provided

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
Hint: For improved responsiveness ensure Javascript is enabled and choose 'Normal' from the Layout dropdown and hit 'Update'.
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
BugTo make your brilliant stuff work more natually...memberMember 128119326 Jun '12 - 12:20 
GeneralRe: To make your brilliant stuff work more natually...memberDavid MacDermot27 Jun '12 - 5:59 
QuestionAdaptation to CMFCToolBarComboBoxButton (VS2010) [modified]membergrosenth27 Dec '11 - 0:32 
AnswerRe: Adaptation to CMFCToolBarComboBoxButton (VS2010)memberDavid MacDermot27 Dec '11 - 8:06 
GeneralRe: Adaptation to CMFCToolBarComboBoxButton (VS2010)membergrosenth29 Dec '11 - 5:57 
GeneralRe: Adaptation to CMFCToolBarComboBoxButton (VS2010)memberDavid MacDermot29 Dec '11 - 6:41 
GeneralThanks David, but please help me a more bitmembernautilusvn21 Jan '11 - 1:32 
GeneralRe: Thanks David, but please help me a more bitmemberDavid MacDermot21 Jan '11 - 7:48 
GeneralRe: Thanks David, but please help me a more bitmembernautilusvn21 Jan '11 - 12:49 
GeneralMy vote of 5membermerano26 Nov '10 - 9:47 
GeneralGetting linking errormemberKShekhar22 Jul '10 - 4:26 
GeneralRe: Getting linking errormemberDavid MacDermot22 Jul '10 - 5:25 
GeneralRe: Getting linking errormemberKShekhar28 Jul '10 - 2:39 
GeneralRe: Getting linking errormemberDavid MacDermot28 Jul '10 - 4:37 
GeneralRe: Getting linking errormembermerano25 Nov '10 - 11:32 
GeneralRe: Getting linking errormemberDavid MacDermot26 Nov '10 - 7:24 
GeneralRe: Getting linking errormembermerano26 Nov '10 - 9:46 
GeneralFantastic -&gt; very usefull codememberLadislav Nevery24 Jul '08 - 22:58 
GeneralThanksmemberZocolf23 Dec '07 - 20:08 
GeneralCompile error.membermhdu19 Aug '07 - 22:51 
GeneralRe: Compile error.memberDavid MacDermot20 Aug '07 - 5:22 
GeneralRe: Compile error. [modified]membermhdu20 Aug '07 - 21:20 
GeneralRe: Compile error.memberJ♥M22 Apr '11 - 22:33 
GeneralVery Nice Code!memberKoderoo30 Jul '07 - 6:40 
GeneralRe: Very Nice Code!memberDavid MacDermot31 Jul '07 - 5:23 
GeneralThanksmemberjsanjosem23 Feb '07 - 2:43 
GeneralcomboboxexmemberSamJo1 Feb '07 - 3:58 
GeneralRe: comboboxex [modified]memberDavid MacDermot2 Feb '07 - 6:27 
GeneralRe: comboboxexmemberDavid MacDermot16 May '07 - 10:11 
GeneralThanks...memberSp1ff11 Jan '07 - 12:31 
GeneralIncorrect download linksmembernat32_support8 Jan '07 - 8:43 
GeneralRe: Incorrect download linksmemberDavid MacDermot9 Jan '07 - 4:40 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web01 | 2.6.130516.1 | Last Updated 28 Jun 2012
Article Copyright 2007 by David MacDermot
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid