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

一个派生自CListCtrl的MFC类,允许将其他“控件”插入到特定单元格中

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (54投票s)

2011年8月2日

CPOL

12分钟阅读

viewsIcon

177298

downloadIcon

14126

一个 CListCtrl 的派生类,可以非常轻松地将编辑控件、组合框、复选框、日期选择器和颜色选择器插入或删除到特定单元格。插入的“控件”不是 CWnd 派生的。

引言

一个常见的需求是拥有一个自定义绘制的 CListCtrl,它允许在特定单元格中包含另一个控件,例如编辑框、组合框、颜色选择器等。这样的控件在许多应用程序中都非常有用,因为它提供了一种轻松配置应用程序特定属性(如背景颜色等)的方法。

本文档介绍了一些实现此功能的代码示例。

背景

多年来,我见过几个允许将 CComboBoxCEdit 控件插入到 CListCtrl 派生控件的特定单元格中的代码示例。我也亲自尝试过。使用这些解决方案,控件本身不是持久的,因此除非单元格有焦点,否则您看不到单元格填充了哪种类型的控件。此外,通常需要激活控件然后操作它,这完全不切实际:例如,您需要先单击复选框以激活它,然后才能选中/取消选中它。另一个问题是您不知道插入控件的所有实现细节,这意味着您可能会得到完全不期望的结果,并且没有修正的可能性。这正是我在使用 Visual Studio 6.0 进行试验时获得的结果。

这让我确信,需要的是一个全新的解决方案,而不是使用插入的 CWnd 派生控件。

有一些系统函数允许您绘制按钮旧式的、无主题的“经典”外观,这些按钮出现在 XP 之前的 Windows 版本中。此外,操作系统还提供了一整套函数来处理主题:OpenThemeData 等等。

利用这些功能,我编写了这个项目,其中不是在 CListCtrl 的单元格中插入另一个 CWnd 派生控件,而是绘制组合框、复选框等的图像。

我已在 Windows 7、Vista 和 XP 上进行了测试,尝试更改主题,并获得了可接受的结果。最大的缺点是,在 CConfigListCtrl 应用程序打开时更改主题不会产生最佳效果:MeasureItem 没有像应有的那样再次调用,这意味着行高没有立即重新计算。我还没有找到一种方法来强制立即重新计算。这并不算太糟糕,因为普通用户大概只会极少更改主题。

正如预期的那样,还有其他一些较小的缺陷:少量闪烁——这可以通过改进代码来消除。

这个项目需要大量工作,零散地分布在多年中(我从 2006 年开始)。仍有许多功能需要添加:没有微调编辑控件,没有可编辑的组合框,文本对齐(左/右/居中)待完成……

否则,所有控件类型在外观和感觉上都相互一致,包括支持主题的颜色选择器。

设计概述

整体概念非常简单:有一个主要的 CConfigListCtrl 类,以及插入到单元格中的 CCellCtrl 派生类。CConfigListCtrl 调用 CCellCtrl 的虚拟函数,例如绘制,并将消息传递给其他 CCellCtrl 虚拟函数,例如鼠标单击。

CConfigListCtrl 对插入的控件的实现细节一无所知,并且可以通过不修改 CConfigListCtrl 源代码来开发新的控件类型。所有需要的是插入的控件必须派生自 CCellCtrl,或者如果它包含一个下拉弹出窗口的句柄,则派生自 CCellDropDown。下拉弹出窗口必须派生自 CListCtrlCellWnd,后者本身派生自 CWnd

CCellCtrl 派生控件的指针存储在 CConfigListCtrlm_CtrlMap 成员变量中。这只是一个 DWORD - CCellCtrl 指针映射,其中 DWORD 包含控件中单元格的位置。

清理是通过遍历 m_CtrlMap 并删除所有已分配的内存来完成的。

下图显示了所有类如何交互。MFC 类以橙色标记。抽象类以灰色标记。

评估示例

我包含了两个可下载的代码示例

  • 一个完整的演示项目,它显示了 CConfigListCtrl 集成在一个完整的项目中。这就是屏幕截图所示的内容。我在此项目中进行了开发和测试。这应该能让读者快速评估控件的功能。我将在下面概述其功能。
  • 一个源代码示例,仅包含 CConfigListCtrl 和相关文件。在“从头开始构建使用 CConfigCtrl 的项目”部分,我将描述如何使用此示例。

任何单元格的值都可以被评估。一个由 CCellCtrl 填充的单元格的值的格式与 `InsertCtrl` 中给出的默认值 `lpszDefaultTextValue` 参数的格式相同,例如,在“主函数描述”部分所述。

支持 Tab 和 Shift+Tab,焦点会移动到 CConfigListCtrl 内相关的 CCellCtrl

