This article describes a drop in method for converting standard out of the toolbox combo and list box controls into owner drawn checked combo and list boxes. The article explores an alternative to message reflection for environments that do not support reflection.
Table of Contents
Some time ago I published checked Combobox and Listbox [^] controls. The approach I took involved creating a custom window class to encapsulate a combobox or listbox and then owner-draw the controls within while routing messages to the parent dialog without. In response to some helpful comments I decided to make a few improvements to the controls, specifically to return Item Data storage functionality to the control (I had previously used it to store the check state of the item), and to eliminate the need to register a custom control class.
The code with this article demonstrates a different simpler approach that can be used to modify the look and behavior of a standard control, using vanilla Win32 C, while retaining code encapsulation within a customization module. We'll get into the details later meanwhile let's have a look at the controls.
Starting with a valid window handle returned from CreateWindowEx()
or, as in the demo, obtained from the dialog designer, pass the handle reference to InstallownerdrawCkComboHandler()
after which items may be added.
BOOL Main_OnInitDialog(HWND hwnd, HWND hwndFocus, LPARAM lParam)
{
HWND hCombo = GetDlgItem(hwnd,IDC_COMBO);
InstallOwnerDrawCkComboHandler(hwnd, &hCombo) ;
CheckedComboBox_SetFlatStyleChecks(hCombo, TRUE);
CheckedComboBox_EnableCheckAll(hCombo, TRUE);
INT idx = 0;
idx = ComboBox_AddString(hCombo,_T("Red"));
ComboBox_SetItemData(hCombo, idx, NewString(_T("Roja")));
That's pretty much all there is to it. No class registrations needed. A word of caution however, any items you might add to the control before the call to InstallOwnerDrawCkComboHandler()
will be lost. The customization process is necessarily destructive.
Note: when adding items with data it is necessary to use ComboBox_SetItemData() to attach the data to the item when using this control.
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 drop down.
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.
VOID CheckedComboBox_SetItemCheck(
HWND hwndCtl
INT iIndex
BOOL fCheck
);
Gets the checked state of an item in a checked combobox
control.
BOOL CheckedComboBox_GetItemCheck(
HWND hwndCtl
INT iIndex
);
Disables or enables an item in a checked combobox
control.
VOID CheckedComboBox_SetItemDisabled(
HWND hwndCtl
INT iIndex
BOOL fDisabled
);
Gets the disabled/enabled state of an item in a checked combobox
control.
BOOL CheckedComboBox_GetItemDisabled(
HWND hwndCtl
INT iIndex
);
Sets the appearance of the check boxes.
VOID CheckedComboBox_SetFlatStyleChecks(
HWND hwndCtl
BOOL fFlat
);
Sets the select/deselect all feature.
VOID CheckedComboBox_EnableCheckAll(
HWND hwndCtl
BOOL fEnable
);
Test a window handle to determine if it belongs to the checked combobox.
BOOL CheckedComboBox_isParent(
HWND hwndCtl
HWND hwndChild
);
Override the default list separator with the one provided.
VOID CheckedComboBox_Setseparator(
HWND hwndCtl
LPCTSTR lpsz
);
The Checked combobox notification messages are the same as those sent by a standard combobox with one exception. I have added the CBCN_ITEMCHECKCHANGED
notification to indicate that the check state of an item in the list has changed. The parent window of the checked combobox receives notification messages through the WM_COMMAND
message. Here is the itemcheckchanged
notification.
CBCN_ITEMCHECKCHANGED
idComboBox = (int) LOWORD(wParam); hwndComboBox = (HWND) lParam;
Like the Checked combobox, using this control is fairly straight-forward. Again starting with a valid window handle returned from CreateWindowEx()
or obtained from the dialog designer, pass the handle reference to InstallOwnerDrawCkListBoxHandler()
after which items may be added..
BOOL Main_OnInitDialog(HWND hwnd, HWND hwndFocus, LPARAM lParam)
{
HWND hList = GetDlgItem(hwnd,IDC_LIST);
InstallOwnerDrawCkListBoxHandler(hwnd, &hList);
CheckedListBox_SetFlatStyleChecks(hList, TRUE);
ListBox_AddString(hList,_T("Ford"));
ListBox_AddString(hList,_T("Toyota"));
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 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.
VOID CheckedListBox_SetItemCheck(
HWND hwndCtl
INT iIndex
BOOL fCheck
);
Gets the checked state of an item in a checked listbox control.
BOOL CheckedListBox_GetItemCheck(
HWND hwndCtl
INT iIndex
);
Disables or enables an item in a checked listbox control.
VOID CheckedListBox_SetItemDisabled(
HWND hwndCtl
INT iIndex
BOOL fDisabled
);
Gets the disabled/enabled state of an item in a checked listbox control.
BOOL CheckedListBox_GetItemDisabled(
HWND hwndCtl
INT iIndex
);
Sets the appearance of the check boxes.
BOOL CheckedListBox_SetFlatStyleChecks(
HWND hwndCtl
BOOL fFlat
);
Sets the select/deselect all feature.
BOOL CheckedListBox_EnableCheckAll(
HWND hwndCtl
BOOL fEnable
);
The Checked listbox notification messages are the same as those sent by a standard listbox with one exception. I have added the LBCN_ITEMCHECKCHANGED
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 itemcheckchanged
notification.
LBCN_ITEMCHECKCHANGED
idListBox = (int) LOWORD(wParam); hwndListBox = (HWND) lParam;
While my previous approach to customizing the combobox and listbox involved the creation of an actual custom windows control with customization code encapsulated within a new window class. This time I decided to hook into an existing control's parent in order to intercept WM_DRAWITEM
messages and handle them in the customization code module, thereby facilitating ease of use.
Ideally one should be able to open the dialog designer, place an existing standard control, set some basic styles EX: CBS_SORT
and then in the code pass the handle to a magic method that converts it to a customized control (in this case a checked combobox.) Let's have a look and see what's going on in said magic method.
BOOL InstallOwnerDrawCkComboHandler(HWND hwnd, HWND *pHwndCombo)
{
if(!*pHwndCombo) return FALSE;
RECT rc = {0};
GetWindowRect(*pHwndCombo,&rc);
MapWindowPoints(HWND_DESKTOP, hwnd, (LPPOINT) & rc.left, 2);
DWORD dwStyle = (DWORD)GetWindowLongPtr(*pHwndCombo,GWL_STYLE);
DWORD dwExStyle = (DWORD)GetWindowLongPtr(*pHwndCombo,GWL_EXSTYLE);
DWORD dwID = (DWORD)GetWindowLongPtr(*pHwndCombo,GWL_ID);
HINSTANCE hInst = (HINSTANCE)GetWindowLongPtr(*pHwndCombo,GWLP_HINSTANCE);
if(!DestroyWindow(*pHwndCombo)) return FALSE;
dwStyle &= ~(CBS_AUTOHSCROLL|CBS_SIMPLE|CBS_DROPDOWN|CBS_OEMCONVERT| \
CBS_OWNERDRAWVARIABLE|CBS_LOWERCASE|CBS_UPPERCASE);
dwStyle |= (CBS_DROPDOWNLIST|CBS_OWNERDRAWFIXED|CBS_HASSTRINGS);
*pHwndCombo = CreateWindowEx(dwExStyle, WC_COMBOBOX, NULL,
dwStyle, rc.left, rc.top, WIDTH(rc), HEIGHT(rc),
hwnd,(HMENU)dwID, hInst, NULL);
if (!*pHwndCombo) return FALSE;
SNDMSG(*pHwndCombo, WM_SETFONT, (WPARAM)GetStockObject(DEFAULT_GUI_FONT), 0);
WNDPROC wProc;
if(NULL == g_ParentHandles)
{
g_ParentHandles = New_HandleList();
}
INT result = HandleList_AddHandle(g_ParentHandles, hwnd);
if(LIST_FULL == result)
{
return FALSE;
}
else if(ADDED == result)
{
wProc = SubclassWindow(hwnd, Parent_Proc);
SetProp(hwnd, PARENTPROC, wProc);
}
wProc = SubclassWindow(*pHwndCombo, ODCCombo_Proc);
SetProp(*pHwndCombo, WPROC, wProc);
SetProp(*pHwndCombo, PROPSTORAGE, (HANDLE) calloc(2, sizeof(LPTSTR)));
SetProp(*pHwndCombo, PROPSEP, (HANDLE)calloc(2, sizeof(TCHAR)));
SetProp(*pHwndCombo, PROPTYPE, (HANDLE)(DWORD*)(1));
SetProp(*pHwndCombo, PROPOPTIONS, (HANDLE)(DWORD*)(0));
return TRUE;
}
My initial idea was to simply modify the styles of an existing combobox to convert it into an owner drawn control. I soon found that some styles cannot be modified once a control is created, which left only one option, destroy the original and replace it with a new one. This is not ideal but a trade off for ease of use. Alternatively one could define the control properly up front in the dialog designer or with CreateWindowEx()
and comment out the first section of this code.
Moving on to the middle section we have several lines of code related to subclassing the parent in order to hook its message procedure and intercept the WM_DRAWITEM
messages. It is not common to subclass a control's parent, indeed it offers certain challenges and I had to really think through a number of scenarios.
Q: What if someone uses this code to customize 2 or more controls on the same dialog?
A: A second attempt to subclass the parent's proc would be unnecessary. Therefore one should compare handles passed to this procedure with those previously passed and skip subclassing if there is a match.
Q: What if someone uses this code to customize 2 or more controls on different dialogs simultaneously? For example on three dialogs displayed in a tab control?
A: Subclassing several parents and sending their messages to our hook proc Parent_Proc
would not be a problem since the setting of a property in each parent using our property tag would succeed as long as there was not another property already attached that had the same tag.
Q: What about the property tag used to set the property on the parent. Perhaps it's already been used?
A: Avoid sensible tags like "wprc"
opt for something like "2b1q"
which is obscure to all but a few.
I asked myself a lot of such questions and to be honest almost gave up on the idea until I put it all together and saw that yes, it could be done. My solution allows for an unlimited (memory permitting of course) number of checked comboxes on a single dialog and currently supports up to 8 parent dialogs simultaneously. But a developer can easily change the upper limit of stored parent handles if necessary.
In the final part of the procedure I subclass the combobox itself, allocate storage and bind it to the combobox class by setting properties. One of these properties, PROPTYPE
, will be used in the WM_DRAWITEM
handler to determine if the item requesting drawing is a Checked combobox or not.
Let's say that we have three checked comboboxes on three separate dialogs displayed in a tab control and we close out one tab destroying the associated dialog and a checked combobox with it. We must unhook from that particular dialog and clean up the allocations associated with that particular control. Let's have a look at the destructors.
static VOID ODCCombo_OnDestroy(HWND hwnd)
{
ComboBox_ResetContent(hwnd);
}
static VOID ODCCombo_OnNCDestroy(HWND hwnd)
{
RemoveProp(hwnd, HWNDLISTBOX);
RemoveProp(hwnd, PROPTYPE);
RemoveProp(hwnd, PROPOPTIONS);
LPTSTR *ppStoriage = (LPTSTR*)GetProp(hwnd, PROPSTORAGE);
if (NULL != ppStoriage)
{
free(ppStoriage[0]);
free(ppStoriage[1]);
free(ppStoriage);
RemoveProp(hwnd, PROPSTORAGE);
}
LPTSTR szText = (LPTSTR)GetProp(hwnd, PROPTEXT);
if (NULL != szText)
{
free(szText);
RemoveProp(hwnd, PROPTEXT);
}
szText = (LPTSTR)GetProp(hwnd, PROPSEP);
if (NULL != szText)
{
free(szText);
RemoveProp(hwnd, PROPSEP);
}
WNDPROC wp = (WNDPROC)GetProp(hwnd, WPROC);
if (NULL != wp)
{
SetWindowLongPtr(hwnd, GWLP_WNDPROC, (DWORD)wp);
RemoveProp(hwnd, WPROC);
}
}
static VOID Parent_OnNCDestroy(HWND hwnd)
{
FORWARD_WM_NCDESTROY(hwnd,Parent_DefProc);
WNDPROC wp = (WNDPROC)GetProp(hwnd, PARENTPROC);
if (NULL != wp)
{
SetWindowLongPtr(hwnd, GWLP_WNDPROC, (DWORD)wp);
RemoveProp(hwnd, PARENTPROC);
HandleList_RemoveHandle(g_ParentHandles, hwnd);
}
if (HandleList_IsEmpty(g_ParentHandles))
{
HandleList_Free(&g_ParentHandles);
}
}
In the first destructor in this snippet the call to reset content will be handled by the combo's CB_RESETCONTENT
handler. Here the item allocations are freed and WM_DELETEITEM
messages are triggered to be processed by the parent. Since this must take place before the control is completely destroyed it is handled in response to the first of two destructor messages WM_DESTROY
which might be thought of as a WM_PREDESTROY
in this context.
The second destructor in this snippet is pretty straight forward all remaining allocations are freed and all properties are removed for each and every Checked combobox customized using this code when they are destroyed. The WM_NCDESTROY
message is the second and final destructor message sent to the control.
The third destructor however is dedicated to un-subclassing the parent and removing the unused hook properties while updating the list of parent handles. The list can hold an unlimited number of parent handles but only 8 at a time as it is currently configured, so each time a parent is destroyed it's handle reference is removed from the list. Finally when the last parent window is destroyed the list allocation itself is recycled.
In my previous treatment of these checked controls [^] I used the item data of each item to store the check state of that item. Unfortunately this didn't allow for the attachment of data to the items. This time I decided to employ a structure with a field for the check state and a field for the item data that would be stored in a combo's list item data property or field. This approach though, came with some challenges.
- The data structure must be allocated and added to each item when that item is added to the control.
- The data structure must necessarily be garbage collected when an item is removed from the control, but independently from the item user data which is the responsibility of the user.
- Every message related to an item that affects the item data must be intercepted and handled in such a way that the user data is either packed into the structure for storage with the check state or unpacked and presented to the user.
- Unfortunately it is impossible to intercept
WM_DELETEITEM
messages in the hook proc Parent_Proc
and I was unable to hook it using other methods.
For a while I thought I was beat. However I coded around the edges of the problem, addressing those parts I understood, and as I a got closer to the WM_DELETEITEM
dilemma it suddenly vanished! Here are some pertinent snippets.
Adding/Inserting an item - First let the combo perform a default addition/insertion then assign our own ITEM
structure to the combo's item data. Note that with an owner drawn combobox or listbox with the HASSTRINGS
style set, as this one has, the only way to set item data is via the respective SETITEMDATA
messages so the overall behavior of the control remains native to the user.
static INT ODCCombo_OnAddString(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
INT index = ODCCombo_DefProc(hwnd, msg, wParam, lParam);
LPITEM lpi = (LPITEM)checked_malloc(sizeof(ITEM));
if(lpi)
{
INT iRtn = ODCCombo_DefProc(hwnd, CB_SETITEMDATA, (WPARAM)index, (LPARAM)lpi);
return (CB_ERR == iRtn || CB_ERRSPACE == iRtn)? iRtn : index;
}
ODCCombo_OnDeleteString(hwnd, msg, (WPARAM)index, lParam);
return CB_ERR;
}
Setting item data - First get a reference to our ITEM
object, then assign the pointer to the lpData member.
static INT ODCCombo_OnSetItemData(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
LPITEM lpi = (LPITEM)ODCCombo_DefProc(hwnd, CB_GETITEMDATA, wParam, 0);
if(lpi)
{
lpi->lpData = lParam;
return CB_OKAY;
}
return CB_ERR;
}
Deleting an item - First get our ITEM
structure then set the item data to point to the user's data before freeing our allocation. Finally we call the default process to actually delete the item with the user's data. This will result in the issuance of the WM_DELETEITEM
message with the user's data instead of our struct which is the desired default behavior.
Resetting content - Same as deleting an item except we loop through the items and set each item data to point the the user's data, then free the ITEM
structure allocations before calling the default process to actually reset the combobox. Again the flurry of WM_DELETEITEM
messages will contain the users data.
static INT ODCCombo_OnDeleteString(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
LPITEM lpi = (LPITEM)ODCCombo_DefProc(hwnd, CB_GETITEMDATA, wParam, 0);
if(lpi)
{
ODCCombo_DefProc(hwnd, CB_SETITEMDATA, wParam, lpi->lpData);
free(lpi);
}
return ODCCombo_DefProc(hwnd, msg, wParam, lParam);
}
Ensure that all item allocations are garbage collected before control expiration - Just a simple one liner in the destructor which I previously discussed. I love it when a plan comes together!
ComboBox_ResetContent(hwnd);
I documented this source with Doxygen [^] comments for those that might find it helpful or useful. Your feedback is appreciated.
- 23rd December, 2021: Version 1.0.0.0