MFC DataGrid控件实战:从数据绑定到常见坑位应对指南
简介面向VC/MFC开发者详解微软DataGridOLEDB 6.0网格控件在对话框程序中的接入与使用聚焦数据库查询结果网格化显示、列宽控制、数字格式化、多行显示与消息排序等核心场景适用于管理信息系统、报表查询等数据表格界面开发。文档从工程准备讲起说明如何插入控件并按需选取CDataGrid、CColumns、CColumn封装类随后演示用ADO封装类打开Access数据库note.mdb示例库、将Recordset对象设为控件数据源以显示查询结果再讲解通过列集与列对象调整列宽、调用SetNumberFormat将小数显示为百分数、设置行高实现多行显示并利用HeadClick消息完成排序附带完整的TestGrid工程源码可在VC6.0与Windows XP调试通过。压缩包内含1个doc文档体积仅226KB轻量易读。文档结尾还提示善用控件自带的帮助文件比对VB与VC中方法命名的异同即可将用法迁移到更多项目场景。已有849人学习下载适合刚接触MFC数据绑定或希望理解ActiveX控件COM封装原理的开发者快速参考。1. MFC DataGrid 控件什么场景下你绕不开它说句实话现在很少有人会主动在 MFC 里想起 DataGrid 这个控件。它长得像一张 Excel 表本质上却是一个 COM 时代的 ActiveX 网格控件负责把二维数据在对话框里直接显示、翻页和就地编辑。新手接手老项目时第一次见到它的“见面礼”往往是两个弹窗一个是“控件未注册”一个是“0x80040112 类未注册”而表格区域一片空白。它解决的核心问题很朴素传统 C 界面里要快速展示数据库记录你没有必要从零去处理单元格绘制、滚动条、键盘编辑和列头排序DataGrid 把这一整套都封装进了控件里。这篇文章适合维护存量 MFC 桌面系统的开发者也适合在工控上位机方案里需要快速组一张数据表的人。我从接入讲起一路讲到绑定、事件、刷新、焦点和封装尽量做到每一步都能照着复现。2. 把 MFC DataGrid 控件放进对话框从设计期拖拽到运行期动态创建2.1 在对话框编辑器里插入 ActiveX 控件并生成包装类先讲最常规的路子用 Visual Studio 的对话框编辑器在工具箱空白处右键选择“选择项”切到 ActiveX 控件页找到 DataGrid Control 6.0 (OLEDB)勾选后把它拖到对话框里。此时编辑器会问是不是要生成包装类选“是”。生成之后工程里会多出 CDataGrid、CColumns、CColumn 等几个类本质上它们是对 COM 类型库的 C 封装控件创建的底层调用最终归于CWnd::CreateControl不是普通窗口的Create。这一步做完对话框头文件里要有一个 CDataGrid 成员变量并在 DoDataExchange 中做关联否则控件只出现在设计器里代码根本碰不到它// DemoDlg.h class CDemoDlg : public CDialogEx { protected: virtual BOOL OnInitDialog() override; virtual void DoDataExchange(CDataExchange* pDX) override; CDataGrid m_grid; // 由 ActiveX 包装类生成的成员 }; // DemoDlg.cpp void CDemoDlg::DoDataExchange(CDataExchange* pDX) { CDialogEx::DoDataExchange(pDX); DDX_Control(pDX, IDC_DATAGRID1, m_grid); }DDX_Control 在这里不是普通控件的 SubclassWindow而是把对话框模板里 IDC_DATAGRID1 占位窗口与 ActiveX 包装类绑定起来。绑定失败最常见的原因是这个控件没有在系统里注册后面排错基本都落在注册和许可证这两件事上。如果你看到的头文件里生成的类名不是 CDataGrid而是 CExplorerDataGrid 之类也没关系只是不同版本向导生成的命名不同操作思路完全一样。这个阶段其实不涉及任何业务逻辑你只需要确认三件事控件能被创建、能被 DDX 绑上、程序运行后显示出一个空白网格。如果这一步都不稳后面所有表格操作都会雪崩。我会建议在 OnInitDialog 里临时弹一条日志输出m_grid.GetSafeHwnd()的值确认 HWND 非空再做后续。2.2 运行期用 CreateControl 创建适合动态多网格场景有些界面不适合在设计器里固定一个网格比如要用标签页切换多个表格、运行期根据权限动态生成两到三块网格。这种场景下可以让 CDataGrid 成员变量默认不关联任何对话框模板在 OnInitDialog 里用 CreateControl 手动创建。BOOL CDemoDlg::OnInitDialog() { CDialogEx::OnInitDialog(); CRect rcGrid(10, 30, 610, 360); BOOL bOK m_grid.CreateControl( _T(MSDATAGRID.DataGrid), // ProgID也可以换成 CLSID 字符串 _T(), WS_CHILD | WS_VISIBLE | WS_TABSTOP, rcGrid, this, IDC_DATAGRID1); if (!bOK) { AfxMessageBox(_T(DataGrid 控件创建失败请确认 MSDATAGRID.OCX 已注册)); return FALSE; } return TRUE; }CreateControl 的第一个参数是 ProgID它等价于注册表里的“MSDATAGRID.DataGrid”这个标识。第二个参数是窗口名对 ActiveX 控件来说通常留空。第三个参数是窗口样式注意要带 WS_CHILD 和 WS_VISIBLE如果希望用户能用 Tab 把焦点移进网格再加 WS_TABSTOP。第四个参数是控件在父窗口里的位置矩形。最后两个参数分别是父窗口和控件 ID控件 ID 主要用来接事件和 DDX并不是真正意义上的子窗口 ID。这里有一个我踩过的坑不要在后台工作线程里调用 CreateControl。ActiveX 控件很多行为依赖 COM 的单元模型它在创建时绑定了线程。如果网格在工作线程里创建等主线程再去访问它轻则没反应重则直接崩溃。我一般会把创建放到主线程的 OnInitDialog 里后台线程只准备数据集做完后 PostMessage 给主线程再由主线程去刷新控件。设计器和 CreateControl 怎么选我的判断标准是一个对话框里只有一张固定表格用设计器表格数量、位置、是否显示会受到用户操作影响用 CreateControl。运行期创建还有一个附带好处就是你可以把创建代码放到一个封装类里后面替换成自绘网格时只动一个文件。3. 把一张空表铺出来DataGrid 的行、列、单元格与样式操作3.1 设置列数和列宽先搞懂缇和像素的换算先说一个前置认知老式 ActiveX 网格控件的列宽单位不是像素而是缇twip。1 缇等于 1/20 磅等于 1/1440 英寸。在 96 DPI 下1 像素约等于 15 缇在 120 DPI 下换算值会变。如果直接用像素数去设置列宽换到高分屏上列会变得特别窄这是网格“看着不对劲”的第一个常见原因。初始化一张空表的典型代码是这样的// 假设 m_grid 已经通过 DDX_Control 或 CreateControl 创建成功 m_grid.SetDataMode(0); // 0非绑定模式 m_grid.SetColumns(5); // 先设 5 列 m_grid.SetRows(2); // 第 0 行留给表头第 1 行做数据演示 m_grid.SetColumnHeaders(TRUE); // 显示列头SetDataMode 是网格工作原理的开关。0 是非绑定模式网格就是一张由行列组成的空表所有内容都由代码手动写入1 是绑定模式网格会从数据源拉取字段集合自动生成列。早期调试时建议先走非绑定模式把行列概念跑通再考虑数据源否则你会分不清当前看到的问题是列设置问题还是 ADO 问题。列宽的换算我建议写成一个小工具函数int PixelsToTwips(CWnd* pWnd, int nPixels) { CClientDC dc(pWnd); int nLogPixelsX dc.GetDeviceCaps(LOGPIXELSX); return MulDiv(nPixels, 1440, nLogPixelsX); } // 调用示例第 0 列给 120 像素第 1 列给 200 像素 m_grid.SetColWidth(0, PixelsToTwips(this, 120)); m_grid.SetColWidth(1, PixelsToTwips(this, 200));MulDiv 是 Windows 自带的一个整数乘除函数它可以避免浮点误差。这里核心是用 GetDeviceCaps(LOGPIXELSX) 拿到当前显示环境的水平 DPI再按比例把像素转成缇。如果你只打算做一个固定 DPI 的工控机显示不做换算也跑得起来但一旦现场机器是 125% 或 150% 缩放这个函数就是你的后悔药。3.2 添加、删除行与单元格定位顺序决定成败非绑定模式下行列操作顺序直接决定你最终看到的结果。我习惯的步骤是先增加行数再把当前行指针移动到新行最后移动列指针。反过来很容易写到上一行去。下面是一段最小示例long nRow m_grid.GetRows(); m_grid.SetRows(nRow 1); m_grid.SetRow(nRow); // 定位到新加的最后一行的第 0 列 m_grid.SetCol(0); m_grid.SetText(_T(设备001));这段代码的逻辑是GetRows 先拿到当前总行数SetRows 加一行SetRow 把“当前行”指向新行SetCol 把“当前列”指向第 0 列最后 SetText 写入文本。这里最容易犯的错是写完 SetRows 后没有 SetRow直接 SetCol 和 SetText结果数据写进了之前选中的那一行。按行追加要特别注意行的删除。老式 DataGrid 没有特别友好的 DeleteRow常用的做法是重建行结构。如果你要做频繁增删行的列表我不建议在这种非绑定模式上硬撑更适合把编辑功能放在对话框下方的输入框里网格只负责展示。删除操作的代价是整表重排数据量大时体验很差。这里也顺带解释一个视觉问题很多人发现设置列宽后没反应。第一个要查的是当前列是否对。SetColWidth 作用于“当前列”的语义在不同包装类里不太一样有的按列对象遍历设置有的按当前列设置。如果当前列还是上次点击的列你设置的可能不是你想改的那一列。稳妥做法是逐列 SetCol 后再调 SetColWidth。3.3 用 Text 属性读写单元格并控制表头、只读模式单元格读写最直观的入口是 Text 属性。但不同 Visual Studio 版本生成的包装类签名不一样有的返回 CString有的返回 VARIANT。遇到 VARIANT 时绝不能直接强转成 char*要先判断 vt 是不是 VT_BSTRCString GetCellText(CDataGrid* pGrid, long nRow, long nCol) { pGrid-SetRow(nRow); pGrid-SetCol(nCol); VARIANT va pGrid-GetText(); // 有的包装类直接返回 CString示例按 VARIANT 写 CString strResult; if (va.vt VT_BSTR va.bstrVal ! NULL) { strResult va.bstrVal; } VariantClear(va); return strResult; }如果你打开本机生成的 CDataGrid 头文件发现 GetText 的返回值类型是 CString那就把 VARIANT 分支去掉直接 strResult pGrid-GetText()。这种差异不涉及逻辑只是类型库封装方式不同。表头的设置路径也不止一条。非绑定模式下可以借助列对象接口拿到每列再设置 Caption。例如某些包装类提供 GetColumns().GetItem(index).SetCaption(...)另一种做法是把第 0 行当作表头行用 SetText 直接写。我一般用列对象方式因为列宽、对齐、是否显示这些属性在同一条调用链上都好处理。绑定模式下则要记得表头会来自数据源的字段名你手动改的 Caption 很可能在刷新后被覆盖。只读模式是实际需求里最常用的配置。把下面三个开关全关掉网格就是一张纯展示表用户最多选中单元格改不了内容m_grid.SetAllowAddNew(FALSE); m_grid.SetAllowUpdate(FALSE); m_grid.SetAllowDelete(FALSE);如果你只是要让“某一列”不可编辑而其他列允许编辑需要在列对象层面设置 Locked 或 Enabled而不是全局关闭 AllowUpdate。这一点和普通 DataGridView 的习惯不同很多人一开始就搞反。4. 给 DataGrid 控件绑定数据源ADO 与中间控件联合使用的典型路径4.1 ADO Data Control 作为中间桥梁最小可用配置DataGrid 控件的数据绑定有两条路直接把 Recordset 赋给网格或者在对话框里放一个 ADO Data Control 6.0OLEDB让 DataGrid 的 DataSource 指向这个中间控件。直接绑 Recordset 在少部分环境下能跑通但连接字符串、游标类型、驱动版本任何一个不匹配网格就可能不显示列。我遇到的项目里九成走的是第二条路它是最不容易翻车的老方案。在对话框模板上加一个 ADO Data Control把它在界面上设为不可见然后和 DataGrid 一样做 DDX 关联// DemoDlg.h 中新增成员 CAdodc m_adodc; // DemoDlg.cpp void CDemoDlg::DoDataExchange(CDataExchange* pDX) { CDialogEx::DoDataExchange(pDX); DDX_Control(pDX, IDC_ADODC1, m_adodc); DDX_Control(pDX, IDC_DATAGRID1, m_grid); }继续在 OnInitDialog 里配置连接串和记录源注意顺序先 Refresh ADODC再把 ADODC 的接口指针设置给 DataGrid最后刷新 DataGrid。BOOL CDemoDlg::OnInitDialog() { CDialogEx::OnInitDialog(); // 1. 配置 ADO Data Control 连接 m_adodc.SetConnectionString(_T( ProviderSQLOLEDB.1; Persist Security InfoFalse; User IDsa;Password123456; Initial Catalogdemo; Data Source127.0.0.1;)); // 2. 设置记录源 m_adodc.SetRecordSource(_T( select dev_id, dev_name, status, update_time from device_list)); // 3. 先刷新 ADODC让它真正打开记录集 m_adodc.Refresh(); // 4. DataGrid 切到绑定模式并绑定数据源 m_grid.SetDataMode(1); m_grid.SetDataSource(m_adodc.GetControlUnknown()); // 5. 刷新网格让列集合从字段集合生成 m_grid.Refresh(); return TRUE; }很多人习惯把 SetDataSource 放在 Refresh 前面结果网格还是空白。原因是此时 ADODC 还没有打开记录集网格拿到的数据源是空壳。顺序必须固定为ADODC.Refresh() 成功之后再去设置 DataGrid 的 DataSource。SetDataSource 接收的是 IUnknown*底层实际是 ADODC 的 IDispatch 接口你不用关心它具体是什么 COM 对象只需要保证这个指针在前面已经成功创建。连接字符串里的密码硬编码只适合快速原型。正式一点的做法是用Data Source主机名;Initial Catalog库名;Trusted_ConnectionSSPI走 Windows 身份验证或者把连接串放到配置文件里。DataGrid 控件不会主动加密连接串数据库密码泄露问题要自己提前想好。4.2 绑定模式下的事件Click、DblClick、键盘输入与取值绑定模式最常见的交互是用户点击一行程序立刻在旁边的编辑框里显示该行关键信息。MFC 接 ActiveX 事件用的是事件映射宏如下所示BEGIN_EVENTSINK_MAP(CDemoDlg, CDialogEx) ON_EVENT(CDemoDlg, IDC_DATAGRID1, DISPID_CLICK, OnGridClick, VTS_NONE) ON_EVENT(CDemoDlg, IDC_DATAGRID1, DISPID_DBLCLICK, OnGridDblClick, VTS_NONE) END_EVENTSINK_MAP()DISPID_Click 这个宏不是你手打的是 Visual Studio 类向导根据控件类型库自动生成的。如果你在自己维护的手工代码里加事件千万不要从网上抄一串数字。最稳的做法是在类视图里选中对话框类使用事件处理程序向导选择 DataGrid 控件和 Click 事件让 IDE 把映射和函数声明都补齐。事件调度 ID 在不同 OCX 版本里可能保持一致但“保持一致”和“你可以手抄”是两回事出错时 Click 会静默失效没有任何提示。事件处理函数本身不复杂核心是从行号里取业务数据void CDemoDlg::OnGridClick() { long nRow m_grid.GetRow(); long nCol m_grid.GetCol(); // 用封装函数读取当前行第 0 列作为主键标识 CString strKey GetCellText(m_grid, nRow, 0); m_strCurrentKey strKey; SetDlgItemText(IDC_EDIT_CURRENT, strKey); } void CDemoDlg::OnGridDblClick() { // 双击默认进入就地编辑状态 // 业务上通常在这里打开一个编辑对话框 }这里有个重要的经验Click 事件给出的行号是“鼠标点击所在行”不是“当前激活编辑单元格所在行”。如果用户先用键盘方向键把高亮移动到第 10 行再用鼠标点第 3 行Click 事件里的 GetRow 一般返回第 3 行但如果你在 GetRowColChange 事件里取可能拿到的是第 10 行。为了避免这种错乱我只在 Click 里取数并且永远用当前行的唯一字段作为业务主键不把行号缓存起来跨事件使用。键盘输入事件常用于输入过滤比如用户输入一个字符时网格自动跳到第一条匹配行。这个需求可以在 KeyPress 事件里做但要记住 KeyPress 拿到的只是字符值你仍然要自己维护当前输入缓冲区。我的建议是把键盘事件用在“只读定位”而不是“即时编辑”否则和 DataGrid 自带编辑状态打架光标跳动逻辑会把你逼疯。绑定模式下如果不想让用户编辑数据第 3 章那三个 Allow 开关依然有效。如果设了只读用户双击单元格就不会弹出编辑状态。这个组合很适合做工控设备列表可以点选可以翻页但不能误改。5. DataGrid 控件避坑合集刷新、焦点、DPI、Unicode 与现场部署5.1 绑定模式下手动设置列或行后刷新全部失效现象在 OnInitDialog 里先设置了 SetColumns(5)、SetColWidth(0, ...)然后数据源 Refresh列数和列宽全部还原成字段默认值。原因绑定模式下 DataGrid 的列集合由数据源的字段集合驱动。每次 Refresh 时网格会清空旧列按 Recordset 的 Fields 重建。手动设置的列数和列宽在重建过程中被丢弃。解决不要和绑定模式对着干。如果列名要改成中文就在 SQL 里用别名例如select dev_id as 设备编号 from device_list。如果列宽要控制在网格 Refresh 完成之后再遍历列对象设置并且不要在后续再次调用会产生列重建的操作。5.2 大量行写入导致界面假死非绑定模式逐行 SetText 太慢现象非绑定模式下用 for 循环逐行 SetText 写入 1 万行数据界面卡住几十秒。原因每一行写入都触发控件内部的更新通知和重绘逻辑1 万行等于 1 万次重绘。有些版本还会同步滚动条范围、列头状态计算性能成倍恶化。解决先关闭重绘批量写入后恢复并且每 500 行主动让消息队列喘口气m_grid.SetRedraw(FALSE); for (long i 0; i nTotal; i) { // 追加一行、定位、写入各列 long nRow m_grid.GetRows(); m_grid.SetRows(nRow 1); m_grid.SetRow(nRow); for (int c 0; c nColCount; c) { m_grid.SetCol(c); m_grid.SetText(arrData[i][c]); } if (i % 500 0) { MSG msg; while (PeekMessage(msg, NULL, 0, 0, PM_REMOVE)) { TranslateMessage(msg); DispatchMessage(msg); } } } m_grid.SetRedraw(TRUE); m_grid.Invalidate();PeekMessage 循环这段不是推荐写法但对老 ActiveX 控件很有效。它的作用是把积压的 WM_PAINT 和鼠标消息处理掉避免界面看起来像“未响应”。如果数据量稳定超过 1 万行我一般直接改成绑定模式让 ADO 客户端游标管理记录集数据搬运由数据源自己完成耗时通常能降到可接受范围内。5.3 高 DPI 下列宽变窄、点击坐标错位现象开发机 96 DPI 正常现场机器 150% 缩放列宽明显偏窄点击某一行时选中的却是上一行或下一行。原因老式 ActiveX 控件的绘制和命中测试基于系统 DPI而 MFC 对话框在 Per-Monitor 缩放下会把窗口坐标进行缩放处理控件内部没有同步换算导致视觉和鼠标命中错位。解决如果整个项目不打算做 Per-Monitor 高 DPI就在应用初始化最开始调用::SetProcessDPIAware()把进程设为系统 DPI Aware让 Windows 做一次整体缩放而不是把窗口和 ActiveX 分开缩放。这个方法简单但意味着窗口在多个不同 DPI 的显示器之间拖动时不会实时缩放。如果你的交付目标是普通办公环境这个方法够用如果是设计类软件级的高分屏体验唯一正解是换掉这个 ActiveX 控件。5.4 Tab 键和方向键被网格吃掉焦点无法跳出现象对话框里只有一个 DataGrid 和一个确定按钮按 Tab 期望跳到确定按钮实际光标一直在网格单元格之间移动。原因ActiveX 控件会把键盘消息先交给内部的编辑窗口处理WM_KEYDOWN 不会按普通控件顺序返回给对话框管理器。Tab 键被网格当作单元格切换键消费掉了。解决在对话框的 PreTranslateMessage 里拦截 Tab 键手动转移焦点BOOL CDemoDlg::PreTranslateMessage(MSG* pMsg) { if (pMsg-message WM_KEYDOWN pMsg-wParam VK_TAB) { CWnd* pFocus GetFocus(); if (pFocus ! NULL pFocus-GetSafeHwnd() m_grid.GetSafeHwnd()) { GetDlgItem(IDC_BTN_OK)-SetFocus(); return TRUE; } } return CDialogEx::PreTranslateMessage(pMsg); }这个方案解决的是“从网格跳出去”的问题。如果界面上有多个控件你要按顺序把下一个控件的指针填进去。更通用的做法是维护一张对话框控件 ID 列表根据当前焦点所在的 ID 计算出下一个 ID。但要注意DataGrid 内部还有一个子编辑框处于激活状态时GetFocus 拿到的 HWND 不是 m_grid 本身会出现拦截失效。此时要判断焦点窗口是否在 m_grid 的子窗口链里比较麻烦所以能避免在同一个对话框里混排多个网格和输入框就尽量避免。5.5 现场机器报“控件未注册”或许可证错误现象开发机上编译运行都正常打包到现场机器后CreateControl 失败错误码可能是 0x80040112 或 0x800401F3甚至弹一个“没有许可证”的提示。原因MSDATAGRID.OCX 和 ADODC.OCX 没有随安装包注册或者控件在开发机上被标记为设计时许可运行时需要许可证信息才能创建。解决安装包里带上 MSDATAGRID.OCX 和 MSADODC.OCX安装结束后执行regsvr32 /s msdatgrd.ocx regsvr32 /s msadodc.ocx注意 32 位和 64 位的注册路径不同。64 位系统上32 位 OCX 要注册到 SysWOW64 目录用 32 位版 regsvr3264 位控件注册到 System32。工程编译目标从 Win32 切成 x64 时控件依赖的 OCX 也要重新注册一遍很多人只切了代码配置忘记处理 COM 注册结果运行时在同一个位置报错。许可证问题更隐蔽。如果你用的是从开发机上提取的 ActiveX 控件它可能带设计时许可运行时缺少许可会拒绝创建。老工程的常规做法是导出 .lpk 许可文件并在安装时合并到注册表或者干脆换成完全免费的替代网格控件省掉许可证依赖。6. 封装 DataGrid 控件让 ActiveX 变成你可替换的网格组件6.1 一个最小封装接口业务层不直接碰 ActiveX经过前面这些折腾你应该已经发现一个事实业务代码里到处是 SetDataMode、SetCol、GetText一旦想换控件所有界面代码都要重写。我习惯在 DataGrid 外面再套一层业务层只操作一个简单的表格接口ActiveX 的细节全部封装在内部。class CGridTable { public: CGridTable() : m_pGrid(NULL) {} void Attach(CDataGrid* pGrid) { m_pGrid pGrid; } bool IsValid() const { return m_pGrid ! NULL; } void SetColumns(const std::vectorCString vecTitles) { if (!IsValid()) return; m_pGrid-SetRedraw(FALSE); // 先设置列数再写列头。 // 具体接口名以本机 CDataGrid 包装类头文件为准。 // m_pGrid-SetColumns((long)vecTitles.size()); for (size_t i 0; i vecTitles.size(); i) { // m_pGrid-SetCol((long)i); // m_pGrid-SetText(vecTitles[i]); } m_pGrid-SetRedraw(TRUE); m_pGrid-Invalidate(); } void AppendRow(const std::vectorCString vecRow) { if (!IsValid()) return; long nRow m_pGrid-GetRows(); // m_pGrid-SetRows(nRow 1); // m_pGrid-SetRow(nRow); for (size_t c 0; c vecRow.size(); c) { // m_pGrid-SetCol((long)c); // m_pGrid-SetText(vecRow[c]); } } CString GetCell(long nRow, long nCol) { if (!IsValid()) return CString(); // 调用前面封装好的 GetCellText return GetCellText(m_pGrid, nRow, nCol); } private: CDataGrid* m_pGrid; };这段代码里我故意把部分接口调用注释掉原因是不同版本生成的 CDataGrid 包装类存在命名差异你需要在己方工程里确认后把注释恢复。封装的价值不在 API 完全统一而在于把“调用 ActiveX”的语句收敛到一个文件里。以后换自绘网格只要保留 CGridTable 的 SetColumns、AppendRow、GetCell 三个对外方法界面层基本不用动。6.2 验证封装结果和何时果断弃用 ActiveX封装完之后至少要跑三组验证第一组初始化后写入 200 行数据检查行号、列宽、表头是否一致第二组用鼠标点击第 1、50、199 行确认 GetCell 读回的内容和界面显示一致第三组把系统 DPI 调到 125% 和 150%重新打开程序确认列宽换算没有失真点击没有错位。第三组验证如果不过并且你的交付目标已经明确包含高 DPI 屏我的建议是尽早放弃 DataGrid ActiveX改用 MFC 社区里流行的自绘网格控件或者直接上 ListView 自绘。判断标准有三条第一列数会动态变化并且变化频率高第二单表数据量超过一万行且需要平滑滚动第三交付机器存在混合 DPI 多显示器的可能性。这三条占任何其中一条ActiveX 老网格都很难体面收场。做封装不是让老控件起死回生而是给自己留一条退路。我经历过一个模拟项目最初用 DataGrid 绑定了三张设备表后期客户要求加自适应列宽和行号背景色改到一半就已经抬不动了。后来把网格全部换成自绘控件界面层几乎没有改动因为所有读写操作都走 CGridTable 这一层。那段血泪经验告诉我老控件的核心价值是让你一周内交付一张可看的数据表不是让你在它身上玩出花来。真正值得投入的方向是一开始就为它做好边界留好替换的切口。希望这些经验能帮你少走几步弯路。本文还有配套的精品资源点击获取

