Click here to Skip to main content
15,860,844 members
Articles / Programming Languages / C

Check It Out, A Simple Win32 SDK Checked Listbox and Combobox

Rate me:
Please Sign up or sign in to vote.
4.90/5 (27 votes)
6 Jun 2017CPOL5 min read 72.6K   8.6K   74   20
Add checkboxes to standard combobox and listbox control
This article describes adding checkboxes to the standard combobox and listbox control.

Contents

Introduction

While I was researching for the Win32 SDK Propertygrid [^] project, I encountered a nice CheckComboBox Control [^] by Magnus Egelberg and decided to code something similar for my ANSI C99 projects. That done, I decided to throw in a custom checked listbox, making a two·fer.

Update: Have a look at my latest treatment of this topic here [^] where I describe a different approach to customizing these controls including support for item user data.

The Checked Combobox

Demo screenshot

Using this control is fairly straight-forward. First, being a custom control, it is necessary to initialize it before use.

C++
int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, 
		LPSTR lpszCmdLine, int nCmdShow)
{
  INITCOMMONCONTROLSEX icc;
  WNDCLASSEX wcx;

  ghInstance = hInstance;

  icc.dwSize = sizeof(icc);
  icc.dwICC = ICC_WIN95_CLASSES;
  InitCommonControlsEx(&icc);

  InitCheckedComboBox(hInstance);

  //
  // Do other stuff
  //

Messages and Macros for the Checked Combobox

Configure the control to do what you want using Windows messages. This control employs the standard combobox messages / macros with a few exceptions.

Using ComboBox_SetText() or sending WM_SETTEXT explicitly to set the control's text will not work since the text displayed in the control is governed by the selections in the dropdown. Also, I have added two messages CBCM_FLATCHECKS and CBCM_CHECKALL to enable some display / behavior customization.

I have created the following macros to be used in addition to the standard Combobox macros when configuring this control. If you prefer to call SendMessage() or PostMessage() explicitly, please refer to the macro defs in the header for usage.

CheckedComboBox_SetCheckState

Checks or unchecks an item in a checked combobox control.

C++
INT CheckedComboBox_SetCheckState(
   HWND hwndCtl
   INT iIndex
   BOOL fCheck
   );
/* Parameters
hwndCtl
   Handle of a checked combobox.
iIndex
   The zero-based index of the item for which to set the check state.
fCheck
   A value that is set to TRUE to select the item, or FALSE to deselect it.

Return Values
   The zero-based index of the item in the combobox. If an error occurs,
   the return value is CB_ERR.*/

CheckedComboBox_GetCheckState

Gets the checked state of an item in a checked combobox control.

C++
BOOL CheckedComboBox_GetCheckState(
   HWND hwndCtl
   INT iIndex
   );
/* Parameters
hwndCtl
   Handle of a checked combobox.
iIndex
   The zero-based index of the item for which to get the check state.

Return Values
   Nonzero if the given item is checked, or zero otherwise.*/

CheckedComboBox_SetFlatStyleChecks

Sets the appearance of the checkboxes.

C++
BOOL CheckedComboBox_SetFlatStyleChecks(
   HWND hwndCtl
   BOOL fFlat
   );
/* Parameters
hwndCtl
   Handle of a checked combobox.
fFlat
   TRUE for flat checkboxes, or FALSE for standard checkboxes.

Return Values
   No return value.*/

CheckedComboBox_EnableCheckAll

Sets the select/deselect all feature.

C++
BOOL CheckedComboBox_SetFlatStyleChecks(
   HWND hwndCtl
   BOOL fEnable
   );
/* Parameters
hwndCtl
   Handle of a checked combobox.
fEnable
   TRUE enables right mouse button select/deselect all feature, or FALSE disables feature.

Return Values
   No return value.*/

Checked Combobox Notifications

The Checked combobox notification messages are the same as those sent by a standard combobox. The parent window of the checked combobox receives notification messages through the WM_COMMAND message. Here is an example of a closeup notification.

C++
CBN_CLOSEUP
idComboBox = (int) LOWORD(wParam); 	// identifier of checked combobox
hwndComboBox = (HWND) lParam;    	// handle of checked combobox 

The Checked Listbox

Demo screenshot

Like the Checked combobox, using this control is fairly straight-forward. Again, being a custom control, it is necessary to initialize it before use.

C++
int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, 
		LPSTR lpszCmdLine, int nCmdShow)
{
  INITCOMMONCONTROLSEX icc;
  WNDCLASSEX wcx;

  ghInstance = hInstance;

  icc.dwSize = sizeof(icc);
  icc.dwICC = ICC_WIN95_CLASSES;
  InitCommonControlsEx(&icc);

  InitCheckedListBox(hInstance);

  //
  // Do other stuff
  //

Messages and Macros for the Checked Listbox

Configure the control to do what you want using Windows messages. This control employs the standard listbox messages / macros with a few exceptions.

I have added two messages LBCM_FLATCHECKS and LBCM_CHECKALL to enable some display / behavior customization.

I have created the following macros to be used in addition to the standard Listbox macros when configuring this control. If you prefer to call SendMessage() or PostMessage() explicitly, please refer to the macro defs in the header for usage.

CheckedListBox_SetCheckState

Checks or unchecks an item in a checked listbox control.

C++
INT CheckedListBox_SetCheckState(
   HWND hwndCtl
   INT iIndex
   BOOL fCheck
   );
/* Parameters
hwndCtl
   Handle of a checked listbox.
iIndex
   The zero-based index of the item for which to set the check state.
fCheck
   A value that is set to TRUE to select the item, or FALSE to deselect it.

Return Values
   The zero-based index of the item in the combobox. If an error occurs,
   the return value is LB_ERR.*/

CheckedListBox_GetCheckState

Gets the checked state of an item in a checked listbox control.

C++
BOOL CheckedListBox_GetCheckState(
   HWND hwndCtl
   INT iIndex
   );
/* Parameters
hwndCtl
   Handle of a checked listbox.
iIndex
   The zero-based index of the item for which to get the check state.

Return Values
   Nonzero if the given item is checked, or zero otherwise.*/

CheckedListBox_SetFlatStyleChecks

Sets the appearance of the checkboxes.

C++
BOOL CheckedListBox_SetFlatStyleChecks(
   HWND hwndCtl
   BOOL fFlat
   );
/* Parameters
hwndCtl
   Handle of a checked listbox.
fFlat
   TRUE for flat checkboxes, or FALSE for standard checkboxes.

Return Values
   No return value.*/

CheckedListBox_EnableCheckAll

Sets the select/deselect all feature.

C++
BOOL CheckedListBox_EnableCheckAll(
   HWND hwndCtl
   BOOL fEnable
   );
/* Parameters
hwndCtl
   Handle of a checked listbox.
fEnable
   TRUE enables right mouse button select/deselect all feature, or FALSE disables feature.

Return Values
   No return value.*/

Checked Listbox Notifications

The Checked listbox notification messages are the same as those sent by a standard listbox with one exception. I have added the LBCN_ITEMCHECK notification to indicate that the check state of an item in the list has changed. The parent window of the checked listbox receives notification messages through the WM_COMMAND message. Here is the itemcheck notification.

C++
LBCN_ITEMCHECK
idListBox = (int) LOWORD(wParam); // identifier of checked listbox
hwndListBox = (HWND) lParam;    // handle of checked listbox

Design Considerations (Examples from Checked Combo)

MFC includes mechanisms to reflect ownerdraw messages back to the child control effectively making them self draw messages. Joseph M. Newcomer provides a nice explanation of how it all works here [^] (see section "Reflected Messages"). Since I am not using MFC and didn't have this method at my disposal, I considered simply subclassing a combobox and calling the FORWARD_ message cracker macro to send WM_DRAWITEM and WM_MEASUREITEM back to my control's subclassed proc.

In the end, I discarded this approach in favor of encapsulating the combobox in an invisible window, this makes the custom control easier to implement but at the cost of one extra window handle per instance. In order to achieve this invisible window approach, I needed to define a custom window class, handle WM_CREATE and WM_SIZE to create the child combobox sized to fill the parent, and finally, route messages transparently to and from the child I was wrapping.

Here, I define the custom control window class and register it.

C++
ATOM InitCheckedComboBox(HINSTANCE hInstance)
{
  WNDCLASSEX wcex;

  // Get standard combobox information
  wcex.cbSize = sizeof(wcex);
  if (!GetClassInfoEx(NULL, WC_COMBOBOX, &wcex))
    return 0;

  // Add our own stuff
  wcex.lpfnWndProc = (WNDPROC)Control_Proc;
  wcex.hInstance = hInstance;
  wcex.lpszClassName = g_szClassName;

  // Register our new class
  return RegisterClassEx(&wcex);
}

Here's the Control's proc with self access to WM_DRAWITEM and WM_MEASUREITEM, notice the default: case call to method DefaultHandler(), I'll get to that in a moment.

C++
static LRESULT CALLBACK Control_Proc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
  switch (msg)
  {
    HANDLE_MSG(hwnd, WM_DRAWITEM, Control_OnDrawItem);
    HANDLE_MSG(hwnd, WM_MEASUREITEM, Control_OnMeasureItem);
    HANDLE_MSG(hwnd, WM_CREATE, Control_OnCreate);
    HANDLE_MSG(hwnd, WM_DESTROY, Control_OnDestroy);
    HANDLE_MSG(hwnd, WM_SIZE, Control_OnSize);
    HANDLE_MSG(hwnd, WM_GETTEXT, Control_OnGetText);
    HANDLE_MSG(hwnd, WM_GETTEXTLENGTH, Control_OnGetTextLength);
    case WM_SETTEXT:
      return 0; // Text to be set by drop down selection only.
    case CBCM_FLATCHECKS:
    {
      DWORD dwUserData = (DWORD)GetWindowLongPtr(GetDlgItem(hwnd, ID_COMBOBOX),
                      GWLP_USERDATA);
      if (FALSE != (BOOL)wParam)
        dwUserData |= FLATCHECKS;
      else
        dwUserData &= ~FLATCHECKS;

      return SetWindowLongPtr(GetDlgItem(hwnd, ID_COMBOBOX),
            GWLP_USERDATA, (LONG_PTR)dwUserData);
    }
    case CBCM_CHECKALL:
    {
      DWORD dwUserData = (DWORD)GetWindowLongPtr(GetDlgItem(hwnd, ID_COMBOBOX),
                      GWLP_USERDATA);
      if (FALSE != (BOOL)wParam)
        dwUserData |= CHECKALL;
      else
        dwUserData &= ~CHECKALL;

      return SetWindowLongPtr(GetDlgItem(hwnd, ID_COMBOBOX),
            GWLP_USERDATA, (LONG_PTR)dwUserData);
    }
    default:
      return DefaultHandler(hwnd, GetDlgItem(hwnd, ID_COMBOBOX), msg, wParam, lParam);
  }
}