F4 打开 CCellDropDown 派生类的弹出窗口,Esc 关闭它而不将控件设置为弹出窗口中当前选定的项。Enter 关闭它,但将控件设置为当前选定的项。箭头键应该允许用户在弹出窗口的项目之间移动。如果没有足够的空间向下显示下拉列表,则向上显示。

可以启用或禁用特定控件中的单元格。见下文

...行和列也可以插入或删除

...控件可以插入或删除到特定单元格中。如果将控件插入到已包含控件的单元格中,则前一个控件将被自动删除。

...可以在包含 CCellComboBox 的单元格中插入、选择或删除项目

...如前所述,支持主题更改。如果更改为“经典”,您将看到以下显示。我建议关闭并重新打开应用程序,因为在主题更改后 CListCtrl 的行高不会立即重新计算。在我看来,这似乎是 MFC 的一个 bug,但我也许错了。

在 XP 中也能正确运行。下面显示了使用橄榄色主题的截图

从头开始构建使用 CConfigCtrl 的项目

本节介绍如何从头开始构建一个使用 CConfigListCtrl 的项目。首先,使用 Visual Studio 2010 创建一个 MFC 应用程序。为简单起见,选择基于对话框的(不是 MDI 或 SDI)。确保选中“通用控件清单”复选框,然后单击“完成”。

ConfigListCtrlSource 中的所有文件提取到一个包含 .cpp.h 文件的文件夹中。将所有根文件包含到项目中。添加一个新筛选器“CellCtrls”,并附加两个新筛选器“Header Files”和“Source Files”。在此处包含 CellCtrls 子文件夹中的所有文件。参见下图。(我将我的项目命名为“MFCTestCConfigCtrl”,但您也可以将其命名为其他名称,当然。)

添加资源:在“资源”选项卡中右键单击您的 *.rc 文件,然后选择“添加资源…”选择“位图”并单击“导入”按钮。导航到 res\ 文件夹并选择 checkbox.bmp。将位图重命名为“IDB_CHECKBOX”。(为该位图打开其“属性”对话框。)

在您的项目配置属性中,选择“链接器”、“输入”,并在“附加依赖项”中添加“UxTheme.lib”。

在您的对话框中,使用工具箱添加一个列表控件。此列表控件必须设置以下属性:“Owner Draw Fixed”必须为“True”。“View”必须为“Report”。

在“类向导”中,为此列表控件添加一个成员变量,例如 m_ConfigListCtrl。在相关的 xxxDlg.h 头文件中,将 m_ConfigListCtrl 的类型从 CListCtrl 更改为 CConfigListCtrl,并在顶部添加 #include "ConfigListCtrl.h" 指令。

xxxDlg.cpp 文件的顶部添加以下代码,以便您可以使用 CCellxxx 控件。

#include "CellCtrls\CellEdit.h"
#include "CellCtrls\CellCheckBox.h"
#include "CellCtrls\CellComboBox.h"
#include "CellCtrls\CellDateCtrl.h"
#include "CellCtrls\CellColorCtrl.h"
#include "CellCtrls\CellTimeCtrl.h"

重写 PreTranslateMessage 虚拟函数,并添加 WM_SIZINGWM_MOVE 的消息处理程序。添加以下代码。

BOOL CMFCTestCConfigCtrlDlg::PreTranslateMessage(MSG* pMsg)
{
    if(pMsg->message==WM_KEYDOWN) 
        m_bKeyUp = FALSE;
    if(pMsg->message==WM_KEYUP) 
        m_bKeyUp = TRUE;
    if((pMsg->wParam == VK_ESCAPE || pMsg->wParam == VK_RETURN))
    {
        if (m_bKeyUp)
        {
            m_bKeyUp = FALSE;
            return m_ConfigListCtrl.OnEnterEsc(pMsg->wParam);
        }
        else
            return TRUE;
    }
    return CDialogEx::PreTranslateMessage(pMsg);
}

void CMFCTestCConfigCtrlDlg::OnMove(int x, int y)
{
    CDialog::OnMove(x, y);
    // Seems no way round this and have to explicitly call m_ConfigListCtrl this way.
    // Note: this is needed because when move, we want to either close any open
    // popups associated with the control or move these as well. It looks funny 
    // otherwise.
    m_ConfigListCtrl.OnParentMove(x, y);
}

void CMFCTestCConfigCtrlDlg::OnSizing(UINT fwSide, LPRECT pRect)
{
    CDialogEx::OnSizing(fwSide, pRect);
    m_ConfigListCtrl.OnSizing(fwSide, pRect);
}

您仍然需要添加 m_bKeyUp,它是一个 BOOL 私有成员变量,在构造函数中初始化为 FALSE

