VC6 MFC CListCtrl 表格可编辑与单元格内联编辑实现 简介面向VC6环境下从事MFC界面开发的初学者与中级程序员这份资源聚焦「CListCtrl表格可编辑」这一实用课题围绕单文档类扩展、控件动态创建与消息映射事件处理展开帮助读者解决列表控件只能看不能改、编辑交互难以落地的问题。压缩包共58个文件、约3.8MB以h头文件与cpp源文件为主体配套dsp、dsw工程文件可直接用VC6打开编译另有rc资源脚本、ico图标、bmp位图及exe可执行文件便于先运行看效果再对照源码调试。资源收录了ControlTracker_Demo示例工程其中ControlTracker、ControlRect与ListCtrlEx等类演示了LVN_BEGINLABELEDIT、LVN_ENDLABELEDIT消息的响应写法以及用控件追踪器优化选择与拖动交互的思路。已有841人学习适合想弄懂MFC消息机制、运行时创建控件、窗口尺寸变化时重排列表控件以及输入校验与数据同步的开发者参考。1. 从只读报表到可录入界面CListCtrl 表格可编辑要解决的实际问题上周一个做设备管理的同行发来一个 VC6 的 MFC 单文档工程界面上一个报表风格的 CListCtrl 列了十几列参数。现场调试时客户提了个需求每一列都得能就地改改完立刻生效。他先试了 LVS_EDITLABELS发现只有第一列能进编辑状态而且编辑框是系统给的那个字体、位置、边框全都动不了跟表格的网格线对不齐看着特别别扭。折腾两天后他回到正路上给 CListCtrl 派生一个子类双击时按单元格矩形动态创建一个 CEdit编辑结束后把值写回列表和内存里的数据模型。这套「CListCtrl 表格可编辑」的骨架本身不复杂但 VC6 下的坐标计算、焦点切换、消息反射三件事只要错一件就会变成闪退或者编辑框乱飘。正在维护老 MFC 项目、需要把只读报表改成可录入界面的开发者这篇可以照着走一遍。2. 工程骨架与视图扩展在单文档里动态创建一张可编辑报表单文档程序的好处是视图类完全由自己掌控没有对话框资源里那套 DDX/DDV 的束缚。这份工程的思路是视图不动资源全部在OnCreate里用代码建控件、建列、建数据。这样二次开发时加一列只需要改一行不用回资源编辑器里拖半天。2.1 放弃 LVS_EDITLABELS 的原因CListCtrl 自带的标签编辑是 ListView 的“重命名”语义不是“编辑表格单元格”。LVN_BEGINLABELEDIT只告诉你第几行要改名拿不到子项索引LVN_ENDLABELEDIT回传的LVITEM.pszText也只作用于 item 的 label也就是第 0 列。想编辑第 3 列的“阈值”这套机制根本不给你入口。自己造编辑框以后麻烦的地方在于三件事全部要自己管编辑框的位置得按单元格矩形算焦点切换得自己控制什么时候销毁也得自己决定。换来的是完全的控制权——字体能继承、边框能贴合网格线、能不能编辑按列配置、回车提交还是 Esc 取消都由业务说了算。这个交换在报表录入场景里是划算的。2.2 三个类的分工文件类职责关键成员EditableListCtrl.h/.cppCEditableListCtrl列表控件子类管编辑生命周期、命中测试、可编辑列掩码m_pEdit、m_nEditItem、m_nEditColInplaceEdit.h/.cppCInplaceEditCEdit 子类管按键、失焦、取消标记m_bCancel、m_nItem、m_nColTableView.h/.cppCTableView视图动态建控件、建列、持有数据模型、做业务校验m_arrRows、m_wndTable分工的关键是边界CEditableListCtrl只管“编辑这个动作”不知道阈值是数字还是字符串CTableView只管“这个值合不合法”不关心编辑框怎么创建。数据写回列表控件之后控件再发一条消息告诉视图视图去改CArray里的行结构。这样后期要把列表换成别的控件校验逻辑一行都不用动。2.3 动态创建与初始化OnCreate 里的五步先解决一个 VC6 的编译问题。VC6 默认的_WIN32_IE是 0x0300GetSubItemRect、NMITEMACTIVATE::iSubItem这些 4.71 以后才有的东西会直接编不过报“未声明的成员”。这个宏必须放在afxcmn.h之前。// stdafx.h —— 顺序不能反宏必须在 afxcmn.h 之前 #define VC_EXTRALEAN #define _WIN32_IE 0x0400 // VC6 默认 0x0300不开这个 GetSubItemRect 编不过 #include afxwin.h #include afxext.h #include afxcmn.h // CListCtrl / CHeaderCtrl / NMITEMACTIVATE然后是视图里的创建代码五步走完一张表就立起来了int CTableView::OnCreate(LPCREATESTRUCT lpCreateStruct) { if (CView::OnCreate(lpCreateStruct) -1) return -1; // 1. 动态创建报表风格 整行选中 一直显示选中态 if (!m_wndTable.Create(WS_CHILD | WS_VISIBLE | WS_BORDER | LVS_REPORT | LVS_SINGLESEL | LVS_SHOWSELALWAYS, CRect(0, 0, 0, 0), this, IDC_TABLE)) return -1; // 2. 扩展风格整行高亮 网格线编辑框贴上去才不会错位 m_wndTable.SetExtendedStyle(LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES); // 3. 字体必须显式设否则子控件继承到的可能是系统默认字体 m_wndTable.SetFont(GetFont()); // 4. 建列宽度和格式都写死在这里列号即索引 m_wndTable.InsertColumn(0, _T(编号), LVCFMT_LEFT, 90); m_wndTable.InsertColumn(1, _T(名称), LVCFMT_LEFT, 140); m_wndTable.InsertColumn(2, _T(阈值), LVCFMT_RIGHT, 80); m_wndTable.InsertColumn(3, _T(单位), LVCFMT_LEFT, 70); // 5. 配置可编辑列第 0 列当主键永远只读 m_wndTable.SetEditableColumn(1, TRUE); m_wndTable.SetEditableColumn(2, TRUE); m_wndTable.SetEditableColumn(3, TRUE); return 0; }Create里传入的CRect(0,0,0,0)是故意的真正的尺寸交给OnSize里的MoveWindow。单文档视图在OnCreate阶段拿到的客户区尺寸还不准这里铺满会在窗口最大化时留白。SetExtendedStyle用成员函数而不是SendMessage(LVM_SETEXTENDEDLISTVIEWSTYLE)VC6 的CListCtrl已经封装好了没必要手写。注意m_wndTable是视图的成员而不是new出来的指针生命周期跟着视图走OnDestroy时自动析构。用指针的话一定要在OnDestroy里delete否则关窗口不报错但每次打开文档都漏一块。3. 坐标与创建一次双击到编辑框落位的完整链路这条链路的顺序不能乱先判断点在哪一行再判断点在哪一列然后算单元格矩形检查矩形有没有被滚出可视区最后才创建编辑框。中间任何一步用了错的坐标系编辑框就会跑到别的地方去。3.1 命中测试先算行再算列行索引直接用HitTest给的点这个接口很老实。列索引没有现成的得自己遍历。见过有人用pt.x累加列宽来判断结果横向滚动条一拖就全错——因为列表控件的客户端坐标本身不包含滚动偏移累加出来的位置和实际绘制位置差了一个GetScrollPos。稳妥的写法是反着来拿每个子项的矩形去PtInRect// 返回点中的列号没命中返回 -1 int CEditableListCtrl::HitTestColumn(int nItem, CPoint pt) { if (nItem 0) return -1; CHeaderCtrl* pHeader GetHeaderCtrl(); if (pHeader NULL) return -1; int nCols pHeader-GetItemCount(); for (int i 0; i nCols; i) { CRect rcSub; // LVIR_BOUNDS 拿的是包含图标和缩进的整格矩形 if (GetSubItemRect(nItem, i, LVIR_BOUNDS, rcSub) rcSub.PtInRect(pt)) return i; } return -1; }GetSubItemRect内部是按当前滚动位置计算的所以横向、纵向滚动都不用额外补偿。代价是每一列都要调一次列多的时候有开销——不过编辑是双击触发的低频操作几十列也就几百次矩形比较完全不用优化。LVIR_BOUNDS和LVIR_LABEL的区别要记清楚LVIR_BOUNDS是整个子项单元格LVIR_LABEL只是文本区域去掉了图标和缩进。做覆盖式编辑框必须用LVIR_BOUNDS用LVIR_LABEL会露出左边一条没被盖住的原文本。3.2 EditCell矩形、字体、全选三个细节真正的创建函数不长但每个参数都有讲究void CEditableListCtrl::EditCell(int nItem, int nCol) { if (nItem 0 || nCol 0) return; // 第 0 列是主键不放行 BOOL bEditable FALSE; if (!m_mapEditable.Lookup(nCol, bEditable) || !bEditable) return; // 列掩码里没有就不编辑 CRect rcCell; if (!GetSubItemRect(nItem, nCol, LVIR_BOUNDS, rcCell)) return; // 滚出可视区的格子不进入编辑否则编辑框会贴在边框外面 CRect rcClient, rcVisible; GetClientRect(rcClient); if (!rcVisible.IntersectRect(rcClient, rcCell)) return; if (m_pEdit ! NULL) EndEdit(TRUE); // 上一格还没落笔先提交 rcCell.DeflateRect(1, 1); // 内缩 1px让编辑框边框压住网格线 m_pEdit new CInplaceEdit; if (!m_pEdit-CreateEdit(this, rcCell, GetItemText(nItem, nCol), nItem, nCol, IDC_CELL_EDIT)) { delete m_pEdit; m_pEdit NULL; return; } m_nEditItem nItem; m_nEditCol nCol; }DeflateRect(1, 1)这个 1 像素不是玄学是两个边框叠在一起的结果编辑框自带WS_BORDER的 1px 边框列表控件有LVS_EX_GRIDLINES的 1px 网格线。不内缩的话编辑框会比单元格大一圈看着像错位。CreateEdit里还有两个容易被忽略的点一个是父窗口一个是字体BOOL CInplaceEdit::CreateEdit(CWnd* pParent, const CRect rcCell, const CString strText, int nItem, int nCol, UINT nID) { m_nItem nItem; m_nCol nCol; m_bCancel FALSE; // 父窗口是列表控件本身不是视图。 // 这样编辑框的坐标天然就是列表客户区坐标不用做 ScreenToClient 换算 if (!Create(WS_CHILD | WS_VISIBLE | WS_BORDER | ES_AUTOHSCROLL | ES_LEFT, rcCell, pParent, nID)) return FALSE; // 字体继承列表控件没设过字体时回退到系统 GUI 字体 CFont* pFont pParent-GetFont(); if (pFont NULL) pFont CFont::FromHandle((HFONT)::GetStockObject(DEFAULT_GUI_FONT)); SetFont(pFont); SetWindowText(strText); SetSel(0, -1); // 全选双击后可以直接覆盖输入 SetFocus(); return TRUE; }父窗口设成列表控件这一步是整套方案的地基。有人把父窗口设成视图然后每次创建前ClientToScreen再ScreenToClient绕一圈滚动一次就崩。父窗口是列表控件时编辑框就是列表控件的子窗口跟着列表一起被裁剪、一起被销毁坐标系天然一致。SetSel(0, -1)加不加体验差别很大不加的话光标停在文本末尾用户双击进去还得先按 Home 再 ShiftEnd。加了之后双击就是“全选待替换”的状态跟 Excel 的行为一致。3.3 双击之后消息怎么进来CEditableListCtrl是CListCtrl的子类通知消息优先用消息反射接在自己的消息映射里而不是丢给视图。这样控件是自包含的拿到别的工程里贴过去就能用。BEGIN_MESSAGE_MAP(CEditableListCtrl, CListCtrl) ON_NOTIFY_REFLECT(N p a hrefhttps://download.csdn.net/download/baodingwang/6524217 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p