简介:面向使用 VC/MFC 开发桌面数据库程序的初中级开发者,这份文档围绕 DataGrid 控件的实际用法展开,重点解决查询结果展示、列显示控制、数据格式化与交互事件处理等常见问题。包内共 1 个 doc 文件,约 226KB,内容以图文说明配合 C++ 代码片段组织,便于在 VC6.0 工程中对照查阅。文档覆盖将 Recordset 设为数据源显示查询结果、通过 CColumns/CColumn 设置列宽、用 SetNumberFormat 输出百分数、调整行高实现多行显示,以及利用 HeadClick 消息处理排序等要点。已有 852 人学习,适合需要快速上手 MFC DataGrid、在现有项目中完成数据网格展示与基础定制的读者参考。
1. 老项目里那块灰扑扑的网格:DataGrid 到底解决什么问题
维护一套老设备点检工具时,MFC DataGrid 用法是绕不过去的一关:对话框上那块网格要显示从 Access 查出来的几十行记录,还要按列宽对齐、把小数值显示成百分数、点列头排个序。这套组合(对话框 + ADO 查询 + ActiveX 网格)在 VC6 时代的存量工程里极其常见,DataGrid 是系统里现成可用的 OLEDB 网格控件,绑记录集一行代码,列级属性通过 CColumn 就能改,省掉的是自绘表格、滚动条、单元格编辑这几千行。
它不适合几十万行的虚拟列表,也不适合要自定义单元格绘制的新界面,但在「查询结果展示 + 少量编辑」这类需求上,它是成本最低的方案。这篇笔记写给两类人:还在维护 MFC 存量工程、需要把网格真正跑起来的同行,以及接手别人代码、看到 CDataGrid / CColumns / CColumn 三个封装类不知道从哪下手的人。
2. 控件插入与封装类生成:CDataGrid、CColumns、CColumn 从哪来
2.1 三个容易混淆的网格控件,选错一次要重做界面
资源编辑器里那个「插入 ActiveX 控件」的列表很长,光看图标和名字,DataGrid、DBGrid、MSHFlexGrid 三个长得差不多,但底层完全不是一回事。选错之后最典型的症状是:代码编译能过,运行时报类型不匹配,或者绑定之后一片空白。
区别在于绑定对象。DataGrid(OLEDB 版本)绑的是 ADO 的 Recordset,走 OLEDB 提供方;DBGrid 绑的是 DAO/RDO 的 Recordset,是更早一代的产物。两者的 OCX 文件、CLSID、生成出来的封装类名全都不一样,DataGrid 生成CDataGrid加CColumns、CColumn,DBGrid 生成的是CDBGrid,没有列集合那套东西。如果工程里连的是 ADO,却拖了一个 DBGrid 上去,后面所有SetRefDataSource的调用都是在错误的对象上做类型转换。
判别方法不复杂:在插入控件对话框里选中某一项,对话框下方会显示对应的类型库路径和类名,认准 CLSID 后面那串 OCX 文件名,再确认一遍控件标题里带的是 OLEDB 还是不带。这一步花三十秒,能省掉后面半天的排查。
至于 MSHFlexGrid,它是纯展示网格,没有记录集绑定能力,数据要自己用TextMatrix(行, 列)一格一格填。它的强项是合并单元格、复杂表头、按行着色这类排版需求。选型上我给一个简单判断:数据来自数据库且需要用户编辑,用 DataGrid;数据是计算出来的结果、只读展示,用 MSHFlexGrid;新项目要自定义绘制,两个都别用。
2.2 插入控件到生成封装类的完整动作
先把对话框资源打开,右键空白处选插入 ActiveX 控件,在列表里找到 DataGrid 那一项,确定后把它拖到对话框上合适的位置,改个能看懂的 ID,比如IDC_DATAGRID1。控件默认 ID 是一长串数字,不改的话后面写代码全是IDC_DATAGRID1和IDC_DATAGRID2分不清。
接下来是生成封装类,这是很多人卡住的地方。按 Ctrl+W 打开 ClassWizard,切到 Member Variables 页,类选对话框类,选中刚才那个控件 ID,点 Add Variable。名字填m_ctrlDG,Category 只有 Control 一个选项,Variable type 下拉里此时会出现CDataGrid,选中它,确定。
确定之后 ClassWizard 会做两件事:在对话框类里加一个CDataGrid m_ctrlDG成员,同时把控件类型库里所有的 coclass 都生成包装类,落到工程目录下新出现的几个文件里。CColumns和CColumn就是这个时候一起生成出来的,不是谁手动写的。
提示:如果 Variable type 下拉里没有
CDataGrid,只有CWnd,说明控件的类型库没有正确注册,先解决 OCX 注册问题,别硬选 CWnd 往下走。
这三个类的本质是 MFC 对控件 dispinterface 的包装,内部靠InvokeHelper按 DISPID 调用。好处是使用的时候几乎感知不到 COM 的存在,坏处是源码不可见,方法名看不太懂就只能去翻对象浏览器。我的习惯是生成完之后先把CColumn那个头文件从头到尾扫一遍,方法名基本都是自解释的,SetWidth、SetCaption、SetNumberFormat、SetAlignment,扫一遍就知道这个控件能改哪些属性,比查文档快。
2.3 工程侧三处必改项:容器支持、OCX 注册、控件 ID
控件拖上去了,封装类也生成了,程序一跑还是一块空白,这是最高频的第一道坎。原因通常出在三个地方,按出现概率从高到低排:没开 ActiveX 容器支持、OCX 没注册、控件创建失败但没有报错。
第一项是应用类的InitInstance。MFC 默认生成的工程里没有AfxEnableControlContainer()这一句,缺了它,对话框上的 ActiveX 控件根本不会被创建,控件区域就是一块灰色的对话框背景,GetDlgItem(IDC_DATAGRID1)返回 NULL,后面的绑定代码全部落空。加上这一句就好,位置放在标准初始化之前:
BOOL CTestGridApp::InitInstance() { // ActiveX 控件容器支持:少了这一句,对话框上的 DataGrid 创建不出来, // 表现为一块空白区域,GetDlgItem 返回 NULL,绑定代码静默失效 AfxEnableControlContainer(); // 标准初始化(省略) CTestGridDlg dlg; m_pMainWnd = &dlg; dlg.DoModal(); return FALSE; }参数没什么可调的,这个函数没有入参,作用就是给进程装一个控件容器。要注意的是它必须在任何含 ActiveX 控件的对话框DoModal之前调用,放到InitInstance开头最保险。
第二项是注册。开发机装了开发环境通常自带注册,但客户机器上不一定有。手动注册用regsvr32,注意 64 位系统上 32 位 OCX 要放对目录:
:: 32 位系统 regsvr32 "C:\Windows\System32\MSDATGRD.OCX" :: 64 位系统上注册 32 位控件,走 SysWOW64 regsvr32 "C:\Windows\SysWOW64\MSDATGRD.OCX"执行成功会弹一个注册成功的提示框。如果报「模块加载失败」或者「找不到入口点」,八成是路径写错或者文件名对应的版本不对,去系统目录里按文件名搜一下再确认。打包发布的时候这一句要写进安装脚本,别指望客户自己注册。
第三项容易被忽略:控件的创建时机。如果在OnInitDialog里调了CDialog::OnInitDialog()之前就去访问m_ctrlDG,控件还没子类化,所有属性设置都会被丢掉,而且不报错。所以列宽、格式这些设置统一放在CDialog::OnInitDialog()之后,顺序在 3.3 节里展开。
3. 绑定 ADO 记录集:SetRefDataSource 的三个前置条件
3.1 连接串、游标位置与 msado15.dll 的引入
DataGrid 只认 ADO 的 Recordset,中间用什么封装都行,底层必须是ADODB::_Recordset。我一般用一套轻量的 ADO 封装类(常见名字是CADODatabase/CADORecordset),把连接、打开、关闭这些样板代码收进去,工程里少写几百行 try/catch。没有封装也不影响,直接用_ConnectionPtr和_RecordsetPtr一样跑。
引入 ADO 类型库,主流做法是#import,注意那个rename("EOF", "adoEOF"),不加的话EOF会和标准库的宏撞名,报一堆莫名其妙的语法错误:
// stdafx.h 或封装类的头文件里 #import "C:\Program Files\Common Files\System\ado\msado15.dll" \ no_namespace rename("EOF", "adoEOF")连接串照抄固定写法就行,Access 库改路径即可:
CString strConn; strConn.Format(_T("Provider=Microsoft.Jet.OLEDB.4.0;Data Source=%s"), _T("note.mdb"));这里有个参数级别的坑:相对路径是按进程当前工作目录解析的,不是按 exe 所在目录。双击运行和在 IDE 里调试,工作目录可能不一样,所以经常出现「IDE 里好好的,拷到别的机器就报找不到库」。稳妥做法是启动时用GetModuleFileName拼出 exe 所在目录,再拼文件名。
更重要的是游标位置。默认情况下 ADO 用的是服务端游标,这种游标下RecordCount返回 -1,Sort属性不生效,AbsolutePosition也不可用。DataGrid 本身能显示,但一旦要做排序或者行数统计就抓瞎。所以在打开记录集之前,把连接对象的游标位置改成客户端:
// 必须在记录集打开之前设置,对 Connection 设置会影响它派生出的记录集 m_DBCn.GetConnection()->CursorLocation = adUseClient; m_Rs.SetDatabase(&m_DBCn); m_Rs.Open(_T("SELECT * FROM test"));参数说明:adUseClient是CursorLocationEnum的枚举值(值为 3),把结果集整体拉到本地,代价是数据量大时内存占用上升。几十行到几千行的报表场景完全没问题,几十万行就别这么干了。
3.2 SetRefDataSource 的参数其实是一个 IUnknown*
绑定这一行代码看起来最简单,实际上是最容易出事的地方:
m_ctrlDG.SetRefDataSource((LPUNKNOWN)m_Rs.GetRecordset());参数类型是LPUNKNOWN,也就是IUnknown*。而GetRecordset()返回的是ADODB::_RecordsetPtr,本质是一个IDispatch*。这里做的是向上转型,IDispatch继承自IUnknown,所以这个转换是安全的,控件内部会自己QueryInterface出需要的接口。
要理解的一点是:这一步没有拷贝数据,控件拿到的只是一个指针,它变成数据和控件之间的桥。控件在需要绘制某个单元格时,会通过这个接口去问记录集要值。这个语义决定了后面所有的生命周期问题——记录集没了,控件就悬空了。
函数的返回值是void,绑定失败不会有任何提示,界面就是一片空白。这是最难受的地方,排查基本靠排除法:先确认记录集自己有没有数据(m_Rs.GetRecordset()->adoEOF是否为真),再确认控件指针是否有效,最后确认是不是选错了控件类型。
3.3 记录集的生命周期:谁持有、什么时候解绑
三个成员变量的作用域,直接决定程序会不会在某些操作后随机崩溃。我见过的写法里,最常见的错误是把CADORecordset定义在某个按钮响应函数里,绑完就返回,函数栈一退,记录集析构,控件手里的指针变成野指针。表现是界面看着正常,鼠标一划过网格或者窗口一重绘就崩,属于典型的黑匣子问题。
正确的做法是把连接、记录集、控件全部挂成对话框类的成员:
// TestGridDlg.h CADODatabase m_DBCn; // 连接对象 CADORecordset m_Rs; // 记录集,生命周期必须覆盖控件的显示期 CDataGrid m_ctrlDG; // 控件,由 ClassWizard 生成 int m_nSortCol; // 当前排序列,-1 表示未排 BOOL m_bAsc; // 升降序标志初始化顺序也有讲究,必须在CDialog::OnInitDialog()之后,因为控件子类化在那一步完成:
BOOL CTestGridDlg::OnInitDialog() { CDialog::OnInitDialog(); // 先走完 DDX,控件在这里完成子类化 m_DBCn.GetConnection()->CursorLocation = adUseClient; // 见 3.1 CString strConn; strConn.Format(_T("Provider=Microsoft.Jet.OLEDB.4.0;Data Source=%s"), _T("note.mdb")); if (!m_DBCn.Open(strConn)) { AfxMessageBox(_T("数据库打开失败,检查 note.mdb 是否在工作目录")); return TRUE; } m_Rs.SetDatabase(&m_DBCn); if (!m_Rs.Open(_T("SELECT * FROM test"))) { AfxMessageBox(_T("查询失败")); return TRUE; } m_ctrlDG.SetRefDataSource((LPUNKNOWN)m_Rs.GetRecordset()); m_nSortCol = -1; m_bAsc = TRUE; return TRUE; }再说重新查询。用户改了筛选条件要重新查一次,很多人直接m_Rs.Close()然后重新Open,再SetRefDataSource。中间那一小段时间里,控件还持有着已经关闭的记录集,运气不好就崩。我的做法是先解绑再关:
void CTestGridDlg::Requery(const CString& strSql) { m_ctrlDG.SetRefDataSource(NULL); // 先解绑,切断控件对旧记录集的引用 if (m_Rs.IsOpen()) m_Rs.Close(); // 这时候再关才安全 if (!m_Rs.Open(strSql)) return; m_ctrlDG.SetRefDataSource((LPUNKNOWN)m_Rs.GetRecordset()); }SetRefDataSource(NULL)传空指针就是解绑,这个用法知道的人不算多,但它是避免崩溃最省事的一招。
4. 列级控制:GetItem 取列、twips 列宽与格式化
4.1 从列集合里取到 CColumn
数据绑上之后,下一件事通常是把列宽调顺眼、把列头改成中文。控件本身不会给你一个「列对象数组」,要按集合的方式取:先拿列集合,再从集合里按索引取列。
// 取第 0 列 VARIANT vt; ::VariantInit(&vt); // 必须初始化,vt 的初值是 VT_EMPTY vt.vt = VT_I4; // 告诉对方这次传的是整数索引 vt.lVal = 0; // 列索引,从 0 开始 CColumns cols = m_ctrlDG.GetColumns(); if (cols.GetCount() > 0) { CColumn col = cols.GetItem(vt); col.SetWidth(1400.0f); // 单位是 twips,见 4.2 col.SetCaption(_T("检测值")); // 列头文本,会覆盖掉字段名 col.SetAlignment(2); // 0 左 / 1 右 / 2 居中 col.SetNumberFormat(_T("0.0%")); // 格式化,见 4.3 } ::VariantClear(&vt);逻辑说明:GetColumns()返回一个CColumns对象,它是控件列集合的包装;GetItem接受一个VARIANT,既可以传整数索引,也可以传VT_BSTR形式的列名(比如_T("Value")),传列名的写法在字段顺序可能变动的场景下更稳。
参数上最容易翻车的是那个VARIANT。不做VariantInit也不赋值vt和lVal,GetItem会直接抛_com_error,错误码是DISP_E_TYPEMISMATCH,翻译过来就是类型不匹配,但错误信息里不会告诉你是 VARIANT 没赋值。还有一处:vt.lVal用的是long,别塞short或者字符。
索引从 0 开始,和数据库字段的物理顺序一致。要注意这是「当前可见列」的顺序,如果中途用 SQL 换了字段列表,索引含义会跟着变,所以固定列序的场景建议用列名取,别用索引。
4.2 列宽、列头与对齐:twips 换算
SetWidth的单位是 twips(缇),不是像素,也不是字符数。1 twip 等于 1/1440 英寸,约 0.0176 毫米。这个单位在 VB 和早期 COM 控件里很常见,但对习惯了像素的人很不直观。
常用的换算关系记一下:1400 twips 大约 2.47 厘米,567 twips 约等于 1 厘米,一个 9pt 的汉字宽度大致在 200 到 240 twips 之间,一个数字字符大约 100 twips。所以我一般按「字符数 × 200」估一个初值,再用GetWidth回读校准。
原文里给的多行显示示例是SetRowHeight(20),这里我得提醒一句:20 twips 约等于 0.035 厘米,行高会被压成一条线,肉眼基本看不到内容。合理取值在 300 左右(约 0.53 厘米),或者干脆传 -1 让控件按字体自动算。这种单位问题不看文档很难发现,因为代码不报错,只是显示不对。
几个常用属性列一下:
| 属性 / 方法 | 作用 | 单位或取值 | 备注 |
|---|---|---|---|
SetWidth | 单列宽度 | twips | 建议 800~3000 |
SetRowHeight | 行高 | twips | -1 为自动 |
SetCaption | 列头文本 | 字符串 | 会覆盖字段名 |
SetAlignment | 单元格对齐 | 0 左 / 1 右 / 2 中 | 数值型一般右对齐 |
SetNumberFormat | 显示格式 | 格式串 | 只影响显示 |
DefColWidth | 默认列宽 | twips | 未单独设过的列生效 |
列宽设置有个前提:控件必须已经有数据源。没有绑定记录集的时候,列集合是空的,GetCount()返回 0,所有设置静默失败。所以顺序永远是先绑数据、再调列宽,不能反过来。
4.3 SetNumberFormat 的格式串怎么写
数值格式化是这套控件少有的、用起来很舒服的功能。原理是把格式串交给控件内部的格式化引擎,绘制单元格时按格式输出,底层数据一个字节都不动。
// 小于 1 的小数显示成百分数:0.1234 -> 12.3% col.SetNumberFormat(_T("0.0%")); // 金额:保留两位,带千分位 col.SetNumberFormat(_T("#,##0.00")); // 日期:以文本形式存的日期字段也能按此格式解析 col.SetNumberFormat(_T("yyyy-mm-dd")); // 固定两位小数,不做千分位 col.SetNumberFormat(_T("0.00"));格式串的语法和常见的数值格式化规则一致:0表示位数不足补零,#表示有值才显示,%会自动乘 100 并加百分号,逗号是千分位分隔符。理解这几个符号就够用了,剩下的组合试两次就能出效果。
这里有两个必须知道的边界。第一个是NumberFormat只影响显示,不做四舍五入回写。用户在单元格里输入12.3,如果这列格式是0.0%,控件显示1230.0%,写回记录集的值仍是 12.3,取值时别拿显示文本去做计算。第二个是格式串对文本型字段无效,Access 里字段类型如果定义成文本,塞一个数值进去,格式化不生效,看起来像是「设了没用」。遇到这种情况先查字段类型。
还有个实际场景:百分比列在用户可编辑时要格外小心。用户看到的是 12.3%,直接编辑成 15%,控件按当前格式解析,可能得到 1500% 而不是 15%。我的处理是把格式化列设成只读,需要编辑的列不加格式,靠旁边的计算列展示百分比。
4.4 行高、自动换行与只读属性的写法
想把备注类字段做成多行显示,需要两个属性一起开:行高要够,换行要开。行高前面说过用 twips,换行属性名是WrapText,是布尔值。
问题在于 ClassWizard 生成的封装类不一定把每个属性都包装成方法。我遇到的版本里,列级方法包装得比较全,控件级的一些布尔属性就要自己想办法。这里给一个按属性名调用的通用写法,绕开封装类的覆盖不全:
// 按属性名给 ActiveX 控件设属性,适用于封装类没暴露的属性 bool SetCtrlPropByName(CWnd* pCtrl, LPCTSTR lpszProp, const VARIANT& vVal) { LPUNKNOWN pUnk = pCtrl->GetControlUnknown(); // 拿到控件的 IUnknown if (pUnk == NULL) return false; CComPtr<IDispatch> spDisp; if (FAILED(pUnk->QueryInterface(IID_IDispatch, (void**)&spDisp)) || spDisp == NULL) return false; CString strName(lpszProp); LPOLESTR szName = strName.GetBuffer(0); DISPID dispid = 0; HRESULT hr = spDisp->GetIDsOfNames(IID_NULL, &szName, 1, LOCALE_USER_DEFAULT, &dispid); strName.ReleaseBuffer(); if (FAILED(hr)) return false; // 属性名拼错就是这一步失败 DISPPARAMS dp = { const_cast<VARIANT*>(&vVal), NULL, 1, 0 }; return SUCCEEDED(spDisp->Invoke(dispid, IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_PROPERTYPUT, &dp, NULL, NULL, NULL)); }用法:
VARIANT b; ::VariantInit(&b); b.vt = VT_BOOL; b.boolVal = VARIANT_TRUE; SetCtrlPropByName(&m_ctrlDG, _T("WrapText"), b); // 开启自动换行 ::VariantClear(&b);逻辑说明:GetControlUnknown()是 CWnd 提供的成员,对 ActiveX 控件返回其IUnknown;GetIDsOfNames按名字查 DISPID,属性名不区分大小写;Invoke带DISPATCH_PROPERTYPUT标志执行赋值。参数上唯一要留意的是DISPPARAMS里的命名参数那个字段给 NULL、参数个数给 1,写反了会返回DISP_E_BADPARAMCOUNT。
只读控制同样是布尔属性,AllowUpdate、AllowAddNew、AllowDelete三个开关,报表场景一般把新增和删除关掉,只留更新或者全关。全关之后控件的行为和一个只读表格没区别,但滚动、选择、复制仍然可用,比 MSHFlexGrid 省事。
5. 列头排序与排查清单:HeadClick 背后的五类翻车现场
5.1 HeadClick 消息的接法与排序字段映射
DataGrid 自带一个HeadClick事件,用户点列头时触发,参数是short类型的列索引。接这个事件不用手写消息映射,ClassWizard 的 Message Maps 页里,类选对话框类,对象选控件 ID,消息列表里就能看到HeadClick,Add Function 之后 MFC 会生成一个形如OnHeadClickDatagrid1(short ColIndex)的处理函数。
拿到列索引之后要解决一个问题:控件不直接告诉你这列对应数据库里哪个字段。列头文本和字段名在你调过SetCaption之后就不一样了。我一般维护一张固定的映射表,列序和 SQL 里的字段顺序保持一致:
void CTestGridDlg::OnHeadClickDatagrid1(short ColIndex) { // 列序 -> 字段名的映射,必须与 SELECT 语句里的字段顺序一致 static const TCHAR* kFields[] = { _T("ID"), _T("Name"), _T("Value") }; if (ColIndex < 0 || ColIndex >= (short)(sizeof(kFields) / sizeof(kFields[0]))) return; // 同列再点一次切换升降序,换列则重置为升序 if (m_nSortCol == ColIndex) m_bAsc = !m_bAsc; else { m_nSortCol = ColIndex; m_bAsc = TRUE; } CString strSort; strSort.Format(_T("%s %s"), kFields[ColIndex], m_bAsc ? _T("ASC") : _T("DESC")); try { _RecordsetPtr spRs = m_Rs.GetRecordset(); spRs->Sort = _variant_t((LPCTSTR)strSort); // 前提是客户端游标 } catch (_com_error& e) { AfxMessageBox(e.Description()); } }参数说明:ColIndex从 0 开始,与GetItem的索引一致;Sort属性接受的字符串格式是「字段名 方向」,多字段排序用逗号分隔,例如Name ASC, ID DESC。这段代码依赖两个前提,缺一不可:游标位置是adUseClient(服务端游标下Sort直接抛异常),以及GetRecordset()返回的记录集还活着。
映射表这种做法有个隐患:SQL 一旦改了字段顺序,映射表必须同步改,忘了就是点 A 列按 B 字段排序。所以我在Requery里加了一个断言,比较GetColumns().GetCount()和映射表长度,不一致就在 Debug 版里直接弹框提醒。
5.2 Sort 属性与重发 SQL:两条路线的取舍
排序有两条路,各有各的适用范围,选错了会在数据量上来之后暴露问题。
| 路线 | 实现方式 | 优点 | 限制 |
|---|---|---|---|
| Sort 属性 | 对本地记录集重排 | 不用重新查库,响应快,能保持当前行位置 | 必须客户端游标,数据全在内存 |
| 重发 SQL | 拼ORDER BY再查一次 | 支持大数据量、支持复杂排序规则 | 每次点击都走一次数据库,慢 |
几百行的配置表、设备台账,直接用 Sort 属性最舒服,点击是瞬时的。上万行的表,或者 SQL 里本来就有复杂连接和过滤条件的,重发 SQL 更稳,把ORDER BY拼进去再走一遍Requery就行,注意字段名要来自白名单,别拿列头文本直接拼进 SQL 字符串。
还有一个折中方案:第一次查询时一次性把数据拉到本地,后面所有排序都在本地做,只在筛选条件变化时才重新查库。设备点检这类场景数据量本来就小,这套做法体验最好。
排序之后要注意一件事:控件的当前行位置会重置,如果你在别处保存了行号(比如用来做删除定位),排序后行号对应的已经是另一条记录了。解决办法是用主键定位,别用行号。
5.3 排查清单:五条现象、原因与处置
下面这五条是我在不同工程里都遇到过的,按出现频率排,每条按现象、原因、处置写。
第一条,界面一片空白,控件区域是对话框背景色。现象是程序能跑,对话框上该有网格的地方什么都没有,代码里GetDlgItem返回 NULL。原因是InitInstance里没有调AfxEnableControlContainer(),ActiveX 容器支持没开,控件根本没被创建。处置是加上那一句,重新编译。如果加了还是空白,接着查 OCX 注册,用regsvr32手动注册一遍看提示。
第二条,列宽和格式设置全部不生效,代码不报错。现象是SetWidth(1400)调了,界面纹丝不动。原因有两个方向:一是设置代码写在了CDialog::OnInitDialog()之前,控件还没子类化,属性调用被丢弃;二是记录集还没绑,列集合是空的,GetCount()返回 0,直接跳过了。处置是把列设置的代码挪到绑定成功之后,并且在设置前加一个GetCount() > 0的判断,不成立就弹框提示。
第三条,窗口一重绘就崩,或者滚动时崩溃。现象是刚打开时好好的,拖动滚动条、切换窗口、最小化再还原,随机崩。原因是记录集对象被提前析构了,控件手里的指针悬空。最常见的是把CADORecordset定义在按钮响应函数或某个局部作用域里,绑定完函数返回,对象就没了。处置是把连接和记录集提升为对话框类成员,保证生命周期覆盖整个对话框。
第四条,GetItem抛异常,错误码DISP_E_TYPEMISMATCH。现象是编译正常,运行到取列那一行直接进 catch。原因是VARIANT变量没有VariantInit也没有赋vt,初值是VT_EMPTY,控件无法解析。处置是老老实实三行:VariantInit、设vt = VT_I4、设lVal,用完VariantClear。这条血泪经验值得写进代码规范,只要用到 VARIANT 就当模板抄。
第五条,点了列头排序没反应,或者排完行数变成 -1。现象是点击事件确实进了,Sort也赋了值,界面顺序不变;或者用RecordCount拿行数一直是 -1。原因是游标位置还是服务端游标。处置是把CursorLocation = adUseClient设在打开记录集之前,设在Connection对象上才会对它派生的所有记录集生效。设完之后如果内存占用敏感,就在筛选条件上多下功夫,别把整表拉下来。
补一条容易被忽略的:包里的 OCX 是 32 位的,工程如果被改成了 64 位编译,运行时直接报「不是有效的 Win32 应用程序」,这时候不是代码问题,是位数不匹配,回 32 位配置就好。
6. 封装初始化函数与上线前验证清单
调通之后我的习惯是把所有列属性从窗口代码里抽出来,写成一个独立的初始化函数,参数用结构体传,这样换一张表只要改配置不用动逻辑:
struct COL_CFG { int nIndex; // 列索引,从 0 开始 float fWidth; // 列宽,twips LPCTSTR lpszCaption; // 列头文本 LPCTSTR lpszFormat; // 格式串,NULL 表示不设 int nAlign; // 0 左 / 1 右 / 2 居中 }; void CTestGridDlg::ApplyColCfg(const COL_CFG* pCfg, int nCount) { CColumns cols = m_ctrlDG.GetColumns(); if (cols.GetCount() <= 0) return; // 没绑数据就直接退出 for (int i = 0; i < nCount; ++i) { if (pCfg[i].nIndex >= cols.GetCount()) continue; VARIANT vt; ::VariantInit(&vt); vt.vt = VT_I4; vt.lVal = pCfg[i].nIndex; CColumn col = cols.GetItem(vt); col.SetWidth(pCfg[i].fWidth); col.SetCaption(pCfg[i].lpszCaption); col.SetAlignment(pCfg[i].nAlign); if (pCfg[i].lpszFormat) col.SetNumberFormat(pCfg[i].lpszFormat); ::VariantClear(&vt); } }参数上唯一的约定是nIndex越界要跳过,因为不同权限的用户看到的列数可能不一样(有些列会被 SQL 裁掉),不跳过就会在越界那一列抛异常。
上线前我会过一遍这张表,都是在客户现场吃过亏之后加的:
| 检查项 | 判定标准 | 不合格的后果 |
|---|---|---|
AfxEnableControlContainer() | InitInstance里有这一句 | 控件不显示 |
| OCX 注册 | 干净机器上regsvr32成功 | 客户机空白 |
| 记录集生命周期 | 是对话框类成员 |
本文还有配套的精品资源,点击获取