简介:这份文档面向使用 VC/MFC 进行桌面开发的初中级程序员,聚焦 DataGrid 网格控件的实际用法,帮助读者解决在对话框中展示数据库查询结果、控制列显示与格式化内容等常见问题。资源包内共 1 个 doc 文件,约 226KB,以图文结合的方式组织内容,便于边看边对照代码实践。文档从插入 Microsoft DataGrid OLEDB 6.0 控件、选择封装类讲起,依次演示将 ADO 记录集设为数据源以显示查询结果、通过 CColumns 与 CColumn 设置列宽、用 SetNumberFormat 把小数格式化为百分数、调整行高实现多行显示,以及借助 HeadClick 消息实现排序等要点,并附有 VC6.0 下调试通过的示例工程说明。目前已有 853 人学习,适合希望快速上手微软原生网格控件、减少重复造轮子的开发者参考。
1. 老 MFC 项目里翻出 DataGrid:它到底还能不能扛活
接手一个十几年前的 MFC 对话框程序,需求很简单——把一张 Access 表里的查询结果塞进界面,能看、能调列宽、能把小数显示成百分数。翻遍工具箱,最省事的还是那个 Microsoft DataGrid OLEDB 6.0 控件。网上关于 MFC DataGrid 用法的资料大多零散,要么只贴一段SetRefDataSource,要么把 VB 的调用方式硬套到 VC 上,真正能照着跑通的完整链路不多。这份资源把显示查询结果、控制列宽、格式化内容、多行显示、表头点击排序这几件事串成了一条线,配套一个 VC6 工程(TestGrid),在 VC6.0 英文版加 Windows XP 环境下调试通过。适合谁?手上还有 MFC 老工程、不想为了一个表格去引入第三方网格库、又希望控件升级时业务代码几乎不动的人。它不新,但够用,边界也清楚。
2. 把 Recordset 挂到 DataGrid:数据源绑定的完整链路
2.1 为什么选微软自家控件而不是第三方网格
先说选型。网上优秀的网格控件不少,功能花哨的也有,但在一个以 VC 为主力工具的老项目里,微软自家控件的优势很实在:与 Windows 系统兼容性好,控件随系统或运行库升级时,业务侧代码往往不用改。ActiveX 本质基于 COM,插入控件时向导会帮你把 COM 查询接口封装成 C++ 类,用起来几乎感觉不到自己在碰 COM。代价是你看不到控件源码,出问题只能靠方法名和帮助文档推断——这一点后面避坑章节会专门讲。我的判断标准很简单:控件能满足需求、升级不折腾业务代码,就用它;满足不了再换。DataGrid 在“显示一张表、调调列、格式化数字”这个量级上,完全够。
2.2 建工程与插入控件时该勾哪几个类
准备工作不复杂,但有两个动作容易做错。第一,主框架选对话框,这样控件直接拖到对话框模板上,省去视图类里折腾。第二,插入控件时在注册的 ActiveX 列表里找 Microsoft DataGrid OLEDB 6.0 控件,确定后会弹出一个类选择对话框,让你挑要生成的封装类。这里只勾三个:CDataGrid、CColumns、CColumn。勾多了会生成一堆用不上的包装类,编译变慢还容易和别的类型撞名;勾少了后面取列、设格式时没有对应类型。三个类的关系是:CDataGrid是控件本体,CColumns是列集合,CColumn是单列,取列必须先拿集合再按索引取单列,这个层级别搞反。
2.3 用 ADO 封装类打开库并绑定数据源
数据库访问这里用了一个 ADO 封装类,换成别的访问方式也不影响控件使用,控件只认最终拿到的 Recordset。核心代码就三段:开库、查询、把记录集指针交给控件。
// 成员变量:数据库对象、记录集对象、控件对象 CADODatabase m_DBCn; CADORecordset m_Rs; CDataGrid m_ctrlDG; // 1. 拼连接串并打开 Access 库 CString strConnection; strConnection.Format(_T("Provider=Microsoft.Jet.OLEDB.4.0;") _T("Data Source=note.mdb")); m_DBCn.Open((LPCTSTR)strConnection); // 2. 记录集挂到数据库上并执行查询 m_Rs.SetDatabase(&m_DBCn); m_Rs.Open(_T("select * from test;")); // 3. 把底层 Recordset 接口指针交给 DataGrid m_ctrlDG.SetRefDataSource((LPUNKNOWN)m_Rs.GetRecordset());逻辑说明:SetDatabase只是把记录集和连接对象关联,真正执行 SQL 的是Open。SetRefDataSource的参数是LPUNKNOWN,传的是记录集底层_Recordset接口指针,控件内部会自己去查IUnknown拿到需要的接口,所以这里必须做一次强制转换,直接传封装类对象是编译不过的。参数上要注意连接串里的Data Source写相对路径时,工作目录取决于进程启动位置,调试时正常、双击 exe 就找不到库,多半是这里。查询语句尽量显式列出字段而不是select *,字段顺序直接决定网格列顺序,后面按索引取列时才不会错位。
2.4 绑定后列顺序和字段名的对应关系
绑定完成后网格会按 Recordset 的字段顺序生成列,列标题默认取字段名。这里有个隐含约定:后面所有“按索引取列”的代码,索引都对应这个字段顺序,一旦 SQL 改成select b,a from ...,原来索引 0 的列就换人了。我的习惯是在绑定后立刻写一段初始化,把每列的宽度、标题、格式一次性设完,而不是散落在各个事件里。这样字段顺序一变,只需要改这一处。另外记录集对象要保证生命周期覆盖控件的显示期,如果记录集是局部变量,函数返回后网格就空了或者直接崩,这是新手最常见的翻车点之一。
3. 列宽、数字格式与多行显示:显示层的三个控制点
3.1 取列集合再取单列:CColumns 与 CColumn 的层级
所有对“列”的操作都走同一条路:先GetColumns拿列集合,再GetItem按 VARIANT 索引拿单列。索引既可以是数字也可以是字段名,数字从 0 开始。
// 取列集合 CColumns cols = m_ctrlDG.GetColumns(); // 用 VARIANT 指定列索引(0 表示第一列) VARIANT vt; vt.vt = VT_I2; vt.iVal = 0; CColumn col = cols.GetItem(vt); // 设置该列宽度(单位与控件一致,通常按像素理解) col.SetWidth(120.0f);逻辑说明:GetItem的参数是 VARIANT,不是 int,直接传0在部分编译环境里会走隐式转换失败或取到错误列,老老实实填vt和iVal最稳。SetWidth接受浮点,宽度单位跟控件当前使用的度量有关,对话框里如果做过 DPI 适配,实际像素会有偏差,别拿设计稿的像素值硬套。取列这一步本身不耗时,但如果在OnPaint或滚动事件里反复取,会有可感知的卡顿,正确做法是在对话框初始化时取一次、设一次。
3.2 SetNumberFormat 把小数变成百分数:格式串怎么写
数字格式化是这份资源里最实用的一段。需求是“小于 1 的小数显示成百分数”,做法是对目标列调SetNumberFormat,传一个格式串。
// 仍以第一列为例,先取到 col col.SetNumberFormat(_T("0.0%")); // 常见变体: // "0%" -> 12% // "0.00%" -> 12.34% // "#,##0.00"-> 千分位,保留两位逻辑说明:格式串语法跟常见电子表格的数值格式一致,0是占位符,小数点后几个0就保留几位,末尾%会让控件把底层数值乘以 100 再显示。注意它只改显示,不改底层数据,导出或再计算时拿到的还是原始小数,这一点在做“显示百分比、存储小数”的需求时正好合用。坑在于格式串对文本列无效,如果某列底层是字符串类型,设了也不生效,排查时先确认字段类型。另外格式串写错(比如中文百分号、全角字符)不会报错,只是显示原样,属于典型的静默失败。
3.3 行高与多行显示:SetRowHeight 的实际效果
多行显示靠调行高实现,让单元格内容能折行展示。
// 设置行高,数值偏大时单元格内容可折行 m_ctrlDG.SetRowHeight(20);逻辑说明:SetRowHeight改的是所有数据行的统一高度,不是单行。想让长文本折行,光调行高不够,还要确认列宽足够窄、单元格允许换行,否则只是行变高、文字仍被截断。行高设得过大,一屏能显示的行数变少,滚动体验变差,20 到 30 之间是比较常见的取值。还有一个细节:表头高度是另一个属性,改行高不影响表头,别指望一个调用把表头也撑开。
3.4 表头点击排序:HeadClick 消息的接线方式
排序不是控件自带开关,而是靠消息机制自己接。控件会发出表头点击通知,你在对话框里映射处理函数,拿到被点的列号,重新组织记录集或对已取到的数据排序,再刷新绑定。
// 消息映射(示意,实际宏名以向导生成为准) BEGIN_EVENTSINK_MAP(CTestGridDlg, CDialog) ON_EVENT(CTestGridDlg, IDC_DATAGRID1, 2 /* HeadClick */, OnHeadClickDataGrid, VTS_I2) END_EVENTSINK_MAP() void CTestGridDlg::OnHeadClickDataGrid(short Col) { // Col 为被点击的表头列索引,从 0 开始 // 这里按该列重新查询或对缓存排序,然后重新 SetRefDataSource }逻辑说明:ActiveX 控件的事件走EVENTSINK_MAP,不是普通MESSAGE_MAP,事件 ID 和参数签名以向导生成的结果为准,手写容易对不上。Col是 short 类型的列索引,拿它拼ORDER BY时要注意字段名映射,别直接把索引当字段名用。排序后重新绑定会丢失当前的列宽和格式设置,所以要么在重绑后重新应用一遍显示设置,要么把显示设置抽成一个函数反复调用。这是排序功能最容易翻车的地方。
4. 避坑与排查:DataGrid 在 VC6 下的五条血泪经验
4.1 现象:编译报错说 SetRefDataSource 参数类型不匹配
原因:SetRefDataSource形参是LPUNKNOWN,而GetRecordset()返回的是封装类里的接口指针类型,两者不是同一类型,C++ 不会自动转。解决:显式强转(LPUNKNOWN),并确认GetRecordset()返回的确实是底层_Recordset接口而不是封装对象本身。如果封装类没有暴露底层指针,就得自己加一个取接口的方法,别硬转封装对象,运行期会崩。
4.2 现象:调试时数据正常,双击 exe 后网格空白
原因:连接串里Data Source=note.mdb是相对路径,调试时工作目录是工程目录,双击运行时工作目录变成 exe 所在目录或快捷方式起始位置,库找不到,Open失败但没检查返回值,后面绑了个空记录集。解决:连接串用绝对路径,或者在程序启动时用GetModuleFileName拼出 exe 目录再拼库文件名;同时给Open加返回值判断,失败就弹提示,别让错误静默吞掉。
4.3 现象:设了 SetNumberFormat 但显示没变化
原因有三类:字段本身是文本类型,格式串对文本无效;格式串里混入了全角字符或中文百分号;设置格式的列索引和实际列对不上(SQL 字段顺序变了)。解决:先确认字段类型,再逐字符检查格式串,最后打印或断点确认取到的列索引对应哪个字段。建议把“取列—设宽—设格式”写成一个按字段名取列的辅助函数,用名字而不是索引定位,从根上避开顺序错位。
4.4 现象:记录集是局部变量,函数返回后网格清空或程序崩溃
原因:SetRefDataSource传的是接口指针,控件并不复制数据,而是持有引用。局部CADORecordset析构后底层接口释放,控件再访问就是野指针。解决:数据库对象和记录集对象都做成对话框类的成员变量,生命周期覆盖整个窗口;切换查询时先解绑或重新赋值,别让旧记录集在控件还引用着的时候被析构。
4.5 现象:排序后列宽和百分数格式全丢了
原因:表头点击后重新SetRefDataSource,控件按新记录集重建列,之前对CColumn做的设置全部失效。解决:把显示层设置抽成ApplyGridStyle()函数,每次重绑后调用一次;或者在排序时只对已有数据做行序调整而不重绑,前者实现简单、后者性能更好,按数据量选。数据量上千行时重绑会有明显闪烁,可以配合SetRedraw(FALSE/TRUE)包一下。
5. 从方法名反推功能:把这份资源用成长期参考
这份资源最值钱的地方其实不是那几段代码,而是它示范了一种用法:插入控件时生成的封装类,方法名基本能顾名思义,SetWidth、SetNumberFormat、SetRowHeight看名字就知道干什么。我后来的习惯是,拿到一个不熟悉的 ActiveX 控件,先看生成的类头文件里有哪些Set/Get成对的方法,功能边界就摸清大半。帮助文档大多是给 VB 写的,调用语法和 VC 差别不小,但方法名高度相似,对照着看能快速迁移。下面这张表是我整理的高频方法对照,按“想做什么—调哪个方法—参数注意点”组织,比翻文档快。
| 想做的事 | 方法 | 参数与注意点 |
|---|---|---|
| 绑定数据源 | SetRefDataSource | 传LPUNKNOWN,需强转底层 Recordset 接口 |
| 取列集合 | GetColumns | 返回CColumns,初始化时取一次即可 |
| 取单列 | GetItem | 参数是 VARIANT,数字索引从 0 开始 |
| 设列宽 | SetWidth | 浮点,受 DPI 影响,别硬套设计稿像素 |
| 设数字格式 | SetNumberFormat | 格式串对文本列无效,全角字符会静默失败 |
| 设行高 | SetRowHeight | 统一行高,不影响表头,折行还需列宽配合 |
| 表头点击 | HeadClick事件 | 走EVENTSINK_MAP,重绑后要重设显示样式 |
验证方法也简单:新建一个对话框工程,插控件、勾三个类、连一个本地 Access 库,按第 2 章的代码跑通绑定,再按第 3 章逐项加列宽、格式、行高,每加一项就编译运行看效果,出问题立刻能定位到是哪一步引入的。别一次性把排序也加上,排序涉及重绑,会把前面的显示设置冲掉,混在一起排查很痛苦。我一般会先做一个只有绑定和列宽的版本,确认数据能出来,再叠格式,最后接排序,每层都留一个能跑的中间态。
从那以后我每次接老 MFC 工程里的 ActiveX 控件,都强制先做一件事:把生成的封装类头文件从头到尾读一遍,把Set/Get方法列出来,再对照帮助文档确认参数类型,最后才动手写业务。这个习惯帮我省掉了大量“设了没反应”的排查时间。希望帮到你。
本文还有配套的精品资源,点击获取