Click here to Skip to main content
Licence CPOL
First Posted 8 Jan 2007
Views 59,778
Downloads 2,007
Bookmarked 76 times

Win32 SDK C Autocomplete Combobox Made Easy

By | 21 May 2010 | Article
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 or Tab should set the selection

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.

Now in 2010, I have a little more experience with custom controls and, revisiting this code, saw room for improvement and simplification.

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;

    hCombo = GetParent(GetParent(hwnd));
    if (!IsExtended(hCombo)) hCombo = GetParent(hwnd);

    switch(msg)
    {
        case WM_GETDLGCODE:
            return VK_TAB == wParam ? FALSE : DLGC_WANTALLKEYS;
        case WM_CHAR:
            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 then
    //  the ComboBox Notification = CBN_SELENDCANCEL
    //  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];
    int index = 0;

    // Handle keyboard input
    if (VK_RETURN == ch)
    {
        ComboBox_ShowDropdown(hwnd, FALSE);
        Combo_SetEditSel(hwnd, 0, -1); //selects the entire item
        ComboBox_GetText(hwnd, buf, NELEMS(buf));
        ComboBox_SetCurSel(hwnd, ComboBox_FindStringExact(hwnd, 0, buf));
    }
    else if (VK_BACK == ch)
    {
        //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 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
            ComboBox_SetCurSel(hwnd, index);
            Combo_SetEditSel(hwnd, _tcslen(toFind), -1);
        }
    }
}

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 CBN_SELENDCANCEL and CBN_SELCHANGE notifications:

void FormMain_OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify)
{
    switch (id)
    {
        case IDC_CBDEMO:
        case IDC_CBEXDEMO:
            if( CBN_SELCHANGE==codeNotify||
                CBN_SELENDCANCEL==codeNotify)
            {
                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);
            }
        default: break;
    }
}

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.

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



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

 
You must Sign In to use this message board. (secure sign-in)
 
Search this forum  
 FAQ
    Noise  Layout  Per page   
  Refresh
QuestionAdaptation to CMFCToolBarComboBoxButton (VS2010) [modified] Pinmembergrosenth0:32 27 Dec '11  
AnswerRe: Adaptation to CMFCToolBarComboBoxButton (VS2010) PinmemberDavid MacDermot8:06 27 Dec '11  
GeneralRe: Adaptation to CMFCToolBarComboBoxButton (VS2010) Pinmembergrosenth5:57 29 Dec '11  
GeneralRe: Adaptation to CMFCToolBarComboBoxButton (VS2010) PinmemberDavid MacDermot6:41 29 Dec '11  
GeneralThanks David, but please help me a more bit Pinmembernautilusvn1:32 21 Jan '11  
GeneralRe: Thanks David, but please help me a more bit PinmemberDavid MacDermot7:48 21 Jan '11  
GeneralRe: Thanks David, but please help me a more bit Pinmembernautilusvn12:49 21 Jan '11  
GeneralMy vote of 5 Pinmembermerano9:47 26 Nov '10  
GeneralGetting linking error PinmemberKShekhar4:26 22 Jul '10  
GeneralRe: Getting linking error PinmemberDavid MacDermot5:25 22 Jul '10  
GeneralRe: Getting linking error PinmemberKShekhar2:39 28 Jul '10  
GeneralRe: Getting linking error PinmemberDavid MacDermot4:37 28 Jul '10  
GeneralRe: Getting linking error Pinmembermerano11:32 25 Nov '10  
GeneralRe: Getting linking error PinmemberDavid MacDermot7:24 26 Nov '10  
GeneralRe: Getting linking error Pinmembermerano9:46 26 Nov '10  
GeneralFantastic -> very usefull code PinmemberLadislav Nevery22:58 24 Jul '08  
GeneralThanks PinmemberZocolf20:08 23 Dec '07  
GeneralCompile error. Pinmembermhdu22:51 19 Aug '07  
GeneralRe: Compile error. PinmemberDavid MacDermot5:22 20 Aug '07  
GeneralRe: Compile error. [modified] Pinmembermhdu21:20 20 Aug '07  
GeneralRe: Compile error. PinmemberJ♥M22:33 22 Apr '11  
GeneralVery Nice Code! PinmemberKoderoo6:40 30 Jul '07  
GeneralRe: Very Nice Code! PinmemberDavid MacDermot5:23 31 Jul '07  
GeneralThanks Pinmemberjsanjosem2:43 23 Feb '07  
Generalcomboboxex PinmemberSamJo3:58 1 Feb '07  

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

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Mobile
Web02 | 2.5.120517.1 | Last Updated 21 May 2010
Article Copyright 2007 by David MacDermot
Everything else Copyright © CodeProject, 1999-2012
Terms of Use
Layout: fixed | fluid