
简介这份资源面向使用 VC/MFC 进行桌面开发的程序员尤其是需要在对话框或文档视图程序中嵌入表格控件、展示数据库查询结果的开发者。内容围绕 MFC 中 DataGrid 控件的实际用法展开涵盖将 ADO 记录集绑定为数据源以显示查询结果、通过 CColumns 与 CColumn 对象控制列宽、利用 SetNumberFormat 格式化数据如将小数显示为百分数、设置行高实现多行显示以及借助 HeadClick 等消息机制完成排序等常见需求属于可直接对照编码的实战型参考。资源包为 1 个 doc 文档约 226KB以文字讲解配合代码片段的方式组织便于边看边在工程中实践。目前已有 848 人学习适合希望快速上手 DataGrid 控件、减少自行摸索成本的 MFC 开发者参考。1. 从对话框里塞进一个 DataGrid为什么老 VC 项目还在用它接手一个 2003 年左右的 MFC 老工程客户要求把查询结果用表格显示出来还要能按列排序、把小数显示成百分数。你打开资源编辑器发现工具箱里根本没有现成的网格控件——这就是很多人第一次接触 MFC DataGrid 的场景。它不是一个随 VC 安装包附带的控件而是 Microsoft DataGrid Control 6.0OLEDB 版本注册后才会出现在插入 ActiveX 控件的列表里。选它的理由很直接这是微软自家东西跟 Windows 兼容性极好升级系统后几乎不用改代码而且它封装了 COM 查询接口用 CDataGrid、CColumns、CColumn 三个类就能操作不用自己去啃 IDispatch。这份资源解决的核心问题就三件事把 ADO 记录集直接绑到网格上显示、按列控制宽度和格式、通过消息机制做排序。适合两类人一是维护老 MFC 项目、需要快速出表格界面的工程师二是想搞明白 ActiveX 控件在 MFC 里怎么包装、怎么调用的学习者。下面按“能跑起来”的顺序拆每一步都落到具体代码和参数上。2. 把 DataGrid 控件插进对话框工程类选择与数据源绑定2.1 插入控件与三个包装类的取舍新建一个基于对话框的 MFC 工程在对话框资源上右键 → Insert ActiveX Control找到Microsoft DataGrid Control 6.0 (OLEDB)确定。此时 VC 会弹出“添加成员变量”向导让你选择要封装的类。这里有个关键取舍向导列出的类不止三个但没必要全选。我一般只勾CDataGrid、CColumns、CColumn这三个。原因很实际——CDataGrid 是控件本体负责 SetRefDataSource、GetColumns 这些顶层操作CColumns 是列集合用来按索引取列CColumn 是单列列宽、数字格式、对齐方式都挂在它身上。其他类比如 CSplit、CHead 之类在常规表格显示场景里用不到多勾一个就多一份 COM 接口封装的维护成本。注意ActiveX 基于 COM这三个类本质是对控件 COM 查询接口的封装。你没法直接看到控件源码调试时只能靠方法名猜功能——好在微软的命名基本能顾名思义。插入完成后给控件绑定一个成员变量类型选 CDataGrid名字比如m_ctrlDG。这一步做完工程里会多出DataGrid.h、DataGrid.cpp之类的包装文件别去改它们。2.2 用 ADO 封装类打开数据库并绑定记录集DataGrid 自己不查数据库它只负责显示一个 Recordset。所以先用 ADO 把数据取出来。原文用的是 CADODatabase / CADORecordset 这套封装类如果你工程里没有可以换成_ConnectionPtr_RecordsetPtr的原始 ADO 写法效果一样。// 成员变量声明对话框类头文件里 CADODatabase m_DBCn; // 数据库连接对象 CADORecordset m_Rs; // 记录集对象 CDataGrid m_ctrlDG; // DataGrid 控件对象 // 初始化函数里比如 OnInitDialog 或按钮响应 CString strConnection; strConnection.Format(_T(ProviderMicrosoft.Jet.OLEDB.4.0;) _T(Data Sourcenote.mdb)); m_DBCn.Open((LPCTSTR)strConnection); // 打开 Access 数据库 m_Rs.SetDatabase(m_DBCn); // 记录集关联到连接 m_Rs.Open(_T(select * from test;)); // 执行查询 // 关键一步把记录集的底层接口指针交给 DataGrid m_ctrlDG.SetRefDataSource((LPUNKNOWN)m_Rs.GetRecordset());逻辑说明SetRefDataSource接收的是LPUNKNOWN也就是记录集对象的 IUnknown 指针。DataGrid 拿到这个指针后内部会 QueryInterface 出它需要的接口来遍历数据。参数上唯一要注意的是连接字符串——Microsoft.Jet.OLEDB.4.0对应 .mdb 文件如果你用的是 SQL Server换成ProviderSQLOLEDB并补上 Server、Database、UID、PWD。m_Rs.GetRecordset()返回的是_RecordsetPtr强转成 LPUNKNOWN 是这套封装类的惯用做法。绑定成功后网格会自动按记录集的字段生成列行数就是查询结果条数。如果网格一片空白先查m_Rs.Open是否真的返回了记录再查控件是否已经 Create 完成——顺序反了会绑不上。3. 列宽、数字格式与多行显示拿到 CColumn 之后能改什么3.1 取列对象CColumns 与 CColumn 的索引方式所有对“某一列”的操作第一步都是拿到 CColumn。路径固定先从控件拿列集合再从列集合按索引拿单列。CColumns cols m_ctrlDG.GetColumns(); // 取得列集合 VARIANT vt; vt.vt VT_I2; // 索引类型2 字节整数 vt.iVal 0; // 第 0 列第一列 CColumn col cols.GetItem(vt); // 取得该列对象参数说明GetItem接收 VARIANT索引既可以是整数VT_I2也可以是列名字符串VT_BSTR。用整数索引时从 0 开始跟数据库字段顺序一致。如果你在运行时动态改了查询语句列顺序变了硬编码的索引就会指错列——这是后面避坑章节要展开的一个点。拿到 col 之后能做的事就多了。原文演示了列宽和数字格式我补充几个实际项目里高频用到的col.SetWidth(120); // 列宽单位是像素 col.SetNumberFormat(_T(0.0%)); // 小于 1 的小数显示成百分数 col.SetAlignment(1); // 0 左对齐1 右对齐2 居中 col.SetCaption(_T(测试结果)); // 改列标题SetNumberFormat的格式串跟 VB 的 Format 函数一致0.0%表示保留一位小数并乘 100 加百分号。注意它只影响显示底层数据不变——如果你后续要拿单元格的值做计算拿到的还是原始小数。3.2 多行显示与行高控制原文提到“多行显示”配了m_ctrlDG.SetRowHeight(20)。这里要澄清一个容易混淆的地方SetRowHeight 设的是行高不是让一行内容折成两行。DataGrid 本身对单元格内换行的支持有限如果你要的是“一行数据在网格里占两行高度来完整展示”常见做法是把行高调大再配合列的 Wrap 属性如果该版本支持。我一般会先设行高到 2430让文字不挤在一起视觉上就够用了。m_ctrlDG.SetRowHeight(24); // 行高 24 像素比默认值宽松行高设太小中文字体会被上下切掉设太大一屏能看的行数变少。24 是我在 1024×768 到 1920×1080 上都试过比较稳的值。如果你的表格要打印或导出行高还得跟打印模板对齐那是另一个话题了。3.3 用 HeadClick 消息做点击列头排序DataGrid 支持 HeadClick 事件点头部时触发。要让它排序得先通过 ClassWizard 给控件添加 HeadClick 消息映射然后在响应函数里根据点击的列索引重新排序记录集或重新查询。// 头文件里声明 afx_msg void OnHeadClickDataGrid1(short ColIndex); // 消息映射 ON_EVENT(IDC_DATAGRID1, 1, DISPID_HEADCLICK, OnHeadClickDataGrid1, VTS_I2) // 实现 void CTestGridDlg::OnHeadClickDataGrid1(short ColIndex) { // ColIndex 是点击的列号从 0 开始 CString strField m_Rs.GetFieldName(ColIndex); // 假设封装类有此方法 CString strSQL; strSQL.Format(_T(select * from test order by %s), strField); m_Rs.Close(); m_Rs.Open(strSQL); m_ctrlDG.SetRefDataSource((LPUNKNOWN)m_Rs.GetRecordset()); m_ctrlDG.Refresh(); // 重新绑定后刷新显示 }逻辑说明HeadClick 的 ColIndex 参数告诉你用户点了哪一列把它映射成字段名拼进 ORDER BY 重新查询再重新绑数据源。这里有个性能取舍——每次点击都重查数据库数据量大时会有明显延迟。数据量小几千行以内无所谓数据量大就改成在内存里对 Recordset 排序或者用断开的 Recordset。参数上注意DISPID_HEADCLICK这个事件 ID 在不同控件版本里可能不一样以 ClassWizard 生成的消息映射为准别手抄。4. 避坑与排查DataGrid 在真实工程里的五个翻车点4.1 控件不在插入列表里现象Insert ActiveX Control 对话框里找不到 Microsoft DataGrid Control 6.0。原因这个控件不是 VC 默认安装的需要单独注册MSDATGRD.OCX。解决确认系统里有这个文件通常在C:\Windows\System32或SysWOW64用regsvr32 MSDATGRD.OCX注册64 位系统上 32 位工程要注册 32 位版本别注册错目录。4.2 绑定后网格空白现象SetRefDataSource调用了但网格一行数据都不显示。原因最常见的是记录集已经关闭或游标类型不对。解决确认m_Rs.Open之后记录集处于打开状态且游标支持遍历adOpenStatic 或 adOpenKeyset另外检查SetRefDataSource是否在控件 Create 之后调用顺序反了绑定无效。4.3 列索引错位现象本来想设第 3 列的格式结果改到了第 5 列。原因查询语句从select a,b,c改成select a,c,b后列顺序变了但代码里硬编码的vt.iVal 2没跟着改。解决用列名而不是索引来取列——把 VARIANT 的 vt 设成 VT_BSTRbstrVal 指向字段名或者把列索引定义成常量集中管理改查询时同步改。4.4 数字格式不生效现象SetNumberFormat(_T(0.0%))调了单元格还是显示原始小数。原因该列的数据类型不是数值型或者格式串跟区域设置冲突。解决先确认数据库字段是 Number/Double 而不是 Text格式串里的分隔符用英文句点别用中文标点如果系统区域设置用逗号做小数点格式串要相应调整。4.5 程序退出时崩溃现象关闭对话框时弹异常或直接崩。原因DataGrid 还持有 Recordset 的接口指针而 Recordset 先于控件被销毁COM 引用计数出问题。解决在OnDestroy里先把数据源置空——m_ctrlDG.SetRefDataSource(NULL)再关闭记录集和连接最后让控件随对话框销毁。顺序不能反。5. 进阶用列名替代索引让格式设置不再随查询语句漂移前面避坑章节提到列索引错位这里给一个我后来固定下来的写法。核心思路是所有对列的设置都走列名不走数字索引。这样查询语句增删字段时只要字段名没变格式代码就不用动。// 辅助函数按列名取 CColumn CColumn GetColumnByName(CDataGrid grid, LPCTSTR lpszName) { CColumns cols grid.GetColumns(); VARIANT vt; vt.vt VT_BSTR; vt.bstrVal _bstr_t(lpszName).Detach(); // 转成 BSTR CColumn col cols.GetItem(vt); SysFreeString(vt.bstrVal); // 释放 BSTR return col; } // 使用不管查询语句怎么改只要字段名在格式就跟着走 CColumn colAmount GetColumnByName(m_ctrlDG, _T(amount)); colAmount.SetNumberFormat(_T(0.00)); colAmount.SetWidth(100); CColumn colRate GetColumnByName(m_ctrlDG, _T(rate)); colRate.SetNumberFormat(_T(0.0%));参数说明_bstr_t是 COM 字符串包装类Detach()把内部 BSTR 的所有权交出来避免析构时提前释放用完必须SysFreeString否则每次调用漏一个 BSTR长时间运行会累积。这个写法比索引多几行但换来的是查询语句调整时不用满工程搜iVal 。验证方法很简单把select * from test改成select rate, amount, name from test字段顺序完全打乱跑一遍看格式是否还落在正确的列上。如果百分数仍然显示在 rate 列、两位小数仍然显示在 amount 列说明列名绑定生效了。还有一个我踩过的坑GetItem传列名时如果列名不存在返回的 CColumn 是空的后续 SetWidth 不会报错但也不生效排查起来很隐蔽。所以我在辅助函数里加了一句判断——if (col.m_pInterface NULL) return;之类的空检查具体成员名看包装类实现。从那以后我每次给 DataGrid 设格式都强制走列名这条路再也没因为改 SQL 而返工过。希望帮到你。本文还有配套的精品资源点击获取