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

0
107

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.

Demo screenshot

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.

CheckedComboBox_SetCheckState

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.*/

CheckedComboBox_GetCheckState

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.*/

CheckedComboBox_SetFlatStyleChecks

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.*/

CheckedComboBox_EnableCheckAll

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; 

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.

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.

CheckedListBox_SetCheckState

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.*/

CheckedListBox_GetCheckState

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.*/

CheckedListBox_SetFlatStyleChecks

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.*/

CheckedListBox_EnableCheckAll

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.

  • Sept. 24, 2010: Version 1.0.0.0.
  • June 06, 2017: Version 1.1.0.0. – Added support for disabled check box item

LEAVE A REPLY