简介CMFCPropertyGridCtrl 使用示例是一份面向 MFC 开发者的属性网格控件演示资源。资源包为 RAR 压缩格式共 40 个文件、约 152KB以 11 个头文件、7 个 C 源文件为主配合 vcxproj 工程文件、rc 资源脚本、aps 辅助资源和调试日志构成可直接打开学习的完整工程。内容预览中可见 test 与 dll1 两套工程分别演示了控件创建、属性添加、子属性组织及界面样式设置等关键操作能够帮助读者快速掌握从初始化到属性变更处理的全流程。目前已有 735 人学习使用适合需要集成属性网格功能或深入理解 MFC 控件扩展机制的开发者。通过对照示例代码读者可以避免常见的编译与界面刷新问题并在此基础上扩展自定义属性编辑器示例代码结构紧凑适合直接导入 Visual Studio 查看调试对于希望快速搭建属性编辑界面的团队具有直接参考价值。 用了这么多年MFC我印象里第一次打开Visual Studio的属性面板时心里想的是这种控件到底怎么实现的。后来才知道微软从BCGControlBar Pro里移植了一套界面组件到MFC其中就包括CMFCPropertyGridCtrl这个属性网格控件。它的出现基本让自绘属性面板这件事成了历史。我最早是在一个内部配置工具里用它替换手写的几十个编辑框和下拉框改完之后整个界面代码量直接砍掉一大半。这篇就从一个实际使用者的角度把CMFCPropertyGridCtrl的初始化、属性项构造、事件回调、数据回流以及我踩过的坑完整过一遍。如果你正在做MFC桌面工具、节点属性编辑、配置面板这类功能这篇文章应该能帮你少走不少弯路。1. 为什么属性网格能免掉你一半的界面代码先说说这个控件到底解决了什么问题。做工具类软件的时候总免不了要给一堆对象编辑属性。比如一个绘图程序的画布属性宽、高、背景色、网格开关、对齐方式、导出路径……如果全用普通控件实现一个属性一个编辑框加一个标签再手动处理布局属性一多界面就乱成一团动态增删属性更是噩梦。CMFCPropertyGridCtrl的工作方式更像一张属性表每一行左侧显示属性名右侧显示属性值。属性可以是文本、数字、下拉选择、颜色、文件路径、字体、布尔开关、甚至自定义的热点按钮。它内置了编辑框、下拉列表、颜色选择器、文件选择对话框还带分组和折叠功能。你只需要往里面塞属性对象剩下的绘制和交互它自己处理。这个控件能省代码的核心在于它把一行属性封装成了一个对象而不是一堆分散的控件。每个CMFCPropertyGridProperty对象自带值、描述、类型、是否可编辑等状态。一个几百行代码的编辑页面用这个控件浓缩成几十行AddProperty调用。另外它还支持运行时增删属性配置文件切换、动态模板这类需求处理起来特别顺手。从源流上看CMFCPropertyGridCtrl是VS2008 SP1开始随MFC一起发布的底层结构和BCGControlBar Pro的属性网格一脉相承。所以你在网上搜到BCG相关的用法放到MFC版本里大多也对得上。它在MFC九年义务教育里属于老师没教但工作中总碰到的控件文档有但比较零散好用但也藏了不少需要注意的地方。2. 启用控件前的三个隐藏前置条件2.1 视觉样式必须提前打开这是新人最容易碰到的第一个坑控件拖上去了代码也写了运行出来却是一片空白或者背景颜色不对。原因多半是没启用视觉样式管理器。CMFCPropertyGridCtrl的绘制内部依赖CMFCVisualManager必须在程序初始化阶段调用BOOL CMyApp::InitInstance() { // 必须在创建属性网格之前执行 CMFCVisualManager::SetDefaultManager(RUNTIME_CLASS(CMFCVisualManagerVS2008)); // 如果你希望控件外观跟随系统主题可以换用 // CMFCVisualManager::SetDefaultManager(RUNTIME_CLASS(CMFCVisualManagerWindows)); ... }这里不调用的后果就是控件能创建但自绘逻辑走不通表现通常是空白或者底色一片异常。还有一个坑是开发机如果关闭了系统的视觉样式比如在某些精简版Windows上CMFCVisualManagerVS2008的绘制会退化这时候换用CMFCVisualManagerWindows往往能救回来。2.2 消息映射用ON_REGISTERED_MESSAGE而不是ON_MESSAGE属性网格的值变化通知是一条注册消息不是普通的WM_COMMAND或WM_NOTIFY。在对话框或视图类的消息映射里要这样写BEGIN_MESSAGE_MAP(CMyDialog, CDialogEx) ON_REGISTERED_MESSAGE(AFX_WM_PROPERTY_CHANGED, CMyDialog::OnPropertyChanged) // AFX_WM_PROPERTY_CHANGING 也可以注册它在编辑器中数据修改但尚未提交时触发 END_MESSAGE_MAP() afx_msg LRESULT OnPropertyChanged(WPARAM wParam, LPARAM lParam);我把这条单独拎出来是因为老代码里很多人习惯性写ON_MESSAGE结果编译报错或者收不到消息折腾半天。AFX_WM_PROPERTY_CHANGED的wParam是控件句柄lParam是发生变化的CMFCPropertyGridProperty指针。注意这个宏必须写在BEGIN_MESSAGE_MAP/END_MESSAGE_MAP之间不需要额外的消息ID定义系统内部已经注册好了。2.3 典型初始化代码骨架控件可以资源编辑器里拖也可以动态创建。我习惯用动态创建因为属性网格多半需要调整位置和大小而且动态创建可以随时RemoveAll重建灵活很多。// 在对话框OnInitDialog里 CRect rectClient; GetClientRect(rectClient); m_wndPropGrid.Create(WS_CHILD | WS_VISIBLE | WS_TABSTOP, CRect(0, 0, 100, 100), this, IDC_PROP_GRID); m_wndPropGrid.EnableHeaderCtrl(TRUE); // 显示属性 / 值列头 m_wndPropGrid.SetAlterColor(RGB(247, 247, 247)); // 交替行底色提升可读性 m_wndPropGrid.SetLeftColumnWidth(160); // 左侧属性名列宽几个细节说明。EnableHeaderCtrl(TRUE)会显示属性/值的分栏标题如果你对界面空间敏感也可以关掉。SetAlterColor是交替行背景色不开也不影响功能但是属性多的时候连续看错行是常有的事开了之后视觉上舒服很多。SetLeftColumnWidth虽然名字看着是设置像素宽度但它有个使用时机问题后面第五部分会专门说我建议在AddProperty全部做完之后调用一次相对稳妥。3. 属性项的构造与常用类型从普通文本到下拉列表3.1 基础文本属性的创建CMFCPropertyGridProperty有几个重载构造函数实际开发中最常用的就两个一个是无描述信息的短版本另一个是带描述和业务ID的完整版本。我强烈建议用完整版本CMFCPropertyGridProperty* pProp new CMFCPropertyGridProperty( _T(项目名称), // 属性显示名称 COleVariant(_T(默认项目)), // 属性值注意要包成COleVariant _T(显示在状态栏的说明文字), // 描述 ID_PROP_PROJECT_NAME); // 业务ID后面用GetData()取回 m_wndPropGrid.AddProperty(pProp);最后一个参数是DWORD_PTR类型的dwData这一步很多人会省觉得没用。但实际项目里属性一多回调里靠属性名字符串比较会很痛苦。SetData/GetData是属性和业务逻辑之间最直接的关联手段传一个枚举或资源ID进去后面值变化的回调处理会清爽很多。3.2 下拉选择、颜色、文件路径、字体属性下拉列表用AddOption逐个添加可选项。注意默认值是通过COleVariant((long)索引)来指定的不是传字符串CMFCPropertyGridProperty* pMode new CMFCPropertyGridProperty( _T(工作模式), COleVariant((long)0), _T(选择程序运行模式)); pMode-AddOption(_T(自动)); pMode-AddOption(_T(手动)); pMode-AddOption(_T(半自动)); pMode-AllowEdit(FALSE); // 禁止手动输入只能从下拉列表选 m_wndPropGrid.AddProperty(pMode);COleVariant((long)0)对应第一个选项。这个写法容易踩坑的地方是有人会传字符串自动作为默认值但下拉属性内部保存的是索引而不是字符串初始化后界面可能显示异常或者是索引号。AllowEdit(FALSE)这个调用经常被忽略如果允许编辑用户就可以在框里输入一个列表里不存在的值后面取值时你会拿到一个非法索引处理起来很麻烦。颜色属性、文件属性和字体属性有专门的派生类// 颜色属性 CMFCPropertyGridColorProperty* pColor new CMFCPropertyGridColorProperty( _T(背景色), RGB(255, 255, 255), _T(界面背景颜色)); // 文件属性第二个参数TRUE表示保存文件对话框FALSE表示打开文件对话框 CMFCPropertyGridFileProperty* pFile new CMFCPropertyGridFileProperty( _T(导出路径), TRUE, _T(C:\\out.txt), _T(txt), _T(文本文件(*.txt)|*.txt||)); // 字体属性 CMFCPropertyGridFontProperty* pFont new CMFCPropertyGridFontProperty( _T(正文字体), _T(微软雅黑), _T(界面正文字体)); m_wndPropGrid.AddProperty(pColor); m_wndPropGrid.AddProperty(pFile); m_wndPropGrid.AddProperty(pFont);这三类是直接从CMFCPropertyGridProperty继承下来的AddProperty接受基类指针所以不用做强制转型。文件属性的过滤器字符串格式和CFileDialog一致最后必须以||结尾这个格式写错的话文件对话框里看不到任何文件。3.3 用分组属性做折叠分区属性一多平铺展示就不够看了。CMFCPropertyGridCtrl的分组功能本质上是构造一个没有值的父属性再把子属性挂到它下面CMFCPropertyGridProperty* pGroup new CMFCPropertyGridProperty(_T(基本设置)); pGroup-AddSubItem(new CMFCPropertyGridProperty( _T(名称), COleVariant(_T(obj1)), _T(对象名称))); pGroup-AddSubItem(new CMFCPropertyGridProperty( _T(可见), COleVariant((BOOL)TRUE), _T(是否可见))); pGroup-AddSubItem(new CMFCPropertyGridProperty( _T(透明度), COleVariant((long)50), _T(百分制))); m_wndPropGrid.AddProperty(pGroup);注意父属性本身不要调用SetValue设置值否则会出现一个分组本身竟然也能编辑值的诡异效果。分组的意义在于视觉层次和折叠它自己不是一条可编辑属性。另外分组属性仍然可以用Enable(FALSE)整组置灰这个技巧在权限控制里很实用一个分组代表一个功能模块禁用的时候就整组disable掉。还有一个批量操作的小经验动态填充大量属性时先把SetRedraw(FALSE)挂起全部AddProperty完毕后再SetRedraw(TRUE)并Invalidate能明显减少闪烁属性多的时候效果尤其突出。4. 值变化回调属性变了业务数据怎么跟上4.1 事件回调里拿到变化的是哪个属性在消息映射里注册了AFX_WM_PROPERTY_CHANGED之后处理函数第一件事是解析lParam确定到底是哪个属性变了。用GetData()拿业务ID是效率最高的判断方式LRESULT CMyDialog::OnPropertyChanged(WPARAM wParam, LPARAM lParam) { CMFCPropertyGridCtrl* pGrid (CMFCPropertyGridCtrl*)wParam; CMFCPropertyGridProperty* pProp (CMFCPropertyGridProperty*)lParam; if (pGrid nullptr || pProp nullptr) return 0; switch (pProp-GetData()) { case ID_PROP_PROJECT_NAME: // 处理项目名称变化 break; case ID_PROP_WORK_MODE: // 处理工作模式变化 break; default: break; } return 0; }如果你当时没SetData那这里就只剩比较属性名的路可走。短属性名倒还好长英文属性名容易写错而且代码里散布一堆魔法字符串重构属性名的时候还不容易查全。所以回到第三部分说的从一开始就养成SetData的习惯。4.2 COleVariant转C数据类型的完整套路回调里拿到CMFCPropertyGridProperty指针后用GetValue()取到的其实是一个COleVariant。不同属性类型对应的字段不一样我整理了一个常用对照属性类型取值方式说明文本属性pProp-GetValue().bstrVal转成CString即可Int/Long属性pProp-GetValue().lVal注意不是intValDouble属性pProp-GetValue().dblVal浮点数值布尔属性pProp-GetValue().boolVal ! VARIANT_FALSEboolVal是VARIANT_BOOL下拉属性pProp-GetValue().lVal拿到的是选项索引颜色属性pProp-GetColor()直接返回COLORREF文件属性pProp-GetValue().bstrVal返回路径字符串CString接收最常见的转换套路是这样// 文本属性 CString strName pProp-GetValue().bstrVal; // 布尔属性 BOOL bVisible (pProp-GetValue().boolVal VARIANT_TRUE); // 整型属性 int nValue (int)pProp-GetValue().lVal; // 下拉属性需要选项字符串时 int nIndex (int)pProp-GetValue().lVal; CString strOption pProp-GetOption(); // 获取当前选中项文本颜色属性和文件属性并没有走通用值那条路它们有自己的专用接口GetColor()返回COLORREFGetValue().bstrVal返回路径字符串。如果你试图从颜色属性里取lVal结果大概率是0或者垃圾值。这个区分在最初接本文还有配套的精品资源点击获取