Here's the WM_CREATE and handler where I achieve (simulated) invisibility of the custom control's window by stripping it of any borders, and sizing it to the static portion of the combobox child. The child now determines the look and feel of the control.

C++
BOOL Control_OnCreate(HWND hwnd, LPCREATESTRUCT lpCreateStruct)
{
  HWND hCombo;

  // Remove CBS_OWNERDRAWVARIABLE if defined and add the bits we need
  lpCreateStruct->style &= ~((DWORD)CBS_OWNERDRAWVARIABLE);

  // Use default strings. We need the itemdata to store checkmarks
  lpCreateStruct->style |= (CBS_DROPDOWNLIST | CBS_OWNERDRAWFIXED | CBS_HASSTRINGS);

  hCombo = CreateWindowEx(lpCreateStruct->dwExStyle, WC_COMBOBOX, NULL,
         lpCreateStruct->style, 0, 0,
         lpCreateStruct->cx, lpCreateStruct->cy, hwnd,
         (HMENU)ID_COMBOBOX, lpCreateStruct->hInstance, NULL);

  if (!hCombo)
    return FALSE;

  SendMessage(hCombo, WM_SETFONT, (WPARAM)GetStockObject(DEFAULT_GUI_FONT), 0);

  // Subclass combobox and save the old proc
  SetProp(hCombo, WPROC, (HANDLE)GetWindowLongPtr(hCombo, GWLP_WNDPROC));
  SubclassWindow(hCombo, Combo_Proc);

  // Configure the parent window to be invisible,
  // combobox child to determine appearance.
  SetWindowLongPtr(hwnd, GWL_STYLE, WS_CHILD |
     (WS_TABSTOP & GetWindowLongPtr(hwnd, GWL_STYLE) ? WS_TABSTOP : 0));

  SetWindowLongPtr(hwnd, GWL_EXSTYLE, 0l);

  // Certain window data is cached, so changes you make using SetWindowLongPtr
  // will not take effect until you call the SetWindowPos() function. SWP_FRAMECHANGED
  // causes the window to recalculate the non client area and, in our case,
  // remove scroll bar and border.
  RECT rc = {0};
  GetClientRect(hCombo, &rc); // Client = just the static field of the combo
  SetWindowPos(hwnd, NULL, lpCreateStruct->x, lpCreateStruct->y,
     lpCreateStruct->cx, rc.bottom - rc.top,
     SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED);

  // Create and store a circular buffer (for Join())
  SetProp(hwnd, PROPSTORAGE, calloc(2, sizeof(LPTSTR)));

  return TRUE;
}

