简介:一套围绕 MFC 属性网格控件开发的示例型资料,面向需要为工具窗口、配置对话框或属性编辑器增加结构化属性列表的 Visual C++ 程序员。内容覆盖控件创建、父属性与子属性组织、常用属性类型定义、修改事件捕获与处理、视觉样式和类别显示设置,全部通过可运行代码串联起来,适合入门级与中级 MFC 开发者边运行边理解。资源包共包含 40 个文件,其中 11 个头文件与 7 个源文件构成核心代码,sln/vcxproj 解决方案与工程文件可直接编译,rc 资源脚本、aps 缓存、def 导出定义共同完善项目结构,整个包仅 152KB,轻量紧凑,还配有日志与 ReadMe 方便排查和阅读。示例中提供了两个相互关联的项目场景,能直观看到属性网格在不同界面模块中的接入方式,包括初始值设定、子节点展开、修改后回调、界面风格切换等关键操作。已有 735 人学习,对刚接触属性网格的开发者而言,这套代码能帮助快速搭建 CMFCPropertyGridCtrl 的使用框架,并为后续扩展自定义编辑器、动态加载属性等功能提供可参考起点。
1. 为什么 MFC 程序员迟早要跟 CMFCPropertyGridCtrl 打交道
接手一个老项目,对话框里参数堆了几十个,密密麻麻的 Edit 和 ComboBox,想加一个参数得调半天布局。后来把 MFC 自带属性表格控件拖进对话框,十几行代码就把参数面板做得像 IDE 的属性窗口。CMFCPropertyGridCtrl 不需要第三方库,是 MFC 扩展库自带的控件,专门用来做分组、分行的键值编辑界面。它支持字符串、数字、枚举、颜色、文件路径等常见属性类型,还能挂自定义编辑器。适合做程序设置面板、对象属性查看器、滤波参数配置这类界面。这篇笔记会把创建、绑定、定制编辑器到序列化串一遍,顺便把我翻过车的几个坑摊开。
如果你还在用多组 Edit + Static 硬拼参数页面,我建议你停下来看一眼这个控件。它最大的价值不是省几个控件,而是把“参数名 + 值 + 分组 + 合法性提示”这套交互模型固化成标准组件,用户一看就懂怎么操作。而且它随 MFC 分发,不需要额外下载动态库,老项目里也能直接用,兼容性比你想象中好。后面所有例子我都基于对话框工程,先把基本用法跑通,再深入定制。
注意:MFC 属性表控件虽然功能强,但有几个历史遗留毛病,比如内部使用 COleVariant 类型,宽窄字符串转换容易踩坑;展开折叠状态不做持久化;动态增删属性项后重绘不及时。这些坑我会在第四章逐条给出现象和解决,你照着排查就行。现在从创建控件开始。
2. 先搭一个能跑的属性表:创建、挂载与基础属性
2.1 控件的创建方式和两种挂载流程
MFC 属性表用起来第一步是把控件放进窗口。常见做法有两种:一种是在对话框模板里放一个 Custom Control 控件,在属性页里把 Class 名称填成 CMFCPropertyGridCtrl;另一种是纯代码动态创建。我一般用第二种,因为属性表内容经常要按运行时数据动态填充,模板方式反而不灵活,还要额外处理子类化。
代码动态创建流程如下。在对话框头文件里声明一个成员变量,然后在 OnInitDialog 中调用 Create 函数。注意 Create 的 ID 最好用一个自定义的、不跟其他控件冲突的 ID,避免消息路由混乱。
// 声明 CMFCPropertyGridCtrl m_PropGrid; // OnInitDialog 中创建 BOOL CMyDialog::OnInitDialog() { CDialogEx::OnInitDialog(); // 创建属性表,位置和大小先给一个临时矩形 CRect rc(10, 10, 400, 300); m_PropGrid.Create(WS_CHILD | WS_VISIBLE | WS_TABSTOP, rc, this, IDC_PROP_GRID); m_PropGrid.EnableHeaderCtrl(FALSE); // 隐藏列头,参数面板通常不需要 m_PropGrid.EnableDescriptionArea(TRUE); // 显示属性描述区 m_PropGrid.SetVSDotNetLook(TRUE); // 用 VS 风格配色 return TRUE; }Create 的第一个参数是窗口样式,至少要 WS_CHILD 和 WS_VISIBLE。如果你的对话框要支持键盘操作,加上 WS_TABSTOP。EnableHeaderCtrl 控制属性表顶部是否显示“属性/值”列头,参数面板一般不显示,因为属性名已经在第一列了,列头反而占空间。EnableDescriptionArea 会在控件底部开辟一块区域,显示当前选中属性的描述说明。SetVSDotNetLook 是外观风格开关,打开后更像现代 IDE,关闭则接近老式网格。
如果坚持用模板方式,在对话框模板上画一个 Custom Control,资源编辑器里把 Class 写为 CMFCPropertyGridCtrl,然后给这个控件添加变量,类型设为 CMFCPropertyGridCtrl。运行时框架会自动子类化,不需要手动 Create。两种方式不能混用,否则会调用两次创建,导致控件句柄被覆盖、界面出现两个实例。
还有一种情况是在非对话框窗口(比如 View)里使用,步骤一样,只是父窗口换成 View 指针。如果是创建在 CFormView 中,记得在 OnInitialUpdate 里创建,不要在构造函数里 Create,因为此时窗口还没准备好。
2.2 属性项的分类与插入顺序
属性表的显示结构是树形的:最外层是分类(Category),每个分类下挂着具体的属性项。分类的作用是分组,属性项才是真正让用户改值的行。要添加一个分类,用 AddCategory 返回分类指针,然后向分类的 AddProperty 传入新建属性项。
// 先创建分类 CMFCPropertyGridProperty* pCat = new CMFCPropertyGridProperty(_T("网络设置")); // 给分类添加属性项 pCat->AddProperty(new CMFCPropertyGridProperty(_T("IP地址"), _T("192.168.1.1"))); pCat->AddProperty(new CMFCPropertyGridProperty(_T("端口号"), (long)8080)); // 把分类挂到控件 m_PropGrid.AddProperty(pCat);这段代码里,“IP地址”和“端口号”就是两个属性项。第一个属性项传入了两个字符串,表示这是字符串类型属性;第二个属性项传入了一个字符串和一个 long,表示整数类型属性。构造函数重载决定了属性类型的内部表示,这会影响后面取值和设值。
多个分类都按 AddProperty 顺序排列。如果想让某个属性插到分类中间,可以使用 AddProperty 的另一个重载,第二个参数传索引,但实际很少用。还有一个细节:分类本身也是一个 CMFCPropertyGridProperty 对象,只是它没有值编辑器,且字号加粗显示。所以分类和属性项在代码里的数据结构是统一的,但用途不同,不要试图给分类 SetValue。
属性项指针的生命周期由属性表控件管理。当控件销毁或调用 RemoveAll 时,内部会释放这些 new 出来的对象。所以不要手动 delete 你在代码里 new 的 CMFCPropertyGridProperty,否则会有双重释放。如果你需要长时间保存某个属性项的指针,注意在控件重建后该指针可能已经失效,务必重新 FindProperty 获取。
2.3 属性值的读写:GetValue/SetValue 与数据刷新
建立好属性表之后,最常见需求是把属性值读出来,或者把一个外部计算出的值写回属性表。取值的 API 是 GetValue(),它返回一个 COleVariant 对象。不同类型对应的字段不同,这一点新手特别容易懵。
// 查找属性项 CMFCPropertyGridProperty* pProp = m_PropGrid.FindProperty(_T("端口号")); if (pProp != NULL) { // 整数属性取 lVal long nPort = pProp->GetValue().lVal; // 字符串属性取 bstrVal,注意是 BSTR CString strIP = pProp->GetValue().bstrVal; }GetValue 返回的 COleVariant 在使用前应该先判断类型。判断方法是用 vt 字段,比如 VT_I4 表示整数,VT_BSTR 表示字符串。如果不判断直接取,可能读到错误字段,因为 COleVariant 的成员是 union,取错字段会得到垃圾值。不过 MFC 属性表在构造属性项时类型基本固定,所以同一个属性项的取值字段是确定的,前提是你没有给属性项 SetValue 设置错误类型的值。
设值使用 SetValue,它接受 COleVariant 或者一些基础类型。常见问题是把字符串传给整数属性项。比如 pProp->SetValue(_T("123")),某些版本会做隐式转换,某些版本不会,转换失败时属性值变成空。我一般按属性类型严格设定:
if (pProp->GetValue().vt == VT_I4) pProp->SetValue((long)123); else if (pProp->GetValue().vt == VT_BSTR) pProp->SetValue(_T("新值"));SetValue 之后界面不一定立即刷新,尤其是属性项处于折叠状态或者控件没有收到重绘通知时。稳妥做法是 SetValue 后调用 m_PropGrid.Invalidate(),必要时再 RedrawWindow()。另外要注意,属性表控件在用户编辑时触发的是 AFX_WM_PROPERTY_CHANGED 消息,而程序自身调用 SetValue 不会触发这个消息,所以如果你要联动其他逻辑,得手动调用或者由后续步骤触发。
3. 常用属性类型与编辑器定制:从下拉框到按钮
3.1 内置属性类选型
MFC 属性表组件包里提供了一些现成的属性类,它们都继承自 CMFCPropertyGridProperty。正确选型可以省去大量自绘和消息处理代码,我在这里列一个常用对照表。
| 属性类 | 编辑器样式 | 适用场景 |
|---|---|---|
| CMFCPropertyGridProperty | 普通编辑框 | 字符串、数值、日期等基础类型 |
| CMFCPropertyGridFileProperty | 右侧浏览按钮 | 文件路径选择 |
| CMFCPropertyGridColorProperty | 点击弹出颜色表 | 颜色配置 |
| CMFCPropertyGridFontProperty | 点击弹出字体选择 | 字体设置 |
| CMFCPropertyGridBoolProperty | 下拉真假列表 | 布尔开关 |
| CMFCPropertyGridCheckBoxProperty | 复选框 | 勾选开关 |
| CMFCPropertyGridComboBoxProperty | 下拉列表 | 枚举及预置选项 |
表格里前两个最常用。CMFCPropertyGridProperty 通过构造函数重载就能区分类型,比如传 CString 是字符串,传 long 是整数,传 double 是浮点,传 BOOL 是逻辑值但显示为 True/False(实际上用 BoolProperty 更标准)。ColorProperty 和 FontProperty 比较复杂,它们内部实现了完整的编辑器,比如 ColorProperty 点击后显示一组预设色块,还有“自定义”按钮。
用 ColorProperty 举个例子:
CMFCPropertyGridColorProperty* pColor = new CMFCPropertyGridColorProperty(_T("高亮色"), RGB(255, 255, 0)); pColor->EnableAutomaticButton(_T("自动"), RGB(255, 255, 255)); pColor->EnableOtherButton(_T("自定义...")); pCat->AddProperty(pColor);这段代码创建了一个颜色属性项,初始值为黄色。EnableAutomaticButton 添加一个“自动”按钮,用户点击后自动设置成 RGB(255,255,255)。EnableOtherButton 添加“自定义...”按钮,点击弹出系统颜色对话框。如果不需要这两个按钮,可以不调用,界面就是一个带颜色预览的下拉块。需要注意,这两个按钮只在属性项被选中时显示在右侧。
还有 ComboBoxProperty,这个也常用来做枚举。用法是先把选项字符串数组准备好,然后调用 AddOption 添加:
CMFCPropertyGridProperty* pMode = new CMFCPropertyGridProperty(_T("工作模式"), _T("自动")); pMode->AddOption(_T("自动")); pMode->AddOption(_T("手动")); pMode->AddOption(_T("半自动")); pCat->AddProperty(pMode);注意 AddOption 必须在添加到属性表之前调用,否则下拉列表不会显示已有选项(有些版本可以后续添加,但显示行为不稳定)。还要知道,这个属性类允许用户在编辑框里输入自定义内容,如果想限制只能选预设项,需要重写 OnEndEdit;BoolProperty 的 False 和 True 选项是自动生成的,不需要 AddOption。
3.2 自定义属性编辑器:文件路径选择器
内置 FileProperty 只能选文件,不能选文件夹;有时还想加一个“清空”按钮,或者点按钮后弹的是我们自己写的对话框。这种情况就要自定义属性类。MFC 属性表提供了两个虚函数作为编辑器插槽:OnClickButton 负责处理右侧按钮点击,OnEndEdit 负责处理编辑完成。
下面是一个文件夹选择属性类的实现骨架,我已经去掉了无关代码,保留最关键的部分。
class CFolderPickerProperty : public CMFCPropertyGridProperty { public: CFolderPickerProperty(const CString& strName, const CString& strFolder) : CMFCPropertyGridProperty(strName, strFolder) { m_bButton = TRUE; // 告诉属性表需要绘制右侧按钮 } virtual BOOL OnClickButton(CPoint /*point*/) { // 用旧版 Shell 文件夹选择对话框 BROWSEINFO bi = {0}; TCHAR szPath[MAX_PATH] = {0}; bi.hwndOwner = GetParent()->GetSafeHwnd(); // 父窗口句柄 bi.lpszTitle = _T("选择文件夹"); LPITEMIDLIST pidl = SHBrowseForFolder(&bi); if (pidl) { SHGetPathFromIDList(pidl, szPath); SetValue(CString(szPath)); // 写回属性表 CoTaskMemFree(pidl); } return TRUE; } virtual BOOL OnEndEdit() { if (!CMFCPropertyGridProperty::OnEndEdit()) return FALSE; // 校验路径是否存在 CString strPath = GetValue().bstrVal; DWORD dwAttr = GetFileAttributes(strPath); if (dwAttr == INVALID_FILE_ATTRIBUTES || (dwAttr & FILE_ATTRIBUTE_DIRECTORY) == 0) AfxMessageBox(_T("请选择有效文件夹路径")); return FALSE; // 这里根据校验结果决定是否接受编辑值 } };OnClickButton 里的 GetParent() 不一定能拿到主对话框,因为属性表控件内部还有子窗口布局。保险做法是在创建属性项时保存一个窗口指针,或者用 GetOwner()。SHBrowseForFolder 是旧接口,但兼容性最好;新式 IFileDialog 接口也可以,只是需要额外的 COM 初始化。SetValue(CString) 会自动更新属性项显示值。
OnEndEdit 的处理需要注意:这个函数在用户敲回车或者焦点转移时触发,返回值决定是否接受编辑结果。我这里简单用了 MessageBox 提示,实际项目里不要这样做,更好的做法是在状态栏或旁边显示错误原因,因为 MessageBox 在编辑过程中弹出来很打断操作。另外,OnEndEdit 在用户点击右边按钮时可能不会触发,所以按钮操作和编辑操作要分开处理。
3.3 属性值变更回调:OnPropertyChanged 的正确姿势
当用户修改属性值后,父窗口会收到一个注册消息。这个消息不是 WM_COMMAND,而是动态注册的 AFX_WM_PROPERTY_CHANGED。处理它的第一步是在消息映射里加上 ON_REGISTERED_MESSAGE。
// 头文件 afx_msg LRESULT OnPropertyChanged(WPARAM wParam, LPARAM lParam); // 实现文件 BEGIN_MESSAGE_MAP(CMyDialog, CDialogEx) ON_REGISTERED_MESSAGE(AFX_WM_PROPERTY_CHANGED, &CMyDialog::OnPropertyChanged) END_MESSAGE_MAP() LRESULT CMyDialog::OnPropertyChanged(WPARAM wParam, LPARAM lParam) { // lParam 是属性项指针,wParam 是属性项 ID(组 ID) CMFCPropertyGridProperty* pProp = (CMFCPropertyGridProperty*)lParam; if (pProp == NULL) return 0; CString strName = pProp->GetName(); // 根据属性名称分发 if (strName == _T("端口号")) { m_nPortNum = pProp->GetValue().lVal; } else if (strName == _T("工作模式")) { m_strMode = pProp->GetValue().bstrVal; } return 0; }wParam 和 lParam 的含义需要确认。在较新的 MFC 版本里,wParam 是属性项 ID,由 AddProperty 传入?实际上 AddProperty 没有 ID 参数,这个 wParam 一般是属性项内部索引或者 0,我见过的代码大多只用 lParam。为了避免版本差异,建议用 GetName() 识别属性项,而不是依赖 wParam。
还有两个容易踩的特性:第一,回调触发时机是“用户提交编辑值”之后,而不是每次击键都触发,所以不要在回调里做复杂的实时校验,否则响应慢;第二,如果程序自己 SetValue,不会触发这个回调,所以你的业务逻辑在程序初始化时设置属性值,需要手动调用一次同步逻辑,或者让初始化代码直接写业务变量。
如果属性表控件在子类窗口里,消息反射可能不工作。这时可以重写控件类的 PreTranslateMessage 或者自绘,我一般不这样做,而是让子窗口把通知转发给主窗口。
4. 避坑与常见问题排查:我踩过的六个坑
4.1 控件盖住了整个对话框:忘记 AdjustLayout
现象:程序运行后,属性表控件显示在对话框左上角,尺寸巨大,把原有控件全部盖住;或者显示区域只有一小条,列宽却不匹配。
原因:最常见的是在 OnInitDialog 里用 Create 创建时给的矩形是固定的像素值,但对话框有 DPI 缩放或者有自绘背景时,实际客户区尺寸和设定不一致。另一个原因是属性表内部有多层子窗口,控件收到 WM_SIZE 后需要重新计算列宽和行高,如果没调用 AdjustLayout,内部布局还是创建时的默认值。
解决:在对话框的 OnSize 响应里处理属性表大小,并且无论如何都要调用 AdjustLayout。我一般这样:
void CMyDialog::OnSize(UINT nType, int cx, int cy) { CDialogEx::OnSize(nType, cx, cy); if (::IsWindow(m_PropGrid.GetSafeHwnd())) { CRect rc; GetClientRect(&rc); rc.DeflateRect(10, 10, 10, 10); m_PropGrid.MoveWindow(rc); m_PropGrid.AdjustLayout(); } }注意要先 MoveWindow 再 AdjustLayout,否则列宽还是按旧尺寸计算。如果属性表是嵌在一个固定的分栏区域里,那么不需要 OnSize,只要在创建后调用一次 AdjustLayout 即可。另外,如果你用模板方式放控件,不需要手动 MoveWindow,但对话框改变大小时依然要调 AdjustLayout。
4.2 属性值不刷新:改完代码没重绘
现象:程序调用 SetValue 后,属性表界面显示的还是旧值,必须用鼠标点击一下或者滚动一下才会刷新。尤其在属性项处于折叠分类里时,新值在多处操作后甚至混乱。
原因:SetValue 更新了内部存储值,但属性项没有主动请求重绘。正常情况下用户编辑时控件内部会触发局部重绘,但程序外部设值时没有这个流程。折叠状态的分类下的属性项连画都不画,自然不会刷新。
解决:设值后强制重绘。轻量方案是:
pProp->SetValue(newValue); m_PropGrid.Invalidate(); m_PropGrid.UpdateWindow();如果需要多个属性连续设值,不要每个都 Invalidate,先把所有值设完,最后 Invalidate 一次。如果属性项很多,可以考虑使用 RedrawWindow 指定区域,但要精确计算坐标很麻烦,多数场景下全量重绘就够了。我踩过这个坑时,一度以为属性项 SetValue 有问题,后面跟踪发现值已变,只是界面没刷,浪费了一个晚上。
4.3 下拉框点不开:属性只读属性项的状态
现象:BoolProperty 或 ComboBoxProperty 点击右侧箭头没有反应,属性值编辑框也不可输入,整个属性项显示为灰色。
原因:属性项被设成了只读或禁用状态。常见症状是调用 Enable(FALSE) 禁用后忘记恢复;或者属性项构造时第三个参数传入 TRUE 表示只读;还有某些操作后 AllowEdit 被内部置为 FALSE。
解决:检查并恢复属性项的交互状态。
pProp->Enable(TRUE); // 启用 pProp->AllowEdit(TRUE); // 允许编辑 m_PropGrid.Invalidate();注意 BoolProperty 的下拉框不是传统 ComboBox,它是点击后弹出一个列表供选择,如果 Enable(FALSE) 后连列表也打不开,这是正常的。另一个隐蔽点:分类项也会显示成可点击的样子,但分类是不可编辑的,如果你把普通属性项误当成分类 AddProperty,它实际会变成分类样式,显示加粗但没有编辑器。判断方法是检查 GetIsCategory() 之类的方法,或者看它的子项数量。
4.4 Unicode 与 ANSI 字符串属性类型混用
现象:中文属性名显示为乱码,属性值从宽字符变量赋值后变成问号,或者在某些机器上字符串被截断。
原因:MFC 属性表内部使用 COleVariant 的 bstrVal,bstrVal 是 BSTR 类型,即 Unicode 字符串。如果你的工程字符集是“多字节字符集”,CString 默认是 ANSI 窄字符,直接传给属性表构造函数或 SetValue 时,MFC 会隐式转换。但某些重载在转换过程中使用了错误的代码页,导致中文字符丢失。
解决:最彻底的是把工程设置为“使用 Unicode 字符集”。如果因为历史原因不能改,就在所有涉及属性表字符串的地方显式使用宽字符临时变量:
// ANSI 工程里,显示正确的中文属性名 CStringW strNameW = L"中文名称"; CMFCPropertyGridProperty* p = new CMFCPropertyGridProperty(CString(strNameW), ...);SetValue 时也类似,先把窄字符串转为宽字符 CStringW 再传给 SetValue。取值时 GetValue().bstrVal 本来就是宽字符,赋给 ANSI 的 CString 会有转换警告,用 CStringW 接收就不会截断。我自己的习惯是属性表代码内部一律使用 Unicode 字符串,界面层只在和业务接口交界处做转换。
4.5 分类折叠后状态丢失
现象:用户把“高级设置”分类折叠了,程序重新填充属性表(比如点击“恢复默认”)后,所有分类全部展开;对话框关闭再打开,之前折叠的状态也丢了。
原因:分类展开状态存储在 CMFCPropertyGridProperty 对象内部,当你 RemoveAll 或者重新创建分类时,旧对象被销毁,状态随之丢失。属性表控件没有自动保存展开状态。
解决:在重建属性表之前备份每个分类的展开状态,重建后恢复。代码示例:
std::map<CString, BOOL> mapStates; for (int i = 0; i < m_PropGrid.GetPropertyCount(); i++) { CMFCPropertyGridProperty* pCat = m_PropGrid.GetProperty(i); mapStates[pCat->GetName()] = pCat->IsExpanded(); } m_PropGrid.RemoveAll(); // 删除所有分类和属性项 // 重新添加分类... // 然后恢复状态 for (auto it = mapStates.begin(); it != mapStates.end(); ++it) { CMFCPropertyGridProperty* pCat = m_PropGrid.FindProperty(it->first); if (pCat) pCat->Expand(it->second); }注意 FindProperty 也能找到分类,因为分类就是特殊属性项。如果只是刷新部分属性,尽量不要删除分类,而是保留分类指针,用 RemoveSubItem 清掉子项再重新 Add。这样分类的展开状态不会丢。我常用一个成员指针数组保存分类指针,但要注意控件重建后这些指针失效,要重新获取。
4.6 双击展开和单选失效:动态修改属性项的坑
现象:程序运行中有些属性项被移除并重新添加后,双击分类不展开,单击选中高亮位置错乱,甚至出现错行。
原因:属性表内部维护了树节点和绘制行的索引,动态修改属性项结构后,这些索引没有同步。尤其是不调用 AdjustLayout 时,内部布局缓存还是旧的,命中测试就会出错。
解决:对属性树进行结构性修改(增删分类或属性项)后,调用 ResetAll 和 AdjustLayout。ResetAll 会强制刷新所有属性项的展开状态和选中状态,也会重建内部布局,但也会把用户当前折叠状态复位。如果不想丢失展开状态,先备份再恢复。
// 备份展开状态(略) m_PropGrid.ResetAll(); m_PropGrid.AdjustLayout();有一个原则:改值不需要 ResetAll,改结构必须 ResetAll。我曾经在一个刷新逻辑里只 AddProperty 不调用 ResetAll,出现间歇性选中错乱,花了两天才定位到是索引缓存问题。从那以后我遵守这个原则,再没翻车。
5. 进阶技巧:把属性表做成真正的配置面板
5.1 用属性项指针做关联映射
如果每个属性都写一个 if-else,属性一多就失控。我习惯用一张映射表把属性名和业务变量绑定。做法是定义一个结构体,包含属性名、类型、目标对象的偏移量或回调。这样填充和回读都能用循环完成。
struct PropertyBinding { LPCTSTR name; int type; // 0=字符串 1=整数 2=颜色 size_t offset; // 在对象中的字节偏移 };遍历绑定表,用 offsetof 或者指针运算读写业务变量。实际项目中我还用过 std::function 绑定 getter 和 setter,代码更安全,但老编译器处理 std::function 到 C++11 兼容性要谨慎。这个技巧最大的好处是:新增配置项只改表,不改一堆处理函数。
5.2 序列化:把属性表内容存到注册表或 Ini
把属性表的值存起来再读回,是配置面板的基本需求。遍历所有分类和子属性,根据类型取 COleVariant 转成字符串,然后用 WritePrivateProfileString 写 Ini。读回时按名称 FindProperty 再 SetValue。要注意:读回时属性表必须先有完整的属性树结构,否则找不到目标属性。顺序是初始化时先构建属性树,再读配置,不然会空指针。
序列化代码里最容易出错的是颜色属性,FormatPropertyValue 返回了类似“RGB(255,0,0)”的显示字符串,解析时要用 sscanf 之类的函数拆出三个分量。文件属性呢,直接存路径字符串就好。我一般会为每种属性类型写一个 ToString/FromString,这样代码结构清晰。
5.3 禁用未授权功能项:灰化与隐藏
权限控制时,未授权的属性项要灰化或者干脆不显示。灰化很简单,Enable(FALSE) 即可。隐藏就不那么直接,属性表没有提供 HideItem 这个 API。常见做法是重新构建属性树时不添加未授权项,但这样会丢展开状态。另一个技巧是不真正移除,而是把属性项放到一个“不可见”的分类里?分类没有显示隐藏属性,无法实现。
我建议的做法:用一个配置函数根据权限生成属性列表,权限变化时重建整棵属性树,同时按 4.5 备份恢复分类展开状态。这样最干净,也好维护。灰化时保留属性项但不让它编辑,用户能看到值,适合“只读预览”场景。
5.4 性能优化:一次性填充 vs 逐个添加
属性项数量超过五百时,逐个 AddProperty 会出现明显卡顿,因为每次添加都会触发内部布局计算和重绘。我常用 LockWindowUpdate 暂停绘制,批量添加后再恢复并 AdjustLayout。注意 LockWindowUpdate 只能锁顶层窗口,且不能嵌套。更好的办法是提前把所有属性项对象都 new 出来,再一次性 Add 进去?MFC 没有批量 Add 接口,所以锁窗口是实际可行的。
实测在常规开发环境下,锁定窗口后批量添加五百个属性项,耗时从近一秒降到几十毫秒。如果属性项不来自数据库,只是简单配置,基本不需要这个优化。另一种优化是复用属性项对象,而不是每次删除重建——属性表控件不提供直接移出某个子项再放回另一处的方法,但可以用 SetValue 改变值而不改变树结构。
我这些年接手的模拟项目里,CMFCPropertyGridCtrl 总是被当成一个“属性表格”来用,其实它完全能承担配置中心的角色。关键是要把映射和序列化做好,让属性表只是表现层,业务数据始终存在独立的结构体里。从那以后我每次做参数面板都强制走一遍流程:先定映射表,再填充属性,回调里只做同步,最后序列化。希望帮到你。
本文还有配套的精品资源,点击获取