ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

MFC DataGrid 控件用法详解:数据绑定、运行时控制与分页优化

MFC DataGrid 控件用法详解:数据绑定、运行时控制与分页优化

简介:这份资源是面向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( L"Provider=Microsoft.Jet.OLEDB.4.0;Data Source=test.mdb;", L"", L"", adConnectUnspecified); m_pRs.CreateInstance(__uuidof(Recordset)); m_pRs->Open( L"SELECT 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 用 Jet,SQL Server 用 SQLOLEDB。游标类型和锁类型的组合决定了你能不能编辑单元格,adLockOptimistic配合adOpenStatic是编辑场景下比较稳的组合。

跑通这一步,你应该能看到表格里出现三列数据。如果窗口一片空白,先看Open有没有抛异常,再看SetRefDataSource的返回值。

3. 运行时控制列与行:宽度、标题、增删改的实操写法

3.1 列宽、列标题与可见性的动态设置

数据绑上去之后,列是自动生成的,但默认列标题就是字段名,宽度也不合适。这些都要在绑定之后手动调。

// 设置列宽(单位是 twips,1 磅约等于 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(L"name")->Value = _variant_t(L"新客户"); m_pRs->Fields->GetItem(L"amount")->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 现象:滚动表格时程序卡死或崩溃

原因:游标类型用了只进游标(adOpenForwardOnly),DataGrid 在滚动时需要前后移动游标,只进游标不支持。

解决:把游标类型改成adOpenStatic或adOpenKeyset。如果数据量特别大,adOpenStatic会把整个结果集拉到本地,内存占用高,这时候要考虑分页查询,而不是一次性绑几万行。

4.5 现象:列标题乱码或显示成问号

原因:工程字符集和数据库字段编码不一致。MFC 工程有 Unicode 和多字节两套,ADO 返回的字符串编码取决于 Provider。

解决:统一用 Unicode 工程,连接字符串里不要混用窄字符。如果数据库是旧版 Access,字段里的中文可能在多字节下正常,Unicode 下需要确认 Provider 是否支持。实在不行,在SetHeadText里手动指定宽字符标题,绕开自动生成。

5. 进阶技巧:用分页查询把十万行数据塞进 DataGrid

DataGrid 直接绑全量数据,在几千行时还能忍,上万行就会明显卡顿,因为adOpenStatic会把整个结果集缓到本地。我一般会用分页查询来绕开这个限制,思路是:每次只取一页,翻页时重新查。

先建一个分页查询的函数:

void CMyDlg::LoadPage(int pageIndex, int pageSize) { int start = pageIndex * pageSize; CString sql; sql.Format( L"SELECT TOP %d id, name, amount FROM orders " L"WHERE id NOT IN (SELECT TOP %d id FROM orders ORDER BY id) " L"ORDER 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,先问数据量,再决定绑全量还是分页。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进