简介:这份资源面向使用 VC/MFC 进行桌面开发的程序员,尤其是需要在对话框或文档视图工程中嵌入表格控件、展示数据库查询结果的开发者。内容围绕 MFC 中 DataGrid 控件的实际用法展开,涵盖将 ADO 记录集绑定为数据源以显示查询结果、通过 CColumns 与 CColumn 对象控制列宽、用 SetNumberFormat 格式化数据(如把小于 1 的小数显示为百分数)、设置行高实现多行显示,以及借助 HeadClick 等消息机制完成排序等常见需求,属于可直接套用的实战型参考。资源包为 1 个 doc 文档,约 226KB,内含完整示例代码与关键步骤说明,并附有在 VC6.0 英文版、Windows XP 下调试通过的源程序工程,便于读者对照修改、快速集成到自己的项目中。目前已有 848 人学习,适合希望减少重复摸索、尽快掌握 DataGrid 控件常用接口与排错思路的中级开发者参考。
1. MFC DataGrid控件用法详解:从“能显示”到“能编辑”的最后一公里
很多做 MFC 桌面程序的人,第一次把数据库查询结果塞进 DataGrid 时都会愣一下:数据明明已经读出来了,界面却像一块灰板,既不能改,也不能按列排序,甚至连列标题都歪歪扭扭。MFC DataGrid 控件用法详解要解决的,正是这个从“数据能显示”到“表格能编辑、能校验、能回写”的最后一公里问题。它适合两类人:一类是刚接手维护老 MFC 项目、需要快速把 DataGrid 跑起来的工程师;另一类是想在对话框程序里做一个近似 Windows 记事本那样轻量、但带表格编辑能力的工具开发者。本文不堆概念,直接按“先跑通最小绑定、再调列与编辑、最后处理回写和刷新”的顺序展开,中间会穿插 mfc 获取列表总列数、datagrid隐藏列、子控件间距这些实际会卡住人的点。你只要跟着把每一步的命令和参数改对,就能得到一个可编辑、可校验、可回写的 DataGrid。
2. 先让 DataGrid 在对话框里跑起来:绑定、列宽与只读开关
2.1 插入控件与最小初始化代码
在 Visual Studio 的资源视图里打开对话框模板,右键插入 ActiveX 控件,选择Microsoft DataGrid Control 6.0 (OLEDB),拖出一个矩形区域。接着给控件绑定一个成员变量,类型选CDataGrid或CMSDataGrid,变量名比如m_grid。如果工具箱里找不到这个控件,先确认 Visual Studio 安装时勾选了 MFC 和 ActiveX 支持;离线安装包缺组件是常见翻车点,后面避坑章会细说。
初始化放在OnInitDialog里,最小可用代码如下:
// 初始化 DataGrid:绑定数据源、设置列宽、允许编辑 BOOL CMyDlg::OnInitDialog() { CDialogEx::OnInitDialog(); // 1. 设置控件允许编辑和选择 m_grid.SetAllowUpdate(TRUE); // 允许修改单元格 m_grid.SetAllowAddNew(TRUE); // 允许新增行 m_grid.SetAllowDelete(TRUE); // 允许删除行 m_grid.SetHeadLines(1); // 列标题占 1 行 // 2. 绑定记录集(这里用 ADO 的 _RecordsetPtr) m_grid.SetRefDataSource((LPUNKNOWN)m_pRecordset); m_grid.SetRefDataSource((LPUNKNOWN)m_pRecordset); // 3. 设置列宽(单位:磅,不是像素) m_grid.SetDefColWidth(80); m_grid.SetColWidth(0, 120); // 第 0 列宽 120 m_grid.SetColWidth(1, 200); // 第 1 列宽 200 return TRUE; }这段代码的逻辑很直白:先打开编辑开关,再把 ADO 记录集指针交给 DataGrid,最后按列设宽度。参数上最容易错的是SetColWidth的单位——它用的是磅值,不是像素,所以设 120 在 96 DPI 下大约对应 160 像素。如果你发现列宽怎么调都不对,先检查是不是把像素值直接填进去了。另外SetRefDataSource必须传LPUNKNOWN,直接传_RecordsetPtr编译不过,这是 COM 接口转换的硬性要求。
2.2 列标题、隐藏列与获取总列数
数据绑上去之后,列标题默认取记录集字段名,但实际项目里往往要改成中文。改标题用SetHeadText,隐藏列用SetColHidden,获取总列数用GetColumns返回的集合再取Count。下面这段代码演示了三个操作连着做:
// 设置中文列标题、隐藏敏感列、统计总列数 void CMyDlg::SetupGridColumns() { // 获取列集合 CColumns cols = m_grid.GetColumns(); long nCols = cols.GetCount(); // 这就是 mfc 获取列表总列数的标准做法 // 逐列设置标题 m_grid.SetHeadText(0, _T("编号")); m_grid.SetHeadText(1, _T("姓名")); m_grid.SetHeadText(2, _T("部门")); m_grid.SetHeadText(3, _T("入职日期")); // 隐藏第 3 列(入职日期),datagrid隐藏列就靠这一句 m_grid.SetColHidden(3, TRUE); // 如果列数超过 4,把多余的列也隐藏掉 for (long i = 4; i < nCols; i++) { m_grid.SetColHidden(i, TRUE); } }这里的关键参数是列索引从 0 开始,SetHeadText的第一个参数就是列号。GetCount()返回的是long,别用int接,虽然一般不会溢出,但老代码里混用类型容易出警告。隐藏列之后,该列数据仍然在记录集里,只是不显示,导出或计算时还能取到,这一点比直接删列安全得多。如果你发现隐藏列之后表格右侧留白太大,那是SetDefColWidth的默认宽度在作怪,把默认宽度调小或者把可见列宽度调大即可。
2.3 只读模式与编辑模式切换
有些场景下表格只用来展示,不允许用户改。切换只读用SetAllowUpdate(FALSE),但要注意:这个属性是控件级别的,一旦设成 FALSE,所有单元格都不能编辑。如果只想让某几列只读,得用SetColLocked逐列设置。下面是一个按权限切换的示例:
// 根据用户权限切换编辑模式 void CMyDlg::ApplyEditMode(BOOL bCanEdit) { if (bCanEdit) { m_grid.SetAllowUpdate(TRUE); m_grid.SetColLocked(0, TRUE); // 编号列始终只读 m_grid.SetColLocked(1, FALSE); // 姓名可编辑 } else { m_grid.SetAllowUpdate(FALSE); // 整表只读 } }参数说明:SetColLocked的第二个参数为TRUE表示锁定该列,用户点进去也改不了。注意锁定和隐藏是两回事,锁定列仍然可见,只是不可编辑。实际项目里常见做法是编号列锁定、状态列锁定,其余列放开。如果你发现设了SetAllowUpdate(FALSE)之后仍然能改,检查是不是在OnInitDialog之后又调用了SetRefDataSource,重新绑定数据源会把编辑属性重置,需要再设一次。
3. 编辑、校验与回写:让 DataGrid 真正能改数据
3.1 捕获单元格修改事件
DataGrid 本身不提供“单元格修改后”的 MFC 消息,得靠 ActiveX 事件。在类向导里给 DataGrid 控件添加事件处理,常用的是BeforeColEdit、AfterColEdit、BeforeUpdate、AfterUpdate。其中AfterUpdate在用户离开单元格、数据已写入记录集之后触发,适合做校验和回写。下面是一个捕获修改并做非空校验的示例:
// 单元格更新后触发:校验姓名不能为空 void CMyDlg::OnAfterUpdateDatagrid1() { // 获取当前行号和列号 long nRow = m_grid.GetRow(); long nCol = m_grid.GetCol(); // 只校验姓名列(第 1 列) if (nCol == 1) { CString strName = m_grid.GetTextMatrix(nRow, nCol); if (strName.IsEmpty()) { AfxMessageBox(_T("姓名不能为空,请重新输入")); m_grid.SetTextMatrix(nRow, nCol, _T("未命名")); // 回填默认值 } } }逻辑说明:GetRow和GetCol返回当前活动单元格的行列号,GetTextMatrix取单元格文本,SetTextMatrix回写。参数上注意行列号都是从 0 开始,GetTextMatrix返回的是CString,如果单元格是日期或数字,取出来是格式化后的字符串,需要自己再转。校验失败时不要直接return,因为数据已经写进记录集了,必须用SetTextMatrix覆盖回去,否则脏数据就留在内存里了。
3.2 批量回写到数据库
编辑完一批数据后,需要把记录集里的改动提交到数据库。如果用的是 ADO 记录集,直接调UpdateBatch或逐行Update。下面是一个批量提交的封装:
// 将 DataGrid 中的修改批量回写到数据库 BOOL CMyDlg::CommitGridChanges() { if (m_pRecordset == nullptr || m_pRecordset->State == adStateClosed) return FALSE; try { m_pRecordset->UpdateBatch(adAffectAll); // 提交所有挂起的修改 return TRUE; } catch (_com_error& e) { CString strErr; strErr.Format(_T("提交失败:%s"), (LPCTSTR)e.Description()); AfxMessageBox(strErr); return FALSE; } }参数说明:adAffectAll表示提交所有挂起的修改,如果只想提交当前行用adAffectCurrent。注意UpdateBatch要求记录集打开时游标类型支持批量更新,通常用adOpenKeyset或adOpenStatic,adOpenForwardOnly不支持。如果你发现提交后数据库没变,先检查连接字符串里的CursorLocation是不是设成了adUseClient,服务端游标在某些驱动下批量更新会静默失败。
3.3 刷新与重新绑定
提交成功后,如果想让表格显示数据库里的最新值,需要重新查询并绑定。直接调ReBind不行,得先把记录集关掉再重新打开,然后重新SetRefDataSource。下面是一个刷新函数:
// 重新查询并刷新 DataGrid 显示 void CMyDlg::RefreshGrid() { if (m_pRecordset != nullptr && m_pRecordset->State == adStateOpen) m_pRecordset->Close(); m_pRecordset->Open("SELECT * FROM Employees ORDER BY ID", m_pConnection.GetInterfacePtr(), adOpenKeyset, adLockBatchOptimistic, adCmdText); m_grid.SetRefDataSource((LPUNKNOWN)m_pRecordset); SetupGridColumns(); // 重新设置列标题和隐藏列 }这里的关键是重新绑定之后必须再调一次列设置,因为SetRefDataSource会把列标题重置为字段名。参数上adLockBatchOptimistic是批量乐观锁,配合UpdateBatch使用。如果你发现刷新后列宽变了,那是控件在重新绑定时恢复了默认宽度,把SetColWidth再执行一遍即可。
4. 避坑与排查:DataGrid 最常见的 5 个翻车现场
4.1 控件工具箱里找不到 DataGrid
现象:资源编辑器右键插入 ActiveX 控件,列表里没有 Microsoft DataGrid Control 6.0。原因:Visual Studio 安装时没有勾选 ActiveX 支持,或者系统没有注册MSDATGRD.OCX。解决:先确认C:\Windows\SysWOW64\MSDATGRD.OCX存在,如果不存在从旧版 VB6 或 Office 安装包里找;存在的话用管理员权限运行regsvr32 MSDATGRD.OCX注册。注册后重启 Visual Studio,工具箱里就能看到了。
4.2 绑定记录集后表格空白
现象:SetRefDataSource调用了,但表格一行数据都不显示。原因:记录集游标类型是adOpenForwardOnly,DataGrid 需要可滚动游标才能显示。解决:把Open的游标类型改成adOpenKeyset或adOpenStatic。另外检查记录集是否已经MoveFirst,有些驱动在打开后指针停在 BOF,需要手动移到第一条。
4.3 编辑后数据没进数据库
现象:界面上改了单元格,关闭程序后数据库里还是旧值。原因:没有调用Update或UpdateBatch,或者记录集锁类型是只读的。解决:确认Open时锁类型用adLockBatchOptimistic或adLockOptimistic,并在保存按钮里调UpdateBatch。如果用的是adLockReadOnly,任何修改都不会写回。
4.4 列宽设置无效
现象:SetColWidth调了,但列宽纹丝不动。原因:单位搞错了,或者设置时机在SetRefDataSource之前。解决:SetColWidth的单位是磅,不是像素;并且必须在数据源绑定之后调用,否则会被重新绑定覆盖。如果还是无效,检查是不是在OnInitDialog里控件还没创建完成,把设置移到OnSize或OnShowWindow里。
4.5 子控件间距导致布局错乱
现象:DataGrid 和其他按钮、编辑框挤在一起,或者窗口缩放时表格不跟着变。原因:对话框资源里控件位置是固定的,没有做动态布局。解决:在OnSize里根据窗口客户区大小重新计算 DataGrid 的MoveWindow参数,留出边距。常见做法是表格左边距 10 像素、右边距 10 像素、上边距 50 像素、下边距 60 像素,按钮区放在底部。如果用了SetDefColWidth还是觉得挤,把默认列宽调小到 60 磅左右。
5. 进阶技巧:用 DataGrid 做一个可编辑的迷你表格工具
如果你已经跑通了上面的绑定、编辑、回写,下一步可以把它做成一个近似 Windows 记事本那样轻量但带表格编辑能力的小工具。核心思路是:用 DataGrid 做数据展示和编辑,用CFileDialog做导入导出,用SetTextMatrix和GetTextMatrix做批量数据搬运。下面是一个导出到 CSV 的示例,顺便演示怎么遍历所有行和列:
// 将 DataGrid 内容导出为 CSV 文件 void CMyDlg::ExportToCSV() { CFileDialog dlg(FALSE, _T("csv"), _T("export.csv"), OFN_OVERWRITEPROMPT, _T("CSV Files (*.csv)|*.csv||")); if (dlg.DoModal() != IDOK) return; CStdioFile file; if (!file.Open(dlg.GetPathName(), CFile::modeCreate | CFile::modeWrite)) return; long nRows = m_grid.GetRowCount(); // 总行数 long nCols = m_grid.GetColumns().GetCount(); // 总列数 // 写标题行 CString strLine; for (long c = 0; c < nCols; c++) { if (m_grid.GetColHidden(c)) continue; // 跳过隐藏列 strLine += m_grid.GetHeadText(c); if (c < nCols - 1) strLine += _T(","); } file.WriteString(strLine + _T("\n")); // 写数据行 for (long r = 0; r < nRows; r++) { strLine.Empty(); for (long c = 0; c < nCols; c++) { if (m_grid.GetColHidden(c)) continue; strLine += m_grid.GetTextMatrix(r, c); if (c < nCols - 1) strLine += _T(","); } file.WriteString(strLine + _T("\n")); } file.Close(); AfxMessageBox(_T("导出完成")); }这段代码里有两个容易忽略的点:GetRowCount返回的是当前显示的行数,如果记录集还没完全加载,可能只返回部分行,导出前先确认记录集已经MoveLast再MoveFirst;另外GetColHidden用来跳过隐藏列,这样导出的 CSV 不会包含敏感字段。参数上CFileDialog的OFN_OVERWRITEPROMPT会在文件已存在时弹确认框,避免误覆盖。
导入 CSV 的逻辑反过来:用CStdioFile逐行读,按逗号拆分,先清空记录集再AddNew逐行写入,最后UpdateBatch。注意 CSV 里的逗号如果出现在字段内容中,需要做转义处理,简单做法是用双引号把字段包起来,读取时再剥掉。这个迷你工具做出来之后,你可以继续加查找替换、列排序、行删除,基本就是一个能用的表格编辑器了。
我自己维护过三个 MFC 老项目,血泪经验是:DataGrid 的坑大多不在控件本身,而在记录集的游标类型和锁类型没配对。每次新建对话框程序,我都会先把adOpenKeyset + adLockBatchOptimistic这组参数固定下来,再动界面。另外,别在OnInitDialog里做太多事,把列设置和绑定拆成独立函数,后面调布局的时候会感谢自己。希望帮到你。
本文还有配套的精品资源,点击获取