ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

MFC DataGrid 用法:ADO绑定、列格式化、排序与部署避坑

MFC DataGrid 用法:ADO绑定、列格式化、排序与部署避坑

简介:《MFC DataGrid控件用法详解》面向使用 VC/MFC 开发桌面程序、需要在对话框中快速呈现数据库表格数据的开发者,聚焦 DataGrid 控件从环境准备到常用功能落地的完整用法。压缩包内共 1 个 doc 文档,约 226KB,以图文结合的方式组织内容,便于按章节查阅。文档从新建对话框工程、插入 Microsoft DataGrid OLEDB 6.0 控件并选择 CDataGrid、CColumns、CColumn 三个封装类讲起,给出通过 ADO 封装类连接 Access 数据库、将 Recordset 设置为控件数据源以显示查询结果的代码;随后说明如何获取列对象并设置列宽、用 SetNumberFormat 将小于 1 的小数显示为百分数、用 SetRowHeight 实现多行显示,以及借助 HeadClick 消息处理实现排序等技巧。示例在 VC6.0 英文版、Windows XP 下调试通过,并附有关键代码与界面图示,可帮助读者理解控件接口与消息机制。已有 852 人学习,适合 MFC 初学者和需要快速集成网格数据显示的开发者参考。

1. 老对话框里塞进一张表:MFC DataGrid 用法到底解决什么问题

接手一个 VC6 时代的对话框程序,最麻烦的往往不是 SQL 怎么写,而是查询结果怎么让用户看得下去。MFC DataGrid 用法之所以值得单独拆一遍,是因为它把“显示”这件事交给了 ActiveX 控件:记录集一挂,列头、滚动条、单元格编辑都现成。它适合还在维护老 MFC 项目、需要快速把 ADO 记录集铺到对话框里的人;不适合追求跨平台或高 DPI 新界面的场景。下面按控件插入、数据绑定、列控制、格式化、排序和部署的顺序,把能跑通的代码和容易翻车的参数一次说清。

2. 把控件放进对话框:从插入 ActiveX 到生成 CDataGrid 类

2.1 插入控件与类向导生成三个包装类

在 VC6 的对话框资源里,右键空白处选 Insert ActiveX Control,找到 DataGrid 控件(OLEDB 版本),确定后把它拖到合适大小。控件 ID 可以保持默认,也可以改成IDC_DATAGRID1,后面代码统一用这个 ID。接着按 Ctrl+W 打开 ClassWizard,切到 Member Variables,选中这个控件 ID,Add Variable,类型选CDataGrid,变量名写m_ctrlDG。如果类型列表里没有CDataGrid,点 Add Class 里的 From ActiveX Control,按提示选控件,ClassWizard 会读控件的类型库,生成CDataGrid、CColumns、CColumn等包装类。

这些类不是 MFC 自带的,而是对控件 COM 接口的封装。生成之后,工程里会多出对应的头文件和实现文件,名字以实际生成为准,常见是DataGrid.h、DataGrid.cpp。对话框头文件里要包含它,否则编译会报CDataGrid未定义。包装类的方法名基本能望文生义,比如GetColumns、SetWidth、SetNumberFormat,但不同版本控件生成的方法名可能略有差异,以你工程里实际生成的头文件为准,不要照抄网上的函数名硬编。

// 对话框头文件:包含类向导生成的头文件 #include "DataGrid.h" // 文件名以实际生成为准 class CTestGridDlg : public CDialog { // ... public: CDataGrid m_ctrlDG; // DataGrid 控件对象 };

这段代码只做一件事:把控件变量声明成对话框成员。放成员而不是局部变量,是因为后面绑定数据源、取列对象都要在多个函数里访问它。参数上没有什么可调的,关键是头文件路径要进工程,类向导生成的 .cpp 也要加入 FileView,否则链接时找不到实现。

2.2 对话框成员变量与初始化顺序

控件变量建好后,要在DoDataExchange里做 DDX 绑定,让对话框创建时自动子类化控件。顺序上,COM 初始化要早于控件使用,控件子类化要早于取列对象,数据源绑定要早于列格式设置。很多“取不到列”“设置没反应”的问题,不是方法用错,而是顺序反了。

