简介这份资源面向使用 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(ProviderMicrosoft.Jet.OLEDB.4.0;) _T(Data Sourcenote.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.003.141593.14保留两位小数#,##012345671,234,567千分位整数0.0%0.15615.6%比率百分数yyyy-MM-dd日期值2025-01-15日期格式化0.00E00123451.23E04科学计数法注意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是控件的资源 ID2是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)并Refreshm_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_I4SQL 里把字段名写全比如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 的老项目都强制先跑一遍列信息遍历确认字段顺序和标题再动格式和排序的代码。希望帮到你。本文还有配套的精品资源点击获取