1. 项目概述与核心价值
在桌面应用开发,特别是那些需要处理大量结构化数据(比如设备管理、库存系统、参数配置工具)的场景里,我们经常会遇到一个经典需求:在一个表格里,不仅要能展示数据,还要能方便地编辑它。最常见的编辑方式之一,就是下拉选择。想象一下,你正在开发一个员工信息管理系统,在“部门”这一列,你肯定不希望用户手动输入“技术部”、“市场部”这些容易打错的名字,而是希望他们从一个预设好的列表里选。这个需求听起来简单,但如果你用的是经典的 MFC(Microsoft Foundation Classes)框架,并且用的是那个功能强大但“原始”的CListCtrl或者第三方网格控件CGridCtrl,就会发现系统并没有直接提供一个“表格里嵌下拉框”的控件。
这就是“GridCtrl与Combo组合控件”项目要解决的核心痛点。它不是一个简单的教学示例,而是一个实战导向的解决方案。CGridCtrl是一个在MFC开发者中广泛使用的开源网格控件,比标准的CListCtrl功能强得多,支持单元格编辑、合并、格式化等。但它的编辑功能默认是文本框。本项目要做的,就是深度定制CGridCtrl,使其特定单元格在进入编辑模式时,弹出来的不是一个输入框,而是一个完整功能的下拉列表框(CComboBox)。这不仅仅是把两个控件摆在一起,而是要解决焦点管理、消息路由、数据同步、UI刷新等一系列底层问题。
对于正在维护或开发基于MFC的遗留系统,或者因某些原因(如对安装包体积、运行时依赖有严格要求)必须使用MFC的开发者来说,这个项目具有直接的工程价值。它避免了引入庞大复杂的第三方UI库(如Qt),在保持MFC轻量级特性的同时,显著提升了数据录入界面的用户体验和健壮性。接下来,我将拆解实现这一功能的全过程,从设计思路到代码细节,再到实际踩过的坑。
2. 核心设计思路与架构拆解
在MFC中实现一个“活”的、嵌入在单元格内的组合框,远比在WinForm或WPF中拖一个DataGridViewComboBoxColumn复杂。因为MFC的文档-视图架构和消息泵机制,要求我们对控件的生命周期和消息流有更精细的控制。不能简单地在网格上覆盖一个Combo控件,那样会带来焦点混乱、滚动时控件位置错位、键盘消息无法正确处理等问题。
2.1 为什么选择继承与自定义绘制路径
最直接的想法可能是:在需要编辑时,动态创建一个CComboBox控件,将其移动到当前单元格的位置并显示。这个思路没错,但关键在于如何让这个动态创建的Combo控件与GridCtrl无缝融合。经过实践,我排除了两种简单但问题多的方案:
方案一:子类化
CGridCellBase并重载Edit函数。- 思路:
CGridCtrl的每个单元格都是一个CGridCellBase或其派生类的对象。其Edit函数负责创建编辑控件。 - 问题:原生的
CGridCellBase的Edit实现是针对单行文本框(CEdit)的。我们需要创建的是CComboBox,其创建参数、样式、消息映射完全不同。直接修改基类或重写一个通用的Edit会很臃肿,且破坏原有逻辑。
- 思路:
方案二:在
CGridCtrl的OnLButtonDown中直接创建 Combo。- 思路:捕获网格的点击事件,判断点击的单元格,然后在那个位置弹出 Combo。
- 问题:控件管理混乱。这个 Combo 不属于任何一个单元格对象,其数据、状态与单元格模型的同步需要额外维护,代码耦合度高,不易复用。
最终采用的方案是:创建一个专用的CGridCellCombo类。
这个方案的核心是组合与委托:
CGridCellCombo继承自CGridCell(它是CGridCellBase的常用派生类,支持文本编辑)。- 在
CGridCellCombo内部,它并不直接“拥有”一个CComboBox控件。相反,它持有一个数据源(如下拉选项列表)和一个当前选中值的索引。 - 当
CGridCtrl需要编辑这个单元格时,CGridCellCombo::Edit方法被调用。此时,它通知父网格控件(CGridCtrl):“我需要一个Combo编辑器”。 CGridCtrl接收到这个请求,动态创建并管理一个CComboBox控件,将其精准定位到该单元格上,并填充CGridCellCombo提供的数据源。- 编辑完成后,
CGridCtrl将 Combo 的选中结果回传给CGridCellCombo对象更新其内部状态,并销毁这个临时的CComboBox控件。
这样做的好处是职责分离:CGridCellCombo只关心数据(选项是什么,当前选哪个),而CGridCtrl负责编辑器控件的UI生命周期和消息处理。这种架构清晰,也便于未来扩展其他类型的编辑器(如日期选择器、颜色选择器)。
2.2 关键类与消息流设计
整个项目主要涉及三个部分的协作:
CGridCellCombo(自定义单元格类):- 属性:存储字符串列表
CStringArray m_aryItems;当前选中索引int m_nSelItem。 - 关键方法:
SetOptions(const CStringArray& items): 设置下拉选项。GetCurSel() / SetCurSel(int nIndex): 获取/设置当前选择。virtual BOOL Edit(...):重写编辑入口,向Grid发送自定义消息或调用Grid的方法来请求创建编辑器。
- 属性:存储字符串列表
CGridCtrl(网格控件,需要增强):- 增强方法:
CreateInPlaceComboCtrl:根据传入的单元格坐标和选项列表,创建、定位并显示CComboBox。OnComboClose:处理 Combo 下拉框关闭(通过选择项或取消)的消息,将结果写回单元格,并销毁编辑器。
- 消息处理:需要处理
WM_LBUTTONDOWN(启动编辑)、WM_KILLFOCUS(编辑器失去焦点时结束编辑),以及自定义的WM_GRID_CELL_EDIT_COMBO消息(由CGridCellCombo::Edit发出)。
- 增强方法:
临时的
CComboBox编辑器:- 由
CGridCtrl在堆上创建(new CComboBox)。 - 样式通常包含
CBS_DROPDOWNLIST(禁止输入,仅选择)和WS_CHILD | WS_VISIBLE | WS_VSCROLL。 - 其父窗口设置为
CGridCtrl本身,以便正确接收消息。 - 需要子类化以捕获
CBN_CLOSEUP或CBN_SELENDOK消息,并通知CGridCtrl完成编辑。
- 由
消息流时序:
- 用户点击一个
CGridCellCombo类型的单元格。 CGridCtrl::OnLButtonDown被触发,调用该单元格的Edit方法。CGridCellCombo::Edit方法向CGridCtrl发送一个请求(如调用GetGrid()->CreateInPlaceComboCtrl(this))。CGridCtrl::CreateInPlaceComboCtrl执行:- 计算单元格的屏幕坐标(需考虑网格滚动偏移)。
new一个CComboBox,调用Create创建窗口。- 将
m_aryItems填充到 Combo 中 (AddString)。 - 设置当前选中项 (
SetCurSel)。 - 显示并下拉 (
ShowDropDown(TRUE))。
- 用户操作 Combo 并选择一项。
- Combo 子类窗口处理
CBN_SELENDOK,通知CGridCtrl::OnComboClose。 OnComboClose从 Combo 获取GetCurSel()和GetLBText,调用CGridCellCombo::SetCurSel和SetText更新单元格数据。OnComboClose销毁 Combo 编辑器窗口 (DestroyWindow并delete)。CGridCtrl重绘该单元格,显示新的文本。
3. 核心实现细节与代码剖析
理解了架构,我们深入到代码层面。这里会省略一些基础的MFC框架代码,聚焦于关键函数的实现。
3.1 CGridCellCombo 类的实现
首先,我们定义自己的单元格类。头文件GridCellCombo.h大致如下:
#pragma once #include "GridCell.h" // 假设这是原版GridCtrl的单元格基类 class CGridCellCombo : public CGridCell { DECLARE_DYNCREATE(CGridCellCombo) public: CGridCellCombo(); virtual ~CGridCellCombo(); // 设置下拉框选项 void SetOptions(const CStringArray& aryItems); const CStringArray& GetOptions() const { return m_aryItems; } // 设置/获取当前选中索引 int GetCurSel() const { return m_nSelItem; } void SetCurSel(int nIndex); // 重写关键虚函数 virtual BOOL Edit(int nRow, int nCol, CRect rect, CPoint point, UINT nID, UINT nChar); virtual void EndEdit(); virtual BOOL Draw(CDC* pDC, int nRow, int nCol, CRect rect, BOOL bEraseBkgnd = TRUE); // 序列化支持(如果需要保存网格状态) virtual void Serialize(CArchive& ar); protected: CStringArray m_aryItems; // 存储所有选项 int m_nSelItem; // 当前选中项的索引,-1表示无选择 CString m_strText; // 当前显示的文本(缓存,避免每次从列表取) DECLARE_MESSAGE_MAP() };源文件GridCellCombo.cpp中的Edit方法是核心,它启动了编辑流程:
BOOL CGridCellCombo::Edit(int nRow, int nCol, CRect rect, CPoint point, UINT nID, UINT nChar) { // 首先调用基类,进行一些基础设置(如设置编辑状态标志) if (!CGridCell::Edit(nRow, nCol, rect, point, nID, nChar)) return FALSE; // 获取父网格控件 CGridCtrl* pGrid = GetGrid(); if (!pGrid || !pGrid->GetSafeHwnd()) return FALSE; // 通知网格控件为此单元格创建Combo编辑器。 // 这里假设我们在CGridCtrl中扩展了一个公有方法。 // 另一种更解耦的方式是发送一个自定义消息。 pGrid->CreateInPlaceComboCtrl(nRow, nCol, rect, m_aryItems, m_nSelItem); return TRUE; // 返回TRUE表示已启动编辑 }Draw方法也需要重写,以确保单元格在非编辑状态下能正确显示选中的文本,而不是显示索引号:
BOOL CGridCellCombo::Draw(CDC* pDC, int nRow, int nCol, CRect rect, BOOL bEraseBkgnd) { // 如果m_strText为空,但m_nSelItem有效,则从m_aryItems获取文本 if (m_strText.IsEmpty() && m_nSelItem >= 0 && m_nSelItem < m_aryItems.GetSize()) { m_strText = m_aryItems.GetAt(m_nSelItem); } // 将我们缓存的文本设置给基类的m_strText,以便基类Draw函数能绘制它 CString strOldText = GetText(); SetText(m_strText); // 调用基类绘制(处理背景、边框、文本对齐等) BOOL bResult = CGridCell::Draw(pDC, nRow, nCol, rect, bEraseBkgnd); // 可选:恢复原来的文本(如果基类Draw修改了它) // SetText(strOldText); return bResult; }3.2 增强 CGridCtrl 以管理编辑器
在CGridCtrl的派生类或直接修改的源码中,我们需要添加创建和管理 Combo 编辑器的能力。首先,在头文件中声明:
class CGridCtrlEx : public CGridCtrl // 假设我们创建了一个增强类 { // ... 其他原有成员 ... public: BOOL CreateInPlaceComboCtrl(int nRow, int nCol, const CRect& rectCell, const CStringArray& items, int nCurSel); afx_msg void OnComboKillFocus(); afx_msg LRESULT OnComboClose(WPARAM wParam, LPARAM lParam); protected: CComboBox* m_pWndInPlaceCombo; // 指向当前活动的编辑器 int m_nComboEditingRow; int m_nComboEditingCol; DECLARE_MESSAGE_MAP() };CreateInPlaceComboCtrl的实现是UI逻辑的关键:
BOOL CGridCtrlEx::CreateInPlaceComboCtrl(int nRow, int nCol, const CRect& rectCell, const CStringArray& items, int nCurSel) { // 如果已有正在编辑的控件,先结束之前的编辑 if (m_pWndInPlaceCombo && ::IsWindow(m_pWndInPlaceCombo->GetSafeHwnd())) { SendMessage(WM_GRID_COMBO_CLOSE, 0, 0); // 发送自定义消息触发关闭流程 } // 计算控件位置。rectCell是单元格在Grid客户区内的坐标。 // 需要考虑Grid的滚动偏移(GetScrollPos)。 CRect rectCombo = rectCell; rectCombo.bottom += 200; // 给下拉列表留出足够高度,这是一个经验值 // 创建ComboBox控件 m_pWndInPlaceCombo = new CComboBox; DWORD dwStyle = WS_CHILD | WS_VISIBLE | WS_VSCROLL | CBS_DROPDOWNLIST | CBS_NOINTEGRALHEIGHT; if (!m_pWndInPlaceCombo->Create(dwStyle, rectCombo, this, IDC_INPLACE_COMBO)) // IDC_INPLACE_COMBO 是一个资源ID { delete m_pWndInPlaceCombo; m_pWndInPlaceCombo = NULL; return FALSE; } // 填充数据 for (int i = 0; i < items.GetSize(); i++) { m_pWndInPlaceCombo->AddString(items.GetAt(i)); } if (nCurSel >= 0 && nCurSel < items.GetSize()) { m_pWndInPlaceCombo->SetCurSel(nCurSel); } // 记录当前编辑的单元格 m_nComboEditingRow = nRow; m_nComboEditingCol = nCol; // 设置字体与Grid一致 CFont* pGridFont = GetFont(); if (pGridFont) { m_pWndInPlaceCombo->SetFont(pGridFont); } // 显示并下拉 m_pWndInPlaceCombo->ShowWindow(SW_SHOW); m_pWndInPlaceCombo->SetFocus(); m_pWndInPlaceCombo->ShowDropDown(TRUE); // 子类化Combo以捕获其消息。这里可以用一个独立的类,也可以用SetWindowLong配合一个静态回调函数。 // 简单起见,我们可以让CGridCtrlEx也处理Combo的通知消息。 // 需要将Combo控件的ID在消息映射中映射到OnComboClose处理函数。 // 更优雅的方式是使用一个专用的“In-place Combo”窗口类。 return TRUE; }处理 Combo 关闭的逻辑至关重要,它负责数据的回写和清理:
// 假设我们通过子类化或消息反射,使得Combo的CBN_CLOSEUP通知能到达这个函数 void CGridCtrlEx::OnComboClose() { if (!m_pWndInPlaceCombo || !::IsWindow(m_pWndInPlaceCombo->GetSafeHwnd())) return; int nSel = m_pWndInPlaceCombo->GetCurSel(); CString strText; if (nSel != CB_ERR) { m_pWndInPlaceCombo->GetLBText(nSel, strText); } // 获取对应的单元格对象 CGridCellBase* pCell = GetCell(m_nComboEditingRow, m_nComboEditingCol); CGridCellCombo* pComboCell = dynamic_cast<CGridCellCombo*>(pCell); if (pComboCell) { pComboCell->SetCurSel(nSel); pComboCell->SetText(strText); // 更新单元格文本 // 触发数据已更改的通知(例如,通知父视图更新文档) SendMessageToParent(m_nComboEditingRow, m_nComboEditingCol, GVN_ENDLABELEDIT); } // 清理工作 m_pWndInPlaceCombo->DestroyWindow(); delete m_pWndInPlaceCombo; m_pWndInPlaceCombo = NULL; // 重绘该单元格,显示新文本 RedrawCell(m_nComboEditingRow, m_nComboEditingCol); // 将焦点返还给Grid SetFocus(); }同时,必须在CGridCtrlEx的消息映射中处理编辑器失去焦点的情况,防止 Combo 窗口“悬空”:
BEGIN_MESSAGE_MAP(CGridCtrlEx, CGridCtrl) ON_WM_KILLFOCUS() ON_CONTROL(CBN_CLOSEUP, IDC_INPLACE_COMBO, OnComboClose) // 需要正确映射 END_MESSAGE_MAP() void CGridCtrlEx::OnKillFocus(CWnd* pNewWnd) { // 如果失去焦点时,Combo编辑器还在,且焦点不是转移给这个Combo本身或其子窗口,则结束编辑 if (m_pWndInPlaceCombo && ::IsWindow(m_pWndInPlaceCombo->GetSafeHwnd())) { CWnd* pWndCombo = m_pWndInPlaceCombo; // 检查新获得焦点的窗口是否是我们的Combo或其子项 BOOL bFocusGoingToCombo = FALSE; CWnd* pTemp = pNewWnd; while (pTemp) { if (pTemp->GetSafeHwnd() == pWndCombo->GetSafeHwnd()) { bFocusGoingToCombo = TRUE; break; } pTemp = pTemp->GetParent(); } if (!bFocusGoingToCombo) { OnComboClose(); // 结束编辑 } } CGridCtrl::OnKillFocus(pNewWnd); }3.3 在项目中使用自定义单元格
最后,在对话框或视图类中,你需要这样使用它:
void CMyDialog::InitGrid() { m_grid.Create(WS_CHILD | WS_VISIBLE | WS_BORDER, CRect(10,10,500,300), this, IDC_GRID); m_grid.SetRowCount(10); m_grid.SetColumnCount(3); m_grid.SetFixedRowCount(1); m_grid.SetFixedColumnCount(1); // 设置第二列为Combo类型 for (int i = 1; i < m_grid.GetRowCount(); ++i) { CGridCellCombo* pComboCell = new CGridCellCombo; CStringArray arrOptions; arrOptions.Add(_T("技术部")); arrOptions.Add(_T("市场部")); arrOptions.Add(_T("行政部")); arrOptions.Add(_T("财务部")); pComboCell->SetOptions(arrOptions); pComboCell->SetCurSel(0); // 默认选中第一个 m_grid.SetCell(i, 2, pComboCell); // 注意:SetCell会取得指针所有权 } // 设置其他单元格为普通文本类型... }4. 实战中的关键问题与避坑指南
在实际集成和测试这个功能时,我遇到了不少棘手的问题。下面把这些“坑”和解决方案记录下来,希望能帮你节省大量调试时间。
4.1 编辑器控件的创建与销毁时机
这是最容易出问题的地方。
问题1:内存泄漏。如果只在
OnComboClose里DestroyWindow而没有delete,或者因为异常路径导致delete没执行,就会泄漏。- 解决:确保
new和delete成对出现,并且放在try...catch块中,或者在类的析构函数中做最终检查清理。
- 解决:确保
问题2:野指针访问。在 Combo 编辑过程中,用户可能快速点击其他单元格或关闭父窗口,导致
m_pWndInPlaceCombo指向的窗口已被销毁,但指针未置空。- 解决:任何使用
m_pWndInPlaceCombo前,都必须用::IsWindow检查其窗口句柄是否有效。这是一个必须养成的习惯。
- 解决:任何使用
void CGridCtrlEx::SomeFunction() { // 错误的做法:直接使用指针 // if (m_pWndInPlaceCombo) { m_pWndInPlaceCombo->DoSomething(); } // 正确的做法:检查窗口是否依然存在 if (m_pWndInPlaceCombo && ::IsWindow(m_pWndInPlaceCombo->GetSafeHwnd())) { m_pWndInPlaceCombo->DoSomething(); } }4.2 坐标计算与滚动偏移
- 问题:创建的 Combo 位置不对,特别是当网格有垂直或水平滚动条时,编辑器可能出现在错误的位置,或者下拉列表被网格边界裁剪。
- 解决:
CreateInPlaceComboCtrl接收的rectCell通常是单元格在网格客户区的坐标。但在有滚动的情况下,你需要加上当前的滚动偏移量。CRect rectCombo = rectCell; CPoint ptScroll = GetScrollPosition(); rectCombo.OffsetRect(-ptScroll.x, -ptScroll.y); // 关键:减去滚动偏移,得到相对于Grid客户区原点的正确位置 rectCombo.bottom += 200; // 增加高度以显示下拉列表 // 还需要确保rectCombo仍在Grid客户区内,必要时调整 CRect rectClient; GetClientRect(&rectClient); if (rectCombo.bottom > rectClient.bottom) { // 如果下拉区域超出底部,可以向上展开,或者限制高度 rectCombo.top = rectCell.top - 200; // 向上展开 rectCombo.bottom = rectCell.top; }
4.3 焦点与消息循环的冲突
- 问题:点击 Combo 的下拉箭头,下拉列表一闪即逝,或者根本无法下拉。
- 原因:MFC 和 Windows 的消息处理机制。当你在
CGridCtrl内部创建了一个子控件,某些消息(如鼠标点击下拉箭头)可能会被父网格控件“吞掉”或处理不当。 - 解决:
- 子类化 Combo 控件:创建一个从
CComboBox派生的类(如CInPlaceComboBox),并在这个子类中处理自己的WM_LBUTTONDOWN等消息,确保下拉逻辑能正常工作。然后,在CreateInPlaceComboCtrl中创建这个子类的实例。 - 正确设置控件样式:确保创建时包含了
CBS_DROPDOWNLIST和WS_VISIBLE。 - 在显示后立即设置焦点并下拉:如示例代码所示,在
Create和填充数据后,立即调用SetFocus()和ShowDropDown(TRUE)。有时甚至需要加一个短暂的Sleep(0)或PeekMessage循环来让消息泵处理完初始消息,但这通常是最后的手段。
- 子类化 Combo 控件:创建一个从
4.4 与键盘导航的集成
- 问题:用户习惯用键盘(Tab, 方向键)在网格中导航,当焦点进入 Combo 单元格时,如何用键盘激活下拉列表(按 F4 或 Alt+Down)?
- 解决:需要在
CGridCtrl中重写PreTranslateMessage或处理WM_KEYDOWN消息。
同时,在BOOL CGridCtrlEx::PreTranslateMessage(MSG* pMsg) { if (m_pWndInPlaceCombo && ::IsWindow(m_pWndInPlaceCombo->GetSafeHwnd())) { // 如果当前有活动的Combo编辑器,先将消息传递给它处理 if (m_pWndInPlaceCombo->PreTranslateMessage(pMsg)) return TRUE; } return CGridCtrl::PreTranslateMessage(pMsg); }CGridCtrl的OnKeyDown中,当当前单元格是CGridCellCombo类型且用户按下 F4 或 Alt+Down 时,可以模拟点击事件,调用该单元格的Edit方法。
4.5 性能与大数据量
- 问题:如果表格有成千上万行,每个 Combo 单元格的选项列表都不同,内存和初始化速度会成为问题。
- 优化:
- 延迟加载选项:不要在初始化网格时就为所有
CGridCellCombo设置SetOptions。可以只在单元格第一次被编辑时,根据行号、列号去动态加载选项列表(例如,从数据库或配置文件读取)。 - 共享选项列表:如果很多单元格共享相同的选项(如“性别”、“是否”),可以创建一个全局的
CStringArray管理器,单元格只保存一个指向共享列表的指针或索引,避免数据重复。 - 虚拟模式:如果网格数据量极大,考虑实现虚拟网格(
SetVirtualMode),单元格数据按需提供。这时CGridCellCombo可能不再持有数据,而是作为一个“编辑器工厂”,在编辑时向数据源请求选项列表。
- 延迟加载选项:不要在初始化网格时就为所有
5. 功能扩展与高级应用
基础功能实现后,可以考虑以下几个实用的扩展方向,让你的网格控件更加专业和易用。
5.1 支持编辑时手动输入(CBS_DROPDOWN)
上面的示例使用的是CBS_DROPDOWNLIST,用户只能选择不能输入。有时我们需要允许用户既可以选择已有项,也可以输入新值(类似于 Excel 中的组合框)。
- 实现:将创建 Combo 时的样式改为
CBS_DROPDOWN。在OnComboClose中,就不能只取GetCurSel了,因为用户可能输入了新文本。需要调用GetWindowText来获取编辑框中的字符串。 - 挑战:需要处理输入验证。新输入的文本可能不在列表中,你需要决定是将其作为新选项加入列表(并通知数据模型),还是仅仅作为一个临时值。这通常涉及到更复杂的业务逻辑回调。
5.2 实现依赖下拉(级联选择)
一个常见的场景是:第一列选择“国家”,第二列(Combo)的选项根据第一列的选择动态变化(“城市”)。
- 实现:
- 为
CGridCellCombo增加一个SetDependency(int nDepCol)方法,记录它依赖于哪一列。 - 在
CGridCtrl中,当依赖列的值发生变化时(通过SendMessageToParent收到GVN_ENDLABELEDIT通知),遍历所有行,找到那些依赖此列的CGridCellCombo单元格。 - 根据新的依赖值(如国家ID),重新查询或计算该单元格的选项列表(
SetOptions),并清空其当前选择(SetCurSel(-1))。 - 重绘这些单元格。
- 为
5.3 美化与视觉增强
原生的CComboBox在视觉上可能和你的应用主题不搭。
- 自绘 Combo (Owner Draw):创建 Combo 时使用
CBS_OWNERDRAWFIXED或CBS_OWNERDRAWVARIABLE样式,并处理WM_DRAWITEM消息。这允许你完全控制下拉列表中每一项的绘制,可以改变颜色、字体、添加图标等。 - 使用第三方美化库:如 BCGControlBar、Xtreme Toolkit 等,它们通常提供了外观更现代的
CComboBox派生类,可以直接集成到上述架构中,替换掉原生的CComboBox创建步骤。
5.4 集成工具提示与数据验证
- 工具提示:当单元格文本因列宽太短而显示不全时,可以重写
CGridCellCombo::Draw,在绘制时判断文本宽度,如果需要,可以设置一个标志,然后在CGridCtrl的OnMouseMove中显示 Tooltip。 - 数据验证:在
OnComboClose将数据写回单元格之前,可以触发一个验证回调。例如,弹出一个对话框确认选择,或者根据业务规则检查选择的合法性(如“财务部”只能选特定人员)。验证失败可以调用m_pWndInPlaceCombo->SetFocus()让编辑器保持打开状态。
这个项目从表面看只是两个控件的结合,但深入下去,涉及了MFC窗口编程、消息机制、资源管理、UI交互等多个核心知识点。把它做稳定、做流畅,对深入理解Windows桌面开发大有裨益。最终的效果,是让那些基于MFC的老旧管理系统,在数据录入体验上获得一次显著的现代化提升,而无需推翻重来。