...就是这样!您已将 CConfigListCtrl 添加到项目中。为了进行一些操作,您可以在 OnInitDialog 的“TODO: 添加额外初始化”区域添加以下代码。

m_ConfigListCtrl.InsertColumn(0, _T("A column"));
m_ConfigListCtrl.SetColumnWidth(0, 160);
m_ConfigListCtrl.InsertItem(0, _T(""));
m_ConfigListCtrl.SetItem(0,0,new CCellComboBox, 
   _T("Selected Item\n1st item\n2nd item\nSelected Item"));

有关如何操作 CCellCtrl 的更多详细信息,请参见下一节。

主函数描述

当然,CListCtrl 中可用的函数仍然可以在 CConfigListCtrl 中使用。最有用的可能是 SetColumnWidthGetItemText。我将仅描述新函数或重载的函数。

int InsertColumn(int nCol, const LVCOLUMN* pColumn);
int InsertColumn(int nCol, LPCTSTR lpszColumnHeading, 
          int nFormat = LVCFMT_LEFT, int nWidth = -1, int nSubItem = -1);
BOOL DeleteColumn(int nCol);
int InsertItem(const LVITEM* pItem);
int InsertItem(int nItem, LPCTSTR lpszItem);
int InsertItem(int nItem, LPCTSTR lpszItem, int nImage);
BOOL DeleteItem(int nItem);
inline BOOL DeleteAllItems();

这些函数都已重载。功能与它们的 CListCtrl 等效函数相同,只是需要更改底层数据放置 CCellCtrl,以便它们保持在正确的单元格中并且不会移位。此外,如果您在开头插入一列,所有单元格中的数据都需要通过编程方式向右移动,否则内容将与列标题不匹配。

此外,还有以下用于在单元格中插入/删除控件的专用函数。

int InsertItem(int nItem, CCellCtrl *pCellCtrl, LPCTSTR lpszDefaultTextValue = _T("\0"));
BOOL SetItem(int nItem, int nSubItem, CCellCtrl *pCellCtrl, 
             LPCTSTR lpszDefaultTextValue = _T("\0"));
void InsertCtrl(int nItem, int nSubItem, CCellCtrl *pCellCtrl);
void DeleteCtrl(int nItem, int nSubItem);

pCellCtrl 可以是指向以下任意一项的指针:CCellEditCCellCheckBoxCCellComboBoxCCellDateCtrlCCellColorCtrl。可以通过在函数调用中直接使用 C++ `new` 指令来创建指针。无需担心相应的 `delete` 语句,因为清理工作已在 CConfigListCtrl 类中完成。

默认值 lpszDefaultTextValue 的用法如下:

  • CCellEdit:您想用任何文本字符串初始化编辑控件。
  • CCellCheckBox:以“0”(复选框未选中)或“1”(复选框已选中)为前缀的文本字符串。
  • CCellComboBox:格式为“sel string a\nstring b\nstring c\nsel string a”的字符串。这将用三个项目填充组合框:“string b”、“string c”和“sel string a”,其中“sel string a”被选中:即,如果列表中的第一项在默认值中重复,则它是选定的项。如果没有重复,组合框将显示一个空字符串。
  • CCellDateCtrl:此字符串必须采用标准格式:“YYYYMMDD”,其中 YYYY 是 4 位年份,MM 和 DD 分别是 2 位月份和日期。
  • CCellColorCtrl:必须采用标准格式:“0X00BBRRGG”,其中 BB、GG 和 RR 是代表 COLORREF 的蓝色、绿色和红色值的两位十六进制数字。
  • CCellTimeCtrl:默认情况下,将显示当前时间。如果您想重写此设置,必须插入格式为“HHmmss”的字符串。

为了方便操作,已编写了以下控件。

BOOL IsOnCellCtrl(int iItem, int iSubItem, CCellCtrl **ppCellCtrl);
CCellCtrl *GetItemCellCtrl(int iItem, int iSubItem);

如果 CConfigListCtrl{iItem, iSubItem} 位置存在相关单元格控件,则返回该控件。IsOnCellCtrl 在存在时返回 TRUE,否则返回 FALSE。如果单元格控件不存在,GetItemCellCtrl 返回 NULL

CCellCtrl *GetActiveCellCtrl() const

返回当前具有焦点的单元格控件。

void EnableCtrl(int iItem, int iSubItem, BOOL Enable = TRUE);
BOOL IsCtrlEnabled(int iItem, int iSubItem);

EnableCtrl 启用/禁用特定单元格中的控件(如果该单元格中有控件)。IsCtrlEnabled 返回单元格中控件是启用还是禁用。如果单元格根本没有控件,则返回的值是 CConfigListCtrl 实例是否已启用。

