This article describes adding checkboxes to the standard combobox and listbox control.
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.
Using this control is fairly straight-forward. First, being a custom control, it is necessary to initialize it before use.
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);
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.
Checks or unchecks an item in a checked combobox
control.
INT CheckedComboBox_SetCheckState(
HWND hwndCtl
INT iIndex
BOOL fCheck
);
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.*/
Gets the checked state of an item in a checked combobox
control.
BOOL CheckedComboBox_GetCheckState(
HWND hwndCtl
INT iIndex
);
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.*/
Sets the appearance of the checkboxes.
BOOL CheckedComboBox_SetFlatStyleChecks(
HWND hwndCtl
BOOL fFlat
);
hwndCtl
Handle of a checked combobox.
fFlat
TRUE for flat checkboxes, or FALSE for standard checkboxes.
Return Values
No return value.*/
Sets the select/deselect all feature.
BOOL CheckedComboBox_SetFlatStyleChecks(
HWND hwndCtl
BOOL fEnable
);
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.*/
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.
CBN_CLOSEUP
idComboBox = (int) LOWORD(wParam); hwndComboBox = (HWND) lParam;
Like the Checked combobox, using this control is fairly straight-forward. Again, being a custom control, it is necessary to initialize it before use.
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);
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.
Checks or unchecks an item in a checked listbox control.
INT CheckedListBox_SetCheckState(
HWND hwndCtl
INT iIndex
BOOL fCheck
);
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.*/
Gets the checked state of an item in a checked listbox control.
BOOL CheckedListBox_GetCheckState(
HWND hwndCtl
INT iIndex
);
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.*/
Sets the appearance of the checkboxes.
BOOL CheckedListBox_SetFlatStyleChecks(
HWND hwndCtl
BOOL fFlat
);
hwndCtl
Handle of a checked listbox.
fFlat
TRUE for flat checkboxes, or FALSE for standard checkboxes.
Return Values
No return value.*/
Sets the select/deselect all feature.
BOOL CheckedListBox_EnableCheckAll(
HWND hwndCtl
BOOL fEnable
);
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.*/
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.
LBCN_ITEMCHECK
idListBox = (int) LOWORD(wParam); hwndListBox = (HWND) lParam;
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.
ATOM InitCheckedComboBox(HINSTANCE hInstance)
{
WNDCLASSEX wcex;
wcex.cbSize = sizeof(wcex);
if (!GetClassInfoEx(NULL, WC_COMBOBOX, &wcex))
return 0;
wcex.lpfnWndProc = (WNDPROC)Control_Proc;
wcex.hInstance = hInstance;
wcex.lpszClassName = g_szClassName;
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.
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; 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.
BOOL Control_OnCreate(HWND hwnd, LPCREATESTRUCT lpCreateStruct)
{
HWND hCombo;
lpCreateStruct->style &= ~((DWORD)CBS_OWNERDRAWVARIABLE);
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);
SetProp(hCombo, WPROC, (HANDLE)GetWindowLongPtr(hCombo, GWLP_WNDPROC));
SubclassWindow(hCombo, Combo_Proc);
SetWindowLongPtr(hwnd, GWL_STYLE, WS_CHILD |
(WS_TABSTOP & GetWindowLongPtr(hwnd, GWL_STYLE) ? WS_TABSTOP : 0));
SetWindowLongPtr(hwnd, GWL_EXSTYLE, 0l);
RECT rc = {0};
GetClientRect(hCombo, &rc); SetWindowPos(hwnd, NULL, lpCreateStruct->x, lpCreateStruct->y,
lpCreateStruct->cx, rc.bottom - rc.top,
SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED);
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.
VOID Control_OnSize(HWND hwnd, UINT state, INT cx, INT cy)
{
HWND hCombo = GetDlgItem(hwnd, ID_COMBOBOX);
RECT rc = {0};
GetClientRect(hCombo, &rc);
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:
static LRESULT DefaultHandler
(HWND hwnd, HWND hChild, UINT msg, WPARAM wParam, LPARAM lParam)
{
switch(msg)
{
case WM_DRAWITEM:
case WM_MEASUREITEM:
case WM_CREATE:
case WM_DESTROY:
case WM_SIZE:
case WM_CTLCOLORMSGBOX:
case WM_CTLCOLOREDIT:
case WM_CTLCOLORLISTBOX:
case WM_CTLCOLORBTN:
case WM_CTLCOLORDLG:
case WM_CTLCOLORSCROLLBAR:
case WM_CTLCOLORSTATIC:
case WM_MOUSEACTIVATE:
case WM_WINDOWPOSCHANGING:
case WM_WINDOWPOSCHANGED:
case WM_NCCALCSIZE:
case WM_PAINT:
break;
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: {
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.
I documented this source with Doxygen [^] comments for those that might find it helpful or useful. Your feedback is appreciated.
- 24th September, 2010: Version 1.0.0.0
- 6th June, 2017: Version 1.1.0.0 - Added support for disabled check box item