CGridListCtrlEx - 基于 CListCtrl 的网格控件






4.91/5 (136投票s)
一个自定义绘制的 CListCtrl,支持子项编辑和格式化
介绍
Microsoft 的 CListCtrl
支持使用报表样式显示网格中的数据,但我们需要进行多项更改才能实现以下功能:
- 排序
- 单元格导航和键盘搜索
- 工具提示
- 隐藏和显示列
- 单元格编辑
- 超链接
- 自定义行和单元格颜色
- 分组
- 剪贴板(仅复制)
- 持久化列宽度、位置和可见性
- OLE 拖放(包括项目重新排序)
本文演示了如何使用 CGridListCtrlEx
,它实现了上述所有功能,同时保持了 Windows XP/Vista 的外观。
如果您需要 Git/Subversion 访问权限,可以使用 GitHub - CGridListCtrlEx,此外还有 Doxygen 文档。
背景
有许多高级网格控件可以扩展 CListCtrl
,其中之一就是 增强列表控件 (CGfxListCtrl)。这个出色的控件提供了上述所有功能,但未能很好地处理 Windows XP 和 Vista。找到一个好的替代品并不容易
- MFC 网格控件 - 不继承自
CListCtrl
,因此不受其限制,但它也无法从 Microsoft 为CListCtrl
添加的任何改进中受益。 - Ultimate Grid - 与 MFC 网格控件一样,它不继承自
CListCtrl
。最初需要购买,但现在可以免费使用。 - CQuickList - 非常接近完美的替代品,但难以添加新的数据显示方式,并且需要
LVS_OWNERDATA
,这使得排序稍显困难。 - XListCtrl - 也是一个非常完整的
CListCtrl
,但难以添加新的数据显示方式,并且不支持LVS_OWNERDATA
。现在需要购买许可证才能获得最新版本。 - 另一个报表列表控件 - 简单易用,但除了使用
CEdit
之外,缺乏其他编辑数据的方式,并且缺少子项导航。 - CListCtrlEx - 实现了很多功能并且文档齐全。最初需要
LVS_OWNERDRAWFIXED
,现在已发展为使用自定义绘制。自定义绘制和所有者绘制的结合使得代码有点复杂,并且它也不支持LVS_OWNERDATA
。
CGridListCtrlEx
插入了一个名为列特征(column traits)的抽象层,该层负责单元格的绘制和编辑。如果 Microsoft 再次扩展其 CListCtrl
,那么 CGridListCtrlEx
的核心应该会继续工作。
如何使用 CGridListCtrlEx
CGridListCtrlEx
尽量保持与 CListCtrl
的一致性,并且不试图替换 CListCtrl
已提供的任何功能。这意味着我们可以直接用 CGridListCtrlEx
替换 CListCtrl
,而无需进行任何其他操作。
建议不要直接使用 CGridListCtrlEx
,而是创建一个新的派生自 CGridListCtrlEx
的类。这样可以更轻松地迁移将来对 CGridListCtrlEx
类的任何更新。
编辑单元格/子项
默认情况下,在 CGridListCtrlEx
中插入列时,它们将被配置为只读,无法编辑。通过使用 CGridListCtrlEx::InsertColumnTrait()
,我们可以提供一个 CGridColumnTrait
类,该类指定了应使用的编辑器类型。
CGridColumnTrait* pTrait = new CGridColumnTraitEdit;
m_ListCtrl.InsertColumnTrait(nCol, title.c_str(), LVCFMT_LEFT, 100, nCol, pTrait);
编辑完项目后,将向 CListCtrl
发送标准的 LVN_ENDLABELEDIT
消息。当 CGridListCtrlEx
收到此消息时,它将自动调用虚方法 CGridListCtrlEx::OnEditComplete()
,允许派生类验证输入并可能更新底层数据模型。
使用组合框编辑单元格/子项
通过使用 CGridListCtrlEx::InsertColumnTrait()
,我们还可以提供一个 CGridColumnTrait
类,该类充当 CComboBox
。
CGridColumnTraitCombo* pTrait = new CGridColumnTraitCombo;
pTrait->AddItem(0, "Hello");
pTrait->AddItem(1, "Goodbye");
m_ListCtrl.InsertColumnTrait(nCol, title.c_str(), LVCFMT_LEFT, 100, nCol, pTrait);
我们可以在插入列时指定 CComboBox
的项目(如上所示)。如果想动态提供 CComboBox
项目,则可以重写 CGridListCtrlEx::OnEditBegin()
。使用 dynamic_cast<>
调用列特征方法 CGridColumnTraitCombo::LoadList()
,或直接处理返回的 CComboBox
编辑器。
如果要获取所选 CComboBox
项目的 itemdata,可以重写 CGridListCtrlEx::OnEditComplete()
并检查参数值 pLVDI->item.lParam
。由于 itemdata 无法存储在 CListCtrl
的本地数据模型中,因此需要将其保存在其他地方。
排序行
默认情况下,GridListCtrlEx
将对所有列启用排序,并执行简单的文本比较。可以通过列特征实现自定义排序,重写 CGridColumnTrait::OnSortRows()
。
配置列特征以进行数字比较排序
CGridColumnTraitEdit* pTrait = new CGridColumnTraitEdit;
pTrait->SetSortFormatNumber(true); // Numeric column
m_ListCtrl.InsertColumnTrait(nCol, title.c_str(), LVCFMT_LEFT, 100, nCol, pTrait);
列特征 CGridColumnTraitDateTime
将自动尝试按日期进行排序。
我们还可以选择重写 CGridListCtrlEx::SortColumn()
方法。然后,只需选择正确的排序方式即可。另请参阅 CListCtrl 和排序行。
显示工具提示
默认情况下,CGridListCtrlEx
将仅将单元格内容显示为工具提示。如果想在工具提示中显示其他内容,可以重写 CGridListCtrlEx::OnDisplayCellTooltip()
方法。
格式化单元格/子项
如果想更改前景色/背景色或字体样式(粗体、斜体、下划线),可以重写 CGridListCtrlEx::OnDisplayCellColor()
和 CGridListCtrlEx::OnDisplayCellFont()
方法。
bool MyGridCtrl::OnDisplayCellColor(int nRow, int nCol, COLORREF& textColor, COLORREF& backColor)
{
if (nRow == 3 && nCol == 3)
{
textColor = RGB(0,255,0);
backColor = RGB(0,0,255);
return true; // I want to override the color of this cell
}
return false; // Use default color
}
显示单元格/子项图像
CGridListCtrlEx
默认启用扩展样式 LVS_EX_SUBITEMIMAGES
,但仍需要使用 CListCtrl::SetImageList()
附加 CImageList
。
附加图像后,就可以使用 CGridListCtrlEx::SetCellImage()
将单元格/子项与 CImageList
中的索引绑定。或者,如果使用 I_IMAGECALLBACK
,则可以通过重写 CGridListCtrlEx::OnDisplayCellImage()
返回图像索引。
CGridListCtrlEx
还默认启用了扩展样式 LVS_EX_GRIDLINES
,这可能导致子项图像与网格边框重叠。可以通过确保图像仅使用 15 个像素(第一个像素透明)来解决此问题。
当使用子项图像并在 Windows XP 或经典样式上运行应用程序时,选定的行将显示白色背景。可以使用 CGridRowTraitXP
来修复此问题。
m_ListCtrl.SetDefaultRowTrait(new CGridRowTraitXP);
复选框支持
CListCtrl
支持标签列的复选框。只需应用扩展样式 LVS_EX_CHECKBOXES
。
m_ListCtrl.SetExtendedStyle(m_ListCtrl.GetExtendedStyle() | LVS_EX_CHECKBOXES);
请注意不要使用 InsertHiddenLabelColumn()
,因为它会隐藏标签列及其复选框。可以使用 GetCheck()
/ SetCheck()
来检索/修改复选框值。
如果想为多个列设置复选框,可以使用 CGridColumnTraitImage
(及其特例)。
// Appends the unchecked/checked state images to the list control image list
int nStateImageIdx = CGridColumnTraitImage::AppendStateImages(m_ListCtrl, m_ImageList);
m_ListCtrl.SetImageList(&m_ImageList, LVSIL_SMALL);
// Creates an image column, that can switch between the 2 images
CGridColumnTrait* pTrait = new CGridColumnTraitImage(nStateIdx, 2);
m_ListCtrl.InsertColumnTrait(nCol, title.c_str(), LVCFMT_LEFT, 20, nCol, pTrait);
for(int i=0; i < m_ListCtrl.GetItemCount(); ++i)
m_ListCtrl.SetCellImage(i, nCol, nStateImageIdx); // Uncheck item
当使用 SetImageList()
分配 CImageList
时,标签列会自动显示图像列。无法禁用此行为,但可以使用 InsertHiddenLabelColumn()
隐藏标签列。
CGridColumnTraitImage
使用单元格图像来绘制复选框,因此无法在同一列中同时拥有单元格图像和复选框。要获取和设置已勾选/未勾选状态,可以使用 GetCellImage()
/ SetCellImage()
。
CGridColumnTraitImage
支持根据复选框是否启用进行排序。使用 CGridColumnTraitImage::SetSortImageIndex()
来启用此功能。
CGridColumnTraitImage
还支持切换所有选定行的复选框。使用 CGridColumnTraitImage::SetToggleSelection()
来启用此功能。
超链接支持
超链接列可以将单元格内容显示为链接,这些链接可以被点击并启动外部应用程序,例如默认的 Web 浏览器(http)或邮件客户端(mailto)。
CGridColumnTraitHyperLink
允许提供单元格文本的前缀(和后缀),这使得我们可以在不显示额外协议详情的情况下添加它们。
CGridColumnTraitHyperLink* pHyperLinkTrait = new CGridColumnTraitHyperLink;
pHyperLinkTrait->SetShellFilePrefix(_T("http://en.wikipedia.org/wiki/UEFA_Euro_"));
m_ListCtrl.InsertColumnTrait(nCol, title.c_str(), LVCFMT_LEFT, 100, nCol, pHyperLinkTrait);
还可以使用 CGridColumnTraitHyperLink::SetShellApplication()
指定一个自定义应用程序来启动,而不仅仅是基于协议前缀启动的默认应用程序。
超链接还可以用来模拟按钮。点击时会发送 LVN_ENDLABELEDIT
通知,可以将其视为父视图中的按钮点击通知。
更改行高
CGridListCtrlEx
使用自定义绘制,因此只有以下解决方案可用:
- 分配一个
CImageList
,其中图像的高度与所需的行高一致。 - 更改网格控件的字体,行高将随之改变。
CGridListCtrlEx::SetCellMargin()
使用此技巧来增加网格控件的字体,同时保持行字体不变。
更改空标记文本
当 CGridListCtrlEx
不包含任何项目时,它将显示标记文本以指示列表为空。
使用 CGridListCtrlEx::SetEmptyMarkupText()
来更改此标记文本。如果提供空文本,则其行为将与普通 CListCtrl
相同。
如果使用 CGridListCtrlGroups
,它将在 Windows Vista 上运行时响应 LVN_GETEMPTYMARKUP
。
加载和保存列宽和位置
CViewConfigSectionWinApp
提供了存储列的宽度、位置和显示状态的功能。在将所有可用列添加到 CGridListCtrlEx
后,使用 CGridListCtrlEx::SetupColumnConfig()
分配 CViewConfigSectionWinApp
的实例,它将通过 CWinApp
恢复上次保存的列配置。
m_ListCtrl.SetupColumnConfig(new CViewConfigSectionWinApp("MyList"));
如果应用程序中的多个地方使用 CGridListCtrlEx
,则应确保为每个地方创建唯一的 CViewConfigSectionWinApp
。
OLE 拖放
CGridListCtrlEx
既可以作为 OLE 拖动源,也可以作为 OLE 放置目标。这允许在 CGridListCtrlEx
与其他窗口和应用程序之间进行拖放操作。
CGridListCtrlEx
支持在进行内部拖放时重新排序行。这是通过特殊的排序操作实现的,因此项目不会被删除/插入。
要为放置操作实现自己的特殊行为,请根据要处理的情况重写 OnDropSelf()
或 OnDropExternal()
。
要控制启动拖动时放置在拖动源中的内容,请重写 OnDisplayToDragDrop()
。
CGridColumnTrait 如何工作
CGridListCtrlEx
尽量避免处理所有关于如何显示和编辑数据的细节。这些细节由 CGridColumnTrait
类处理,如果我们想修改数据显示方式,那么“只需”创建一个新的 CGridColumnTrait
类即可。
在插入列时,我们可以为该列分配一个 CGridColumnTrait
。当需要绘制该列中的单元格或编辑该列中的单元格时,CGridListCtrlEx
将激活相应的 CGridColumnTrait
。
CGridColumnTrait
包含一些称为元数据(meta-data)的特殊成员。这些成员可以被派生自 CGridListCtrlEx
的类使用,因此我们可以轻松地为列添加额外的属性。
继承自 CGridColumnTrait
时,我们必须考虑以下几点:
- 如果执行自定义绘制,我们还必须处理选择和焦点着色。
- 如果执行编辑,我们必须确保编辑器在失去焦点时关闭,并在编辑完成后发送
LVN_ENDLABELEDIT
消息。
CGridRowTrait 如何工作
它的想法与 CGridColumnTrait
相同,但操作级别是行而不是列。这对于需要修改所有列的显示行为的情况很有用。
Using the Code
源代码包括以下类:
CGridListCtrlEx
- 特别的CListCtrl
CGridListCtrlGroups
- 扩展了CGridListCtrlEx
,支持分组CGridColumnTrait
- 指定列特征的接口CGridColumnTraitText
- 实现单元格格式化CGridColumnTraitImage
- 通过在图像之间切换来实现单元格编辑(可以模拟复选框)CGridColumnTraitEdit
- 使用CEdit
实现单元格编辑CGridColumnTraitCombo
- 使用CComboBox
实现单元格编辑CGridColumnTraitDateTime
- 使用CDateTimeCtrl
实现单元格编辑CGridColumnTraitHyperLink
- 实现单元格作为超链接的功能
CGridRowTrait
- 指定行特征的接口CGridRowTraitText
- 实现行格式化CGridRowTraitXP
- 使用经典或 XP 样式绘制子项图像背景
CViewConfigSection
- 用于持久化列设置的抽象接口CViewConfigSectionWinApp
- 实现接口并可以在多个列设置之间切换。
待办事项
CGridListCtrlEx
尽量避免自行绘制。这意味着以下功能/错误将不会得到太多关注:
- 进度条支持 - 需要一个绘制整个单元格的
CGridColumnTrait
类。
实现一个绘制整个单元格的 CGridColumnTrait
类,可能需要借鉴 ListCtrl - 具有 Windows Vista 样式项目选择的 WTL 列表控件 的代码。
非常欢迎向此项目贡献。
历史
- 版本 1.0 (2008-09-04) 首次发布
- 版本 1.1 (2008-09-18)
- 添加了对
CGridListCtrlGroups
分组的支持 - 添加了
CDateTimeCtrl
编辑器 - 修复了在使用经典和 XP 样式时的绘制错误
- 修复了选定子项的图像背景色(不再是白色)
- 修复了滚动左右时网格边框消失的问题
- 当列表没有项目时显示列表为空的指示
- 扩展了
CComboBox
编辑器,使其能够自动调整下拉列表的宽度以适应内容
- 添加了对
- 版本 1.2 (2008-09-24)
- 用
CGridRowTraitXP
替换了CGridListCtrlXP
- 修复了一些报告的错误
- 用
- 版本 1.3 (2008-10-09)
- 修复了在
CView
中使用时的扩展样式 - 修复了使用键盘快捷方式 (SHIFT+F10) 时上下文菜单的定位
- 修复了使用 Visual Studio 6 (VC6) 时出现的编译器错误
- 修复了在
- 版本 1.4 (2008-11-07)
- 添加了剪贴板支持,用于复制选定单元格/行的内容
- 将“
Callback
”函数重命名为“OnDisplay
”,以模仿 MFC 的命名约定 - 修复了一些报告的错误
- 版本 1.5 (2009-03-29)
- 添加了列管理器
CGridColumnManager
- 添加了对 VC6 和平台 SDK 的分组支持
- 为不同版本的 Visual Studio 添加了示例项目
- 通过 Doxygen 注释改进了文档
- 添加了列管理器
- 1.6 版 (2009-09-13)
- 添加了 OLE 拖放支持
- 在 LVS_OWNERDATA 中添加了对复选框样式
LVS_EX_CHECKBOX
的支持 - 为 LVS_OWNERDATA 添加了更好的键盘搜索支持
- 修复了多个错误
- 1.7 版 (2009-12-12)
- 添加了
CGridColumnTraitImage
,它可以模仿任何列的复选框编辑 - 将
OnTraitEditBegin()
重命名为OnEditBegin()
- 将
OnTraitEditComplete()
重命名为OnEditComplete()
- 将
OnTraitCustomDraw()
重命名为OnCustomDrawCell()
- 修复了多个错误(主要是行和单元格着色)
- 添加了
- 1.8 版 (2010-10-01)
- 将
CGridColumnTraitImage
设为所有编辑器列特征的基类,以便它们都可以模拟复选框支持 - 实现了多选复选框支持,以便为所有选定行翻转复选框
- 通过基类列特征实现了最小和最大列宽
- 修复了多个错误
- 将
- 版本 1.9 (2011-05-30)
- 将
CGridColumnTrait::OnSortRows
的参数从字符串更改为LVITEM
- 将
CGridColumnConfig
重命名为CViewConfigSection
- 移除了
CGridColumnManager
,并将LoadState
/SaveState
移至CGridListCtrlEx
(破坏性更改)
- 将
- 版本 2.0 (2012-05-01)
- 为
CDateTimeCtrl
编辑器添加了复制/粘贴支持(DTS_APPCANPARSE
) - 当按列分组时,列标题单击现在进行排序而不是按列分组
- 默认情况下,行排序更改为不区分大小写
- 修复了单元格编辑器丢弃使用鼠标执行的更改(通过右键菜单复制/粘贴)的错误
- 修复了多个编译器警告和小错误
- 为
- 版本 2.1 (2012-06-20)
- 修复了 2.0 版本引入的错误,即分组项在 WinXP 上不再工作。
- 提高了对项目组进行排序的性能,尤其是在 Vista/Win7+ 上。
- 添加了新的列特征
CGridColumnTraitHyperLink
,它可以将单元格文本显示为 Web 链接。更新了演示应用程序以显示新的列类型。 - 添加了新的列特征
CGridColumnTraitMultilineEdit
,它可以编辑包含换行符的单元格文本(仍显示为单行)。 - 修复了多个小错误
- 版本 2.2 (2012-11-11)
CGridColumnTraitCombo
现在可以选择在编辑开始时显示下拉列表(SetShowDropDown
)- 所有列特征编辑器现在都可以选择在第一次鼠标单击时启动编辑器(
SetSingleClickEdit
) CGridColumnTraitHyperLink
在鼠标单击时会向父级发送LVN_ENDLABELEDIT
通知(模拟按钮点击)- Windows 7/8 上子项使用的复选框状态图标已得到改进,避免了图像缩放
- 修复了多个小错误
- 版本 2.3 (2015-03-17)
- 由于 Google Code 即将关闭,项目已迁移到 GitHub。
- 实现了对
IDropSource::GiveFeedback
的支持,允许在拖放期间更改鼠标光标 - 单元格焦点着色现在默认禁用。调用
SetInvertCellSelection(true)
可启用。 - 修复了多个小错误