BOOL SetCtrlAlignment(int iItem, int iSubItem, Alignment align);
BOOL GetCtrlAlignment(int iItem, int iSubItem, Alignment &align);

SetCtrlAlignmentGetCtrlAlignment 将设置或获取单元格的对齐方式(如果该单元格包含 CCellCtrl)。这些函数在单元格中有控件时返回 TRUE,否则返回 FALSE。对齐方式仅对 CCellEditCCellCheckBoxCCellComboBox 有效。对 CCellDateCtrl 无效,因为尚未实现。对齐方式在标准的 MFC 日期选择器控件上也没有实现(因为可能不太有用)。对齐方式对 CCellColor 控件没有意义。对于右对齐的 CCellComboBox,按钮保留在右侧。标准的 MFC CComboBox 在左侧显示按钮,在 Windows 7 中存在一个小的 bug:按钮的角落向错误的一侧圆角,这很奇怪,因为主题中提供了右侧按钮样式。

有时确定某个单元格控件类型的行为很有用。这可以通过使用 `dynamic_cast` 指令来完成,例如:

CCellComboBox * pComboCrtl = 
   dynamic_cast<CCellComboBox *>(m_ListCtrl.GetItemCellCtrl(iRow, iColumn));

if (pComboCrtl)
    pComboCrtl->SetSelectedItem(iIndex);

以下是专用 CCellCtrl 控件函数的列表。

BOOL CCellComboBox::InsertItem(int Idx, LPCTSTR strText);
BOOL CCellComboBox::RemoveItem(int Idx);

在组合框中插入或删除项目。插入发生的位置由 Idx 指定。如果 Idx 超出现有值的范围,则返回 FALSE,且不执行任何操作。否则,返回 TRUE

BOOL CCellComboBox::SetSelectedItem(LONG lSelectedItem);
LONG CCellComboBox::GetSelectedItem() const;

设置或获取组合框的当前选定项。

LONG CCellComboBox::GetDisplayedRows() const; 
void CCellComboBox::SetDisplayedRows(LONG lDisplayedRows);

设置或获取组合框的下拉列表打开时显示的选定行数。

static void CCellDateCtrl::SetDateFormat(const CString & strDateFormat);

此静态函数将设置应用程序中使用 CConfigListCtrl 的所有 CCellDateCtrl 的日期格式。默认情况下,日期格式由计算机上指定的短日期格式给出(在英国通常为 DD/MM/YYYY)。strDateFormat 可以使用的标签由以下项给出:日、月、年和时代格式图片

void CCellDateCtrl::SetYMD(WORD Y, WORD M, WORD D);

将设置控件的年、月、日值。

void CCellTimeCtrl::SetTimeFormat(const CString& NewTimeFomat);

将设置控件的时间格式。可使用的标签为“H”、“HH”、“h”、“hh”、“m”、“mm”、“s”、“ss”、“t”和“tt”。

COLORREF CCellColorCtrl::GetSelectedColor()

将以 COLORREF 格式获取颜色。

限制

我知道以下缺点

  • 更改主题时,CListCtrl 行高未重新计算。
  • 似乎没有用于绘制非编辑式 Windows Vista/7 组合框按钮的主题部分标识符,该按钮占据组合框的全部长度,但箭头在右侧。
  • CMonthCalCtrl::GetMinReqRect 并不总是给出正确的矩形大小。如果将主题更改为 Classic,则返回的矩形不正确。如果注释掉 stdafx.h 中的清单依赖项代码,则矩形会再次正确。
  • 没有可编辑的组合框或微调编辑控件(带向上/向下按钮)。
  • 应该允许在 PopupColorBar 中使用旧式其他颜色对话框。“Other”应该放在字符串表中,如同工具提示中的项目一样,这样在其他国家使用时文本就可以被翻译。显示的颜色也应该是可配置的。总之,CCellColor 需要更多工作。
  • 也许应该有一个样式,以便在单击单元格时不会突出显示行。

致谢

我是在阅读了 Gamma、Helm、Johnson 和 Vlissides 的《设计模式》一书后开始这个项目的,所以这本书给了我灵感。此外,我所有的资源都来自 MSDN。

对于新的 CCellSpinCtrl 的重复效果,以下文章非常有帮助:鼠标重复

历史

  • 2011 年 8 月 1 日:初始版本。
  • 2011 年 8 月 9 日:实现了右对齐和居中对齐,并修复了许多恼人的 CCellEdit bug(复制、粘贴、滚动……)。
  • 2011 年 11 月 25 日:应用户要求添加了 CCellTimeCtrl
  • 2014 年 1 月 5 日:修复 bug 并添加了 CCellPushButton。特别感谢 Alexey Pismenny 为我提供了 pushbutton 代码,并为 RemoveCtrl 提供了改进。
© . All rights reserved.