65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (10投票s)

2021年12月23日

LGPL3

11分钟阅读

viewsIcon

10259

downloadIcon

1142

本文介绍了如何为标准的组合框和列表框控件添加复选框。

目录

引言

前不久我发布了勾选组合框和列表框控件[^]。我当时的方法是创建一个自定义窗口类来封装组合框或列表框,然后在其中进行所有者绘制,同时将消息路由到父对话框。响应一些有益的评论,我决定对控件进行一些改进,特别是将项目数据存储功能恢复给控件(我之前用它来存储项目的勾选状态),并消除注册自定义控件类的需要。

本文中的代码展示了一种不同的、更简单的方法,可以使用纯净的 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
© . All rights reserved.