相关新闻

多模态无监督持续后训练:视觉依赖感知框架解析

多模态无监督持续后训练:视觉依赖感知框架解析

多模态模型的持续更新一直有个很现实的问题:新数据来了,直接继续训练容易忘掉旧能力;不做训练,新场景又用不上。如果数据还没有人工标注,问题会更麻烦。这次我们看的这个框架,名字叫A Visual Dependence-Aw…

2026/10/9 10:33:01 阅读更多 →
SpringBoot+SpringCloud电商源码实战:微服务启动顺序与避坑指南

SpringBoot+SpringCloud电商源码实战:微服务启动顺序与避坑指南

简介:这是一套面向计算机相关专业在校学生与教师的电商系统课程设计/毕业设计源码包,基于Spring Boot与Spring Cloud构建,采用Spring Security、MyBatis、Redis、Docker、Elasticsearch等技术栈,并运用分布式微服务架构&#xff0…

2026/10/9 10:33:01 阅读更多 →
惠普战66拔掉耳机后扬声器无声

惠普战66拔掉耳机后扬声器无声

机型 HP ZHAN 66 Pro A 14 G4 | Windows 10 | 声卡 Realtek ALC236帖主的问题最终还是借助 Cursor 得以修复,下附 Cursor 总结的具体的问题表现、排查过程及结论,供有需要的同仁参考。一、问题描述耳机插上以后,声音正常。耳机拔掉以后&a…

