简介:这是一份面向VC6/MFC开发者的CListCtrl表格可编辑实战资源,解决在单文档应用程序中让列表控件支持单元格编辑的实际需求。资源完整演示了通过LVS_EDITLABELS风格创建可编辑CListCtrl,并围绕LVN_BEGINLABELEDIT与LVN_ENDLABELEDIT消息编写响应函数,实现内容校验与数据更新;同时引入ControlTracker控件追踪器,优化编辑过程中的选择、拖动等交互细节。压缩包共58个文件,包含7个头文件、6个C++源文件,还有可执行程序、pdb调试符号、obj中间文件、rc资源脚本等类型;Debug目录下的pdb、obj、ilk等可用于断点调试和构建过程分析,ico、bmp、rc等记录了程序界面素材,整体仅3.8MB,结构紧凑、便于按需查阅。已有841人学习下载,适合正在学习MFC控件定制、单文档架构扩展或表格交互开发的读者参考。通过对照源码与工程配置,可清晰理解消息映射机制的完整流程,并直接复用ControlTracker等辅助类,显著缩短表格编辑功能的开发周期。
1. 双击单元格就能改数据:一个 VC6 单文档项目里的 CListCtrl 表格可编辑扩展
做 VC6 编程的老人应该都有过这种经历:单文档类扩展出来的数据录入界面,列表用的是 CListCtrl,用户录完发现某个单元格填错了,第一反应是双击去改。但 CListCtrl 默认只能整行增删、整行替换,双击除了选中行什么都做不了。大部分项目只能塞一个“编辑”按钮,选中行后弹出对话框,来回切窗口非常割手。
这个项目解决的正是这件事:在不换控件、不改数据结构的前提下,给 CListCtrl 补上“表格可编辑”能力。核心思路是动态创建控件——点击单元格时,在对应的矩形位置上生成一个 CEdit 或 CComboBox,接管文本输入;失焦、回车或 Esc 时把内容写回列表并销毁控件。整套事件处理封装在宿主类里,一个头文件加一个源文件就能接入现有工程。适合还在维护 MFC 老系统、或者想在轻量界面里快速获得 Excel 式编辑体验的开发者。
2. 架构与类设计:用宿主类加两个临时输入控件,绕开重绘噩梦
2.1 为什么不重写表格:三种实现路线的取舍
给 CListCtrl 做单元格编辑,网上能查到的方案大致有三类。
第一类是直接换第三方网格控件,功能完整但代价大。老工程里 CListCtrl 往往已经被多个视图引用,数据通过 CImageList、LVCOLUMN、SetItemData 挂接,换控件意味着所有调用点都要改,而且第三方控件的配色、字体和原界面很难保持一致。
第二类是 owner-draw 自绘。重写 DrawItem,在选中态下画一个假的“编辑框”,再拦截键盘输入自己维护光标。这个方案视觉效果最统一,但实现量相当于写一个迷你文本框,光处理光标闪烁、选区、IME 输入就够折腾,不适合快速交付。
第三类就是本项目采用的:动态创建控件覆盖上去。列表本身不动,点击可编辑列时,CListCtrl 的派生类在单元格矩形上生成一个真正的 CEdit 或 CComboBox,输入完成后取文本、SetItemText、销毁控件。对视图中任何既有逻辑透明,失败回退也简单——不创建就是了。
这个取舍对老项目是最小的侵入,我一般也优先推荐这条路。它唯一的风险是输入控件和列表滚动、焦点管理之间的配合,这部分正是后面避坑章节要展开的。
2.2 类职责划分与消息流转
这套实现核心只有两个类,外加两个 MFC 自带的临时控件。
CEditableListCtrl 是宿主类,从 CListCtrl 派生。它负责三件事:记录哪些列可编辑、可编辑列的类型;响应 LVN_CLICK 通知,把点击坐标翻译成行号列号;维护当前存在的编辑控件的生命周期,包括创建、提交、销毁。
CEdit 和 CComboBox 是动态创建的临时控件,它们的父窗口是宿主 CListCtrl。它们不承担业务逻辑,只负责输入。MFC 会把 EN_KILLFOCUS 通知发送给父窗口,宿主类在消息映射里接收并提交数据。
视图类只需要在 OnInitialUpdate 或 OnInitDialog 里创建列表、设置列头,然后调用 SetEditable 声明“第几列可以编辑”。整个调用链一句话就能说清。
以下是类职责对照表:
| 类 / 对象 | 职责 | 对外关键方法 |
|---|---|---|
| CEditableListCtrl | 命中测试、编辑控件生命周期、提交回写 | SetEditable、StartEditCell、SetColumnType |
| CEdit | 文本输入 | 由宿主类动态创建,处理键盘消息 |
| CComboBox | 下拉选择输入 | 由宿主类动态创建,下拉列表数据由外部传入 |
| CView / CDialog | 创建列表、填充数据 | 列表初始化后调用 SetEditable |
消息流转顺序是:用户在可编辑列上单击,系统向父窗口发送 LVN_CLICK;CEditableListCtrl 通过 ON_NOTIFY_REFLECT 把这个通知反射回自己;解析 NMITEMACTIVATE 的 iRow、iCol;调用 StartEditCell 创建临时控件;用户输入完后,回车、Esc 或点击别处触发提交;宿主类取回文本,写入列表行,销毁控件。
2.3 文件结构与接入方式
整个工程只需要新增两个文件:EditableListCtrl.h 和 EditableListCtrl.cpp。加上一个可选的 CComboEntry 辅助类,用来承载下拉选项。
接入时,视图类里原本创建列表的代码不动,只需要在创建完列头后追加一行声明。下面是头文件里的核心声明,能看出这个类对外接口非常收敛:
// EditableListCtrl.h #pragma once class CEditableListCtrl : public CListCtrl { public: enum CellType { CELL_TEXT = 0, // 文本编辑 CELL_COMBO, // 下拉框编辑 }; CEditableListCtrl(); // 设置某列为可编辑 // iCol: 列号(从0开始),nType: CELL_TEXT / CELL_COMBO // pOptions: 当 nType==CELL_COMBO 时传入下拉选项 void SetEditable(int iCol, int nType = CELL_TEXT, const CStringArray* pOptions = NULL); // 主动触发某个单元格进入编辑状态(供外部调用) void StartEditCell(int iRow, int iCol); protected: struct COLINFO { int nType; // 列类型 CStringArray arOptions; // 下拉选项 }; CMap<int, int, COLINFO*, COLINFO*> m_mapCols; // 列配置表 int m_iEditRow; // 当前编辑中的行 int m_iEditCol; // 当前编辑中的列 CEdit* m_pEdit; // 当前活动编辑框 CComboBox* m_pCombo; // 当前活动下拉框 void CommitEdit(); // 提交并销毁当前编辑控件 void CancelEdit(); // 取消并销毁当前编辑控件 void GetCellRect(int iRow, int iCol, CRect& rcCell); // 下面这些是 MFC 消息处理函数 afx_msg void OnLvnClick(NMHDR* pNMHDR, LRESULT* pResult); afx_msg void OnEditKillFocus(); afx_msg void OnComboKillFocus(); afx_msg void OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar); afx_msg void OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar); afx_msg void OnSize(UINT nType, int cx, int cy); virtual BOOL PreTranslateMessage(MSG* pMsg); };调用方式简洁直接,在视图类的 OnInitialUpdate 里,创建完列表和列头之后加上:
m_listCtrl.SetEditable(1, CEditableListCtrl::CELL_TEXT); // 第1列文本可编辑 m_listCtrl.SetEditable(2, CEditableListCtrl::CELL_COMBO, &options); // 第2列下拉可编辑SetEditable的作用是把列配置塞进m_mapCols,后续点击事件会查询这张表决定是否进入编辑。m_mapCols用CMap而不是数组,是因为列号不一定从 0 连续递增,如果只是固定第 1、2 列,也可以直接用数组。
3. 核心实现:从鼠标点击到编辑框创建、提交、销毁的完整链路
3.1 命中测试:把点击转换成可编辑单元格
进入编辑状态的第一步,是判断用户点在了哪一行、哪一列。CListCtrl 会在鼠标单击时发送 LVN_CLICK 通知,携带一个 NMITEMACTIVATE 结构。这里的关键是把iItem和iSubItem解析干净。
需要特别提醒一点:只有当列表设置了 LVS_EX_FULLROWSELECT 扩展风格时,iSubItem才会准确返回非 0 列的信息。否则点击第二列时,通知里拿到的 iItem 可能是 -1。所以视图创建列表时要先设置这个扩展风格。
代码如下:
void CEditableListCtrl::OnLvnClick(NMHDR* pNMHDR, LRESULT* pResult) { NMITEMACTIVATE* pActivate = (NMITEMACTIVATE*)pNMHDR; *pResult = 0; // 空白区域点击、或者点击了第0列(通常是序号列),不进入编辑 if (pActivate->iItem < 0 || pActivate->iSubItem <= 0) return; // 查询列配置,没有配置说明该列不可编辑 COLINFO* pInfo = NULL; if (!m_mapCols.Lookup(pActivate->iSubItem, pInfo)) return; // 保证目标行是可见的,否则编辑框会定位到窗口外面 EnsureVisible(pActivate->iItem, TRUE); StartEditCell(pActivate->iItem, pActivate->iSubItem); }NMITEMACTIVATE的iItem是行索引,iSubItem是列索引。第 0 列跳过编辑是刻意留的口子,通常表格里的第 0 列放序号或状态图标,强行编辑反而会让用户困惑。EnsureVisible这一步很重要,用户通过滚动条把目标行滚到可视区外时,如果还按原坐标创建编辑框,控件会被创建到列表窗口之外,看起来就像“点了没反应”。
3.2 创建编辑控件:位置、字体、焦点全部要对齐
确定目标单元格后,下一步是拿到该单元格的屏幕矩形,然后在这个矩形范围内创建 CEdit 或 CComboBox。VC6 的 CListCtrl 没有现成的 GetSubItemRect 成员函数,需要通过 LVM_GETSUBITEMRECT 消息获取。
这个窗口消息的用法有点怪,矩形变量的 top 和 left 两个字段在发送前要分别填入行号和列号,发送成功后传出真实的像素矩形。这也是 VC6 编程里被问过很多次的经典传参方式。
void CEditableListCtrl::GetCellRect(int iRow, int iCol, CRect& rcCell) { // 入参:top 填行号,left 填列号 rcCell.top = iRow; rcCell.left = iCol; // 该消息会改写 rcCell 为实际的客户区坐标矩形 ::SendMessage(m_hWnd, LVM_GETSUBITEMRECT, (WPARAM)GetParent()->GetSafeHwnd(), (LPARAM)&rcCell); }拿到矩形之后,创建编辑控件的代码写在StartEditCell里。一开始写这个函数的时候没做“上一次编辑未提交”的兜底,连续快速点击两个不同单元格,项目里会出现两个编辑框同时存在的乱象。后来在函数入口先CancelEdit一次,把上一个残留控件清理干净,才消停。
void CEditableListCtrl::StartEditCell(int iRow, int iCol) { // 如果上一次编辑控件还在,先按提交处理 if (m_pEdit || m_pCombo) CommitEdit(); COLINFO* pInfo = NULL; if (!m_mapCols.Lookup(iCol, pInfo)) return; CRect rcCell; GetCellRect(iRow, iCol, rcCell); // 把矩形向内缩2像素,避免编辑框边框和列表网格线重叠 rcCell.DeflateRect(2, 2); m_iEditRow = iRow; m_iEditCol = iCol; CString strText = GetItemText(iRow, iCol); if (pInfo->nType == CELL_COMBO) { m_pCombo = new CComboBox(); m_pCombo->Create(WS_CHILD | WS_VISIBLE | WS_TABSTOP | CBS_DROPDOWNLIST | WS_VSCROLL, rcCell, this, 0x6001); // 下拉框选项从列配置里填充 for (int i = 0; i < pInfo->arOptions.GetSize(); i++) m_pCombo->AddString(pInfo->arOptions[i]); m_pCombo->SetFont(GetFont()); // 当前单元格已有值时,选中对应项 m_pCombo->SelectString(-1, strText); m_pCombo->SetFocus(); } else { m_pEdit = new CEdit(); m_pEdit->Create(WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_BORDER | ES_AUTOHSCROLL, rcCell, this, 0x6002); m_pEdit->SetFont(GetFont()); // 字体必须和列表一致 m_pEdit->SetWindowText(strText); m_pEdit->SetFocus(); // 全选原文本,用户直接输入就能覆盖 m_pEdit->SetSel(0, -1); } }几个参数值得单独说明。0x6001 和 0x6002 是临时控件的 ID,宿主类在消息映射里靠它们区分 EN_KILLFOCUS 通知来源。父窗口this传给 Create 时,必须保证是 CListCtrl 本身,而不是视图,否则子控件的通知消息会走视图的消息映射,宿主类收不到。ES_AUTOHSCROLL让文本编辑框在内容超出列宽时自动横向滚动,不回行。CBS_DROPDOWNLIST保持下拉框收起,避免展开的下拉列表把界面撑乱。
3.3 提交与销毁:输入结束后的三路出口
编辑控件的生命周期结束有三种出口:用户点击了列表其他地方,编辑框失焦;用户按回车确认;用户按 Esc 取消。后两个要靠重写 PreTranslateMessage 捕获,第一个靠 EN_KILLFOCUS。
提交的核心函数是CommitEdit。这里有个新手很容易踩的坑:先销毁控件再去 GetWindowText,取到的一定是空字符串。必须先取文本、写回列表,再销毁。
void CEditableListCtrl::CommitEdit() { // 文本类编辑控件 if (m_pEdit && ::IsWindow(m_pEdit->GetSafeHwnd())) { CString strValue; m_pEdit->GetWindowText(strValue); // 第一步:取文本 SetItemText(m_iEditRow, m_iEditCol, strValue); // 第二步:写回列表 m_pEdit->DestroyWindow(); // 第三步:销毁窗口 delete m_pEdit; // 第四步:释放对象 m_pEdit = NULL; } // 下拉框类编辑控件,处理方式相同 if (m_pCombo && ::IsWindow(m_pCombo->GetSafeHwnd())) { int nIndex = m_pCombo->GetCurSel(); CString strValue; if (nIndex != CB_ERR) m_pCombo->GetLBText(nIndex, strValue); SetItemText(m_iEditRow, m_iEditCol, strValue); m_pCombo->DestroyWindow(); delete m_pCombo; m_pCombo = NULL; } }CancelEdit和CommitEdit结构一致,只是不取文本直接销毁,这里不再重复贴代码。销毁后必须把指针置 NULL,否则动态创建的窗口在 OnKillFocus 回调里会反复触发提交逻辑。
键盘出口通过重写 PreTranslateMessage 拦截。因为消息会先经过焦点窗口的 PreTranslateMessage,再向上传递到父窗口,所以即使焦点在子编辑框里,宿主类也能拦到:
BOOL CEditableListCtrl::PreTranslateMessage(MSG* pMsg) { // 只有消息目标是当前编辑控件时才处理 if (pMsg->hwnd == m_pEdit->GetSafeHwnd() || pMsg->hwnd == m_pCombo->GetSafeHwnd()) { if (pMsg->message == WM_KEYDOWN) { switch (pMsg->wParam) { case VK_RETURN: CommitEdit(); // 回车确认 return TRUE; // 不继续下发消息 case VK_ESCAPE: CancelEdit(); // Esc 取消 return TRUE; } } } return CListCtrl::PreTranslateMessage(pMsg); }回车和 Esc 在文本框里原本都有默认行为,回车可能触发父窗口的默认按钮,Esc 可能触发系统菜单。这里拦截后直接吞掉消息,保证编辑流程完全可控。判断pMsg->hwnd而不是GetFocus(),是为了防止输入法窗口或下拉列表子窗口抢走焦点判断。
3.4 滚动、列宽与删行:外部状态变化时的同步
单元格编辑最容易被忽略的场景是:用户正在输入,却按下了滚动条或者拖动了列分隔线。列表内容滚动了,可编辑框还是创建时那个位置,会出现“控件漂在别的行上”的诡异效果。
处理策略是:不尝试移动编辑框,而是先提交再销毁。滚动这种强交互状态,没必要维持正在输入的上下文,反而会给自己惹麻烦。
void CEditableListCtrl::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar) { // 横向滚动必然导致列偏移,先提交当前编辑 if (m_pEdit || m_pCombo) CommitEdit(); CListCtrl::OnHScroll(nSBCode, nPos, pScrollBar); } void CEditableListCtrl::OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar) { if (m_pEdit || m_pCombo) CommitEdit(); CListCtrl::OnVScroll(nSBCode, nPos, pScrollBar); }列表的 WM_SIZE 也做同样处理,窗口尺寸变化时,列的显示位置必然变化,编辑框跟着销毁是最省事的方案。删除行、全选清空这些操作,在视图层触发前调用一次 CommitEdit 即可。这样外部操作永远只会看到一个状态:要么编辑框存在并且位置正确,要么根本不存在。
4. 避坑记录:单元格编辑最常见的 5 个翻车现场与排查办法
4.1 编辑框出现后立刻消失,像是闪了一下
现象:单击可编辑单元格,编辑框刚显示出来马上就不见了。如果仔细看,能看到边框闪一下。
原因:这是单击和双击的竞争问题。CListCtrl 的 LVN_CLICK 和 LVN_DBLCLK 是两条独立通知。用户双击时,第一次点击触发创建编辑框,第二次点击因为编辑框已经覆盖在单元格上,点击位置落在编辑框内,编辑框立刻失去焦点,EN_KILLFOCUS 触发了提交销毁。所以看到的“闪一下”其实是编辑框完成了从创建到销毁的完整生命周期。
解决:在消息映射里也加上 LVN_DBLCLK 的处理。双击时,第二次点击的 LVN_CLICK 里 iItem 值不会变,可以记录一个时间戳或标志,在 OnLvnClick 开头检查是否刚处理过同一行的编辑,如果是则跳过。更简单的做法是编辑框创建后设置一次焦点,失焦提交前判断焦点是否是编辑框本身,如果焦点没动过就不提交。我当时直接用了一个 300 毫秒的临界判断,双击时只有第一次进入编辑,第二次被忽略。
4.2 滚动列表后编辑框留在原地,数据却跟着列表跑了
现象:正在编辑某一行,拖动垂直滚动条,列表行滚上去了,但编辑框还停留在原来的像素位置,感觉像文本框悬浮在列表上。
原因:CListCtrl 客户区的行坐标在滚动后会整体重排,编辑框是子窗口,坐标是基于客户区的绝对像素,不会跟着某个行移动。
解决:在 OnVScroll 和 OnHScroll 里提交销毁,这是最稳的。有人尝试在滚动时用 LVM_GETTOPINDEX 重新定位编辑框位置,做出来也能用,但代价是编辑框频繁移动会闪烁,用户正在输入时文本跳动很恼人。不如滚动即提交,改完再下去点一次就是了。
4.3 回车键提交失效,焦点跑到视图的某个按钮上
现象:在编辑框里按回车,数据没有提交,界面上的某个默认按钮反而被触发了,或者焦点直接转移到其他控件。
原因:回车键是对话框和单文档视图里最容易被“瓜分”的键。视图的加速键表、默认按钮逻辑会在 PreTranslateMessage 的父窗口链上截获回车消息。我的实现是在宿主类的 PreTranslateMessage 里拦截,但宿主类的 PreTranslateMessage 执行顺序取决于焦点窗口链,如果视图类重写了 PreTranslateMessage 且没有先调用基类,宿主类的拦截就失效。
解决:在接收焦点的那一层,也就是视图或对话框的 PreTranslateMessage 里,转发优先判断。判断目标窗口是否是宿主类里的编辑框,如果是,把消息重新发送给宿主类的编辑框窗口,保证回车能被处理到:
BOOL CMyView::PreTranslateMessage(MSG* pMsg) { if (pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_RETURN && m_listCtrl.IsEditing()) { // 把回车直接发给编辑框,走 CEdit 自己的翻译链 ::SendMessage(pMsg->hwnd, WM_KEYDOWN, VK_RETURN, pMsg->lParam); return TRUE; } return CView::PreTranslateMessage(pMsg); }这只是其中一个方案,核心原则是:不要让回车消息在到达编辑框之前被视图的加速键逻辑消耗掉。
4.4 整行高亮背景下,编辑框里的文字看不清
现象:点击第二列开始编辑,编辑框创建了,但背景是选区蓝色或黑色,里面的文字和光标看不清。
原因:CListCtrl 设置过 LVS_EX_FULLROWSELECT 后,点击任意列都会选中整行,系统绘制选中状态时会把整行背景刷成高亮色。动态创建的 CEdit 默认背景是白色,但文字绘制时,父窗口的选中态背景透过编辑框边缘缝隙显示出来,看起来就是白底黑字里夹着一块深色背景。
解决:点击进入编辑前,先用 SetItemState 把当前选中行取消选中,把编辑框的父窗口背景刷成白色,或者在宿主类的 OnCtlColor 里拦截编辑框的背景画刷返回白色。最省事的是在 StartEditCell 里加上一句:
SetItemState(m_iEditRow, 0, LVIS_SELECTED);选中态去掉后,单元格底色恢复正常,编辑框白底也干净了。代价是编辑结束后行不再高亮,但数据写回后如果需要,可以恢复选中态。
4.5 退出视图时野指针崩溃,或者内存泄漏
现象:关闭单文档视图时程序异常退出,崩溃点往往在 CEdit 的销毁流程里;不崩溃的话,用内存检测工具能看到动态创建的 CEdit 对象泄漏。
原因:CommitEdit只在正常的编辑闭环里调用。如果用户在输入过程中直接关闭窗口、切换视图,编辑框窗口和宿主类一起被销毁,但动态 new 出来的 CEdit 对象没有走delete,析构函数不会执行,野指针悬在消息映射里,下次访问就崩。
解决:在宿主类的 OnDestroy 里兜底清理:
void CEditableListCtrl::OnDestroy() { // 销毁视图时强制丢弃未提交的编辑控件 if (m_pEdit && ::IsWindow(m_pEdit->GetSafeHwnd())) { m_pEdit->DestroyWindow(); delete m_pEdit; m_pEdit = NULL; } if (m_pCombo && ::IsWindow(m_pCombo->GetSafeHwnd())) { m_pCombo->DestroyWindow(); delete m_pCombo; m_pCombo = NULL; } CListCtrl::OnDestroy(); }从这里得到的血泪教训是:所有动态创建的控件,必须有一个统一的“销毁出口”。要么 Commit,要么 Cancel,要么窗口销毁时兜底,三选一,绝不能裸 new 不负责。
5. 二次开发:把文本编辑扩展成下拉框、数字校验和多行控件
5.1 增加下拉框列类型
上文已经看到 CELL_COMBO 的雏形。再往前走一步,下拉框的数据来源应该从外部传入,而不是在宿主类里写死。合理的做法是让 SetEditable 的第三个参数做成CStringArray,每个单元格可以在进入编辑前临时替换选项数组。比如“状态”列下拉框里是“未开始、进行中、已完成”,直接传入即可。
要让下拉框在编辑状态下展开,可以在创建后发一条 CB_SHOWDROPDOWN 消息:
m_pCombo->ShowDropDown(TRUE);这样用户点进去直接看到选项列表,省一次点击。
5.2 提交时按列做输入校验
表格可编辑的终极需求通常是“能改,但别乱改”。在 CommitEdit 里按列类型做规则检查即可。比如某列是数量,要求数字;某列是日期,要求格式正确。校验失败时不清空编辑框,也不销毁控件,而是弹提示后把焦点还回去:
if (pInfo->nType == CELL_TEXT && pInfo->nMask & COL_FLAG_NUMBER) { if (!IsNumeric(strValue)) { MessageBox("请输入数字", "输入错误", MB_ICONWARNING | MB_OK); m_pEdit->SetFocus(); return; // 不提交也不销毁 } }思路是列配置里加一个掩码字段,SetEditable同时接收掩码,提交函数里根据掩码走校验分支。这样外部调用不用改,只是扩展参数而已。
5.3 多行文本与更多自定义控件
遇到长文本列,CEdit 可以改成 ES_MULTILINE 样式,编辑框高度拉大,文本换行显示。不过列表行高固定,多行编辑框会覆盖住下方几行,这个体验不太理想。更好的做法是保留单行编辑,提交后单元格文本放不下时用 ToolTip 展示全文。
这套架构里,宿主类对输入控件的类型没有写死,只要有窗口句柄就可以扩展。后续改成日期选择控件、数字调节框,只需要在 StartEditCell 里加一个创建分支,并在 CommitEdit 里按 ID 分流取数据即可。
从那以后,我每次给 CListCtrl 加这种动态控件,都会强制走一遍“创建-编辑-提交-销毁”的闭环验证,确认单击、双击、滚动、关闭视图四条路径都不会留下野生编辑框。希望帮到你。
本文还有配套的精品资源,点击获取