Check It Out 2.0,一步到位 Win32 SDK C 勾选组合框和列表框





5.00/5 (10投票s)
本文介绍了如何为标准的组合框和列表框控件添加复选框。
- 下载 odChkList 源码 - 8.1 KB
- 下载 Article_odChkList_prj_pelles.zip - 12.7 KB
- 下载 Article_odChkList_prj_MSVC.zip - 13.7 KB
- 下载 odChkCombo 源码 - 10.2 KB
- 下载 Article_odChkCombo_prj_pelles.zip - 15.1 KB
- 下载 Article_odChkCombo_prj_MSVC.zip - 16.3 KB
目录
引言
前不久我发布了勾选组合框和列表框控件[^]。我当时的方法是创建一个自定义窗口类来封装组合框或列表框,然后在其中进行所有者绘制,同时将消息路由到父对话框。响应一些有益的评论,我决定对控件进行一些改进,特别是将项目数据存储功能恢复给控件(我之前用它来存储项目的勾选状态),并消除注册自定义控件类的需要。
本文中的代码展示了一种不同的、更简单的方法,可以使用纯净的 Win32 C 来修改标准控件的外观和行为,同时将代码封装在一个自定义模块中。我们稍后将详细介绍,但现在先来看看这些控件。
勾选组合框
从 CreateWindowEx()
返回的有效窗口句柄开始,或者像演示中那样从对话框设计器获取,将句柄引用传递给 InstallownerdrawCkComboHandler()
,之后就可以添加项目了。
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"))); // Add more items...
基本上就是这样。不需要类注册。但请注意,在调用 InstallOwnerDrawCkComboHandler()
之前添加到控件中的任何项目都将丢失。自定义过程是破坏性的。
注意: 在添加带有数据的项目时,使用此控件时必须使用 ComboBox_SetItemData() 将数据附加到项目。
勾选组合框的消息和宏
使用 Windows 消息配置控件以满足您的需求。该控件采用标准的组合框消息/宏,但有几个例外。
使用 ComboBox_SetText()
或显式发送 WM_SETTEXT
来设置控件文本将不起作用,因为控件中显示的文本由下拉列表中的选择决定。
我创建了以下宏,以便在使用此控件配置此控件时,可以将其与标准的组合框宏一起使用。如果您希望显式调用 SendMessage()
或 PostMessage()
,请参阅头文件中的宏定义以了解用法。
CheckedComboBox_SetItemCheck
在勾选的 combobox
控件中勾选或取消勾选某个项目。
VOID CheckedComboBox_SetItemCheck( 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 No return value.*/
CheckedComboBox_GetItemCheck
获取勾选的 combobox
控件中某个项目的勾选状态。
BOOL CheckedComboBox_GetItemCheck( 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_SetItemDisabled
禁用或启用勾选的 combobox
控件中的某个项目。
VOID CheckedComboBox_SetItemDisabled( HWND hwndCtl INT iIndex BOOL fDisabled ); /*Parameters hwndCtl Handle of a checked combobox. iIndex The zero-based index of the item for which to set the check state. fDisabled A value that is set to TRUE to disables the item, or FALSE to enable it. Return Values No return value.*/
CheckedComboBox_GetItemDisabled
获取勾选的 combobox
控件中某个项目的禁用/启用状态。
BOOL CheckedComboBox_GetItemDisabled( 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 disabled, or zero otherwise.*/
CheckedComboBox_SetFlatStyleChecks
设置复选框的外观。
VOID CheckedComboBox_SetFlatStyleChecks( HWND hwndCtl BOOL fFlat ); /*Parameters hwndCtl Handle of a checked combobox. fFlat TRUE for flat check boxes, or FALSE for standard check boxes. Return Values No return value.*/
CheckedComboBox_EnableCheckAll
设置全选/全不选功能。
VOID CheckedComboBox_EnableCheckAll( 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.*/
CheckedComboBox_isParent
测试窗口句柄以确定它是否属于勾选组合框。
BOOL CheckedComboBox_isParent( HWND hwndCtl HWND hwndChild ); /*Parameters hwndCtl Handle of a checked combobox. hwndChild Handle of a potential child of the checked combobox EX: hList or hEdit. Return Values TRUE if hwndChild belongs to the checked combobox.*/
CheckedComboBox_Setseparator
使用提供的分隔符覆盖默认列表分隔符。
VOID CheckedComboBox_Setseparator( HWND hwndCtl LPCTSTR lpsz ); /*Parameters hwndCtl Handle of a checked combobox. lpsz A string containing a single separator character. Return Values No return value.*/
勾选组合框通知
勾选组合框的通知消息与标准组合框发送的消息相同,但有一个例外。我添加了 CBCN_ITEMCHECKCHANGED
通知,以指示列表中某个项目的勾选状态已更改。勾选组合框的父窗口通过 WM_COMMAND
消息接收通知消息。这是 itemcheckchanged
通知。
CBCN_ITEMCHECKCHANGED idComboBox = (int) LOWORD(wParam); // identifier of checked combobox hwndComboBox = (HWND) lParam; // handle of checked combobox
勾选列表框
与勾选组合框一样,使用此控件也相当直接。再次从 CreateWindowEx()
返回的有效窗口句柄开始,或者从对话框设计器获取,将句柄引用传递给 InstallOwnerDrawCkListBoxHandler()
,之后就可以添加项目了。
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")); // Add more items...
勾选列表框的消息和宏
使用 Windows 消息配置控件以满足您的需求。该控件采用标准的列表框消息/宏,但有几个例外。
我创建了以下宏,以便在使用此控件配置此控件时,可以将其与标准的列表框宏一起使用。如果您希望显式调用 SendMessage()
或 PostMessage()
,请参阅头文件中的宏定义以了解用法。
CheckedListBox_SetItemCheck
在勾选的列表框控件中勾选或取消勾选某个项目。
VOID CheckedListBox_SetItemCheck( 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 No return value.*/
CheckedListBox_GetItemCheck
获取勾选的列表框控件中某个项目的勾选状态。
BOOL CheckedListBox_GetItemCheck( 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_SetItemDisabled
禁用或启用勾选的列表框控件中的某个项目。
VOID CheckedListBox_SetItemDisabled( HWND hwndCtl INT iIndex BOOL fDisabled ); /*Parameters hwndCtl Handle of a checked listbox. iIndex The zero-based index of the item for which to set the check state. fDisabled A value that is set to TRUE to disables the item, or FALSE to enable it. Return Values No return value.*/
CheckedListBox_GetItemDisabled
获取勾选的列表框控件中某个项目的禁用/启用状态。
BOOL CheckedListBox_GetItemDisabled( 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 disabled, or zero otherwise.*/
CheckedListBox_SetFlatStyleChecks
设置复选框的外观。
BOOL CheckedListBox_SetFlatStyleChecks( HWND hwndCtl BOOL fFlat ); /*Parameters hwndCtl Handle of a checked listbox. fFlat TRUE for flat check boxes, or FALSE for standard check boxes. Return Values No return value.*/
CheckedListBox_EnableCheckAll
设置全选/全不选功能。
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.*/
勾选列表框通知
勾选列表框的通知消息与标准列表框发送的消息相同,但有一个例外。我添加了 LBCN_ITEMCHECKCHANGED
通知,以指示列表中某个项目的勾选状态已更改。勾选列表框的父窗口通过 WM_COMMAND
消息接收通知消息。这是 itemcheckchanged
通知。
LBCN_ITEMCHECKCHANGED idListBox = (int) LOWORD(wParam); // identifier of checked listbox hwndListBox = (HWND) lParam; // handle of checked listbox
设计考量(以勾选组合框为例)
在我以前定制组合框和列表框的方法中,我创建了一个实际的自定义窗口控件,并将定制代码封装在一个新的窗口类中。这次我决定钩入一个现有控件的父窗口,以便拦截 WM_DRAWITEM
消息并在自定义代码模块中处理它们,从而提高易用性。
安装所有者绘制钩子和处理程序
理想情况下,应该能够打开对话框设计器,放置一个现有的标准控件,设置一些基本样式,例如 CBS_SORT
,然后在代码中将句柄传递给一个神奇的方法,该方法将其转换为自定义控件(在本例中为勾选组合框)。让我们来看看 said 神奇的方法中发生了什么。
BOOL InstallOwnerDrawCkComboHandler(HWND hwnd, HWND *pHwndCombo) { if(!*pHwndCombo) return FALSE; // Get all necessary info from combobox 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; // Remove incompatible style bits if defined dwStyle &= ~(CBS_AUTOHSCROLL|CBS_SIMPLE|CBS_DROPDOWN|CBS_OEMCONVERT| \ CBS_OWNERDRAWVARIABLE|CBS_LOWERCASE|CBS_UPPERCASE); // Add the style bits we need dwStyle |= (CBS_DROPDOWNLIST|CBS_OWNERDRAWFIXED|CBS_HASSTRINGS); // Recreate window with the modified style bits. *pHwndCombo = CreateWindowEx(dwExStyle, WC_COMBOBOX, NULL, dwStyle, rc.left, rc.top, WIDTH(rc), HEIGHT(rc), hwnd,(HMENU)dwID, hInst, NULL); if (!*pHwndCombo) return FALSE; //Set the font to default 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) { // Subclass the Parent window so that we can intercept messages // but only once per parent. wProc = SubclassWindow(hwnd, Parent_Proc); SetProp(hwnd, PARENTPROC, wProc); } // else PREVIOUSLY_ADDED so proceed // Subclass combobox and save the old proc wProc = SubclassWindow(*pHwndCombo, ODCCombo_Proc); SetProp(*pHwndCombo, WPROC, wProc); // Create and store a circular buffer (for Join()) SetProp(*pHwndCombo, PROPSTORAGE, (HANDLE) calloc(2, sizeof(LPTSTR))); // Create storage for the separator character SetProp(*pHwndCombo, PROPSEP, (HANDLE)calloc(2, sizeof(TCHAR))); // Store the control type id tag SetProp(*pHwndCombo, PROPTYPE, (HANDLE)(DWORD*)(1)); // Create place to store options SetProp(*pHwndCombo, PROPOPTIONS, (HANDLE)(DWORD*)(0)); return TRUE; }
我的最初想法是简单地修改现有组合框的样式,将其转换为一个所有者绘制控件。我很快发现,一旦控件创建后,有些样式就无法修改了,这只留下了一个选择:销毁原来的并用一个新的替换它。这不是理想的,但为了易用性是可以接受的。或者,也可以在对话框设计器或使用 CreateWindowEx()
中预先正确定义控件,并注释掉此代码的第一部分。
接着是中间部分,我们有几行代码与子类化父窗口有关,以便钩入其消息过程并拦截 WM_DRAWITEM
消息。子类化控件的父窗口并不常见,它确实带来了一些挑战,我需要仔细考虑许多场景。
问: 如果有人使用此代码来定制同一个对话框中的 2 个或更多控件怎么办?
答: 重复子类化父窗口的过程是不必要的。因此,应该将传递给此过程的句柄与之前传递的句柄进行比较,如果匹配则跳过子类化。
问: 如果有人同时在不同的对话框中使用此代码来定制 2 个或更多控件怎么办?例如,在选项卡控件中显示的三个对话框上?
答: 子类化多个父窗口并将它们的消息发送到我们的钩子过程 Parent_Proc
不成问题,因为只要没有其他属性附加了相同的标签,在每个父窗口上使用我们的属性标签设置属性就会成功。
问: 那么用于在父窗口上设置属性的属性标签呢?也许它已经被使用了?
答: 避免使用像 "wprc"
这样的常见标签,选择像 "2b1q"
这样的模糊标签,它除了少数人之外,很少有人知道。
我问了自己很多这样的问题,说实话,几乎放弃了这个想法,直到我把它们都整合在一起,才发现是的,可以做到。我的解决方案允许在单个对话框上拥有无限数量(当然,取决于内存)的勾选组合框,目前同时支持多达 8 个父对话框。但是,开发人员可以根据需要轻松更改存储的父句柄的上限。
在过程的最后一部分,我子类化了组合框本身,分配存储并将其绑定到组合框类,方法是设置属性。其中一个属性 PROPTYPE
将在 WM_DRAWITEM
处理程序中使用,以确定请求绘制的项目是否是勾选组合框。
移除钩子和清理
假设我们在三个单独的对话框(显示在选项卡控件中)上有三个勾选组合框,我们关闭了一个选项卡,销毁了关联的对话框及其勾选组合框。我们必须从该特定对话框取消钩子并清理与该特定控件关联的分配。让我们看看析构函数。
/// @brief Handle WM_DESTROY combobox message. /// /// @param hwnd Handle of parent. /// /// @returns VOID. static VOID ODCCombo_OnDestroy(HWND hwnd) { //Free ITEM storage and trigger WM_DELETEITEM messages to parent ComboBox_ResetContent(hwnd); } /// @brief Handle WM_NCDESTROY combobox message. /// /// @param hwnd Handle of parent. /// /// @returns VOID. 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); } } /// @brief Handle WM_NCDESTROY hook message. /// /// @param hwnd Handle of parent. /// /// @returns VOID. static VOID Parent_OnNCDestroy(HWND hwnd) { //Forward message to be handled by other parent subclass // procedures (if any). //Note: This must be called first! 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); } }
在此代码片段的第一个析构函数中,调用重置内容将由组合框的 CB_RESETCONTENT
处理程序处理。在此,项目分配被释放,并触发 WM_DELETEITEM
消息由父窗口处理。由于这必须在控件完全销毁之前发生,因此它是在响应两个析构消息中的第一个 WM_DESTROY
(在这个上下文中可以看作是 WM_PREDESTROY
)时处理的。
在此代码片段的第二个析构函数中,一切都很简单,所有剩余的分配都会被释放,并且在使用此代码定制的每个勾选组合框被销毁时,所有属性都会被移除。WM_NCDESTROY
消息是发送到控件的第二个也是最后一个析构消息。
然而,第三个析构函数专门用于取消父窗口的子类化,移除未使用的钩子属性,同时更新父窗口句柄列表。该列表可以容纳无限数量的父窗口句柄,但目前配置为一次只能容纳 8 个,因此每次销毁父窗口时,其句柄引用都会从列表中移除。最后,当最后一个父窗口被销毁时,列表分配本身将被回收。
跟踪项目勾选和项目数据
在我上一次处理这些勾选控件[^]时,我使用每个项目的项目数据来存储该项目的勾选状态。不幸的是,这不允许将数据附加到项目。这次我决定采用一个结构,其中包含一个用于勾选状态的字段和一个用于项目数据的字段,该数据将存储在组合框的列表项目数据属性或字段中。然而,这种方法带来了一些挑战。
- 当将数据结构添加到控件中的每个项目时,必须分配并添加该数据结构。
- 数据结构在项目从控件中移除时必须被垃圾回收,但独立于项目用户数据,用户数据由用户负责。
- 必须拦截并以某种方式处理与影响项目数据的项目相关的每个消息,以便将用户数据打包到结构中与勾选状态一起存储,或者解包并呈现给用户。
- 不幸的是,无法在钩子过程
Parent_Proc
中拦截WM_DELETEITEM
消息,并且我无法通过其他方法对其进行钩子。
我曾一度认为自己不行了。然而,我通过边缘处理问题,解决了那些我理解的部分,当接近 WM_DELETEITEM
的困境时,它突然消失了!这里有一些相关的代码片段。
添加/插入项目 - 首先让组合框执行默认的添加/插入,然后将我们自己的 ITEM
结构分配给组合框的项目数据。请注意,对于具有 HASSTRINGS
样式的、所有者绘制的组合框或列表框(就像这个一样),设置项目数据的唯一方法是通过相应的 SETITEMDATA
消息,因此控件的整体行为对用户来说仍然是原生的。
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; } //if we get here we were unsuccessful so undo AddString ODCCombo_OnDeleteString(hwnd, msg, (WPARAM)index, lParam); return CB_ERR; }
设置项目数据 - 首先获取我们 ITEM
对象的引用,然后将指针分配给 lpData 成员。
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; }
删除项目 - 首先获取我们的 ITEM
结构,然后将项目数据设置为指向用户数据,然后再释放我们的分配。最后,我们调用默认过程来实际删除带有用户数据的项目。这将导致发出 WM_DELETEITEM
消息,其中包含用户数据而不是我们的结构,这是期望的默认行为。
重置内容 - 与删除项目相同,只是我们遍历项目并将每个项目数据设置为指向用户数据,然后在调用默认过程实际重置组合框之前释放 ITEM
结构分配。同样,一系列 WM_DELETEITEM
消息将包含用户数据。
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); }
确保所有项目分配在控件到期前被垃圾回收 - 在我之前讨论过的析构函数中,只需一行简单的代码。我喜欢计划如期实现!
//Free ITEM storage and trigger WM_DELETEITEM messages to parent ComboBox_ResetContent(hwnd);
最终评论
我使用 Doxygen[^]注释记录了此源代码,供可能觉得有用的人参考。您的反馈非常受欢迎。
历史
- 2021 年 12 月 23 日:版本 1.0.0.0