尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

MFC扩展列表控件CListCtrlEx实战:内嵌进度条、编辑框与复选框

MFC扩展列表控件CListCtrlEx实战:内嵌进度条、编辑框与复选框 简介该压缩包提供MFC中CListCtrlEx扩展控件的完整实现与示例工程面向需要增强列表视图交互能力的Windows桌面开发者。通过继承CListCtrlEx并重写OnCreate可在列表行内嵌入进度条、编辑框和复选框适合任务进度展示、行内直接编辑、多选管理等场景。包内共22个文件包括7个头文件、5个C源文件以及Visual Studio工程与解决方案文件vcproj/sln/rc等压缩包总大小4.41MB目录结构清晰便于直接编译对照学习。已有301人学习下载。资源附带了完整的对话框集成代码、消息映射及数据绑定写法可帮助开发者快速掌握CListCtrlEx的派生、子控件动态添加与事件响应流程减少自行摸索的时间。1. 从 CListCtrl 到 CListCtrlEx为什么默认列表控件总差一口气做 MFC 界面的人迟早会撞上同一个尴尬CListCtrl 自带的报表视图只能显示静态文本你想在某一列里放个进度条、让用户直接双击改内容、或者每行加个复选框都得自己造轮子。这套 CListCtrlEx 正是冲着这三个需求去的——它在标准 CListCtrl 的基础上把进度条CProgressCtrl、编辑框CEdit、复选框CButton塞进列表的每一行让你不用再维护一套平行的控件坐标数组。适合谁用手头维护着老 VS 工程、想在列表里做行内编辑和状态展示、又不想引入 DirectUI 之类重型方案的 MFC 开发者。这篇笔记从工程文件拆起把它的实现套路、参数设置和踩过的坑一次讲透。2. 拆开 ListCtrl 工程看清 CListCtrlEx 的三个关键设计2.1 工程文件里谁是谁从 .sln 到 ThirdDefine.h 的职责划分这套工程是典型的 VS2008 时代 MFC 对话框程序如果你拿到手的是压缩包先别急着编译把文件角色理清楚再动手后面改起来才不迷路。ListCtrl.sln 和 ListCtrl.vcproj 是解决方案与工程文件ListCtrl.vcproj.SKSOFT.huaw.user 是机器相关用户配置可以直接忽略ListCtrlDlg.cpp / ListCtrlDlg.h 是主对话框负责承载 CListCtrlExListCtrl.cpp / ListCtrl.h 是应用入口和主窗框架ListCtrlEx.cpp / ListCtrlEx.h 才是这套资源的核心CListCtrlEx 类的全部实现都在这两个文件里。ThirdDefine.h 值得单独说一句它通常放第三方宏定义和公共头文件引用比如为了兼容不同 SDK 版本而做的条件编译开关。resource.h、ListCtrl.rc、ListCtrl.rc2、ListCtrl.ico 是资源相关文件。ListCtrl.suo 和 ListCtrl.ncb 是 Visual Studio 的临时文件编译不过时先删掉这两个再重新生成解决方案能排除不少缓存导致的假故障。2.2 继承还是组合CListCtrlEx 的类设计思路CListCtrlEx 没有绕弯子直接公有继承 CListCtrl把扩展能力封装在子类内部。这意味着所有 CListCtrl 的既有方法照常可用你不需要改变原有调用习惯。它的核心数据结构是一个行状态数组每一行对应一个结构体记录该行是否内嵌编辑框、是否内嵌进度条、当前是否处于编辑态。这种设计相比「外部维护控件句柄数组」的好处在于列表滚动时控件位置跟着行走删除行时子控件自动清理。常见做法是重写 PreSubclassWindow 或 OnCreate在控件窗口创建完成后初始化内部状态数组而不是在构造函数里做——因为构造时 Windows 控件句柄还没建立拿不到客户区尺寸。在消息映射上CListCtrlEx 拦截两类消息一类是父窗口转发过来的 WM_MEASUREITEM / WM_DRAWITEM用于自绘进度条和复选框另一类是子控件自身的通知消息比如编辑框的 EN_KILLFOCUS、复选框的 BN_CLICKED通过反射机制ON_NOTIFY_REFLECT在控件内部消化掉外面调用者只收到一个简洁的 LVN_ENDLABELEDIT 之类通知。2.3 消息流走向控件、视图、对话框之间怎么配合理解这套工程的消息流是改代码的前提。用户点了一下某行的编辑框Windows 先把 WM_COMMAND 发给编辑框的父窗口——也就是 CListCtrlEx 本身。CListCtrlEx 在 ON_NOTIFY_REFLECT 里拦截 EN_KILLFOCUS判断文本是否合法合法就把新值写回内部数据数组再向对话框发一个自定义消息比如 WM_ITEM_DATA_CHANGED参数带上行号和列号。对话框收到后更新自己的业务数据。反方向的数据流对话框要刷新某行进度条时不直接找进度条句柄而是调用 CListCtrlEx 的 SetProgressValue(row, percent)由 CListCtrlEx 内部定位子控件并转发消息。这种封装的直接收益是对话框代码里少了一大堆 GetDlgItem 和强制类型转换行号与控件句柄的对应关系只在 CListCtrlEx 内部维护一处出错概率大幅降低。3. 在列表行里塞进度条自绘与消息转发的实战配置3.1 创建与初始化范围、位置、步长怎么定进度条进入列表行的方式有两种用 CProgressCtrl 直接创建子窗口或者走自绘。CListCtrlEx 这类扩展控件一般用第一种创建时机选在 OnCreate 之后、首次显示之前。范围设置上有讲究常见做法是先把范围设为 0 到 100用 SetProgressValue 传百分比避免调用者每次自己换算也有工程把范围设为任务的实际步数上限此时进度值就是绝对数值。位置需要跟着行走ListView 的行高受字体影响用 ListView_GetItemRect 拿当前行的 RECT把进度条塞进指定列的子矩形。CListCtrlEx 的做法通常是在内部维护一个 CRect 数组列表滚动或尺寸变化时统一重算一次而不是每一个进度条都监听滚动消息。// 初始化第 row 行、第 col 列的内嵌进度条 void CListCtrlEx::InitProgress(int row, int col, int nRange /* 100*/) { CRect rcItem; ListView_GetItemRect(m_hWnd, row, rcItem, LVIR_BOUNDS); // 拿到整行矩形 // 再取指定列的宽度确定进度条显示区域 int nColWidth GetColumnWidth(col); rcItem.right rcItem.left nColWidth - 8; // 左右各留 4 像素边距 CProgressCtrl* pProg new CProgressCtrl; pProg-Create(WS_CHILD | WS_VISIBLE, rcItem, this, IDC_PROGRESS_BASE row); pProg-SetRange(0, nRange); pProg-SetPos(0); // 存入行状态数组便于后面对号入座 m_arrRowState[row].progressWnd pProg; }ListView_GetItemRect 的第三个参数用 LVIR_BOUNDS 是拿整行矩形如果直接取子项矩形需要用 LVIR_LABEL这两种模式返回的坐标基准不一样混用会错位。IDC_PROGRESS_BASE row 这个 ID 方案有个边界问题行数一多容易撞上对话框其他控件 ID建议范围从 3000 起步或者用 GetDlgCtrlID 动态分配。SetRange 之后的 SetPos 要紧接着调用否则控件会以默认范围 0 到 100 显示导致百分比显示异常。3.2 刷新策略SetProgressValue 与 UI 错位进度条创建好之后真正的更新逻辑集中在 SetProgressValue 里。常见做法是先按行号从状态数组取出控件指针判断为空就直接返回避免野指针然后调用 SetPos 更新位置。但有经验的人都知道SetPos 不会主动触发重绘如果你在循环里密集更新几十行进度条界面会闪得厉害。我的处理方式是先 SetPos 然后 InvalidteRect 一个合并后的区域而不是逐个失效。CListCtrlEx 里一般会做一次窗口绘制锁定用 SetRedraw(FALSE) 冻结重绘批量更新完再恢复。注意 SetRedraw(FALSE) 之后要成对调用 SetRedraw(TRUE) 并 InvalidateRect否则界面会一直黑着。光标悬停到进度条上时Windows 默认会把它当作一个独立窗口不会触发列表的鼠标消息——这会导致点击进度条时列表选中态不更新。处理办法是在进度条子类里捕获 WM_MOUSEACTIVATE返回 MA_NOACTIVATE把激活事件交还给列表。// 批量刷新进度:先冻结重绘批量更新再解冻 void CListCtrlEx::RefreshAllProgress() { SetRedraw(FALSE); // 关闭重绘避免闪烁 for (int i 0; i m_arrRowState.GetSize(); i) { if (m_arrRowState[i].progressWnd m_arrRowState[i].bHasProgress) { m_arrRowState[i].progressWnd-SetPos(m_arrRowState[i].nCurProgress); } } SetRedraw(TRUE); InvalidateRect(NULL); }这里 SetRedraw(FALSE) 与 InvalidateRect(NULL) 的组合需要注意的是InvalidateRect 放在 SetRedraw(TRUE) 之后才会真正触发重绘顺序写反会出现界面短暂空白。另一个细节m_arrRowState 是 CArray 而不是 CList因为你按行号随机访问的频率远高于插入删除CArray 的随机访问性能更好而 CList 的插入删除更快——大多数列表场景是刷新多于增删选 CArray 合理。3.3 自绘模式当默认进度条样式不够用时如果觉得 CProgressCtrl 的经典条纹样式太丑CListCtrlEx 也可以走自绘路线。OnDrawItem 里拿到行号和列号后自己用 Rectangle 填充背景再用 PatBlt 画进度块。自绘的好处是样式完全可控比如可以做圆角进度块或渐变色坏处是命中测试和焦点管理都要自己写。自绘模式下有一个常见的误区把进度计算直接写在 OnDrawItem 里每次重绘都重新读业务数据。正确做法是在业务数据更新时主动调 InvalidateRect 指定行让 OnDrawItem 只做绘制不做计算。CListCtrlEx 里通常用一个成员变量缓存当前进度值OnDrawItem 只读缓存。缓存更新与绘制的时序如果不一致会出现进度条来回跳的视觉问题——先更新缓存再刷新区域两者要放在同一个函数里顺序执行。4. 内嵌编辑框实战创建、消息路由与焦点管理4.1 让列表项可以直接改文本编辑框的创建方式CListCtrlEx 里编辑框的创建不是提前铺满所有行而是按需创建。用户在某个单元格上双击或按下 F2控件动态 new 一个 CEdit定位到对应的子矩形上然后 SetFocus 并选中文本。这种按需创建相比提前建好全部编辑框节省窗口句柄而且不会在列表滚动时拖累绘制性能。创建编辑框的时机和位置是第一个坑必须用 ListView_GetSubItemRect 而不是 ListView_GetItemRect因为后者返回的是整行矩形前者才能精确到子项。子项矩形拿到后要在矩形四周多一点边距我一般留 2 到 3 像素否则编辑框的边框会和列表单元格的网格线重叠出现两像素宽的深色竖线。编辑框作为子窗口创建后要把字体设置得和列表字体一致否则输入的文字和旁边的单元格字体不一样看起来像是排版错误。获取列表字体用 GetFont然后编辑框 SetFont这是最容易漏的一步。// 开启第 row 行第 col 列的行内编辑 void CListCtrlEx::StartEdit(int row, int col) { if (m_pEditWnd) // 上一次的编辑框还在先销毁 { m_pEditWnd-DestroyWindow(); delete m_pEditWnd; m_pEditWnd NULL; } CRect rcSub; ListView_GetSubItemRect(m_hWnd, row, col, LVIR_BOUNDS, rcSub); rcSub.DeflateRect(2, 2); // 内缩 2 像素防止覆盖网格线 m_pEditWnd new CEdit; m_pEditWnd-Create(WS_CHILD | WS_VISIBLE | WS_BORDER | ES_AUTOHSCROLL, rcSub, this, IDC_EDIT_ITEM); m_pEditWnd-SetFont(GetFont()); // 字体必须跟随列表 m_pEditWnd-SetWindowText(GetItemText(row, col)); // 预填当前文本 m_pEditWnd-SetSel(0, -1); // 全选文本方便直接覆盖输入 m_pEditWnd-SetFocus(); }ListView_GetSubItemRect 的第四个参数在 Win32 里原来是 int 类型到了 Vista 之后加了 LVIR_BOUNDS 等取值但某些旧 SDK 头文件里声明不一致要确认你用的 SDK 支持这个取值。编辑框创建后别忘了把行号列号存进成员变量销毁时靠它定位写回数据。4.2 EN_KILLFOCUS 与回车确认编辑结束的两种触发路径编辑框接收消息丢失焦点或用户敲回车两种路径都要结束编辑状态但涉及的细节不同。回车确认走 WM_KEYDOWN捕捉 VK_RETURN 后立即提交数据并把焦点还给列表失焦确认走 EN_KILLFOCUS需要判断焦点是否落到了别的控件比如用户点击了另一个列表项。这里最容易翻车的是回车路径如果编辑框是 ES_AUTOHSCROLL 单行模式回车键默认触发父窗口的 IDOK 按钮对话框可能直接关闭。必须在编辑框子类里捕获 WM_KEYDOWN 并屏蔽默认处理再向父窗口发自定义结束编辑消息。CListCtrlEx 里常见的做法是定义一个 CEdit 的子类在 PreTranslateMessage 里拦截回车而不是在父窗口拦截——父窗口收到时编辑框已经处理完默认逻辑了。提交数据后要判断文本是否为空空的直接还原成原来的文本不要写进业务数据避免数据层出现空字符串导致后续运算崩溃。判断的标准常见做法是用 CString::Trim() 后判空不要直接比较否则用户输入的全角空格或制表符会蒙混过关。// 编辑框子类中拦截回车键 BOOL CListCtrlExEdit::PreTranslateMessage(MSG* pMsg) { if (pMsg-message WM_KEYDOWN pMsg-wParam VK_RETURN) { // 通知父窗口提交数据父窗口再写回数据数组 ::SendMessage(GetParent()-m_hWnd, WM_EDITBOX_CONFIRM, 0, 0); return TRUE; // 阻止默认行为防止触发 IDOK } return CEdit::PreTranslateMessage(pMsg); }SendMessage 是同步调用父窗口在处理函数里完成数据写入后返回编辑框才会销毁这个时序能保证不会出现正在写入的时候控件被销毁的竞态条件。如果用 PostMessage 改成异步父窗口处理时编辑框可能已经销毁再取编辑框文本就是野指针。4.3 输入验证与异常数据编辑框不完全自由行内编辑并不适合所有数据。CListCtrlEx 工程里常见的做法是让调用方在启动编辑前注册一个验证回调编辑结束时把新文本交给回调返回值决定是接受还是回滚。比如某一列要求数字回调里做类型检查不合法就弹出提示并让编辑框继续保持焦点。实现验证回调时要注意回调函数的执行环境是编辑框的通知消息上下文不要在里面做耗时操作比如数据库查询或文件写入。文本验证本身也是 UI 流程的一部分超过 100ms 用户就会感到卡顿。更合理的做法是先做快速格式检查把慢操作丢给工作线程等结果出来再更新列表。一个具体的坑如果验证不通过你需要让编辑框继续保持焦点并在 OnKillFocus 里阻止失焦。但 Windows 的焦点转移机制是异步的KillFocus 消息发出来时目标窗口已经拿到焦点。所以在 EN_KILLFOCUS 里再 SetFocus 到编辑框可能出现焦点在两个窗口之间跳来跳去的闪烁。解决方法是设置一个 m_bAllowKillFocus 标志位验证不通过时置为 FALSE在 KillFocus 处理函数里检查这个标志并决定要不要恢复焦点。5. 避坑手册CListCtrlEx 使用中的五个高频翻车现场5.1 控件跟着行一起滚走了滚动条与子控件不同步现象拖动列表滚动条后进度条和复选框还在原地和文本行脱离开。原因CListCtrlEx 的子控件是独立窗口滚动时列表控件只重绘自身客户区子控件不会自动平移。解决在 WM_VSCROLL / WM_HSCROLL 的消息处理里调 MoveWindow 把子控件坐标加上滚动偏移量或者干脆响应 WM_PAINT 时统一重算所有子控件位置。更省事的方案是只在当前可视区域内创建子控件滚动时销毁不可见行的控件但频繁创建销毁对性能不友好折中方案是创建一个可复用编辑框滚动时重定位。5.2 编辑框点一下没反应Z 序和父窗口抢占焦点现象点击编辑框时焦点闪了一下又回了列表文本没法输入。原因列表控件会拦截鼠标消息把焦点抢回自己身上编辑框作为子窗口在 Z 序上被列表盖住。解决在编辑框的 WM_MOUSEACTIVATE 里返回 MA_NOACTIVATE让鼠标点击只激活编辑框内部不激活整个窗口同时在列表的 NM_CLICK 通知里判断点击位置如果落在已有编辑框的行上就不要重新设置选中态。5.3 中文输入法弹不出来IME 与编辑框焦点的纠葛现象编辑框能输入英文但切换输入法后中文候选框不出现。原因CEdit 没有正确关联 IME 上下文常见于编辑框创建时没指定 ES_IME 相关样式或父窗口是对话框时 IME 窗口被其他控件遮挡。解决创建编辑框时加上 ES_IME 样式并在编辑框获得焦点时调用 ImmAssociateContext 把输入法上下文设置到编辑框句柄上。老工程里常见 OS 版本兼容问题Vista 之后的系统还得分情况处理 ImmSetOpenStatus。5.4 复选框点击命中错位图标坐标与实际点击区域不一致现象整行点击都能切换复选框状态或者点击复选框旁边的文字却没反应。原因CListCtrlEx 的复选框是自绘的状态图片绘制在单元格左侧但点击命中测试还是走列表默认逻辑没有判断点击位置是否在图标矩形内。解决在 NM_CLICK 通知里用 GetMessagePos 换算客户区坐标再与图标矩形做 PtInRect 判断只在这个区域内才切换状态。// NM_CLICK 里判断点中是否落在复选框图标区域 void CListCtrlEx::OnNMClick(NMHDR* pNMHDR, LRESULT* pResult) { LPNMITEMACTIVATE pNMItem (LPNMITEMACTIVATE)pNMHDR; if (pNMItem-iItem 0) { CRect rcCheck; GetCheckRect(pNMItem-iItem, rcCheck); // 自己维护的复选框矩形 if (rcCheck.PtInRect(pNMItem-ptAction)) { ToggleCheck(pNMItem-iItem); // 只有图标区域内才切换状态 } } }GetCheckRect 返回的坐标是客户区还是屏幕坐标要和 pNMItem-ptAction 保持一致。NM_CLICK 的 ptAction 默认是客户区坐标但如果你早先调过 MapWindowPoints 就变了排查时先从坐标一致性入手。画复选框用 DrawFrameControl 的 DFC_BUTTON 系列自带选中和按下的视觉效果不建议自己画矩形否则 DPI 缩放时会糊。5.5 行删除时控件没有回收内存泄漏与句柄泄漏一起出现现象程序运行内存持续增长用任务管理器能看到句柄数飙到几千。原因CListCtrlEx 添加了子控件后删除行时只删了列表项没有销毁子窗口对象导致窗口句柄泄漏new 出来的 CProgressCtrl 等对象没 delete内存也泄漏。解决重写 DeleteItem先遍历行状态数组取出对应行的子控件指针调用 DestroyWindow 和 delete再调用基类的 DeleteItem。批量删除时用 DeleteAllItems循环里先全部清子控件再调基类。6. 进阶一步从静态行状态到动态数据联动的三处改造这套 CListCtrlEx 在演示工程里是静态的行内控件但真正用到实际项目中你会希望它跟业务数据实时联动。我一般会在拿到这类扩展控件后先做三处改造第一为 CListCtrlEx 增加 SetDataSource 方法传入一个数据数组的指针行状态不再由控件内部分配而是完全由外部数据驱动第二把进度条的 SetProgressValue 改成监听业务数据更新消息业务层发一个带行号和数据值的自定义消息控件收到后自动刷新对应行第三为重绘增加一个行级别标志位允许调用方指定「只刷新这一行」减少无效重绘。在 ListCtrlDlg 里实际用起来时做法是维护一个结构体数组每个元素存任务名、进度、勾选状态CListCtrlEx 显示前先绑定这个数组地址。用户改了编辑框或复选框控件直接把新值写回数组不再经过对话框中转一遍——这样对话框代码从「事件处理」退化到「数据初始化」维护成本反而更低。SetDataSource 的原理注意数组生命周期控件不拷贝数据只是持有指针数组被外部释放前必须先调用 DetachDataSource 通知控件解除绑定否则悬垂指针会随时炸。这三处改造做完这个控件就从「演示代码」变成了「业务组件」。从那以后我每次拿到网上这类扩展控件都会先检查两件事数据持有方式是拷贝还是指针、析构时是否释放所有子控件。这两点不过关不管演示多花哨都先放一边。希望帮到你。本文还有配套的精品资源点击获取
返回列表