简介:这份资源面向使用 VC/MFC 进行桌面开发的程序员,尤其是需要在对话框或文档程序中嵌入表格控件、展示数据库查询结果的开发者。内容围绕 MFC 中 DataGrid 控件的实际用法展开,涵盖将 ADO 记录集绑定为数据源以显示查询结果、通过 CColumns 与 CColumn 对象控制列宽、用 SetNumberFormat 格式化数据(如把小于 1 的小数显示为百分数)、设置行高实现多行显示,以及借助 HeadClick 等消息机制完成排序等常见需求,属于可直接对照编码的实战型参考。资源包为 1 个 doc 文档,约 226KB,内含完整示例代码与工程说明,源程序在 VC6.0 英文版、Windows XP 下调试通过,便于读者边看边改、快速套用到自己的项目中。目前已有 849 人学习,适合希望用微软原生控件替代第三方网格、减少代码细节投入的开发者参考。
1. 从一张没人愿意维护的对话框说起:MFC DataGrid 控件到底值不值得用
接手过一个老工控上位机项目,主框架是对话框,数据展示区域用的是 MFC 自带的 DataGrid 控件。前任开发者离职后,这块代码成了没人敢碰的黑匣子——列宽写死、百分数格式错乱、点表头排序直接崩。我花了两个晚上把 Microsoft DataGrid OLEDB 6.0 控件的接口翻了一遍,才发现问题根本不在控件本身,而在于大多数人只抄了SetRefDataSource那一行,后面的列对象、格式控制、消息映射全没跟上。
MFC DataGrid 控件是 VC 环境下用来显示表格数据的 ActiveX 网格控件,底层基于 COM,通过CDataGrid、CColumns、CColumn三个封装类暴露接口。它最大的优势是跟 Windows 系统兼容性极好,升级系统几乎不用改代码,适合那些不想引入第三方网格库、又需要快速展示 ADO 查询结果的对话框工程。如果你手头正好有一个 VC6.0 或 VS 的 MFC 项目,需要把数据库查询结果塞进网格里,并且希望列宽、格式、排序都能自己控制,那这份用法梳理就是给你准备的。下面按「显示数据 → 控制外观 → 格式化内容 → 处理事件 → 避坑」的顺序拆开讲,每一步都落到能直接抄的代码和参数上。
2. 把 ADO 记录集塞进 DataGrid:数据源绑定与列对象获取
2.1 为什么选 ADO 而不是 ODBC 或 DAO
在 MFC 里往 DataGrid 喂数据,常见做法有三种:ODBC API、DAO、ADO。我一般会选 ADO,原因很直接——DataGrid 控件本身是 OLEDB 体系的,SetRefDataSource接收的是一个LPUNKNOWN接口指针,而 ADO 的Recordset对象底层就是 OLEDB 行集,两者对接不需要额外转换层。ODBC 需要先拿到HSTMT再包装,DAO 跟 Jet 引擎绑得太死,换数据库时改动量大。
项目正文里用了一个 ADO 封装类CADODatabase和CADORecordset,这是很多老项目的常见做法。如果你手头没有现成封装,直接用_ConnectionPtr和_RecordsetPtr智能指针也行,原理一样。关键点是:DataGrid 要的是Recordset的IUnknown接口,不是记录集本身的数据副本,所以绑定之后记录集不能提前释放。
2.2 绑定数据源的完整代码与参数说明
先看一段可以直接放进OnInitDialog或按钮响应函数里的代码:
// 数据库连接对象和记录集对象,通常作为对话框类的成员变量 CADODatabase m_DBCn; CADORecordset m_Rs; CDataGrid m_ctrlDG; // DataGrid 控件绑定的成员变量 // 构造连接字符串,Provider 用 Jet 4.0 访问 .mdb 文件 CString strConnection; strConnection.Format(_T("Provider=Microsoft.Jet.OLEDB.4.0;") _T("Data Source=note.mdb")); // 打开数据库,失败时弹出提示 if (!m_DBCn.Open((LPCTSTR)strConnection)) { AfxMessageBox(_T("数据库打开失败,检查 note.mdb 路径")); return FALSE; } // 把数据库对象关联到记录集,再执行查询 m_Rs.SetDatabase(&m_DBCn); m_Rs.Open(_T("select * from test;")); // 关键一步:把 Recordset 的 IUnknown 指针交给 DataGrid m_ctrlDG.SetRefDataSource((LPUNKNOWN)m_Rs.GetRecordset());逻辑说明:SetDatabase只是让记录集知道去哪个库执行 SQL,真正打开记录集的是Open。GetRecordset()返回的是底层_RecordsetPtr的接口,强制转成LPUNKNOWN后传给SetRefDataSource。参数方面,连接字符串里的Data Source可以是绝对路径或相对路径,相对路径以工程工作目录为基准,调试时经常因为工作目录不对导致找不到文件,后面避坑章节会细说。
2.3 取得列对象:CColumns 与 CColumn 的关系
数据绑上去之后,网格会自动根据记录集字段生成列。但如果你想改列宽、改标题、改格式,就必须先拿到列对象。DataGrid 的列集合是CColumns,单列是CColumn,获取方式如下:
// 取得列集合 CColumns cols = m_ctrlDG.GetColumns(); // 用 VARIANT 指定列索引,0 表示第一列 VARIANT vt; vt.vt = VT_I2; vt.iVal = 0; CColumn col = cols.GetItem(vt); // 设置列宽,单位是像素 col.SetWidth(120);这里有个容易翻车的地方:GetItem的参数是VARIANT,不是简单的int。很多示例代码直接写cols.GetItem(0),在部分编译器上能过,但运行时可能因为 VARIANT 类型不明确而取不到列。稳妥做法是显式构造VT_I2类型的 VARIANT。另外,列索引从 0 开始,跟数据库字段顺序一致,但如果 SQL 里用了select *,字段顺序由表结构决定,改表之后列索引可能错位,建议显式写出字段名。
提示:
CColumns和CColumn是控件插入时生成的封装类,如果你在类向导里只选了CDataGrid没选这两个,代码编译会报类型未定义。重新走一遍「添加类」流程,把三个类都勾上。
3. 列宽、行高与显示格式:让网格不再像半成品
3.1 列宽控制的三种粒度和适用场景
列宽设置看起来简单,但实际项目里需求往往分三层:全局统一宽度、按列单独设宽、根据内容自适应。DataGrid 原生只支持前两种,自适应需要自己算。
全局设置可以用SetDefColWidth,单位是像素:
// 所有未单独设置宽度的列,默认宽度为 100 像素 m_ctrlDG.SetDefColWidth(100);按列设置就是前面提到的col.SetWidth(fWidth)。我一般会在对话框初始化时遍历所有列,根据字段类型给不同宽度——文本字段宽一些,数值和日期窄一些。比如:
CColumns cols = m_ctrlDG.GetColumns(); long nColCount = cols.GetCount(); // 获取列总数 for (long i = 0; i < nColCount; i++) { VARIANT vt; vt.vt = VT_I4; vt.lVal = i; CColumn col = cols.GetItem(vt); // 前两列是名称和编号,给宽一点 if (i < 2) col.SetWidth(150); else col.SetWidth(90); }参数说明:GetCount()返回long类型,循环变量别用int,否则在 64 位编译下会有警告。vt.vt用VT_I4对应long,比VT_I2更通用。
3.2 行高与多行显示的取舍
项目正文里提到SetRowHeight(20)实现多行显示,这里要澄清一个概念:SetRowHeight设置的是单行高度,不是「多行显示」。DataGrid 默认每行显示一条记录,行高加大只是让单元格内容更宽松,不会把一条记录拆成多行。真正要在一行里显示多行文本,需要单元格内容包含换行符,并且列样式允许换行。
// 设置所有行的统一高度为 24 像素 m_ctrlDG.SetRowHeight(24); // 如果某列内容较长,可以开启该列的自动换行 VARIANT vt; vt.vt = VT_I2; vt.iVal = 3; CColumn col = m_ctrlDG.GetColumns().GetItem(vt); col.SetWrapText(TRUE); // 允许换行SetWrapText是CColumn的方法,设为TRUE后,单元格里带\n的文本会折行显示,行高会自动撑开。注意这个属性对性能有影响,数据量大时不要全列开启。
3.3 数字格式化的实际效果与格式串写法
把小于 1 的小数显示成百分数,是项目正文里的典型需求。SetNumberFormat接收的是格式字符串,规则跟 VB 的Format函数类似:
VARIANT vt; vt.vt = VT_I2; vt.iVal = 2; // 第三列,假设是比率字段 CColumn col = m_ctrlDG.GetColumns().GetItem(vt); // 显示成百分数,保留一位小数 col.SetNumberFormat(_T("0.0%"));格式串"0.0%"的含义:0表示占位符,不足补零;.是小数点;%会把原数值乘以 100 并追加百分号。如果原值是0.156,显示为15.6%。常见格式串还有:
| 格式串 | 原值 | 显示结果 | 适用场景 |
|---|---|---|---|
0.00 | 3.14159 | 3.14 | 保留两位小数 |
#,##0 | 1234567 | 1,234,567 | 千分位整数 |
0.0% | 0.156 | 15.6% | 比率百分数 |
yyyy-MM-dd | 日期值 | 2025-01-15 | 日期格式化 |
0.00E+00 | 12345 | 1.23E+04 | 科学计数法 |
注意:
SetNumberFormat只对数值型字段生效,文本字段设了也没用。如果发现格式没变化,先确认数据库里该字段的类型是不是数字或货币。
4. 表头点击排序与消息映射:让网格能交互
4.1 HeadClick 消息的映射方式
DataGrid 控件支持表头点击事件,消息名是HeadClick。在 MFC 里给 ActiveX 控件加事件处理,跟普通控件不太一样,需要走类向导的「消息映射」或者手动写BEGIN_EVENTSINK_MAP。
手动映射的写法如下,放在对话框类的.cpp文件里:
// 在类声明里加上事件处理函数声明 afx_msg void OnHeadClickDatagrid1(short ColIndex); // 在 BEGIN_MESSAGE_MAP 之后、END_MESSAGE_MAP 之前加事件映射 BEGIN_EVENTSINK_MAP(CTestGridDlg, CDialog) ON_EVENT(CTestGridDlg, IDC_DATAGRID1, 2, OnHeadClickDatagrid1, VTS_I2) END_EVENTSINK_MAP()参数说明:IDC_DATAGRID1是控件的资源 ID;2是HeadClick事件的调度 ID,这个值由控件类型库决定,不同版本可能不同,可以在类向导生成的事件映射里确认;VTS_I2表示处理函数接收一个short参数,即列索引。
4.2 排序逻辑的实现与记录集重查
拿到列索引之后,排序有两种做法:一是对已绑定的记录集重新排序,二是重新执行带ORDER BY的 SQL。第一种需要记录集支持排序,ADO 的Recordset可以设Sort属性,但 DataGrid 绑定后不一定实时刷新。我一般用第二种,简单可靠:
void CTestGridDlg::OnHeadClickDatagrid1(short ColIndex) { // 根据列索引映射到字段名 CString strField; switch (ColIndex) { case 0: strField = _T("name"); break; case 1: strField = _T("id"); break; case 2: strField = _T("rate"); break; default: return; } // 切换排序方向,用成员变量记录当前状态 static BOOL bAsc = TRUE; CString strOrder = bAsc ? _T(" ASC") : _T(" DESC"); bAsc = !bAsc; // 重新查询并绑定 CString strSQL; strSQL.Format(_T("select * from test order by %s%s"), strField, strOrder); m_Rs.Close(); m_Rs.Open(strSQL); m_ctrlDG.SetRefDataSource((LPUNKNOWN)m_Rs.GetRecordset()); }逻辑说明:每次点击表头,先关闭旧记录集,再用新的ORDER BY语句重新打开,最后重新绑定数据源。static BOOL bAsc用来在升序和降序之间切换,实际项目里应该做成成员变量,避免多个网格互相干扰。
4.3 排序时的界面闪烁与性能问题
重新绑定数据源会导致网格重绘,数据量大时能看到明显闪烁。缓解办法是在重查前先SetRedraw(FALSE),绑定完再SetRedraw(TRUE)并Refresh:
m_ctrlDG.SetRedraw(FALSE); // ... 重新查询和绑定 ... m_ctrlDG.SetRedraw(TRUE); m_ctrlDG.Refresh();如果记录超过几千条,建议在 SQL 里加TOP限制或者做分页,DataGrid 本身不是虚拟列表,全量加载会拖慢对话框响应。
5. 避坑与排查:DataGrid 用不好多半是这几个原因
5.1 控件插入后类向导里找不到 CDataGrid
现象:在对话框上放了 DataGrid 控件,但「添加类」时列表里没有CDataGrid、CColumns、CColumn。
原因:插入控件时选的是「Microsoft DataGrid OLEDB 6.0」,但类向导只列出了部分接口,或者控件注册表信息不完整。
解决:重新走一遍「项目 → 添加类 → ActiveX 控件中的 MFC 类」,在列表里找到Microsoft DataGrid Control 6.0 (OLEDB),把CDataGrid、CColumns、CColumn三个都勾上。如果列表里根本没有这个控件,说明系统没注册MSDATGRD.OCX,需要用regsvr32注册。
5.2 SetRefDataSource 之后网格空白
现象:代码没报错,但网格里一行数据都不显示。
原因:最常见的是记录集已经关闭或超出作用域。SetRefDataSource只是传递接口指针,DataGrid 不会复制数据,如果m_Rs在函数返回后被析构,网格就空了。
解决:把CADODatabase和CADORecordset定义为对话框类的成员变量,保证生命周期覆盖整个对话框。另外检查Open的返回值,SQL 写错时记录集是空的,但不会抛异常。
5.3 列宽设置无效或列索引错位
现象:调了SetWidth但列宽没变,或者改完表结构后设置的列跟预期对不上。
原因:GetItem的 VARIANT 参数类型不对,或者 SQL 用了select *导致字段顺序变化。
解决:VARIANT 显式设vt = VT_I2或VT_I4;SQL 里把字段名写全,比如select name, id, rate from test,这样列索引跟字段顺序稳定对应。
5.4 百分数格式显示成原值
现象:设了SetNumberFormat(_T("0.0%")),但单元格里还是0.156而不是15.6%。
原因:字段在数据库里的类型是文本,不是数字。DataGrid 对文本字段忽略数字格式。
解决:检查表结构,把该字段改成Single或Double类型。如果数据库不能改,只能在 SQL 里用FORMAT函数预处理,或者绑定时用计算字段。
5.5 表头点击没反应
现象:写了OnHeadClickDatagrid1,但点击表头不触发。
原因:事件映射的调度 ID 写错,或者控件属性里HeadClick事件被禁用。
解决:用类向导重新生成事件映射,确认ON_EVENT的第三个参数跟控件类型库一致。另外检查 DataGrid 的AllowUpdate属性,某些版本下该属性为False时事件不派发。
6. 一个被忽略的调试习惯:用方法名反推控件能力
项目正文结尾提到「查看相关类的方法名能顾名思义」,这句话我一开始没当回事,后来发现是快速上手 ActiveX 控件的捷径。CDataGrid、CColumns、CColumn这三个类的方法名基本就是控件功能的英文直译,比如SetHeadFont设表头字体、SetBackColor设背景色、SetColAlignment设列对齐方式。遇到没文档的功能,直接在类视图里展开方法列表,比翻帮助文件快得多。
我现在的习惯是:拿到一个不熟悉的 ActiveX 控件,先把它生成的封装类头文件打开,把所有Set开头的方法扫一遍,记下哪些属性可以运行时改。然后写一个临时的「属性遍历」按钮,把常用属性挨个设一遍看效果。比如下面这段代码,一次性把表头、网格线、选中行颜色都调一遍:
// 表头背景色和字体 m_ctrlDG.SetHeadBackColor(RGB(0, 120, 215)); m_ctrlDG.SetHeadForeColor(RGB(255, 255, 255)); m_ctrlDG.SetHeadFont(_T("微软雅黑"), 10, TRUE); // 网格线颜色 m_ctrlDG.SetGridLineColor(RGB(200, 200, 200)); // 选中行高亮 m_ctrlDG.SetSelBackColor(RGB(255, 240, 200)); // 禁止用户拖动列宽(只读展示时有用) m_ctrlDG.SetAllowColResize(FALSE);参数说明:SetHeadFont的三个参数分别是字体名、字号、是否加粗;颜色值用RGB宏生成COLORREF。这些属性在对话框OnInitDialog里设置一次即可,不需要每次刷新都调。
还有一个验证技巧:DataGrid 绑定记录集后,如果想知道当前实际有多少列、列标题是什么,可以用CColumns的GetCount和CColumn的GetCaption遍历打印:
CColumns cols = m_ctrlDG.GetColumns(); long n = cols.GetCount(); for (long i = 0; i < n; i++) { VARIANT vt; vt.vt = VT_I4; vt.lVal = i; CColumn col = cols.GetItem(vt); CString strCaption = col.GetCaption(); TRACE(_T("列 %d 标题: %s\n"), i, strCaption); }TRACE输出到调试窗口,不用弹对话框就能确认列信息。从那以后我每次接手带 DataGrid 的老项目,都强制先跑一遍列信息遍历,确认字段顺序和标题,再动格式和排序的代码。希望帮到你。
本文还有配套的精品资源,点击获取