MFC DataGrid 控件用法详解:从数据绑定到列宽格式与排序
简介这份资源面向使用 VC/MFC 进行桌面开发的程序员尤其是需要在对话框或视图里快速呈现数据库查询结果的开发者系统讲解 MFC 中 DataGrid 控件的实际用法。内容围绕显示查询结果、控制列宽等显示属性、格式化数据、多行显示以及 HeadClick 消息排序等典型场景展开并给出可直接参考的代码片段与调试环境说明。资源包为单个 doc 文档约 226KB属于轻量级技术笔记便于离线查阅与随工程对照。目前已有 851 人学习说明其在 MFC 数据网格应用上具备一定参考价值。读者可从中获得从 ADO 记录集绑定到列对象操作、数字格式设置与事件响应的完整思路适合作为 VC6.0 环境下 DataGrid 控件的入门与速查材料。1. 从一次“数据能查出来却显示不全”说起有个做设备点检上位机的朋友找我说他在对话框里塞了个表格控件ADO 查询明明返回了 200 多条记录界面上却只看到前几行列宽还挤成一团小于 1 的良品率全显示成 0。他一度怀疑是数据库连接池的问题折腾了半天连接字符串。其实问题根本不在数据层而在 MFC 里那个常被低估的 DataGrid 控件——它默认的列宽、行高和格式都没配数据源挂上去了显示效果却全靠默认值撑着。这篇就围绕 MFC DataGrid 控件用法详解这个主题把从“挂数据源”到“列宽、格式、多行、排序”这一整条链路拆开讲。适合两类人一是刚在 VC 对话框工程里拖进 Microsoft DataGrid OLEDB 6.0 控件、不知道怎么让它听话的新手二是用过但被列对象、VARIANT 索引、HeadClick 排序这些细节绊过的熟手。下面所有代码都基于对话框主框架数据库访问用 ADO 封装类控件本身走 ActiveX 封装出来的 CDataGrid、CColumns、CColumn 三个类。2. 把 DataGrid 控件挂进对话框工程类选择与数据源绑定2.1 插入控件时到底该勾哪几个类在资源视图里右键对话框选“插入 ActiveX 控件”找到 Microsoft DataGrid OLEDB 6.0。确定之后 VC 会弹出一个“添加成员变量”向导里面列了一长串封装类。很多人图省事全勾上结果工程里多出一堆用不到的包装类编译变慢不说还容易和别的 ActiveX 类重名。我一般只勾三个CDataGrid、CColumns、CColumn。理由很直接——CDataGrid 负责控件本体和数据源CColumns 是列集合CColumn 是单列。列宽、格式、对齐这些操作全落在 CColumn 上列集合只是用来按索引取列。其余像 CSplit、CHead 之类的类除非你要做拆分窗口或自定义表头否则先不勾等真需要了再补。提示ActiveX 控件本质是基于 COM 的这三个类只是对控件查询接口的封装。你没法直接看到控件源码但方法名基本能望文生义这也是后面能快速上手的原因。勾完类之后向导会帮你生成成员变量。假设变量名是 m_ctrlDG类型就是 CDataGrid。到这一步控件在对话框上还是个空壳接下来要给它喂数据。2.2 用 ADO 封装类把 Recordset 挂上去数据源这块原文用的是 CADODatabase 和 CADORecordset 两个封装类。这套封装在 VC6 时代很常见核心思路是把 ADO 的 Connection 和 Recordset 包一层让调用方少写 COM 的 VARIANT 和 _bstr_t。下面这段是打开库、执行查询、把记录集挂到控件上的完整流程// 数据库对象、记录集对象、控件对象 CADODatabase m_DBCn; CADORecordset m_Rs; CDataGrid m_ctrlDG; // 拼接 Jet 引擎的连接串指向同目录下的 note.mdb CString strConnection; strConnection.Format(_T(ProviderMicrosoft.Jet.OLEDB.4.0;) _T(Data Sourcenote.mdb)); // 打开数据库失败会抛异常实际工程里要 try/catch m_DBCn.Open((LPCTSTR)strConnection); // 把记录集和数据库对象关联再执行查询 m_Rs.SetDatabase(m_DBCn); m_Rs.Open(_T(select * from test;)); // 关键一步把底层 Recordset 接口指针交给 DataGrid m_ctrlDG.SetRefDataSource((LPUNKNOWN)m_Rs.GetRecordset());逻辑说明SetRefDataSource 的参数是 LPUNKNOWN也就是一个未指定类型的 COM 接口指针。这里把 ADO Recordset 的接口指针直接传进去DataGrid 内部会去查询它需要的接口然后自动把字段名映射成列标题、把记录填成行。参数上唯一要注意的是连接串里的 Data Source写相对路径时基准是进程当前目录不是 exe 所在目录调试时如果提示找不到 note.mdb先确认工作目录。这一步做完控件里应该能看到数据了。如果看不到先别怀疑控件按顺序查三件事连接串是否打开成功、SQL 是否真的返回了行、SetRefDataSource 的返回值有没有报错。我见过有人把 m_Rs.Open 写在 SetRefDataSource 之后结果控件拿到一个空记录集界面自然一片空白。2.3 数据源绑定的两种常见误用第一种误用是把 DataGrid 当成数据容器想自己往单元格里塞值。DataGrid 的设计是绑定型控件数据来自 Recordset不是逐格赋值。真要手工填得走 CColumns/CColumn 的 Value 属性但那样就失去了绑定查询结果的意义。第二种误用是查询还没执行就挂数据源。SetRefDataSource 必须在 Recordset 处于打开状态、且至少已经定位到有效记录时调用。顺序错了控件可能显示列标题但没有任何行。稳妥写法就是上面那样先 Open再 SetRefDataSource。3. 列宽、行高与显示控制拿到 CColumn 之后能做什么3.1 从列集合里按索引取列要控制某一列第一步是拿到 CColumn 对象。路径是控件 → 列集合 → 单列。代码如下// 取得列集合 CColumns cols m_ctrlDG.GetColumns(); // 用 VARIANT 指定列索引0 表示第一列 VARIANT vt; vt.vt VT_I2; vt.iVal 0; // 从列集合里取出对应列 CColumn col cols.GetItem(vt); // 设置该列宽度单位是逻辑单位跟字体和 DPI 有关 col.SetWidth(120);逻辑说明GetColumns 返回的是整个列集合GetItem 的参数是 VARIANT这是 ActiveX 接口的通用做法——索引既可能是整数也可能是字符串。这里用 VT_I2 加 iVal 指定第 0 列。SetWidth 的单位不是像素而是与控件字体相关的逻辑单位所以同一份代码在不同 DPI 下视觉宽度会不一样。参数上列索引从 0 开始越界会抛异常循环设列宽时务必先取 GetCount 做边界判断。3.2 列宽、行高与多行显示的配合列宽设完行高也得管。默认行高偏小中文或长数字容易显示不全。SetRowHeight 可以调整// 设置行高让每行有足够空间显示内容 m_ctrlDG.SetRowHeight(22); // 如果希望表头也高一点可以单独取表头对象调整 // 常见做法是配合列宽一起设避免表头文字被截断这里有个容易翻车的点SetRowHeight 设得太小多行文本会被裁掉设得太大一屏能看到的记录数变少用户要不停滚动。我一般按控件字体高度加 6 到 8 个逻辑单位来估。原文提到的“多行显示”本质就是行高足够时单元格内换行内容能完整呈现而不是控件有独立的多行模式开关。列宽和行高都调完之后建议在对话框初始化阶段统一设一遍不要散落在各个按钮响应里。否则用户点一次查询就重设一次列宽会被反复重置体验很差。3.3 显示控制里最容易被忽略的列顺序DataGrid 默认按 Recordset 的字段顺序生成列。如果 SQL 里是 select *列顺序就跟着表结构走。想调整显示顺序有两个办法一是 SQL 里显式写字段顺序二是拿到列集合后设置列的 Visible 和位置属性。前者简单直接后者灵活但代码多。我一般优先改 SQL因为列顺序本质是数据层的事放在查询里表达最清楚。只有当同一份 Recordset 要按不同角色显示不同列时才在控件层动态设 Visible。比如普通操作员看不到成本列就把那一列 SetVisible(FALSE)而不是重新查一遍。4. 单元格格式与 HeadClick 排序让数据“像那么回事”4.1 用 SetNumberFormat 把小数变成百分数原文里那个“小于 1 的小数显示成百分数”的需求在良品率、合格率这类场景里非常典型。数据库里存的是 0.856 这样的浮点界面上要显示成 85.6%。做法还是先取列再设格式// 取第 3 列索引 2假设它是良品率字段 VARIANT vt; vt.vt VT_I2; vt.iVal 2; CColumn col m_ctrlDG.GetColumns().GetItem(vt); // 设置数字格式为保留一位小数的百分数 col.SetNumberFormat(_T(0.0%));逻辑说明SetNumberFormat 接受的是格式字符串语法和 Excel 的自定义格式类似。0.0% 表示按百分比显示并保留一位小数控件会自动把 0.856 乘 100 再加百分号。参数上要注意格式只影响显示不改底层数据导出或再次计算时拿到的仍是原始小数。如果某列既有整数又有小数格式串要写成 0.0% 而不是 0%否则整数会被强行补小数位。4.2 格式串的常见写法和边界除了百分数常用的还有千分位、货币、固定小数位。下面这张表是我在实际项目里用得最多的几种格式串显示效果适用场景0.003.14固定两位小数#,##01,234整数千分位#,##0.001,234.56金额0.0%85.6%比率yyyy-mm-dd2024-01-01日期边界在于格式串只对数值和日期类型生效字符串列设了也没用。另外如果 Recordset 里字段类型是文本但内容是数字SetNumberFormat 不会自动转换得先在 SQL 里用转换函数处理成数值类型。4.3 HeadClick 消息与排序的落地方式DataGrid 支持点表头排序靠的是 HeadClick 消息。在 MFC 里给 ActiveX 控件加事件处理通常通过类向导添加消息映射。处理函数里拿到被点击的列号然后重新组织 Recordset 的排序。// 在对话框类中声明并绑定 HeadClick 处理 // 伪代码示意事件入口实际由类向导生成映射 void CTestGridDlg::OnHeadClickDatagrid1(long Col) { // Col 是被点击列的索引 CString strSQL; strSQL.Format(_T(select * from test order by [%s]), GetColumnName(Col)); // 需自行实现列名映射 m_Rs.Close(); m_Rs.Open(strSQL); m_ctrlDG.SetRefDataSource((LPUNKNOWN)m_Rs.GetRecordset()); }逻辑说明HeadClick 事件带回列索引但索引不等于字段名所以需要一个列索引到字段名的映射。简单做法是维护一个 CStringArray初始化时按 Recordset 字段顺序填好。参数上重新查询后必须重新 SetRefDataSource否则控件还指着旧的记录集。排序方向升/降可以用一个成员变量记录每次点击取反。注意重新挂数据源会重置列宽和格式所以排序前要把当前列宽和格式缓存下来挂完再恢复。这是很多人做排序时列宽突然变默认值的根因。5. 避坑与排查DataGrid 用起来最容易翻车的五件事5.1 控件显示空白但查询确实有数据现象SetRefDataSource 调用后界面无行无列。原因通常是 Recordset 未打开或已关闭也可能是连接串指向的库文件路径不对。解决在 SetRefDataSource 前加断言确认 m_Rs 处于打开状态并打印连接串核对 Data Source 的绝对路径。5.2 列宽设了没反应现象调用 SetWidth 后列宽不变。原因多是把 CColumn 当成了引用语义实际上 GetItem 返回的是一个临时封装对象改它不影响控件。解决确认拿到的是有效列对象并且设置时机在数据源绑定之后。绑定前设列宽会被随后的数据源刷新覆盖。5.3 百分数格式显示成 0.0%现象设了 0.0% 却显示 0.0%。原因是底层字段是文本类型控件按字符串处理不套用数字格式。解决在 SQL 里用 CDbl 或类似函数把字段转成数值或者改表结构。格式串只对数值类型生效这点没有后悔药。5.4 排序后列宽和格式全丢现象点表头排序后之前设的列宽、百分数格式回到默认。原因是重新 SetRefDataSource 会重建列集合。解决排序前把各列宽度和格式存进数组重新绑定后逐列恢复。这个坑几乎每个做 HeadClick 排序的人都会踩一次。5.5 大数据量下界面卡顿现象查询返回几千行时挂数据源和滚动都明显卡。原因是 DataGrid 一次性把整个 Recordset 拉进控件渲染。解决在 SQL 里加分页或者用控件的 AllowAddNew/AllowUpdate 关掉不必要的交互减少重绘。数据量超过一两千行时绑定型表格控件本身就不是最优解该考虑虚拟列表或分页查询。6. 一个我常用的列初始化封装与验证习惯上面几章把单点操作都拆开了但实际工程里如果每处都手写 VARIANT 和 GetItem代码会又长又容易错。我一般会封一个小的列初始化函数把“取列、设宽、设格式、设标题”四件事一次做完然后在数据源绑定之后统一调用。这样排序重绑时也能复用同一份配置。// 列配置结构描述每一列的显示要求 struct ColumnCfg { int index; // 列索引 int width; // 列宽 LPCTSTR format; // 数字格式可为 NULL LPCTSTR title; // 列标题 }; // 按配置批量初始化列 void InitGridColumns(CDataGrid grid, const ColumnCfg* cfgs, int count) { CColumns cols grid.GetColumns(); for (int i 0; i count; i) { VARIANT vt; vt.vt VT_I2; vt.iVal cfgs[i].index; CColumn col cols.GetItem(vt); col.SetWidth(cfgs[i].width); if (cfgs[i].format ! NULL) { col.SetNumberFormat(cfgs[i].format); } if (cfgs[i].title ! NULL) { col.SetCaption(cfgs[i].title); } } }逻辑说明这个函数把列索引、宽度、格式、标题打包成配置数组调用方只关心“我要哪些列、长什么样”不用重复写 VARIANT 样板。参数上index 必须和 Recordset 字段顺序一致format 传 NULL 表示不设格式title 传 NULL 表示沿用字段名。排序重绑后只要再调一次 InitGridColumns列宽和格式就全回来了。验证方面我习惯在对话框初始化末尾加一段自检遍历列集合确认列数和预期一致再抽查第一列的宽度和格式是否生效。如果列数对不上多半是 SQL 字段变了而配置没同步。这个习惯帮我省过好几次“界面看着正常、导出却少一列”的返工。从那以后我每次在对话框工程里挂 DataGrid都强制走一遍“先绑数据源、再批量初始化列、排序后重放配置”的流程不再零散地设列宽。希望帮到你。本文还有配套的精品资源点击获取