ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

MFC CListCtrlEx 实现任意单元格原地编辑:封装思路与避坑指南

MFC CListCtrlEx 实现任意单元格原地编辑:封装思路与避坑指南 简介面向MFC界面开发者的CListCtrlEx控件扩展示例包演示如何在高列表行中嵌入进度条、编辑框与复选框解决标准CListCtrl交互单一、无法直接展示复合数据的问题。包体共22个文件包含7个头文件、5个源文件以及工程配置、资源脚本、解决方案文件等覆盖控件派生、消息映射、子控件创建与数据绑定环节压缩包约4.41MB适合本地对照阅读也可作为二次开发的基础模板。目前已有301人学习下载。通过源码可掌握CListCtrlEx派生类的搭建方式学习在OnCreate中挂接进度条、编辑框和复选框处理BN_CLICKED等事件并理解列表项内嵌编辑与复选联动的实现思路还可直接复用其中控件布局、消息处理和资源定义写法为自研MFC列表视图提供可落地的参考。适合正在做MFC列表增强或希望提升界面交互性的开发者。1. 双击表格却改不了数据CListCtrlEx 解决的是 MFC 里那个老掉牙的痛点做 MFC 桌面工具的人十有八九遇到过这个场景列表控件CListCtrl里排了一堆设备台账或参数用户双击某个单元格只想改一个数值结果程序只能弹出一个对话框让他填完再点确定。一次两次能忍几百条数据录入时这种打断感能把人逼疯。CListCtrlEx 就是从 CListCtrl 派生出来的扩展类核心目标就是让任意单元格支持「原地编辑」双击弹出一个内嵌的编辑框回车或失焦后自动写回列表。它适合维护老 MFC 工程、不想为一张表把整个项目推倒重写成 Qt 的人也适合正在新写桌面工具、需要列表快速支持录入、复选和自绘的从业者。这个封装不花哨但它能把表格交互从「弹窗确认」拉回「所见即所得」。2. 原生 CListCtrl 的编辑短板以及 CListCtrlEx 的封装思路2.1 原生控件不给任意单元格编辑弹窗是下策MFC 的 CListCtrl 不是完全没有编辑能力它有个标签编辑Label Edit机制前提是给控件加上 LVS_EDITLABELS 样式并且用户只能编辑第一列、整行的标签双击其他列没有任何反应。这个机制本意是给资源管理器那种「重命名文件」用的硬扛到数据录入场景里完全不对路。退一步说就算你接受弹窗方案还得自己维护当前选中行、把编辑后的值回写、处理用户中途取消弹窗代码和主界面代码来回传数据时间一长就是一团乱麻。所以「继承 CListCtrl、自己挂一个编辑框进去」成了 MFC 圈子里最主流的做法CListCtrlEx 这个命名也约定俗成地指向这类扩展类。2.2 三条常见封装路线重写消息、内嵌 CEdit、动态创建我见过三种实现路线。第一种是重写鼠标和键盘消息双击时判断点击的是第几行第几列然后创建一个 CEdit 子窗口覆盖在目标单元格上失焦或回车时把值写回去再销毁编辑框。第二种是子类化 CEdit让它自带上交、取消逻辑这样编辑框的行为可以复用。第三种是干脆用自定义绘制Owner Draw模拟编辑状态点击时画一个像编辑框的矩形实际上还是弹真实的 CEdit。三种路线里第一种最直观、依赖最少也是 CListCtrlEx 最常见的形态第二种是对第一种的增强解决了回车键被对话框抢走这类问题第三种绘制工作量偏大一般只在需要特殊外观时才用。我一般会直接用「继承 内嵌 CEdit」的组合不要为了炫技把简单事做复杂。2.3 一个最小可用的 CListCtrlEx 类骨架网上流传的 CListCtrlEx.rar 里绝大多数解压出来就是两个文件一个 .h 一个 .cpp类本身不超过三百行。核心结构其实很简单不必把它当成黑匣子。头文件里要做的就是把基类换成 CListCtrl再声明几个编辑相关的成员// CListCtrlEx.h #pragma once #include afxcmn.h class CListCtrlEx : public CListCtrl { public: CListCtrlEx(); virtual ~CListCtrlEx(); // 对外接口进入或结束单元格编辑 void StartEdit(int nRow, int nCol); void EndEdit(BOOL bCommit TRUE); protected: CEdit* m_pEdit; // 内嵌的编辑框指针 int m_nEditRow; // 正在编辑的行号 int m_nEditCol; // 正在编辑的列号 BOOL m_bEditing; // 是否处于编辑状态 afx_msg void OnLvnItemActivate(NMHDR* pNMHDR, LRESULT* pResult); afx_msg void OnHdnEndtrack(NMHDR* pNMHDR, LRESULT* pResult); DECLARE_MESSAGE_MAP() };// CListCtrlEx.cpp #include pch.h #include CListCtrlEx.h BEGIN_MESSAGE_MAP(CListCtrlEx, CListCtrl) ON_NOTIFY_REFLECT(LVN_ITEMACTIVATE, CListCtrlEx::OnLvnItemActivate) ON_NOTIFY_REFLECT(HDN_ENDTRACK, CListCtrlEx::OnHdnEndtrack) END_MESSAGE_MAP() void CListCtrlEx::StartEdit(int nRow, int nCol) { EndEdit(TRUE); // 如果上一格还没提交先提交掉 if (nRow 0 || nCol 0 || GetItemCount() nRow) return; CRect rcCell; GetSubItemRect(nRow, nCol, LVIR_BOUNDS, rcCell); if (m_pEdit nullptr) m_pEdit new CEdit(); m_pEdit-Create(WS_CHILD | WS_VISIBLE | WS_BORDER | ES_AUTOHSCROLL | ES_LEFT, rcCell, this, 0x9901); m_pEdit-SetFont(GetFont()); m_pEdit-SetWindowText(GetItemText(nRow, nCol)); m_pEdit-SetFocus(); m_nEditRow nRow; m_nEditCol nCol; m_bEditing TRUE; }逻辑说明StartEdit 第一件事是调用 EndEdit(TRUE)避免上一次编辑还没提交就被新编辑覆盖。GetSubItemRect 用 LVIR_BOUNDS 标志拿到单元格矩形这是定位编辑框的关键拿到的坐标是相对于列表客户区的。创建 CEdit 时用的四个样式各有讲究WS_CHILD 表示子窗口、WS_VISIBLE 立刻显示、WS_BORDER 给编辑框画边框、ES_AUTOHSCROLL 允许横向滚动最后一个 ES_LEFT 是文本左对齐。控件 ID 我用了固定值 0x9901如果你工程里已经有资源使用了这个 ID就换成别的。OnLvnItemActivate 和 OnHdnEndtrack 分别是双击进入编辑、拖完列头后重新定位编辑框的入口后面章节会展开。这里要注意一点CEdit 的 Create 必须传父窗口指针为 this也就是列表控件自己而不是对话框否则编辑框的坐标系和列表消息都对不上。3. 在现有 VS MFC 工程里接入 CListCtrlEx控件变量、状态栏联动3.1 把类文件加进工程的三种常见做法拿到 CListCtrlEx 的 .h 和 .cpp或者你自己照着上一步写了一份之后接入工程有三种做法。第一种是把两个文件直接拖进 Visual Studio 的解决方案资源管理器编译即可最简单但文件会落在工程根目录。第二种是在「类向导 → 添加类 → MFC 类」里新建一个基类为 CListCtrl 的类再把上一步的代码拷进去这样做的好处是 Visual Studio 能自动管理消息映射适合需要继续扩展的工程。第三种是手动把这两个文件加入 .vcxproj 文件列表适合用 CMake 或非 VS 工具链维护的工程。注意一点如果工程开了预编译头CListCtrlEx.cpp 第一行必须是 #include pch.h老工程可能是 stdafx.h不然会报一堆 C 标准头文件找不到的错。我一般会用第一种直接把文件拖进去简单直接。类的扩展一般不会很大没有必要让类向导去管理它。3.2 用 DDX_Control 把对话框里的 CListCtrl 换掉假设你的对话框资源里已经放了一个 List Control 控件ID 是 IDC_LIST_DEVICE现在要把它的关联变量从 CListCtrl 换成 CListCtrlEx。先在对话框头文件里改声明// CMyDialog.h class CMyDialog : public CDialogEx { // ... CListCtrlEx m_list; // 原来是 CListCtrl m_list; };然后到 DoDataExchange 里DDX_Control 那一行不需要改DDX 机制只看控件 ID不看变量类型。但这里有一个 VS 的坑你用类向导重新绑定变量时它会把类型强制写成 CListCtrl如果你在向导里直接点完成它会把头文件里的 CListCtrlEx 改回去。我建议的做法是头文件先手写 CListCtrlExDoDataExchange 里的 DDX_Control 行保持原样绝不要用向导重新绑定同一个控件。3.3 初始化列表样式并把选中行信息显示到状态栏接入之后就是常规的列表初始化。CListCtrlEx 继承自 CListCtrl所有 CListCtrl 的样式设置方法原样可用。常见的初始化代码长这样// OnInitDialog 里 m_list.SetExtendedStyle( LVS_EX_FULLROWSELECT | // 整行选中 LVS_EX_GRIDLINES | // 网格线 LVS_EX_DOUBLEBUFFER); // 双缓冲减少刷新闪烁 m_list.InsertColumn(0, _T(设备名), LVCFMT_LEFT, 120); m_list.InsertColumn(1, _T(IP 地址), LVCFMT_LEFT, 140); m_list.InsertColumn(2, _T(状态), LVCFMT_CENTER, 80); m_list.InsertItem(0, _T(PLC-01)); m_list.SetItemText(0, 1, _T(192.168.0.10)); m_list.SetItemText(0, 2, _T(在线));说明LVS_EX_FULLROWSELECT 让用户点任意列都能选中整行这对后续做状态栏联动很重要。LVS_EX_GRIDLINES 显示网格线视觉上更像表格但要注意它和自绘排序箭头配合时网格线会盖掉箭头后面第 6 章会说。LVS_EX_DOUBLEBUFFER 在 XP 之后的系统上能明显减少刷新闪烁。状态栏联动是 MFC 里很常见的诉求选中列表里某一行窗口底部的状态栏立刻显示这行的关键信息。做法是在对话框里接 LVN_ITEMCHANGED 通知// 消息映射里加 // ON_NOTIFY(LVN_ITEMCHANGED, IDC_LIST_DEVICE, CMyDialog::OnLvnItemchangedListDevice) void CMyDialog::OnLvnItemchangedListDevice(NMHDR* pNMHDR, LRESULT* pResult) { LPNMLISTVIEW pNMLV reinterpret_castLPNMLISTVIEW(pNMHDR); CString strStatus _T(未选中); if ((pNMLV-uChanged LVIF_STATE) (pNMLV-uNewState LVIS_SELECTED)) { strStatus.Format(_T(第 %d 行%s | %s | %s), pNMLV-iItem, m_list.GetItemText(pNMLV-iItem, 0), m_list.GetItemText(pNMLV-iItem, 1), m_list.GetItemText(pNMLV-iItem, 2)); } m_wndStatusBar.SetPaneText(0, strStatus); *pResult 0; }这里的关键是 pNMLV-uChanged 要按位判断 LVIF_STATE因为 LVN_ITEMCHANGED 在勾选、选中、焦点变化时都会触发不判断就会反复刷状态栏。SetPaneText 的第一个参数是状态栏窗格索引0 就是最左边那个窗格如果你在状态栏资源里只放了一个 pane用它就对了。这套写法也回答了那个经典问题「mfc 状态栏怎么显示信息」状态栏本质上就是一个 CStatusBar任何时刻你想让它显示什么直接 SetPaneText 就行不用额外创建窗口。4. 双击单元格弹出编辑框消息映射、编辑框参数与数据回写4.1 从 LVN_ITEMACTIVATE 到显示编辑框的完整流程双击进入编辑入口是 LVN_ITEMACTIVATE 通知它在用户双击列表项时触发。CListCtrlEx 内部通过 ON_NOTIFY_REFLECT 反射处理它不需要对话框参与。处理函数里拿到行号和列号交给 StartEditvoid CListCtrlEx::OnLvnItemActivate(NMHDR* pNMHDR, LRESULT* pResult) { LPNMITEMACTIVATE pNMIA reinterpret_castLPNMITEMACTIVATE(pNMHDR); if (pNMIA-iItem 0 pNMIA-iSubItem 0) StartEdit(pNMIA-iItem, pNMIA-iSubItem); *pResult 0; }逻辑说明pNMIA-iItem 是被双击的行号pNMIA-iSubItem 是列号注意列号从 1 开始是数据列0 是第一列通常叫主项列。这个判断兜底了空区域双击的情况。拿到行列后调 StartEdit上一步的类骨架里已经实现了创建编辑框和定位。有一点要注意双击默认会先触发单击选中再触发 LVN_ITEMACTIVATE所以 StartEdit 执行时行数据已经处于选中状态这是预期行为。4.2 CEdit 创建参数与定位细节字体、边框、坐标转换编辑框能不能准确盖住单元格取决于两个细节一是 Create 时传入的 rcCell 矩形必须来自 GetSubItemRect 且标志是 LVIR_BOUNDS二是编辑框字体必须和列表控件一致。第一个细节如果写错标志比如用了 LVIR_LABEL矩形会缩小成文本区域而不是整格边框只框住文字视觉上很怪。第二个细节尤其容易踩如果不 SetFont(GetFont())编辑框会用系统默认 UI 字体在 125% 缩放的屏幕上会比单元格内容大一圈。以下是创建编辑框的标准姿势m_pEdit-Create( WS_CHILD | WS_VISIBLE | WS_BORDER | ES_AUTOHSCROLL | ES_LEFT, rcCell, this, 0x9901); m_pEdit-SetFont(GetFont()); m_pEdit-SetWindowText(GetItemText(nRow, nCol)); m_pEdit-SetSel(0, -1); // 全选文本方便直接覆盖输入 m_pEdit-SetFocus();参数说明WS_BORDER 决定是否有边框如果你后面要自绘边框可以去掉ES_AUTOHSCROLL 在内容超过列宽时让光标能横向滚动不加的话超出部分会被截断显示SetSel(0, -1) 是关键细节进入编辑时把文本全选用户直接打字就会覆盖旧值不用先按 DeleteSetFocus 让光标进编辑框。创建完成后记得把 CEdit 指针保存为成员变量不然 MFC 的消息路由会找不到这个子窗口输入法和快捷键都会失效。4.3 提交与取消KILLFOCUS、回车、Esc 三路收口编辑框的生命周期管理是最容易出 bug 的部分。常见的收口方式有三种失焦提交、回车提交、Esc 取消。失焦提交是底保保证用户点别的地方时数据不会丢回车和 Esc 是显式操作。要让回车和 Esc 生效必须子类化 CEdit因为默认情况下回车键会被对话框当成默认按钮处理这就是后面避坑章要展开的问题。先给一个 CEditCell 子类的骨架// CEditCell.h #pragma once #include afxwin.h class CEditCell : public CEdit { public: CListCtrlEx* m_pOwner; // 指向宿主列表用于回调提交/取消 afx_msg void OnKillFocus(CWnd* pNewWnd); virtual BOOL PreTranslateMessage(MSG* pMsg); DECLARE_MESSAGE_MAP() };// CEditCell.cpp BEGIN_MESSAGE_MAP(CEditCell, CEdit) ON_WM_KILLFOCUS() END_MESSAGE_MAP() BOOL CEditCell::PreTranslateMessage(MSG* pMsg) { if (pMsg-message WM_KEYDOWN) { if (pMsg-wParam VK_RETURN) { if (m_pOwner) m_pOwner-EndEdit(TRUE); // 回车提交 return TRUE; // 吞掉消息 } if (pMsg-wParam VK_ESCAPE) { if (m_pOwner) m_pOwner-EndEdit(FALSE); // Esc 取消 return TRUE; } } return CEdit::PreTranslateMessage(pMsg); } void CEditCell::OnKillFocus(CWnd* pNewWnd) { // 失焦时提交但如果是因为编辑框被销毁而失焦要跳过 if (m_pOwner ::IsWindow(GetSafeHwnd())) m_pOwner-EndEdit(TRUE); CEdit::OnKillFocus(pNewWnd); }逻辑说明PreTranslateMessage 是 MFC 消息流转的必经路口在消息到达对话框的默认按钮逻辑之前拦截 VK_RETURN 和 VK_ESCAPE。return TRUE 表示消息已处理不再往下传。OnKillFocus 里加了个 IsWindow 判断是因为 EndEdit 里销毁编辑框时会触发二次失焦不做判断就会递归。实际工程里还可以加一个标志位 m_bDestroying 做防重入保护我一般在 EndEdit 里先 Detach 窗口再 delete 对象从根上规避这个问题。4.4 哪些列允许编辑用可编辑列掩码控制不是每个单元格都该被编辑比如第一列是设备编号第三列是状态这些通常要禁止修改。常见做法是给 CListCtrlEx 加一个可编辑列集合StartEdit 里先检查列号是否允许void CListCtrlEx::SetEditableCols(const std::vectorint cols) { m_editableCols cols; // 比如 {1, 2}表示第 1、2 列可编辑 } bool CListCtrlEx::IsCellEditable(int nCol) const { if (m_editableCols.empty()) return true; // 默认全部可编辑 for (int c : m_editableCols) if (c nCol) return true; return false; } // StartEdit 开头加 if (!IsCellEditable(nCol)) return;参数说明m_editableCols 用 vector 保存允许编辑的列索引空 vector 表示全放行兼顾了初始化顺序问题——有时列表还没 SetEditableCols 就被双击了。这种掩码控制比硬编码列号可维护得多以后加一列可编辑字段只要改数组。注意列索引的语义和 pNMIA-iSubItem 一致0 是第一列1 是第二列别搞混。5. CListCtrlEx 避坑要点编辑框闪烁、回车弹窗、滚动错位5.1 双击后编辑框一闪而过现象双击单元格编辑框闪了一下就消失了数据没改成。原因StartEdit 里创建 CEdit 后马上 SetFocus但双击动作本身会让列表控件获得焦点两个焦点切换事件先后到达触发 OnKillFocus 里的 EndEdit(TRUE)编辑框被立刻销毁。解决进入编辑前先记录状态KillFocus 回调里判断「如果编辑框是被列表自己的双击事件创建的给它一个短暂豁免期」。最简单的做法是用 PostMessage 延迟创建编辑框让当前双击消息处理完再弹编辑框例如在 OnLvnItemActivate 里 PostMessage 一个自定义消息收到后再调 StartEdit。这个玄学问题当年让我卡了一下午现在我对所有「闪一下就没」的窗口问题都先用 PostMessage 试延迟创建治好了八成。5.2 回车不提交反而把对话框的默认按钮点了现象双击进入编辑输入内容后按回车编辑框的值没有写回反而关掉了对话框或触发了确定按钮逻辑。原因MFC 对话框会把回车键路由到 IDOK 按钮除非当前焦点控件拦截了它。你的 CEdit 没有子类化时WM_KEYDOWN 根本轮不到编辑框处理。解决给编辑框挂 CEditCell 子类在 PreTranslateMessage 里拦截 VK_RETURN 并调用 EndEdit见 4.3 节的代码。额外的经验如果你不想为编辑框单独写一个类也可以在宿主 CListCtrlEx 里重写 PreTranslateMessage判断当前子窗口是不是编辑框再处理但代码分散不如 CEditCell 干净。5.3 滚动条拖动后编辑框还停在原来的位置现象正在编辑某个单元格时拖动列表滚动条编辑框不跟随滚动留在原地和下面的行错位。原因CEdit 是子窗口列表滚动时单元格矩形变了但没有人通知编辑框重新定位。解决在 CListCtrlEx 里处理 WM_VSCROLL 和 WM_HSCROLL滚动后重新取 GetSubItemRect 并 SetWindowPos。代码示意void CListCtrlEx::OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar) { CListCtrl::OnVScroll(nSBCode, nPos, pScrollBar); if (m_bEditing m_pEdit ::IsWindow(m_pEdit-GetSafeHwnd())) { CRect rcCell; GetSubItemRect(m_nEditRow, m_nEditCol, LVIR_BOUNDS, rcCell); m_pEdit-SetWindowPos(nullptr, rcCell.left, rcCell.top, rcCell.Width(), rcCell.Height(), SWP_NOZORDER | SWP_NOACTIVATE); } }注意OnVScroll 里必须先调父类实现让列表真正滚动然后再重新定位编辑框顺序反了拿到的是滚动前的矩形。如果你允许用户在编辑状态下滚动列表这个处理必须加如果编辑框弹出后你禁用了滚动条这段可以不写但对用户不友好不建议这么干。5.4 拖动列宽后编辑框错位点开的还是旧列现象正在编辑时用鼠标拖动列头边线列宽变了编辑框尺寸没跟上文字被截断或露出白边。原因列头拖动结束时发送 HDN_ENDTRACK 通知列表列的物理宽度已经变化但没有代码重新计算编辑框矩形。解决在类骨架里的 OnHdnEndtrack 中重新定位或直接结束编辑void CListCtrlEx::OnHdnEndtrack(NMHDR* pNMHDR, LRESULT* pResult) { LPNMHEADER pNMH reinterpret_castLPNMHEADER(pNMHDR); if (m_bEditing m_pEdit ::IsWindow(m_pEdit-GetSafeHwnd())) { // 如果拖动的列正好是正在编辑的列直接提交退出 if (pNMH-iItem m_nEditCol) EndEdit(TRUE); // 如果是其他列只重新定位即可 else { CRect rcCell; GetSubItemRect(m_nEditRow, m_nEditCol, LVIR_BOUNDS, rcCell); m_pEdit-SetWindowPos(nullptr, rcCell.left, rcCell.top, rcCell.Width(), rcCell.Height(), SWP_NOZORDER | SWP_NOACTIVATE); } } *pResult 0; }参数说明pNMH-iItem 是被拖动的列索引正好等于正在编辑列时直接提交是因为列宽变化后文本显示区域变化继续编辑容易把用户搞蒙拖其他列时编辑框还在只是换个位置。EndEdit(TRUE) 会把旧值写回随后用户双击重新进入编辑即可。这里顺带提醒HDN_ENDTRACK 是 Header Control 的通知不用 ON_NOTIFY_REFLECT 接收的话会从列表控件反射到父窗口容易漏。5.5 高 DPI 下编辑框文本和单元格对不齐现象在 125% 或 150% 缩放的显示器上编辑框里的文字比列表单元格里的文字大一号边框位置也有偏移。原因列表控件的字体由系统按 DPI 缩放渲染而新建的 CEdit 如果直接用 Create 默认字体不会自动继承列表的字体和缩放信息。解决创建编辑框后必须 SetFont(GetFont())这是 4.2 节强调过的另外如果你的程序在清单里声明了 PerMonitorV2 DPI 感知GetFont 返回的字体句柄已经带缩放过直接 Set 即可不需要额外改字号。这个坑回报率很高一行代码解决的是看起来特别劣质的高分屏显示问题。6. 让 CListCtrlEx 更顺手排序、复选与类型化编辑6.1 表头点击排序LVN_COLUMNCLICK 的排序状态机列表一旦能编辑用户就忍不住想排序。常见做法是在 LVN_COLUMNCLICK 通知里切换排序方向再用 CListCtrl::SortItems 配合比较回调int CALLBACK CompareRows(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort) { CListCtrlEx* pList (CListCtrlEx*)lParamSort; int nCol pList-GetSortColumn(); BOOL bAsc pList-GetSortAscending(); CString str1 pList-GetItemText((int)lParam1, nCol); CString str2 pList-GetItemText((int)lParam2, nCol); int nResult str1.CompareNoCase(str2); // 字母/数字混合可以用 StrCmpLogicalW return bAsc ? nResult : -nResult; }注意SortItems 的 lParam1/lParam2 是行的 LPARAM不是行号所以插入行时要把行号写进 SetItemData。比较数值列时别用字符串比较先 _ttoi 转成整数再比否则「10」会排在「2」前面。排序方向用一个 BOOL 成员记录点击同一列时反转点击新列时重置为升序。6.2 LVS_EX_CHECKBOXES 实现批量操作给列表加上复选框是最快的批量交互方案SetExtendedStyle 里加上 LVS_EX_CHECKBOXES然后通过 GetCheck/SetCheck 读写勾选状态。注意 LVS_EX_CHECKBOXES 只对第一列生效列表视图会把它当成复选框列你的第一列不能再放数据。批量操作的典型写法是遍历所有行收集勾选行做动作for (int i 0; i m_list.GetItemCount(); i) { if (m_list.GetCheck(i)) { // 比如批量启动/停止设备 } }6.3 我的一个习惯编辑框字体永远继承列表控件我做了几年 MFC 维护最大的一个教训就是凡是动态创建的控件第一行属性设置永远是 SetFont(GetFont())第二行才是 SetWindowText。一开始偷懒想省事结果在高分屏和不同 Windows 版本上字体忽大忽小用户截图过来全是错位最后花的时间比老老实实写这一行多得多。另外一个习惯是编辑框控件 ID 一定要固定且唯一不要用 -1 或 0否则消息反射偶尔会把消息送给父对话框导致焦点乱跳。CListCtrlEx 这类封装说穿了就是把别人踩过的坑提前替你踩一遍双击进编辑、回车提交、失焦兜底、滚动同步。你不需要把它想得多高深按着这套结构在自己的 MFC 工程里搭一次以后任何表格录入场景都能往里套。希望帮到你。本文还有配套的精品资源点击获取
返回列表