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






4.92/5 (54投票s)
一个 CListCtrl 的派生类,可以非常轻松地将编辑控件、组合框、复选框、日期选择器和颜色选择器插入或删除到特定单元格。插入的“控件”不是 CWnd 派生的。
引言
一个常见的需求是拥有一个自定义绘制的 CListCtrl
,它允许在特定单元格中包含另一个控件,例如编辑框、组合框、颜色选择器等。这样的控件在许多应用程序中都非常有用,因为它提供了一种轻松配置应用程序特定属性(如背景颜色等)的方法。
本文档介绍了一些实现此功能的代码示例。
背景
多年来,我见过几个允许将 CComboBox
或 CEdit
控件插入到 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
派生控件的指针存储在 CConfigListCtrl
的 m_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_SIZING
和 WM_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
中使用。最有用的可能是 SetColumnWidth
和 GetItemText
。我将仅描述新函数或重载的函数。
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
可以是指向以下任意一项的指针:CCellEdit
、CCellCheckBox
、CCellComboBox
、CCellDateCtrl
或 CCellColorCtrl
。可以通过在函数调用中直接使用 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);
SetCtrlAlignment
和 GetCtrlAlignment
将设置或获取单元格的对齐方式(如果该单元格包含 CCellCtrl
)。这些函数在单元格中有控件时返回 TRUE,否则返回 FALSE。对齐方式仅对 CCellEdit
、CCellCheckBox
和 CCellComboBox
有效。对 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
提供了改进。