2026/10/9 10:33:01 阅读更多 →

最新新闻

OpenCV颜色通道详解:从RGB原理到通道分离合并实战

OpenCV颜色通道详解:从RGB原理到通道分离合并实战

1. 颜色通道到底在折腾什么很多人学图像处理,卡在颜色通道这一关就再也没往前走。不是因为它难,而是因为一开始没搞明白“通道”到底是个什么东西。你打开一张彩色图片,看到的是红花绿叶蓝天,但在计算机眼里,它其实是三…

2026/10/9 11:11:00 阅读更多 →
claude-mem:让Claude Code拥有跨会话记忆的工程化指南

claude-mem:让Claude Code拥有跨会话记忆的工程化指南

1. 从“新同事每天入职”到“老搭档回归”:Claude Code的记忆痛点你有没有过这种经历:跟Claude Code高强度配合一个下午,把项目的目录结构、代码风格、部署链路、踩过的坑都交代清楚了,它写得又快又对,你差点以为它就是…

2026/10/9 11:11:00 阅读更多 →
WordPress客户账户管理实战:权限分级、404排查与图片故障处理

WordPress客户账户管理实战:权限分级、404排查与图片故障处理

1. 开头引言干WordPress运维这行,真正拉开差距的往往不是你会不会装个LNMP、配个缓存,而是客户账户管理做得够不够细。2026年了,客户手上握着的不只是几个插件权限,还有一整套运营后台、站点审计、应用中心、对象存储资源的访问入…

2026/10/9 11:11:00 阅读更多 →
3步做一张BI报表:从需求翻译到交互看板的实战路径

3步做一张BI报表:从需求翻译到交互看板的实战路径

1. 为什么“3步做一张BI报表”不是标题党,而是真实可行的路径很多人第一次听到“3步做一张BI报表”,第一反应是“又来了,营销号标题”。我一开始也这么想。但真正在数据岗摸爬滚打几年之后,我发现这个说法其实相当克制——如果你把…

2026/10/9 11:11:00 阅读更多 →
飞鲲GEO案例拆解:一家装备厂的AI问答翻身路径

飞鲲GEO案例拆解:一家装备厂的AI问答翻身路径

一、企业数字资产的GEO价值的四个常见问题不少制造型企业发现,官网内容在传统搜索里排名尚可,但在豆包、DeepSeek等AI问答里却很少被引用,甚至被竞争对手的泛泛内容盖过。问题出在哪?第一,企业内容缺乏结构化数据支撑&…

2026/10/9 11:10:59 阅读更多 →
等保2.0数据库测评通关指南:MySQL/Oracle/SQL Server/PostgreSQL/Redis五类数据库加固与自查

等保2.0数据库测评通关指南:MySQL/Oracle/SQL Server/PostgreSQL/Redis五类数据库加固与自查

