资讯详情

资讯详情

MFC列表控件扩展:CListCtrlEx子控件嵌入与自绘优化实战

简介这份资源面向具备一定MFC基础的C开发者聚焦CListCtrlEx控件的扩展用法解决标准CListCtrl难以在列表项内嵌复杂交互控件的问题。CListCtrlEx在列表视图每一行中支持插入进度条、编辑框与复选框可用于展示任务进度、就地编辑数据以及批量勾选操作适合需要构建高交互性列表界面的桌面应用场景。压缩包共22个文件约4.41MB以h头文件与cpp源文件为主体配合vcproj工程、sln解决方案、rc资源脚本及ico图标等构成可直接编译运行的完整示例工程。目前已有301人学习下载。通过阅读源码读者可以掌握从CListCtrlEx派生新类、重写OnCreate添加子控件、编写消息映射响应BN_CLICKED等事件以及数据绑定与输入验证的完整实现思路为定制功能丰富的MFC列表视图提供可复用的参考代码。1. 从一份 MFC 列表扩展包说起CListCtrlEx 到底补了哪些短板做过 MFC 桌面端的人多半有过这种体验客户要在列表里直接改进度、改数值、勾选状态标准 CListCtrl 只能显示文本和图标想塞个编辑框就得自己算坐标、手动创建、处理焦点丢失代码写到最后变成一堆GetSubItemRect和SetWindowPos的堆叠。这份 CListCtrlEx.rar 就是冲着这个痛点来的——它在 CListCtrl 基础上封装了子控件嵌入能力让进度条、编辑框、复选框能像普通列表项一样被管理。包里是完整的 VC 工程ListCtrl.sln、ListCtrl.vcproj带 ReadMe.txt、资源脚本和对话框示例拿到手就能编译跑起来看效果。适合正在维护 MFC 老项目、又不想把界面推倒重写的开发者也适合想搞清楚子控件嵌入列表底层机制的人。下面按「它怎么实现的 → 怎么接进自己的工程 → 哪里容易翻车」这条线拆开讲。2. 子控件嵌入列表的实现原理从 CListCtrl 到 CListCtrlEx 的继承链2.1 为什么标准 CListCtrl 塞不进编辑框标准 CListCtrl 的每个单元格本质上是绘制出来的文本区域不是独立窗口。它没有子窗口句柄所以你没法直接给某个单元格Create一个 CEdit。常见的土办法是在列表上方浮一个编辑框靠NM_CLICK通知算出点击的是哪一行哪一列再把编辑框移到对应矩形上。这个方案能用但问题一堆滚动时编辑框不跟着走、列宽一变位置就错、多个单元格同时编辑直接打架。CListCtrlEx 的思路是把这套「浮层 坐标计算 生命周期管理」的逻辑收进控件内部。它继承自 CListCtrl在OnCreate阶段就把需要的子控件创建好作为列表控件的子窗口存在。因为子控件的父窗口是列表本身滚动、重绘、销毁这些事就跟着列表一起走了不用你在外面手动同步。2.2 继承链与关键成员从包里的 ListCtrlEx.h / ListCtrlEx.cpp 看核心类就是CListCtrlEx : public CListCtrl。它比标准控件多出来的东西大致分三类子控件容器保存每个单元格对应的 CEdit / CProgressCtrl / CButton 指针通常用CMap或二维数组按「行-列」索引。创建与布局在OnCreate或首次需要时创建子控件在OnSize、滚动消息里重新定位。消息转发子控件的事件编辑框的 EN_KILLFOCUS、复选框的 BN_CLICKED要转回列表让使用方能在列表层面处理。ThirdDefine.h / ThirdDefine.cpp 里放的是一些公共定义和辅助函数比如列索引常量、控件类型枚举。resource.h 和 ListCtrl.rc 是资源定义ListCtrlDlg 是演示对话框。这套结构不复杂但把「谁创建、谁销毁、谁定位」这三件事分清楚了后面接进自己工程才不会乱。2.3 子控件定位的核心GetSubItemRect 的正确用法不管嵌什么控件第一步都是拿到单元格的屏幕矩形。MFC 提供GetSubItemRect但它的参数有坑// 获取第 nRow 行、第 nCol 列单元格的矩形 CRect rect; // 注意LVIR_BOUNDS 拿到的是包含图标和文本的整格区域 // nCol 传 0 时部分版本行为不一致建议从 1 开始测 if (m_list.GetSubItemRect(nRow, nCol, LVIR_BOUNDS, rect)) { // rect 此时是客户区坐标转屏幕坐标再 SetWindowPos ClientToScreen(rect); m_edit.MoveWindow(rect); }逻辑说明GetSubItemRect返回的是相对于列表客户区的坐标而MoveWindow对子控件用的是父窗口客户区坐标两者在列表没有滚动时一致一旦滚动就会偏。稳妥做法是先ClientToScreen转屏幕坐标再用ScreenToClient转回列表客户区或者直接用MapWindowPoints。参数上LVIR_BOUNDS拿整格LVIR_LABEL只拿文本区嵌编辑框一般用 BOUNDS 让编辑框铺满单元格。第 0 列图标列在某些 MFC 版本里GetSubItemRect行为特殊建议先在第 1 列之后测通再回头处理第 0 列。3. 把 CListCtrlEx 接进自己的工程四步落地与参数配置3.1 文件拷贝与工程配置拿到包后不要直接改示例工程先把可复用的文件挑出来。需要拷进自己项目的是ListCtrlEx.h、ListCtrlEx.cpp、ThirdDefine.h、ThirdDefine.cpp。ListCtrlDlg 和 ListCtrl.cpp 是演示用的参考即可。拷进来后做三件事在 VS 里右键项目 → 添加 → 现有项把四个文件加进去。在要用到列表的对话框头文件里#include ListCtrlEx.h。把对话框上原来的 CListCtrl 控件变量类型从CListCtrl改成CListCtrlEx。如果是用 DDX 绑定的改DoDataExchange里的类型如果是手动 SubclassDlgItem改子类类型。// 对话框头文件里 CListCtrlEx m_list; // 原来是 CListCtrl // DoDataExchange 里 void CMyDlg::DoDataExchange(CDataExchange* pDX) { CDialogEx::DoDataExchange(pDX); // 控件 ID 换成你自己的 DDX_Control(pDX, IDC_LIST1, m_list); }参数说明DDX_Control的第三个参数类型决定绑定行为改成 CListCtrlEx 后MFC 会用你的派生类去 Subclass 那个控件OnCreate才有机会被调到。如果这里没改后面所有子控件都不会创建表现为「代码没错但编辑框不出现」。3.2 初始化列与插入带子控件的行列的定义和标准 CListCtrl 一样用InsertColumn。区别在插入行之后要告诉 CListCtrlEx 这一行的哪一列需要什么控件。// 初始化列在 OnInitDialog 里 m_list.InsertColumn(0, _T(名称), LVCFMT_LEFT, 120); m_list.InsertColumn(1, _T(进度), LVCFMT_LEFT, 150); m_list.InsertColumn(2, _T(备注), LVCFMT_LEFT, 150); m_list.InsertColumn(3, _T(启用), LVCFMT_LEFT, 60); // 插入一行 int row m_list.InsertItem(0, _T(任务A)); m_list.SetItemText(row, 2, _T(可编辑)); // 给第 1 列挂进度条第 2 列挂编辑框第 3 列挂复选框 // 具体函数名以 ListCtrlEx.h 里的声明为准常见命名如下 m_list.AddProgressCtrl(row, 1); // 进度条 m_list.AddEditCtrl(row, 2); // 编辑框 m_list.AddCheckBox(row, 3); // 复选框逻辑说明InsertItem只负责文本行子控件要单独挂。挂的时候传行号和列号CListCtrlEx 内部会创建对应控件并保存指针。参数上进度条通常还要设范围和当前值编辑框要设初始文本复选框要设初始勾选状态这些一般在 Add 之后紧跟一句设置调用。3.3 进度条、编辑框、复选框的参数设置三种控件的配置方式不同分开说。进度条创建后设范围再设位置。范围默认 0~100如果你的任务总量不是百分比要显式设。CProgressCtrl* pProg m_list.GetProgressCtrl(row, 1); if (pProg) { pProg-SetRange(0, 1000); // 总量 1000 pProg-SetPos(350); // 当前 350 }编辑框初始文本、输入限制、焦点处理都要管。输入限制用CEdit::SetLimitText焦点处理靠 EN_KILLFOCUS 消息在失去焦点时把内容写回列表项文本。CEdit* pEdit m_list.GetEditCtrl(row, 2); if (pEdit) { pEdit-SetWindowText(_T(初始值)); pEdit-SetLimitText(64); // 最多 64 字符 }复选框初始状态用SetCheck状态变化靠 BN_CLICKED 消息。注意复选框在列表里通常是「点一下切换」不需要额外按钮。CButton* pCheck m_list.GetCheckBox(row, 3); if (pCheck) { pCheck-SetCheck(BST_CHECKED); // 初始勾选 }参数说明SetRange的上下限是 int注意别超过 int 范围SetLimitText只对单行编辑框有效多行要另设SetCheck用 BST_CHECKED / BST_UNCHECKED不要传 TRUE/FALSE虽然部分版本能编译但语义不对。3.4 消息映射与事件响应子控件的事件不会自动冒泡到对话框要在 CListCtrlEx 内部或对话框里加消息映射。常见做法是在对话框里处理因为业务逻辑在对话框层。// 对话框头文件里声明 afx_msg void OnCheckChanged(); afx_msg void OnEditKillFocus(); // 消息映射 BEGIN_MESSAGE_MAP(CMyDlg, CDialogEx) ON_BN_CLICKED(IDC_CHECK_BASE, CMyDlg::OnCheckChanged) ON_EN_KILLFOCUS(IDC_EDIT_BASE, CMyDlg::OnEditKillFocus) END_MESSAGE_MAP()逻辑说明这里有个关键点——子控件的 ID。如果 CListCtrlEx 内部动态创建控件时用了固定 ID多个单元格的复选框会共用同一个 ID消息就分不清是哪一行。稳妥做法是给每个子控件分配唯一 ID或者在 CListCtrlEx 内部用ON_CONTROL_RANGE按 ID 范围处理再通过GetDlgCtrlID反查行号。包里的 ThirdDefine.h 很可能就是干这个的接工程时先看一眼里面有没有 ID 分配规则。4. 避坑与排查子控件不显示、错位、消息丢失的五个血泪经验4.1 子控件完全不出现现象代码编译通过列表能显示文本但进度条、编辑框、复选框一个都看不到。原因最常见的是控件变量类型没从 CListCtrl 改成 CListCtrlEx导致OnCreate没被调用子控件创建逻辑根本没执行。其次是OnCreate里创建子控件时父窗口传错传了对话框而不是列表。解决先确认DoDataExchange里的类型再在OnCreate里下断点看有没有进去。如果进去了但控件没显示检查Create的父窗口参数是不是this列表本身以及WS_CHILD | WS_VISIBLE样式有没有带全。4.2 滚动后子控件错位现象不滚动时位置正确一滚动编辑框就跑到别的行上去了。原因子控件定位用的是创建时的坐标滚动后没有重新计算。标准 CListCtrl 滚动不会通知子控件需要自己处理。解决响应列表的LVN_ITEMCHANGED不够要处理WM_VSCROLL和WM_HSCROLL在滚动后遍历所有子控件重新MoveWindow。更省事的做法是在OnPaint或OnEraseBkgnd之后统一重定位但要注意别造成闪烁。4.3 编辑框输入后内容丢失现象在编辑框里改了值点到别处后列表项文本没更新或者更新了但编辑框又变回旧值。原因EN_KILLFOCUS 消息处理里没有把编辑框内容写回列表项或者写回了但没刷新显示。另一个可能是编辑框被重复创建旧指针失效。解决在 EN_KILLFOCUS 里先GetWindowText拿到内容再SetItemText写回列表最后调Invalidate刷新。如果发现指针失效检查是不是在每次重绘时都重新 Add 了控件应该只在插入行时 Add 一次。4.4 复选框消息分不清行号现象点了第 3 行的复选框结果第 1 行的状态也变了或者处理函数里拿到的行号永远是 0。原因多个复选框共用同一个控件 IDON_BN_CLICKED只能按 ID 匹配分不清是哪个。或者处理函数里用GetDlgItem拿控件再反查行号时逻辑写错。解决给每个复选框分配唯一 ID比如IDC_CHECK_BASE row然后用ON_CONTROL_RANGE处理一个范围。在处理函数里用GetDlgCtrlID() - IDC_CHECK_BASE反算行号。注意 ID 范围别和对话框上其他控件冲突。4.5 大量数据时界面卡顿现象插入几百行带子控件的列表后滚动明显卡甚至无响应。原因每行都创建真实窗口控件几百个窗口的创建和重绘开销很大。MFC 的列表本身是虚拟列表友好的但子控件嵌入和虚拟列表天生冲突。解决数据量大时不要每行都挂子控件改成「点击哪格才创建哪格的控件」用完销毁。或者用自绘Custom Draw模拟进度条和复选框的外观只有编辑框用真实控件。包里的示例是演示性质行数少直接套到生产环境的大列表上要改。5. 进阶技巧用自绘替代部分子控件把性能拉回来子控件嵌入的代价是窗口数量。一个 500 行的列表如果每行挂 3 个子控件就是 1500 个窗口Windows 的窗口管理再高效也扛不住频繁重绘。我一般的做法是分层进度条和复选框用自绘编辑框保留真实控件。进度条自绘很简单在NM_CUSTOMDRAW的CDDS_ITEMPREPAINT阶段拿到单元格矩形用FillSolidRect画背景再按百分比画前景。// 在 NM_CUSTOMDRAW 处理里 case CDDS_ITEMPREPAINT: { // 只处理进度列 if (dwItemSpec nProgressCol) { CRect rc ...; // 从 lplvcd 拿单元格矩形 int percent GetProgressPercent(lplvcd-nmcd.dwItemSpec); CRect rcFill rc; rcFill.right rc.left rc.Width() * percent / 100; CDC* pDC CDC::FromHandle(lplvcd-nmcd.hdc); pDC-FillSolidRect(rc, RGB(240, 240, 240)); // 底 pDC-FillSolidRect(rcFill, RGB(0, 120, 215)); // 前景 return CDRF_SKIPDEFAULT; } }逻辑说明dwItemSpec是行号lplvcd里能拿到子项索引和矩形。参数上percent从你的数据模型算不要从控件读。返回CDRF_SKIPDEFAULT告诉列表这格已经画完了别再画默认内容。复选框自绘同理画个方框加勾。编辑框因为要处理输入法、光标、选择自绘成本太高保留真实控件但可以只在鼠标点击某格时才创建失去焦点就销毁。这样任意时刻最多只有一个编辑框存在窗口数量从 O(行数) 降到 O(1)。验证方法在任务管理器里看 GDI 对象数和 USER 对象数自绘改造后这两个数应该明显下降。滚动时用QueryPerformanceCounter打点看单帧耗时有没有回到 16ms 以内。从那以后我每次接这类列表扩展控件都先问一句「数据量上限多少」超过 200 行就直接上自绘方案不再无脑挂子控件。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →