
简介这份资源是面向MFC初学者与VC桌面开发者的DataGrid控件用法详解文档针对在对话框工程中集成Microsoft DataGrid OLEDB 6.0控件、显示数据库查询结果这一常见需求系统梳理了控件的核心调用方式。内容涵盖通过CADODatabase与CADORecordset将记录集绑定为数据源、利用CColumns与CColumn对象控制列宽、使用SetNumberFormat格式化数值如将小数显示为百分数、设置行高实现多行显示以及借助HeadClick消息机制完成排序等典型场景并附有可直接参考的代码片段。资源包为1个doc文档约226KB体量轻便适合边看边在VC6.0工程中对照实践。目前已有849人学习说明其在MFC数据网格应用上具备一定参考价值能帮助读者快速掌握控件绑定、显示控制与格式化的完整思路。1. 从一张“跑不起来”的表格说起MFC DataGrid 到底在解决什么如果你在维护一套有些年头的 Windows 桌面程序大概率遇到过这种场景需求方甩过来一句“把数据库里这几万条记录显示出来能排序、能改、能导出”而你手里只有一个基于 MFC 的对话框工程。ListView 做只读展示还行真要在单元格里编辑、按列排序、动态增删行代码量立刻失控。这时候多数老工程师会想到 DataGrid——准确说是微软那套绑定型网格控件在 MFC 里的落地用法。MFC DataGrid 控件用法详解这个题目核心不是教你拖一个控件上去而是讲清楚三件事控件怎么绑数据源、列和行怎么在运行时控制、以及为什么你照着网上片段抄完却弹不出窗口。它适合两类人一类是接手遗留 MFC 项目、必须用原生控件交付的维护者另一类是需要在对话框里快速搭出可编辑表格、又不想引入整套第三方 UI 框架的开发者。下面按“先立住原理、再动手复现、最后排坑”的顺序拆开讲。2. 先搞懂 DataGrid 的绑定模型它和 ListCtrl 根本不是一类东西2.1 绑定型网格与自绘列表的本质区别很多人第一次用 DataGrid 翻车是因为拿 ListCtrl 的思维去套它。ListCtrl 是“我塞什么它就显示什么”数据在你自己手里DataGrid 是“我告诉你数据源在哪你自己去取”。这个差别决定了后面所有代码的写法。DataGrid 依赖的是 OLE DB 或 ADO 这套数据访问体系它通过一个数据源对象常见的是 ADO 的 Recordset拿到行集然后按列自动生成单元格。你不需要手动 InsertItem 一行行填而是把 Recordset 挂上去控件自己渲染。代价是数据源的字段类型、可更新性、游标类型都会直接影响控件行为。比如一个只进只读的游标你在单元格里敲字它也不会回写数据库。常见做法是用 ADO 的_ConnectionPtr打开连接_RecordsetPtr执行查询然后把 Recordset 赋给 DataGrid 的DataSource。这里有个容易忽略的点——DataGrid 认的是IDispatch接口不是任意对象所以 Recordset 必须是可被自动化调用的那种。2.2 控件注册为什么你的对话框里根本找不到它DataGrid 不是 MFC 自带的标准控件它来自一个 ActiveX 组件。你在对话框编辑器里找不到它是因为没注册。注册方式有两种一是用 regsvr32 注册对应的 ocx 文件二是在工程里通过“添加 ActiveX 控件”导入。我一般会先确认系统里有没有这个组件。打开一个管理员权限的命令行执行注册命令regsvr32 C:\Windows\SysWOW64\MSDATGRD.OCX这条命令把 32 位的 DataGrid 控件注册到系统。注意路径里的 SysWOW64 是给 32 位程序用的如果你的工程是 64 位需要换成 System32 下的对应文件而且很多老版本 ocx 根本没有 64 位版本这是后面会踩的坑之一。注册成功后在 Visual Studio 的资源视图里右键对话框选“插入 ActiveX 控件”列表里应该能看到它。插入后 VS 会自动生成包装类通常叫CMSDataGrid之类。这个包装类是你后面调用属性和方法的入口。参数说明regsvr32后面跟的是 ocx 的完整路径路径带空格要加引号。如果提示“模块加载失败”先检查文件是否存在、是否被杀软隔离、以及位数是否匹配。2.3 最小可运行工程从空对话框到显示一行数据光讲模型没用直接给一个能跑的最小流程。假设你已经建好一个基于对话框的 MFC 工程并且插入了 DataGrid 控件变量名叫m_grid。第一步在对话框初始化里打开数据库并取记录集// 在 OnInitDialog 中 CoInitialize(NULL); // ADO 需要 COM 环境 try { m_pConn.CreateInstance(__uuidof(Connection)); m_pConn-Open( LProviderMicrosoft.Jet.OLEDB.4.0;Data Sourcetest.mdb;, L, L, adConnectUnspecified); m_pRs.CreateInstance(__uuidof(Recordset)); m_pRs-Open( LSELECT id, name, amount FROM orders, m_pConn.GetInterfacePtr(), adOpenStatic, adLockOptimistic, adCmdText); // 关键一步把记录集挂到 DataGrid m_grid.SetRefDataSource(m_pRs.GetInterfacePtr()); m_grid.Refresh(); } catch (_com_error e) { AfxMessageBox(e.Description()); }逻辑说明CoInitialize是 ADO 的前置条件漏了它后面所有 COM 调用都会失败。Open的第三个参数是游标类型这里用adOpenStatic是为了让 DataGrid 能自由滚动如果用默认的只进游标控件滚动时会报错。SetRefDataSource是包装类对DataSource属性的封装传入的是 Recordset 的IDispatch指针。最后Refresh触发重绘。参数说明连接字符串里的 Provider 随数据库类型变Access 用 JetSQL Server 用 SQLOLEDB。游标类型和锁类型的组合决定了你能不能编辑单元格adLockOptimistic配合adOpenStatic是编辑场景下比较稳的组合。跑通这一步你应该能看到表格里出现三列数据。如果窗口一片空白先看Open有没有抛异常再看SetRefDataSource的返回值。3. 运行时控制列与行宽度、标题、增删改的实操写法3.1 列宽、列标题与可见性的动态设置数据绑上去之后列是自动生成的但默认列标题就是字段名宽度也不合适。这些都要在绑定之后手动调。// 设置列宽单位是 twips1 磅约等于 20 twips m_grid.SetColWidth(0, 800); m_grid.SetColWidth(1, 2000); // 修改列标题 m_grid.SetHeadText(L编号, 0); m_grid.SetHeadText(L客户名称, 1); // 隐藏某一列 m_grid.SetColVisible(2, FALSE);逻辑说明列索引从 0 开始顺序和 SELECT 里的字段顺序一致。SetColWidth的单位是 twips不是像素所以数值看起来偏大是正常的。SetHeadText只改显示不影响底层字段名。SetColVisible传 FALSE 隐藏但数据还在记录集里。参数说明twips 和像素的换算大致是 1 像素约等于 15 twips取决于 DPI。如果你想让列宽自适应内容DataGrid 本身没有直接的 AutoFit需要在绑定后遍历记录算最大宽度或者干脆给一个固定值。3.2 增删行为什么直接操作 Recordset 比操作控件靠谱DataGrid 本身不提供“插入一行”的按钮行的增删本质是对 Recordset 的操作控件只是跟着刷新。新增一行m_pRs-AddNew(); // 进入新行编辑状态 m_pRs-Fields-GetItem(Lname)-Value _variant_t(L新客户); m_pRs-Fields-GetItem(Lamount)-Value _variant_t(100.0); m_pRs-Update(); // 提交 m_grid.Refresh(); // 让控件重新拉数据删除当前行m_pRs-Delete(adAffectCurrent); m_pRs-MoveNext(); if (m_pRs-EndOfFile) m_pRs-MoveLast(); m_grid.Refresh();逻辑说明AddNew之后必须给字段赋值再Update否则会插入一条空记录。删除时如果删的是最后一行游标会失效所以要判断EndOfFile并回退。每次操作后调Refresh是为了让 DataGrid 重新读取记录集否则界面和底层数据会不一致。参数说明adAffectCurrent表示只删当前行。如果你的游标类型不支持删除Delete会抛异常这时候要回头检查Open时的锁类型。3.3 单元格编辑与回写锁类型决定成败让用户在单元格里直接改是 DataGrid 最常用的场景。但能不能改、改了能不能存回去取决于两个地方Recordset 的锁类型以及 DataGrid 的AllowUpdate属性。m_grid.SetAllowUpdate(TRUE); // 允许控件内编辑 m_grid.SetAllowAddNew(TRUE); // 允许在控件里新增逻辑说明这两个属性只是打开控件的编辑入口真正回写还是靠 Recordset 的Update。如果锁类型是adLockReadOnly你在单元格里敲完字一移开焦点就恢复原值看起来像“改了没生效”。参数说明AllowUpdate和AllowAddNew默认可能是 FALSE插入控件后要显式打开。另外如果字段是自增主键编辑时不要动它否则Update会失败。4. 避坑与排查五个让 DataGrid 当场翻车的典型问题4.1 现象对话框一打开就报“未注册类”原因控件没有正确注册或者工程位数和 ocx 位数不匹配。32 位工程用了 64 位系统里注册的 64 位控件或者反过来。解决确认工程平台Win32 还是 x64然后用对应位数的 regsvr32 注册对应位数的 ocx。如果系统里只有 32 位 ocx就把工程切成 Win32。注册后在资源编辑器里重新插入一次控件让 VS 重新生成包装类。4.2 现象数据绑上去了但表格是空的原因最常见的是SetRefDataSource传进去的指针不对或者 Recordset 已经关闭。另一个可能是Refresh没调用。解决在SetRefDataSource之后打印一下返回值确认非空。检查 Recordset 的State属性是不是adStateOpen。如果用的是_RecordsetPtr注意GetInterfacePtr()返回的指针在 Recordset 释放后会失效所以 Recordset 的生命周期要覆盖整个对话框。4.3 现象单元格能编辑但一保存就丢原因锁类型是只读或者Update没调用或者事务没提交。解决把Open的锁类型改成adLockOptimistic或adLockBatchOptimistic。如果是批量模式最后要调UpdateBatch。另外检查数据库文件是不是只读属性Access 数据库被设为只读时所有回写都会静默失败。4.4 现象滚动表格时程序卡死或崩溃原因游标类型用了只进游标adOpenForwardOnlyDataGrid 在滚动时需要前后移动游标只进游标不支持。解决把游标类型改成adOpenStatic或adOpenKeyset。如果数据量特别大adOpenStatic会把整个结果集拉到本地内存占用高这时候要考虑分页查询而不是一次性绑几万行。4.5 现象列标题乱码或显示成问号原因工程字符集和数据库字段编码不一致。MFC 工程有 Unicode 和多字节两套ADO 返回的字符串编码取决于 Provider。解决统一用 Unicode 工程连接字符串里不要混用窄字符。如果数据库是旧版 Access字段里的中文可能在多字节下正常Unicode 下需要确认 Provider 是否支持。实在不行在SetHeadText里手动指定宽字符标题绕开自动生成。5. 进阶技巧用分页查询把十万行数据塞进 DataGridDataGrid 直接绑全量数据在几千行时还能忍上万行就会明显卡顿因为adOpenStatic会把整个结果集缓到本地。我一般会用分页查询来绕开这个限制思路是每次只取一页翻页时重新查。先建一个分页查询的函数void CMyDlg::LoadPage(int pageIndex, int pageSize) { int start pageIndex * pageSize; CString sql; sql.Format( LSELECT TOP %d id, name, amount FROM orders LWHERE id NOT IN (SELECT TOP %d id FROM orders ORDER BY id) LORDER BY id, pageSize, start); m_pRs-Close(); m_pRs-Open(_bstr_t(sql), m_pConn.GetInterfacePtr(), adOpenStatic, adLockReadOnly, adCmdText); m_grid.SetRefDataSource(m_pRs.GetInterfacePtr()); m_grid.Refresh(); }逻辑说明这是 SQL Server 常见的分页写法用NOT IN跳过前 start 条。Access 不支持TOP嵌套时可以改用ORDER BY配合AbsolutePosition但效率差一些。每次翻页重新打开 Recordset然后重新绑定数据源。参数说明pageSize建议 200 到 500太小翻页频繁太大失去分页意义。pageIndex从 0 开始。翻页按钮里调用LoadPage并更新当前页码。配合两个按钮做上一页下一页void CMyDlg::OnBnClickedNext() { m_pageIndex; LoadPage(m_pageIndex, 300); } void CMyDlg::OnBnClickedPrev() { if (m_pageIndex 0) { m_pageIndex--; LoadPage(m_pageIndex, 300); } }这里有个细节分页模式下锁类型用adLockReadOnly就够了因为编辑场景通常不在分页浏览时发生。如果确实要边翻页边改就得把修改暂存到本地结构翻页前统一提交否则游标一关未提交的编辑就丢了。还有一个验证方法在LoadPage里加一句计时看每次查询耗时。如果超过 200 毫秒说明NOT IN的子查询在数据量大时退化严重这时候要换成基于游标键的分页比如记住上一页最后一行的 id用WHERE id lastId来取下一页。这个改法在 SQL Server 和 Access 上都适用代价是不能跳页只能顺序翻。我自己踩过的坑是早期图省事直接绑了全表测试时数据少没感觉上线后用户表里有八万行对话框一打开就假死十几秒。后来改成每页 300 行首屏加载降到一秒以内。这个习惯一直保留到现在——只要用 DataGrid先问数据量再决定绑全量还是分页。希望帮到你。本文还有配套的精品资源点击获取