简介:这份作业指导书面向数据库安全测评人员、等保合规工程师及运维人员,系统梳理了MySQL、Oracle、SQL Server、Postgres、Redis五类主流数据库在等保测评中的实操要点,帮助读者快速定位各数据库的测评项与查询方法。资源包内含1个docx文档&…

2026/10/9 11:09:59 阅读更多 →

日新闻

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API这个话题,隔三差五就会在群里被翻出来讨论一次。上周还有个同事线上处理一个订单超时问题,排查到最后发现是ZonedDateTime序列化后时区丢了,用户在下单当天晚上看到的时间整整差了8个小时。这类问题几乎每个做Java开发的人都遇到过…

2026/10/9 0:00:49 阅读更多 →
EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

前几个月我手头有好几台机器需要互相访问:办公室台式机、家里 NAS、还有一台云主机。如果只是偶尔传个文件倒还好,问题是工作场景经常要在几处环境之间来回切换,每次都先登录跳板机再层层代理,实在折腾。我先后试过端口映射、自建…

2026/10/9 0:00:49 阅读更多 →
AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent 这个词在过去一年里被反复提及,但真正动手搭过一套能跑起来的 Agent 系统的人都知道,从"知道它是什么"到"让它稳定干活"之间隔着一整套工程决策。我前后参与过几个 Agent 项目的落地,从最初用现成框架拼装&…

2026/10/9 0:01:50 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:32 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:40 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 10:11:06 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 21:13:17 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 6:17:20 阅读更多 →