And the WM_SIZE handler where the control's height is pegged to the height of the static portion of the combobox child.

C++
VOID Control_OnSize(HWND hwnd, UINT state, INT cx, INT cy)
{
  HWND hCombo = GetDlgItem(hwnd, ID_COMBOBOX);
  RECT rc = {0};
  GetClientRect(hCombo, &rc); // Client = just the field of the combo

  //Size comboBox component to fill parent
  SetWindowPos(hCombo, NULL, 0, 0, cx, rc.bottom - rc.top,
    SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
}

Finally, I route most messages transparently to the child control but not all messages. Some messages originate from the child so routing them back will result in a loop until they overflow the stack. Other messages or notifications are intended to be handled by the parent of the hidden window. These exceptions are handled nicely by the following method:

C++
static LRESULT DefaultHandler
	(HWND hwnd, HWND hChild, UINT msg, WPARAM wParam, LPARAM lParam)
{
  switch(msg)
  {
    // These messages to be handled by this window
    case WM_DRAWITEM:
    case WM_MEASUREITEM:
    case WM_CREATE:
    case WM_DESTROY:
    case WM_SIZE:

    // Sending these to child will cause stack overflow
    case WM_CTLCOLORMSGBOX:
    case WM_CTLCOLOREDIT:
    case WM_CTLCOLORLISTBOX:
    case WM_CTLCOLORBTN:
    case WM_CTLCOLORDLG:
    case WM_CTLCOLORSCROLLBAR:
    case WM_CTLCOLORSTATIC:
    case WM_MOUSEACTIVATE:

    // Sending these to child will cause improper sizing / positioning
    case WM_WINDOWPOSCHANGING:
    case WM_WINDOWPOSCHANGED:
    case WM_NCCALCSIZE:

    // Sending this to child will mess up child paint
    case WM_PAINT:
      break; //<- End Fallthrough

    // Pass child notifications to parent
    case WM_COMMAND:
      FORWARD_WM_COMMAND(GetParent(hwnd), GetDlgCtrlID(hwnd), hwnd,
         HIWORD(wParam), SNDMSG);
       return 0;
    case WM_NOTIFY:
      ((LPNMHDR)lParam)->hwndFrom = hwnd;
      ((LPNMHDR)lParam)-&idFrom = GetDlgCtrlID(hwnd);
      return FORWARD_WM_NOTIFY(GetParent(hwnd), ((LPNMHDR)lParam)->idFrom,
        (LPNMHDR)lParam, SNDMSG);

    default: // The rest of the messages passed to child (if it exists)
    {
      if(NULL != hChild)
        return SNDMSG(hChild, msg, wParam, lParam);
    }
  }
  return DefWindowProc(hwnd, msg, wParam, lParam);
}

The checked listbox shares this same basic architecture but was, of course, simpler to implement.

Final Comments

I documented this source with Doxygen [^] comments for those that might find it helpful or useful. Your feedback is appreciated.

History

  • 24th September, 2010: Version 1.0.0.0
  • 6th June, 2017: Version 1.1.0.0 - Added support for disabled check box item

License

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


Written By
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionAnother problem: Controls steal LPARAM data from us Pin
Javene Mcgowan1-Nov-21 20:56
Javene Mcgowan1-Nov-21 20:56 
AnswerRe: Another problem: Controls steal LPARAM data from us Pin
David MacDermot6-Nov-21 13:49
David MacDermot6-Nov-21 13:49 
GeneralRe: Another problem: Controls steal LPARAM data from us Pin
Javene Mcgowan28-Feb-23 10:39
Javene Mcgowan28-Feb-23 10:39 
GeneralRe: Another problem: Controls steal LPARAM data from us Pin
David MacDermot28-Feb-23 20:46
David MacDermot28-Feb-23 20:46 
QuestionAnother problem/ pitfall with your widgets - Reviving win32 Pin
Javene Mcgowan1-Nov-21 12:01
Javene Mcgowan1-Nov-21 12:01 
QuestionExcellent But Here's a suggestion - Reviving Win32 API Pin
Javene Mcgowan31-Oct-21 17:17
Javene Mcgowan31-Oct-21 17:17 
QuestionVirusTotal flags as trojan the precompiled EXE file Pin
Hugo González Castro30-Nov-18 3:40
professionalHugo González Castro30-Nov-18 3:40 
AnswerRe: VirusTotal flags as trojan the precompiled EXE file Pin
David MacDermot9-Dec-18 6:32
David MacDermot9-Dec-18 6:32 
GeneralRe: VirusTotal flags as trojan the precompiled EXE file Pin
Hugo González Castro10-Dec-18 12:15
professionalHugo González Castro10-Dec-18 12:15 
QuestionHow to get item checked with only one click? Pin
neuron228-Mar-18 4:24
neuron228-Mar-18 4:24 
QuestionDisable some checkboxes Pin
Zahi Mashael23-May-17 4:48
professionalZahi Mashael23-May-17 4:48 
AnswerRe: Disable some checkboxes Pin
David MacDermot4-Jun-17 21:01
David MacDermot4-Jun-17 21:01 
NewsRe: Disable some checkboxes Pin
David MacDermot6-Jun-17 21:05
David MacDermot6-Jun-17 21:05 
Questionerror.how to compile it?use which tool? Pin
Member 1116326523-Oct-14 23:59
Member 1116326523-Oct-14 23:59 
AnswerRe: error.how to compile it?use which tool? Pin
David MacDermot24-Oct-14 4:51
David MacDermot24-Oct-14 4:51 
GeneralC99 rather than Visual C++... Pin
Member 219045725-Sep-14 1:16
Member 219045725-Sep-14 1:16 
GeneralExcellent! Pin
Hans Dietrich30-Sep-10 2:08
mentorHans Dietrich30-Sep-10 2:08 
GeneralRe: Excellent! Pin
David MacDermot30-Sep-10 5:45
David MacDermot30-Sep-10 5:45 
GeneralRe: Excellent! Pin
Hans Dietrich30-Sep-10 19:10
mentorHans Dietrich30-Sep-10 19:10 
GeneralRe: Excellent! Pin
David MacDermot1-Oct-10 8:02
David MacDermot1-Oct-10 8:02 
Nice color picker and article. I miss that one when it was published.

Yes, some things are a little harder, mainly strings. (Now that I think about it, I should have included a little piece in the article concerning the Join() function I employed in the combobox control.)

I find these C controls interesting little challenges and fun. If I had a big project and a deadline to meet, that would be another matter as frustration might supplant fun. There is a certain appeal to C, I can't put my finger on it.

Perhaps one of these days I'll publish an article in C++, C#, or VB.NET. It'd probably have a broader appeal.

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

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