简介:面向VC/MFC开发者的DataGrid控件实战讲解文档,聚焦在对话框程序中借助Microsoft DataGrid OLEDB 6.0控件完成数据网格展示的常见需求,适合具备C++与MFC基础、希望在既有工程中快速接入表格控件的初中级开发者参考。内容围绕查询结果显示、列宽等显示控制、列内容格式化三条主线展开,并延伸到多行显示与HeadClick消息排序,配有CADODatabase、CADORecordset与CDataGrid配合的示例代码,以及CColumns、CColumn取列、SetWidth设宽、SetNumberFormat设百分比格式的具体写法,可对照工程逐步落地。资源包内含1个doc文档,约226KB,轻量易取,附有VC6.0英文版、Windows XP下的调试说明与示例工程思路,便于排查环境差异。已有851人学习,适合作为网格控件上手与查漏补缺的速查材料。
1. 老工程里那个一拖就能出表格的控件,到底是什么
前段时间接手一个对话框框架的老工程,需求很朴素:查询结果直接铺成表格,能改、能按列排序、能把列宽调好。用 CListCtrl 手写一遍,建列、塞数据、处理就地编辑、维护行号映射,稳稳几百行;换成 MFC DataGrid 控件,一行SetRefDataSource就把 ADO 记录集铺成了带列头、可编辑、能排序的网格。它本质是一个 ActiveX 控件,通过 ClassWizard 生成CDataGrid、CColumns、CColumn三个包装类,写起来几乎感觉不到 COM 的存在。适合的场景很明确:VC6.0 或后续 VS 版本的对话框程序、数据来自 ADO 记录集、界面不需要花哨皮肤;不适合的也同样明确:几十万行要虚拟列表、单元格要自绘、要跨平台。下面按「装进去、喂数据、控显示、做排序、避坑」的顺序把 MFC DataGrid 控件用法拆一遍。
2. 把控件装进对话框:三个包装类与一条数据链
DataGrid 用起来顺不顺,八成取决于工程配置阶段有没有踩空。这一步要做的事情只有三件:让对话框能承载 ActiveX 控件、让 ClassWizard 生成包装类、让记录集活到网格销毁之后。三件事任何一件没做,后面的代码写得再对也是白屏。
2.1 插入控件与生成 CDataGrid / CColumns / CColumn
先在资源视图里打开对话框模板,右键选择插入 ActiveX 控件,在列表里找到注册名带 OLEDB 字样的 DataGrid 控件,确定后在对话框上拉出一个矩形。接着右键该控件选择建立类向导,给它绑定一个成员变量,类选CDataGrid,变量名习惯写m_ctrlDG。关键在下一步:类向导弹出的「从 ActiveX 控件添加类」对话框里,会列出这个控件暴露的所有接口封装类,必须把CDataGrid、CColumns、CColumn三个都勾上。只勾CDataGrid,后面写CColumns cols = m_ctrlDG.GetColumns();会直接编译不过——这是新手最常见的第一个跟头。
工程层面还有两行初始化不能少,很多人是从别人的老工程里拷代码,把这两行弄丢了,结果对话框一创建就失败,DoModal()返回 -1,调试半天找不到原因:
BOOL CTestGridApp::InitInstance() { // 初始化 OLE/COM 库。DataGrid 是 ActiveX 控件,ADO 也走 COM, // 缺这一行,控件创建阶段就会失败 if (!AfxOleInit()) return FALSE; // 允许对话框承载 ActiveX 控件。缺这一行,控件位置是一块空白, // 不报错、不崩溃,最难查 AfxEnableControlContainer(); CTestGridDlg dlg; m_pMainWnd = &dlg; dlg.DoModal(); // 退出前反初始化 COM,否则调试版退出时容易在断言里停住 AfxOleTerm(FALSE); return FALSE; }AfxOleInit()的返回值必须判断,它内部会调用OleInitialize,在同一线程重复初始化会返回失败;如果工程里别的地方已经调过CoInitialize,这里就要改成不重复初始化的写法。AfxEnableControlContainer()没有参数,作用是给CWnd挂上COccManager,让对话框模板里的 ActiveX 占位符能被真正创建成控件窗口。AfxOleTerm(FALSE)的参数表示是否释放 OLE 相关的剪贴板与拖放资源,一般传 FALSE 即可。
2.2 用 ADO 记录集喂数据:SetRefDataSource 到底收什么
DataGrid 不认识 SQL,也不认识 MFC,它只认一个IUnknown。所以数据链路是:ADO 连接 → 记录集 → 取_Recordset的接口指针 → 强转LPUNKNOWN→ 交给SetRefDataSource。原文用的是一个流传很广的 ADO 封装类(CADODatabase/CADORecordset),如果手头没有这套封装,直接用_ConnectionPtr/_RecordsetPtr智能指针完全等价,只是少了几层便利方法。
// TestGridDlg.h —— 三个对象都必须是成员变量 CADODatabase m_DBCn; // 连接,整个对话框生命周期内保持打开 CADORecordset m_Rs; // 记录集,网格的数据源,不能提前析构或 Close CDataGrid m_ctrlDG; // 控件包装类对象 BOOL CTestGridDlg::OnInitDialog() { CDialog::OnInitDialog(); CString strConnection; // Data Source 用相对路径时,基准是 exe 所在目录,不是源码目录 strConnection.Format(_T("Provider=Microsoft.Jet.OLEDB.4.0;Data Source=%s"), _T("note.mdb")); if (!m_DBCn.Open((LPCTSTR)strConnection)) { AfxMessageBox(_T("数据库打开失败,检查 mdb 路径")); return TRUE; // 这里不要 return FALSE,否则对话框直接销毁 } m_Rs.SetDatabase(&m_DBCn); if (!m_Rs.Open(_T("SELECT * FROM test"))) { AfxMessageBox(_T("查询失败,检查表名")); return TRUE; } // _Recordset 的 IUnknown 交给控件;控件收到后会 QI 出它要的接口并 AddRef m_ctrlDG.SetRefDataSource((LPUNKNOWN)(_Recordset*)m_Rs.GetRecordset()); ApplyGridStyle(); // 绑定完成之后才能设列宽与格式 return TRUE; }SetRefDataSource只有一个参数,类型是LPUNKNOWN,编译器不会帮你做_Recordset*到LPUNKNOWN的隐式转换,所以括号要写全:先转_Recordset*,再转LPUNKNOWN。Provider=Microsoft.Jet.OLEDB.4.0适合 mdb 文件库,换成 SQL Server 就改成Provider=SQLOLEDB并补上Server、Initial Catalog、User ID、Password。SELECT *在演示里省事,实际项目里建议把列名写全,列顺序稳定,后面按索引取列做格式化才不会因为表结构变动而错位。
ApplyGridStyle()是什么、什么时候调、里面写什么,是第 3 章的主体,这里先记住一个硬性顺序:先绑数据源,再取列设格式。反过来做,设置会全部丢失。
2.3 VARIANT 索引与列对象的取得方式
DataGrid 的列集合是 COM 集合,取单个元素的方法签名是GetItem(const VARIANT&),所以传进去的必须是一个VARIANT,不能直接给个int。而且索引是 0 基的,第 1 列对应 0,第 3 列对应 2。这一点跟很多人先入为主的「第几列就是几」正好相反,越界时抛的是COleDispatchException,消息还不太直观。
// 取第 nCol 列(0 基),返回 CColumn 包装对象 CColumn CTestGridDlg::GetColumn(int nCol) { CColumns cols = m_ctrlDG.GetColumns(); // 列集合,每次都重新取,别缓存 long nCount = cols.GetCount(); // 当前数据源的列数 ASSERT(nCol >= 0 && nCol < nCount); VARIANT vt; VariantInit(&vt); vt.vt = VT_I4; // 明确告诉控件这是个 4 字节整数 vt.lVal = nCol; CColumn col = cols.GetItem(vt); // 内部走 IDispatch,失败会抛异常 VariantClear(&vt); return col; }三个细节值得说清。第一,vt.vt必须显式赋成VT_I4,只给lVal赋值而忘了vt成员,控件收到的是一个未定义类型的 VARIANT,行为不可预期。第二,CColumns不要做成成员变量缓存起来,每次用的时候重新GetColumns(),因为重新绑定数据源之后列集合对象内部指针会换,缓存的那份可能指向已经失效的集合。第三,GetCount()返回的是当前数据源的列数,不是你在设计器里画的列数,绑了空记录集时它可能是 0,写循环前先判一下。
CColumn对象本身是个轻量包装,赋值和返回都走引用计数,函数返回CColumn是安全的,不会因为局部变量析构就把列设置弄丢。
3. 列宽、行高与格式串:把网格调成能交付的样子
数据能显示只是及格线,交付给业务方之前,列宽、列头文案、数字格式、对齐方式、能不能编辑,都得按字段逐个调。这一章的做法是把所有配置收敛到一个函数里,绑定数据源之后调一次,后面任何一次重绑都再调一次。
3.1 CColumn 的属性清单与设置时机
CColumn的方法名基本都是属性名的直译,看名字就知道干什么。下面这张表是我实际项目里用得最多的几个,其余方法可以顺着包装类的头文件往下翻:
| 方法 | 作用 | 常用取值 / 注意点 |
|---|---|---|
SetWidth(float) | 列宽,单位是磅 | 取 0 会隐藏;建议按内容长度估 |
SetCaption(LPCTSTR) | 列头文字 | 与数据库字段名解耦,直接写中文 |
SetNumberFormat(LPCTSTR) | 显示格式串 | 0.0%、#,##0.00、yyyy-mm-dd |
SetAlignment(long) | 对齐方式 | 0 左、1 右、2 居中 |
SetAllowSizing(BOOL) | 是否允许拖动列宽 | 交付版一般给 TRUE |
SetLocked(BOOL) | 单元格是否只读 | 按字段设,别整表锁死 |
SetWrapText(BOOL) | 单元格内是否自动换行 | 配合行高使用 |
SetVisible(BOOL) | 列是否显示 | 隐藏主键列比删列安全 |
设置时机是这一节的重点。SetRefDataSource内部会重建整个列集合,之前对CColumn做的宽度、格式、标题全部作废。所以正确顺序永远是:开库 → 查数据 → 绑定 → 取列 → 逐列配置。有些同学习惯在OnInitDialog开头就把列宽设好,运行起来发现全是默认值,翻来覆去找不到原因,问题就在这个顺序上。
列宽还有一个全局兜底值SetDefColWidth(float),设一次,所有没单独指定宽度的列都按它来,适合字段多、懒得逐列调的场合。
3.2 SetNumberFormat 的格式串怎么写才不翻车
格式串沿用通用的数字与日期格式规则,0.0%表示保留一位小数并乘 100 加百分号,#,##0.00表示千分位加两位小数,yyyy-mm-dd用于日期。原文里「将小于 1 的小数显示成百分数」用的就是0.0%,这个写法本身没问题,但翻车点集中在两处。
void CTestGridDlg::ApplyGridStyle() { if (m_ctrlDG.GetColumns().GetCount() == 0) return; // 空结果集,直接返回,别硬设 CColumns cols = m_ctrlDG.GetColumns(); // 第 0 列:编号,居中对齐,只读 CColumn col0 = GetColumn(0); col0.SetCaption(_T("编号")); col0.SetWidth(50); col0.SetAlignment(2); col0.SetLocked(TRUE); // 第 1 列:名称,允许拖动列宽 CColumn col1 = GetColumn(1); col1.SetCaption(_T("名称")); col1.SetWidth(140); col1.SetAllowSizing(TRUE); // 第 2 列:比率字段,显示成一位小数的百分数 CColumn col2 = GetColumn(2); col2.SetCaption(_T("完成率")); col2.SetWidth(80); col2.SetAlignment(1); col2.SetNumberFormat(_T("0.0%")); // 第 3 列:金额,千分位两位小数 CColumn col3 = GetColumn(3); col3.SetCaption(_T("金额")); col3.SetWidth(100); col3.SetAlignment(1); col3.SetNumberFormat(_T("#,##0.00")); // 第 4 列:日期,mdb 里必须是日期/时间类型才生效 CColumn col4 = GetColumn(4); col4.SetCaption(_T("录入日期")); col4.SetWidth(100); col4.SetNumberFormat(_T("yyyy-mm-dd")); // 全局兜底行高,后面章节还会再提 m_ctrlDG.SetRowHeight(20); }第一处翻车点是字段类型。SetNumberFormat只对数值型和日期型字段生效,Access 里如果你把比率字段建成了文本型(很多人为了省事就这么干),格式串写得再漂亮也不会变,单元格里还是"0.85"。这种情况只有两条路:改表结构,或者在 SQL 里用FORMAT类函数先转成字符串再拼%。第二处是区域设置,格式串里的小数点符号跟随系统区域,换到某些区域设置下0.0%会被解析成别的意思,表现是整列数字不动或者显示异常。真遇到这种环境,最稳的做法是显示层用格式串,导出层自己Format一遍,两边不互相依赖。
SetNumberFormat只影响显示,不影响值。用户在单元格里编辑后,控件回写的是原始数值,这一点在做保存逻辑时要想清楚,别指望从单元格读出来的是格式化后的字符串。
3.3 行高、多行显示与列头字体
SetRowHeight(float)是整表统一行高,单位同样是磅,给它 20 大约是常见表格的舒适高度。如果某一列内容特别长,希望单元格内换行,就对该列SetWrapText(TRUE),再把行高提高,让两行甚至三行能放得下。原文提到的「多行显示」,落到代码上就是这两步的组合,单靠SetRowHeight不会自动换行。
SetDefColWidth设全局列宽兜底,SetHeadFont收一个LPUNKNOWN,用来指定列头字体;这个接口在 MFC 包装里用起来比较别扭,需要先把CFont选进内存 DC 拿到HFONT,再包成IFontDisp,一般项目里不折腾,保持默认字体反而更稳。
还有一个容易被忽略的开关组合:如果这个网格只用于展示,务必把SetAllowAddNew(FALSE)、SetAllowDelete(FALSE)、SetAllowUpdate(FALSE)一起关掉。默认是全开的,用户点到最后一行的空白处敲几个字,控件内部就新建了一条记录,如果你没写保存逻辑,界面上看着像脏数据,实际上什么都没落库,排查起来很费时间。
4. HeadClick 排序:从事件映射到重绑定的完整链路
按列头排序是业务方最基础的要求,实现链路有三段:事件要能进来、排序要真生效、排完之后格式不能丢。三段里任何一段断了,现象都是「点了没反应」或者「排完列宽全乱了」。
4.1 事件映射与 OnHeadClickDataGrid 的参数
DataGrid 的列头点击事件叫HeadClick,ClassWizard 会在消息映射里生成一条ON_EVENT宏,处理函数名带控件变量前缀,形如OnHeadClickDataGrid。参数只有一个,就是被点击列的索引,类型是short。
// TestGridDlg.h afx_msg void OnHeadClickDataGrid(short ColIndex); DECLARE_EVENTSINKMAP() // TestGridDlg.cpp BEGIN_EVENTSINKMAP(CTestGridDlg, CDialog) ON_EVENT(CTestGridDlg, IDC_DATAGRID1, 2, OnHeadClickDataGrid, VTS_I2) END_EVENTSINKMAP() void CTestGridDlg::OnHeadClickDataGrid(short ColIndex) { SortByColumn(ColIndex); // 具体排序实现见下一节 }ON_EVENT那一行里,第三个参数是事件的 dispid,HeadClick通常是 2,用 ClassWizard 添加会自动填对,手写容易写错。第四个参数是处理函数名,第五个VTS_I2表示参数类型是 2 字节整型,和数据源里的short必须对得上。如果这里误写成了VTS_I4,运行期点列头不会有任何反应,也不报错,属于典型黑匣子故障。IDC_DATAGRID1是控件的资源 ID,每个工程不一样,照着自己资源视图里的填。
处理函数签名如果写成long,编译能过(MFC 只是取参数地址),但取到的值会错,列索引可能变成几万,排序逻辑直接崩。签名一律按 ClassWizard 生成的那份抄。
4.2 三种排序实现的取舍与 CursorLocation 的硬性前提
排序本身不在控件里做,控件只负责告诉你「用户点了第几列」。真正排数据的方案有三种,选哪种取决于数据量和是否允许重查。
第一种是直接给记录集赋值Sort属性,形如"字段名 ASC",改动最小、速度最快,但它有一个硬性前提:记录集必须是客户端游标。ADO 默认的服务器端游标不支持Sort属性,赋值时会抛「提供程序不支持该方法」的异常,或者静默不生效。所以建记录集时要把CursorLocation设成adUseClient,再打开。
// 客户端游标是 Sort 属性的前提,顺序不能反:先设游标位置,再 Open m_Rs.GetRecordset()->CursorLocation = adUseClient; m_Rs.Open(_T("SELECT * FROM test")); void CTestGridDlg::SortByColumn(int nCol) { CColumns cols = m_ctrlDG.GetColumns(); if (nCol < 0 || nCol >= cols.GetCount()) return; // 排序字段名要用数据源里的真实字段名,不能用列头文字 CString strField = GetFieldNameByIndex(nCol); m_strSortField = strField; // 记住当前排序字段 m_bAscending = !m_bAscending; // 再点一次反向 try { CString strSort; strSort.Format(_T("%s %s"), (LPCTSTR)strField, m_bAscending ? _T("ASC") : _T("DESC")); m_Rs.GetRecordset()->Sort = (_bstr_t)(LPCTSTR)strSort; // 重绑数据源,否则网格不会刷新;绑完必须重设列格式 m_ctrlDG.SetRefDataSource((LPUNKNOWN)(_Recordset*)m_Rs.GetRecordset()); ApplyGridStyle(); } catch (_com_error& e) { AfxMessageBox(e.Description()); } }第二种是带ORDER BY重新查询,兼容性最好,不依赖游标位置,缺点是每次点列头都往数据库跑一趟,数据量大时体验差,而且要额外处理「重查后光标回到哪一行」。第三种是把记录集内容读进内存自己排,适合纯展示、不带编辑的场合。
我的取舍是:数据量在几千行以内、允许客户端游标,就用第一种,代码最短;数据来自视图或大数据集,用第二种,把ORDER BY字段白名单校验一遍再拼串,别直接拿列头文字拼 SQL。
SortByColumn里的GetFieldNameByIndex得自己实现,映射关系在绑定数据源时用_Recordset的Fields集合遍历一遍存下来最稳。直接拿列头文字当字段名,中文列头一定失败。
4.3 重绑定之后必须重设格式:一个函数救回来
上面代码里SetRefDataSource之后紧跟ApplyGridStyle(),这两行是绑在一起的。重绑会重建列集合,之前设的宽度、格式、列头文字全部回到默认值,现象是「排序是生效了,但列宽全被拉宽、百分比变回小数」。把配置收成一个幂等函数,任何一次重绑后面都调它,是唯一靠谱的做法。
ApplyGridStyle()要写成可重复调用的:内部每次都重新GetColumns()、重新判空、逐列设属性,不依赖任何一次性的状态。同时它要容忍列数为 0,因为排序后可能返回空集。
编辑态的处理也要注意。如果用户正在某个单元格里输入,此时点列头排序,控件会先触发BeforeColUpdate,你如果没有对应的校验逻辑,正在输入的内容会丢失。折中的做法是在BeforeColUpdate里把值先写回记录集再重绑,或者干脆在这些场景下把网格设为只读,排序和编辑不同时开放。
5. 避坑与排查:五条血泪记录
下面这五条,是这套控件在真实工程里最容易被卡住的地方,每一条都按「现象 → 原因 → 解决」写,遇到对应症状可以直接对号入座。
5.1 对话框弹不出来,DoModal 返回 -1
现象很干脆:程序能编译、能运行,调用DoModal()直接返回 -1,主窗口一闪而过,没有任何提示。
原因通常是两个。一是控件没注册,工程拷到另一台机器上,注册表里没有这个 ActiveX 控件的 CLSID,对话框模板创建时找不到对应控件,整个模板创建失败。二是AfxEnableControlContainer()没调,MFC 没有挂上COccManager,遇到 ActiveX 占位符同样创建失败。
解决顺序是先补AfxEnableControlContainer(),再确认控件注册。排查注册问题别去翻注册表,写几行代码直接问系统要 CLSID 更快:用CLSIDFromProgID传控件的 ProgID,返回S_OK说明注册正常,返回失败就是没注册。换机器部署时把控件随安装包一起注册,别指望目标机自带。
5.2 网格只有列头,一行数据都没有
现象是控件正常显示出来了,列头也在,下面一片空白;有时候连列头都没有,就是一块灰。
先分清是数据问题还是绑定问题。最省事的判定方法是在SetRefDataSource之前打印m_Rs.GetRecordset()->RecordCount(客户端游标下才准确),如果是 0,那就是 SQL 或库路径的问题,跟控件无关。库路径是最常见的坑:连接串里的相对路径以 exe 所在目录为基准,调试时 exe 在 Debug 目录下,而 mdb 放在源码目录,本地跑得好好的,换台机器就空。
如果RecordCount不为 0 但网格还是空,检查记录集是不是在绑定之后被Close()了。SetRefDataSource会 AddRef 接口,对象不会立刻销毁,但游标一关,控件就拿不到行,表现就是只剩列头。把记录集做成对话框成员、不在中途 Close,这个问题就消失了。
5.3 SetNumberFormat 设了没效果
现象是百分比列还是显示成0.85,或者日期列显示成一串数字。
三个原因按概率排:一是设置时机在绑定之前,格式被随后的重绑冲掉了;二是列索引写错,0 基写成了 1 基,格式设到了隔壁列上;三是字段类型不对,文本型字段不接受数字格式。前两个改代码,第三个只能改表结构或者在 SQL 层拼串。
排查手段很简单:在ApplyGridStyle()里对每一列SetCaption一个带序号的临时标题(比如「列0」「列1」),跑起来看序号和内容的对应关系,索引错位一眼就能看出来。这个方法比打断点快,我每次接新表结构都会先这么过一遍。
5.4 点列头排序没反应,或者排完格式全乱
现象分两种。点了完全没反应,通常是事件没进来:检查ON_EVENT的 dispid 和VTS_I2是否正确、处理函数签名是不是short。如果日志里能看到函数进来了但数据没变,就是Sort属性没生效,九成是记录集用了服务器端游标。
排完格式全乱是另一码事:重绑数据源之后没有重设列格式。解决办法就是把ApplyGridStyle()跟在每一次SetRefDataSource后面,一次都别漏。我吃过一次亏,排序功能加了三天,业务方反馈「排序完列宽就变了」,最后发现是重绑那段漏了一行。
5.5 程序退出时崩溃或停在断言里
现象是关对话框的瞬间崩,或者调试版停在某个 ASSERT 上,调用栈里能看到 OLE 相关的函数。
常见原因是 COM 初始化与反初始化不配对:AfxOleInit()调了,退出时没调AfxOleTerm();或者反过来,在别处已经CoInitialize过,这里又初始化一次。另一个原因是控件变量与数据对象的析构顺序,如果记录集是局部对象、控件是成员变量,控件析构时记录集早就没了,控件内部去 Release 接口就会出问题。
处理办法是把数据对象和控件变量都做成成员,让它们跟着对话框一起析构,析构顺序由成员声明顺序的反序决定,把m_ctrlDG声明在m_Rs之前,控件先析构、记录集后释放,顺序就对了。
6. 把配置收敛成一张表:三步验证与一个可复用封装
列一多,ApplyGridStyle()里就会堆出一长串GetColumn(n),加一列改三处,改错一次就是线上问题。我的做法是把「列索引 → 列头 → 宽度 → 格式 → 对齐 → 只读」做成一张静态表,函数只负责遍历,加列只改表。
struct GRIDCOL_CFG { int nIndex; // 0 基列索引 LPCTSTR szCaption; // 列头文字 float fWidth; // 列宽,磅 LPCTSTR szFormat; // 格式串,NULL 表示不设 long nAlign; // 0 左 1 右 2 居中 BOOL bReadOnly; // 是否只读 }; static const GRIDCOL_CFG g_cols[] = { { 0, _T("编号"), 50, NULL, 2, TRUE }, { 1, _T("名称"), 140, NULL, 0, FALSE }, { 2, _T("完成率"), 80, _T("0.0%"), 1, FALSE }, { 3, _T("金额"), 100, _T("#,##0.00"),1, FALSE }, { 4, _T("录入日期"), 100, _T("yyyy-mm-dd"), 1, TRUE }, }; void CTestGridDlg::ApplyGridStyle() { CColumns cols = m_ctrlDG.GetColumns(); long nCount = cols.GetCount(); if (nCount == 0) return; for (int i = 0; i < _countof(g_cols); ++i) { const GRIDCOL_CFG& c = g_cols[i]; if (c.nIndex >= nCount) continue; // 表结构变了也不至于崩 CColumn col = GetColumn(c.nIndex); col.SetCaption(c.szCaption); col.SetWidth(c.fWidth); col.SetAlignment(c.nAlign); col.SetLocked(c.bReadOnly); if (c.szFormat) col.SetNumberFormat(c.szFormat); } m_ctrlDG.SetRowHeight(20); }配置表化的好处不只是少写代码,更重要的是把「列顺序」这个易变项集中到一处,表结构加了字段、调了顺序,只改这张表,GetColumn的索引不会散落在各个函数里。
配置改完,交付前我会固定跑三步验证。第一步,用一个必然返回空集的查询条件打开对话框,确认程序不崩、控件正常显示空表、ApplyGridStyle里的判空生效。第二步,往文本字段塞一条 200 字的超长内容,确认列宽和换行设置在超长文本下不会把整个表格撑出屏幕,必要时给该列加SetWrapText(TRUE)并调高行高。第三步,连续点同一列头三次,确认升序、降序切换正确,且每一次重绑之后列宽、格式、中文列头都还在——这一步能抓出九成的重绑漏调问题。
最后说个习惯。这套控件的方法名起得确实直白,SetWidth、SetNumberFormat、SetAllowSizing基本看名字就知道干什么,遇到不会的属性,我一般直接翻包装类的头文件顺着方法名找,比查文档快。从那以后我每次往工程里加这个控件,都会先确认AfxOleInit、AfxEnableControlContainer、三个包装类、记录集做成员这四件事齐了再写业务代码,省下的排查时间远比这几分钟多。希望帮到你。
本文还有配套的精品资源,点击获取