void CTestGridDlg::DoDataExchange(CDataExchange* pDX) { CDialog::DoDataExchange(pDX); // 把控件 ID 和成员变量绑定,对话框创建时完成子类化 DDX_Control(pDX, IDC_DATAGRID1, m_ctrlDG); } BOOL CTestGridDlg::OnInitDialog() { CDialog::OnInitDialog(); // 如果 App::InitInstance 里没有调用 AfxOleInit,这里补一次 // 注意:AfxOleInit 只能成功初始化一次,重复调用会返回 FALSE // if (!AfxOleInit()) return FALSE; // 此时控件已经子类化,可以取列、绑数据源 // BindData(); // 见第 3 章 // ApplyColumnStyle(); // 见第 4 章 return TRUE; }

DDX_Control的第三个参数是控件 ID,必须和资源里的 ID 一致;第二个参数是对话框指针,由框架传入。OnInitDialog里不要在CDialog::OnInitDialog()之前访问控件,那时控件还没创建。AfxOleInit一般放在CTestGridApp::InitInstance()里更稳,返回 FALSE 时直接结束程序,不要继续往下走。

提示:如果对话框是用Create动态创建的,DDX 仍然有效,但要确保Create返回成功后再访问控件句柄,否则GetSafeHwnd()为空。

2.3 工程配置与 OCX 注册

开发机上能插入控件,不代表客户机能跑。DataGrid 是 ActiveX 控件,客户机必须注册对应的 OCX,并且位数要匹配。VC6 编出来的是 32 位程序,就必须用 32 位的注册工具;用 64 位注册工具注册 32 位控件,表面上没报错,程序启动照样提示“未注册类”。

常见做法是把控件 OCX 和依赖的运行库一起放进安装包,安装时调用注册命令。注册命令本身不复杂,关键是路径和权限:

:: 以管理员身份运行 32 位注册工具,/s 表示静默 regsvr32 /s "你的安装目录\控件文件.ocx"

/s只是不弹成功提示,失败时仍要看系统事件或换用不带/s的方式排查。路径里有空格要加引号。客户机如果装了多个版本的控件,优先注册程序目录下的那一份,避免系统目录里的旧版本抢先被加载。发布前在干净虚拟机里走一遍安装、启动、查询、关闭,比在开发机上点一百次都管用。

检查项正确做法常见错误
程序位数VC6 工程按 32 位编译误编成 64 位或混用库
注册工具32 位 regsvr32用 64 位工具注册 32 位控件
控件位置随安装包分发并注册只拷 exe,不注册 OCX
权限安装时管理员权限普通用户下注册失败
依赖库一并打包客户机缺运行库,控件加载失败

3. 绑定数据源:ADO 记录集接到 SetRefDataSource 的完整链路

3.1 连接字符串与记录集打开

DataGrid 只负责显示,数据从哪来它不管。老项目里常见做法是用 ADO 封装类,比如CADODatabase、CADORecordset,把连接和记录集包一层。原文的写法是先开数据库,再把记录集对象和数据库对象关联,执行查询,最后把记录集指针交给控件。这里要注意,CADODatabase不是 MFC 自带类,如果你的工程没有这套封装,用原生 ADO 智能指针替代,逻辑一样。

// 假设工程里已有 ADO 封装类 CADODatabase / CADORecordset CADODatabase m_DBCn; // 数据库连接对象,建议放对话框成员 CADORecordset m_Rs; // 记录集对象,必须活到控件销毁 CString strProvider = _T("..."); // 填你机器上安装的 Access 驱动名 CString strDbPath = _T("note.mdb"); CString strConnection; strConnection.Format(_T("Provider=%s;Data Source=%s;"), strProvider, strDbPath); try { m_DBCn.Open((LPCTSTR)strConnection); // 打开数据库 m_Rs.SetDatabase(&m_DBCn); // 记录集关联连接 m_Rs.Open(_T("select * from test;")); // 执行查询 } catch (_com_error& e) { AfxMessageBox(e.Description()); // 把 COM 错误码翻译成人话 }

Provider填你机器上实际安装的 Access 驱动名,不要照抄别人环境里的字符串;Data Source可以是相对路径,但相对的是进程当前目录,客户机上容易跑飞,建议用绝对路径或程序目录拼接。m_Rs.Open的 SQL 不要带order by以外的花活,先把数据铺出来,再谈排序和格式化。异常捕获不要只写catch (...),_com_error能拿到Description(),排查连接字符串错误时省很多时间。

如果工程里没有封装类,用原生 ADO 的写法如下,效果等价:

#import "msado15.dll" no_namespace rename("EOF", "adoEOF") _ConnectionPtr spConn; _RecordsetPtr spRs; spConn.CreateInstance(__uuidof(Connection)); spRs.CreateInstance(__uuidof(Recordset)); spConn->Open(_bstr_t(strConnection), "", "", adConnectUnspecified); spRs->CursorLocation = adUseClient; // 需要排序时开客户端游标 spRs->Open(_bstr_t(_T("select * from test")), spConn.GetInterfacePtr(), adOpenStatic, adLockReadOnly, adCmdText);

CursorLocation设成adUseClient是为了后面用Sort属性排序;如果不排序,用默认的服务器端游标也行。adOpenStatic是静态游标,适合只读展示;adLockReadOnly避免误改数据。#import会把 ADO 类型库导入工程,生成的智能指针包装类在编译期展开,VC6 下要确保msado15.dll在系统路径里能找到。

3.2 SetRefDataSource 的指针类型与生命周期

绑定数据源的核心就一行:把记录集的接口指针传给SetRefDataSource。参数类型是LPUNKNOWN,实际传的是 ADO Recordset 的IUnknown接口。控件内部会QueryInterface到它需要的接口,所以强转本身不是问题,问题在生命周期。

// 把记录集挂到控件上 m_ctrlDG.SetRefDataSource((LPUNKNOWN)m_Rs.GetRecordset());

这行代码之后,控件并不拥有记录集对象,它只是引用。如果记录集是局部变量,函数返回时析构,控件手里就是悬空指针,界面要么空白,要么在点列头时直接崩。常见做法是把记录集声明成对话框成员,和控件同生命周期;刷新查询时先解绑旧数据源,再关旧记录集,最后开新记录集并重新绑定。

void CTestGridDlg::RefreshGrid() { // 先解绑,避免控件访问即将关闭的记录集 m_ctrlDG.SetRefDataSource(NULL); if (m_Rs.IsOpen()) m_Rs.Close(); m_Rs.Open(_T("select * from test;")); m_ctrlDG.SetRefDataSource((LPUNKNOWN)m_Rs.GetRecordset()); ApplyColumnStyle(); // 重新绑定后列对象会重建,格式要重新设置 }

SetRefDataSource(NULL)是解绑,不是清空显示那么简单,它告诉控件“别再引用旧记录集了”。IsOpen()的方法名以你的封装类为准,没有就换成GetRecordset()->State判断。重新Open之后列对象会重建,之前设置的列宽、格式会丢,所以ApplyColumnStyle()要再走一遍。这个顺序看起来啰嗦,但能避开大部分关闭时崩溃。

3.3 查询结果刷新与空结果处理

记录集打开成功不代表有数据。空结果时 DataGrid 显示一片空白,用户会以为程序卡了。常见做法是先判断记录数或adoEOF,空的时候给提示,或者至少把列头绑出来。原生 ADO 可以用spRs->RecordCount,客户端游标下这个值才准确;服务器端游标可能返回 -1。

if (m_Rs.GetRecordset()->adoEOF) { // 空结果:可以提示,也可以只显示列头 m_ctrlDG.SetRefDataSource((LPUNKNOWN)m_Rs.GetRecordset()); AfxMessageBox(_T("查询没有匹配记录。")); } else { m_ctrlDG.SetRefDataSource((LPUNKNOWN)m_Rs.GetRecordset()); ApplyColumnStyle(); }

adoEOF是#import时把EOF重命名后的名字,避免和 MFC 的EOF宏冲突。空结果也绑定一次,是为了让列头显示出来,用户至少知道表结构。数据量大时不要一次select *全拉,老项目里常见做法是加top N或按时间条件分页,DataGrid 本身不做分页,记录集太大滚动会明显卡顿。

连接参数作用注意点
Provider指定数据驱动以客户机实际安装为准
Data Source数据库文件路径建议绝对路径或程序目录拼接
CursorLocation游标位置排序用adUseClient
CursorType游标类型只读展示用静态游标
LockType锁定类型展示场景用只读锁

4. 列级控制与格式化:CColumns/CColumn 的常用参数

4.1 取列对象的正确姿势

DataGrid 的列控制要先拿列集,再从列集里取单列。GetColumns()返回CColumns,GetItem()的参数是VARIANT,不是int。直接传 0 可能编译不过,也可能运行时类型不匹配。索引从 0 开始,取列之前先确认控件已经绑定了数据源,否则列数为 0。

CColumns cols = m_ctrlDG.GetColumns(); VARIANT vt; VariantInit(&vt); vt.vt = VT_I4; vt.lVal = 0; // 第 0 列 CColumn col = cols.GetItem(vt); // 取列对象 VariantClear(&vt);

VariantInit和VariantClear要成对出现,vt的vt成员指定类型,lVal才是索引值。列索引越界时GetItem可能返回空对象,后面调SetWidth就会异常,所以取列前最好判断cols.GetCount(),方法名以生成的头文件为准。取到CColumn后,列宽、标题、对齐、格式化都在这上面做。

4.2 列宽、标题、对齐与只读

列宽的单位是 twips,1 英寸等于 1440 twips,96 DPI 下 1 像素约等于 15 twips。直接把像素值传给SetWidth,列会窄得看不见,这是很常见的翻车点。我一般写一个换算函数,按当前设备的 DPI 算,避免高 DPI 客户机上列宽离谱。

void CTestGridDlg::SetColumnWidthPixels(int nCol, int nPixels) { CColumns cols = m_ctrlDG.GetColumns(); if (nCol < 0 || nCol >= cols.GetCount()) return; VARIANT vt; VariantInit(&vt); vt.vt = VT_I4; vt.lVal = nCol; CColumn col = cols.GetItem(vt); CClientDC dc(this); int dpi = dc.GetDeviceCaps(LOGPIXELSX); // 96 或 120 等 long twips = (long)(nPixels * 1440.0 / dpi); col.SetWidth(twips); // 列宽,单位 twips VariantClear(&vt); }

nPixels是你期望的屏幕像素宽度,dpi从设备上下文取,1440.0是每英寸 twips 数。col.SetCaption(_T("金额"))可以改列头文字,col.SetAlignment(1)可以设对齐,常见取值是 0 左、1 右、2 居中,具体以控件类型库为准。只读用col.SetLocked(TRUE)锁单列,或者m_ctrlDG.SetAllowUpdate(FALSE)锁整个控件。锁整表更省事,但用户连复制都不方便时,就只锁关键列。

列属性方法示例说明
列宽col.SetWidth(1500)单位 twips,不是像素
列头col.SetCaption(_T("金额"))只改显示,不改字段名
对齐col.SetAlignment(1)0 左、1 右、2 居中
只读col.SetLocked(TRUE)单列锁定
整表只读m_ctrlDG.SetAllowUpdate(FALSE)防止误编辑

4.3 SetNumberFormat 的格式串与百分比场景

SetNumberFormat只影响显示,不改记录集里的值。百分比格式最典型:数据库里存 0.123,格式串0.0%显示 12.3%;如果数据库里存的是 12.3,同一个格式串会显示 1230.0%,这是血泪经验里最常见的一种“格式不对”。设之前先确认字段存的是小数还是百分数,别在显示层硬拧。

CColumns cols = m_ctrlDG.GetColumns(); VARIANT vt; VariantInit(&vt); vt.vt = VT_I4; vt.lVal = 2; // 假设第 2 列是比率 CColumn col = cols.GetItem(vt); col.SetNumberFormat(_T("0.0%")); // 0.1234 -> 12.3% // col.SetNumberFormat(_T("#,##0.00")); // 千分位,两位小数 // col.SetNumberFormat(_T("0.00;-0.00;0.00")); // 正数;负数;零 VariantClear(&vt);

格式串的段用分号隔开,依次是正数、负数、零;只写一段时所有值共用。0.0%会保留一位小数并乘 100,#,##0.00适合金额。空值一般显示空白,如果业务要求显示 0,在 SQL 里用ISNULL处理,别指望格式串把 NULL 变成 0。客户机区域设置不同,小数点符号可能变化,要统一显示就在格式串里写死,或者在 SQL 层格式化好再给控件。

注意:格式串只认数值列。如果列本身是文本类型,SetNumberFormat不会报错,但也不会生效,先看记录集字段类型再调格式。

4.4 多行显示与行高

多行显示不是自动换行,得行高和列宽配合。SetRowHeight的单位同样是 twips,原文里传 20 如果按 twips 算只有不到 1.5 像素,肯定看不到效果;如果包装类把它当像素处理,20 又偏小。稳妥做法是按 DPI 换算,先设一个肉眼可见的行高,再看列里有没有换行属性。

// 行高约 30 像素,按 96 DPI 换算约 450 twips CClientDC dc(this); int dpi = dc.GetDeviceCaps(LOGPIXELSX); long rowTwips = (long)(30 * 1440.0 / dpi); m_ctrlDG.SetRowHeight(rowTwips); // 如果生成的 CColumn 类里有 WrapText 属性,再打开列内换行 // col.SetWrapText(TRUE);

SetRowHeight影响所有行,行高太大一屏显示不了几条记录,太小多行文本会截断。列内换行要看控件版本和包装类是否暴露WrapText,有就设TRUE,没有就只加大行高和列宽,不要硬编一个不存在的方法名。多行显示更适合备注、地址这类短文本,长文本还是双击弹窗看更靠谱。

5. 避坑与排查:DataGrid 在 VC6 里最容易翻车的五个点

5.1 控件类生成失败与编译报错

现象是 ClassWizard 里找不到CDataGrid,或者编译时报CDataGrid未定义、IDC_DATAGRID1未声明。原因通常有三类:控件没注册,ClassWizard 读不到类型库;插入的不是同一个版本的控件;生成的类文件没加入工程,或者对话框头文件没包含生成的头文件。解决时先用 32 位注册工具确认控件已注册,再重新插入控件、重新生成类;生成后检查 FileView 里有没有对应的 .cpp,头文件里有没有#include。清理工程重新编译一次,很多“明明生成了却找不到”是中间文件缓存造成的。

5.2 运行时空白、崩溃与数据源失效

现象是对话框打开后网格空白,点列头崩溃,或者关闭对话框时崩在控件析构里。原因多半是记录集生命周期不对:SetRefDataSource传了局部记录集的指针,函数一返回对象就没了;或者记录集没打开就绑定;或者关闭时先关了记录集,控件还在访问。解决思路是把记录集放成对话框成员,绑定前判断IsOpen(),关闭时按“先SetRefDataSource(NULL)解绑,再关记录集,最后关连接”的顺序走。崩溃在关闭阶段时,重点查OnDestroy里的释放顺序,不要在控件已经销毁后再访问它。

5.3 列宽、格式化和排序的失效场景

现象是SetWidth调了没反应,SetNumberFormat设了不生效,点列头排序后列格式全丢。列宽没反应,先查单位是不是把像素当 twips 传了,再查列索引有没有越界,最后看是不是绑定数据源之前就设了列属性。格式化不生效,先确认列是数值类型,再看格式串和数据的实际含义是否匹配,百分比最容易把 12.3 再乘 100。排序后格式丢失,是因为重新查询或重新绑定导致列对象重建,解决办法是排序后重新调用ApplyColumnStyle(),把列宽、标题、格式再套一遍。

5.4 排序事件重复触发与游标报错

现象是点一次列头排两次,或者排序时抛 COM 异常。重复触发通常是ON_EVENT映射加了两次,或者对话框里又用OnNotify手动处理了一遍;检查消息映射里同一个事件只留一条。排序报错则多半是记录集用了服务器端游标,Sort属性不支持,把CursorLocation改成adUseClient再试。排序字段名不要直接用列头文字,列头可能是中文,SQL 里字段名是英文,维护一个字段名数组按列号映射更稳。

5.5 发布到客户机提示控件未注册

开发机跑得好好的,客户机一启动就提示“未注册类”或控件创建失败。原因是安装包只拷了 exe,没注册 OCX,或者注册的是 64 位工具,或者客户机上已有旧版本控件抢先加载。解决时把控件和依赖库一起打包,安装脚本里用 32 位注册工具注册,注册前先判断目标文件是否存在。测试时用干净虚拟机,不要用装过开发工具的机器,否则系统目录里已有控件,测不出缺失问题。

6. 排序与交付前的验证:HeadClick 排序、释放顺序和一套自检清单

6.1 用 HeadClick 做排序:字段名映射比列号靠谱

列头点击排序用控件的HeadClick事件。不要手抄事件映射里的 DISPID,不同控件版本可能不一样,用 ClassWizard 在 Message Maps 里选控件事件,让它生成ON_EVENT映射。处理函数拿到的是列号,排序时要把列号映射成数据库字段名,不能直接拿列头文字拼 SQL。

// 对话框头文件 afx_msg void OnHeadClickDataGrid(long Col); // ClassWizard 生成的映射形如: // ON_EVENT(CTestGridDlg, IDC_DATAGRID1, <DISPID>, OnHeadClickDataGrid, VTS_I4) void CTestGridDlg::OnHeadClickDataGrid(long Col) <p> <a href="https://download.csdn.net/download/shuzu/1694838" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>
返回列表