简介:围绕CMFCPropertyGridCtrl属性网格控件的一套完整MFC示例工程,面向需要在应用程序中快速构建属性编辑界面的MFC开发者。工程内含test与dll1两个子项目,演示了从对话框添加属性控件、初始化成员变量到通过AddProperty逐步添加父子属性的完整流程,并涉及属性值变更回调、字符串与整型等常见类型定义,适合初学者对照练习。压缩包共40个文件,以.h头文件(11个)、.cpp实现文件(7个)为主,配合sln/vcxproj工程配置、rc资源文件、ReadMe说明文档以及Visual Studio生成的aps、suo等辅助文件,整体仅152KB,结构清晰明了可直接打开编译查看。已有735人学习下载,内容还覆盖了控件样式设置、类别显示开关、视觉风格切换等细节;对于自定义属性编辑器、动态加载属性等进阶需求,也给出了明确且可操作的基于继承扩展CMFCPropertyGridCtrl的入手思路。
1. MFC Property Grid Ctrl:给设置页减负的属性表格控件
一个维护中的老工具要在界面里加二十几个配置项,挨个摆编辑框、下拉框、复选框再关联变量,没有两三天下不来。MFC 的 CMFCPropertyGridCtrl(也就是常说的 MFC Property Grid Ctrl)把这件事压缩到半天:它自带字符串编辑、下拉选择、颜色选取、文件路径浏览和分组折叠,界面长得跟 IDE 属性面板一样。真正耗时间的不是把控件 Create 出来,而是搞懂属性项怎么分组、值变化后去哪取、回调里哪些指针不能缓存。这篇笔记适合正在给 MFC 工程做设置页、属性面板,或者想给现有工具加参数配置区的开发者。核心结论先放这:这个控件工作量 20% 在装配,80% 在回调。
2. 先把控件装配起来:占位控件、动态创建与属性分组
2.1 在对话框里放占位控件,OnInitDialog 里再真正创建
不要直接想着在资源模板上拖一个“属性网格”。这类功能包控件的实际做法是:先放一个 Static 控件占好位置,然后在 OnInitDialog 里按占位控件的位置创建真正的属性网格。这样做的好处是不依赖 IDE 版本对功能包控件的支持程度,代码在哪个环境都能编译。占位控件可以给它一个明确的 ID,比如 IDC_PROP_PLACEHOLDER。
// ConfigDlg.h 成员声明 CMFCPropertyGridCtrl m_wndPropGrid; // ConfigDlg.cpp BOOL CConfigDlg::OnInitDialog() { CDialogEx::OnInitDialog(); CWnd* pPlaceholder = GetDlgItem(IDC_PROP_PLACEHOLDER); if (pPlaceholder == nullptr) return TRUE; CRect rc; pPlaceholder->GetWindowRect(&rc); ScreenToClient(&rc); if (!m_wndPropGrid.Create(WS_CHILD | WS_VISIBLE | WS_TABSTOP, rc, this, IDC_PROP_GRID)) { TRACE(_T("属性网格创建失败\n")); return TRUE; } m_wndPropGrid.SetGroupNameBold(TRUE); m_wndPropGrid.SetAlterRowColor(TRUE); m_wndPropGrid.SetDescriptionRows(2); m_wndPropGrid.SetShowAlwaysDescription(TRUE); return TRUE; }逻辑说明:创建前把占位 Static 控件的文字清空或直接隐藏,否则会出现一个标签叠在属性网格左上角。Create 的矩形直接取占位控件的窗口矩形,再转为客户区坐标;占位控件本身不参与任何交互,只是用来占住网格的位置和大小。如果资源里没有合适的位置,也可以用代码动态计算矩形,下一节会讲。
参数说明:WS_CHILD | WS_VISIBLE | WS_TABSTOP 是属性网格最常用的三件套,缺 WS_VISIBLE 会看不到控件,缺 WS_TABSTOP 则键盘 Tab 无法进入。SetGroupNameBold(TRUE) 让分组名加粗显示;SetAlterRowColor(TRUE) 让相邻行底色不同,长列表时眼睛不容易看串行;SetDescriptionRows(2) 设定底部描述区的高度,SetShowAlwaysDescription(TRUE) 保证没选中属性时描述区也占着空间,切换页面时布局不会突然跳一下。
2.2 纯代码动态创建:给工具窗口加属性面板的省事路径
如果不是对话框而是普通工具窗口,或者属性面板需要在运行期按条件显示和隐藏,常见做法是直接在代码里算矩形后 Create。比如左侧放一个树控件、右侧放属性网格,属性网格宽度固定,高度随窗口大小调整。
CRect rcClient; GetClientRect(&rcClient); CRect rcGrid(rcClient); rcGrid.DeflateRect(10, 10, 10, 10); if (!m_wndPropGrid.Create(WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_VSCROLL, rcGrid, this, IDC_PROP_GRID)) { return FALSE; }逻辑说明:DeflateRect 在四周留出边距,适合属性区占满整个窗口的布局。如果属性面板要靠在某个固定面板右侧,就手动把 rcGrid 的 left 设置为面板右边界加间距。动态创建方式下,父窗口的 OnSize 里要跟着调用 MoveWindow 或 SetWindowPos,否则窗口拉大后属性网格还停留在初始大小,下半截全是空白。
参数说明:WS_VSCROLL 是属性项超过可视区时出现垂直滚动条的关键,不加这一条长列表无法滚动。IDC_PROP_GRID 是自定的控件 ID,后续接收 AFX_WM_PROPERTY_CHANGED 消息时靠它识别控件的子类化状态,也可以用 GetDlgItem(IDC_PROP_GRID) 在别处取到这个网格的指针。
2.3 分组和属性项:组也是一个属性对象
很多人第一次接触会被类名绕晕。CMFCPropertyGridCtrl 管理的是一个 CMFCPropertyGridProperty 对象的树,“组”不是独立的控件类型,它本质就是一个属性项,只是这个属性项可以 AddSubItem 往里面挂子属性。子属性也可以继续挂子属性,形成嵌套树。折叠分组靠的就是这个“组属性项”自带的展开收起状态。
CMFCPropertyGridProperty* pGroup = new CMFCPropertyGridProperty(_T("网络参数")); pGroup->AddSubItem(new CMFCPropertyGridProperty(_T("IP 地址"), _T("192.168.1.10"), _T("设备的 IP,长按可复制"))); pGroup->AddSubItem(new CMFCPropertyGridProperty(_T("端口号"), (LONG)502, _T("Modbus TCP 服务端口"))); CMFCPropertyGridProperty* pGroupIo = new CMFCPropertyGridProperty(_T("IO 通道")); pGroupIo->AddSubItem(new CMFCPropertyGridProperty(_T("通道数"), (LONG)8, _T("低于 1 会被自动钳制"))); m_wndPropGrid.AddProperty(pGroup); m_wndPropGrid.AddProperty(pGroupIo); m_wndPropGrid.ExpandAll(TRUE);逻辑说明:AddProperty 是把组挂到网格根节点,AddSubItem 是把子属性挂到组下面。属性对象一旦 AddProperty 成功,所有权就转移给属性网格,之后不要手动 delete,否则析构时二次释放直接崩。ExpandAll(TRUE) 展开所有分组;如果默认全部折叠,用户第一眼只看到两个组名,还会以为界面是残缺的。
参数说明:构造函数第一个参数是显示名,第二个是初始值,第三个是底部描述文字。构造重载按第二个参数的类型区分属性类型:传 _T("192.168.1.10") 生成字符串属性,传 (LONG)502 生成整数属性,传 VARIANT_TRUE 生成开关属性。陷阱在于传 int 和传 LONG 会被解析成不同的 VARIANT 类型,统一用 (LONG) 强转比较稳,别混着写。
3. 属性类型盘点:文本、数字、颜色、文件路径和下拉框的取值方式
3.1 一张表看清六种常用属性类型的构造和取值
| 属性类型 | 使用类 | 构造关键参数 | 常用取值 API |
|---|---|---|---|
| 文本 | CMFCPropertyGridProperty | 字符串初始值 | GetValue().bstrVal |
| 整数 | CMFCPropertyGridProperty | (LONG) 初始值 | GetValue().lVal |
| 开关 | CMFCPropertyGridProperty | VARIANT_TRUE / FALSE | GetValue().boolVal |
| 颜色 | CMFCPropertyGridColorProperty | COLORREF | GetColor() |
| 文件路径 | CMFCPropertyGridFileProperty | 路径 + 过滤器 | GetValue().bstrVal |
| 下拉选择 | CMFCPropertyGridProperty + AddOption | 先构造后逐条 AddOption | GetOption() / GetValue().lVal |
这个表的现实意义是:属性类型不靠猜,构造时传什么类型的值,界面上就出现什么编辑器。字符串属性点进去是文本编辑框,整数属性旁边会出现上下箭头,颜色属性点击后弹出取色对话框。把类型选对,编辑行为自动就有了。
CMFCPropertyGridProperty* pBasic = new CMFCPropertyGridProperty(_T("基本设置")); pBasic->AddSubItem(new CMFCPropertyGridProperty(_T("设备名称"), _T("PLC-01"), _T("最长 32 字节"))); CMFCPropertyGridProperty* pCount = new CMFCPropertyGridProperty(_T("采集次数"), (LONG)100, _T("1-1000")); pBasic->AddSubItem(pCount); CMFCPropertyGridProperty* pEnable = new CMFCPropertyGridProperty(_T("启用采样"), VARIANT_TRUE, _T("关闭后暂停采集")); pBasic->AddSubItem(pEnable); CMFCPropertyGridColorProperty* pColor = new CMFCPropertyGridColorProperty(_T("报警颜色"), RGB(255, 0, 0), nullptr, _T("触发报警时的前景色")); pBasic->AddSubItem(pColor); CMFCPropertyGridFileProperty* pFile = new CMFCPropertyGridFileProperty(_T("导出路径"), _T("C:\\data\\record.csv"), _T("CSV 文件|*.csv|所有文件|*.*||"), _T("报表导出位置")); pBasic->AddSubItem(pFile); CMFCPropertyGridProperty* pProtocol = new CMFCPropertyGridProperty(_T("协议类型"), (LONG)0, _T("连接方式")); pProtocol->AddOption(_T("TCP")); pProtocol->AddOption(_T("UDP")); pProtocol->AddOption(_T("串口")); pBasic->AddSubItem(pProtocol); m_wndPropGrid.AddProperty(pBasic);逻辑说明:每个属性都被挂到“基本设置”这个组下面,组再挂到网格上。颜色属性和文件属性是独立子类,行为已经封装好,文件属性点击后会弹出系统文件对话框,颜色属性点击后弹出颜色选择对话框,不需要自己写任何弹窗代码。
参数说明:下拉框属性有个顺序要求——先 AddOption 把所有选项加完,再 SetValue((LONG)0) 或构造时传 (LONG)0 设置初始索引。如果反过来先 SetValue 后 AddOption,索引和选项对应关系会错位,下拉框可能显示空或显示成第一项的重复值。文件路径字段里的双反斜杠是字符串转义,过滤器用竖线和双竖线分隔,最后记得以 || 收尾。
3.2 取值:GetValue 返回 VARIANT,类型全在联合里
属性值的读取不能一刀切。GetValue() 返回的是 VARIANT 结构,实际数据放在哪个字段取决于属性内部保存的类型。字符串看 bstrVal,整数看 lVal,布尔看 boolVal。写代码时脑子里得有这个映射表,否则拿到一个 0 还以为是整数 0,实际可能是空字符串转换失败的结果。
CString strIP = pProp->GetValue().bstrVal; int nPort = static_cast<int>(pProp->GetValue().lVal); BOOL bEnable = pProp->GetValue().boolVal == VARIANT_TRUE;逻辑说明:bstrVal 是 BSTR 类型,MFC 里赋给 CString 会自动转换,但要注意空值判断。整数属性统一读 lVal,布尔属性读 boolVal 后跟 VARIANT_TRUE 比较,不要直接当 int 用。调试时可以打开 Watch 窗口看 VARIANT 的 vt 字段,VT_I4 对应 lVal,VT_BSTR 对应 bstrVal,VT_BOOL 对应 boolVal,一眼就能确认类型有没有传错。
参数说明:VARIANT 的字段很多,intVal 和 lVal 都是整数但位数不同,传参时如果混用会导致高位意外截断。我自己吃过这个亏:构造时传了 int 的 1,读取时却按 lVal 拿,在 64 位下低 32 位碰巧对上没事,数值一大就翻车。构造时用了 (LONG) 强转,读取时就用 lVal,前后保持一致。
3.3 区分属性业务:用 SetData 挂业务 ID,而不是比较显示名称
比较属性名在中文界面下容易出问题:界面文案换了,回调逻辑就得跟着改。更稳的做法是构造后给每个属性挂一个业务 ID,回调里用 GetData 判断是哪个属性变了。这样属性名只负责显示,业务 ID 负责逻辑分支。
pCount->SetData(ID_PROP_COUNT); pProtocol->SetData(ID_PROP_PROTOCOL); // 回调里 switch (pProp->GetData()) { case ID_PROP_COUNT: { int nCount = static_cast<int>(pProp->GetValue().lVal); // 更新业务数据 } break; case ID_PROP_PROTOCOL: { CString strProtocol = pProp->GetOption(); // 切换协议相关显示 } break; }逻辑说明:dwData 是 DWORD_PTR 类型,可以存枚举、指针或索引。回调里先 GetData 再按业务 ID 分支,后续属性改名只改构造函数里的第一个参数,回调代码一行不用动。GetData 的值不会显示在界面上,也不会在编辑过程中被清掉。
参数说明:switch 的每个 case 里如果声明局部变量,记得用花括号包一层,否则编译器会报“跳过初始化”的错。SetData 存的是属性对象上的附加数据,不参与序列化,程序重启后属性 ID 需要重新设置。
4. 属性变化回调:AFX_WM_PROPERTY_CHANGED 与两处指针血泪
4.1 注册消息映射:属性网格的通知走的是注册消息
用户修改属性后,属性网格会发送一条注册消息 AFX_WM_PROPERTY_CHANGED。注意这是注册消息,不是普通 WM_ 开头的窗口消息,不能用 ON_MESSAGE 直接写,得用 ON_REGISTERED_MESSAGE。
BEGIN_MESSAGE_MAP(CConfigDlg, CDialogEx) ON_REGISTERED_MESSAGE(AFX_WM_PROPERTY_CHANGED, &CConfigDlg::OnPropChanged) END_MESSAGE_MAP() LRESULT CConfigDlg::OnPropChanged(WPARAM wParam, LPARAM lParam) { CMFCPropertyGridProperty* pProp = reinterpret_cast<CMFCPropertyGridProperty*>(lParam); if (pProp == nullptr) return 0; DWORD_PTR dwData = pProp->GetData(); if (dwData == ID_PROP_COUNT) { int nCount = static_cast<int>(pProp->GetValue().lVal); m_nCount = nCount; } else if (dwData == ID_PROP_IP) { m_strIP = pProp->GetValue().bstrVal; } return 0; }逻辑说明:lParam 里装的是发生变化的属性对象指针,wParam 在本消息里用不到。OnPropChanged 是自定义函数名,返回 LRESULT 即可。回调里先判空,再 GetData 或 GetName 判断业务分支,然后立即从 GetValue 读取最新值。不要在回调里跑耗时逻辑,界面会卡顿;只把值落成员变量,等需要时再统一使用。
参数说明:AFX_WM_PROPERTY_CHANGED 在功能包公共头文件里定义,只要包含头文件就能直接用。这个回调只在用户交互修改值时触发,程序里调用 SetValue 不会每次都发消息,这是很多坑的根源——以为改值会通知业务逻辑,实际只是改了界面显示。
4.2 指针失效:属性值一变,你手里的对象可能已经不是原来那个
写回调时最容易踩的坑:把 pProp 保存到成员变量里,等下一次事件再来时继续用。我一开始也这么干,结果第二次读取要么值还是旧的,要么直接访问违规。现象很迷惑,调试器里看地址没变,但内容已经换了。原因在于 CMFCPropertyGridCtrl 编辑完一个属性后会把原属性对象回收,再按当前值构造一个新属性对象来替换。旧指针在内存释放后就成了悬垂指针。
解决思路只有一条:不要缓存属性指针,存业务 ID、属性名或要同步出去的“值”,每次回调都从事件参数里重新拿属性对象。
// 按 ID 找属性的通用工具函数 CMFCPropertyGridProperty* CConfigDlg::FindPropertyByData(DWORD_PTR dwTarget) { int nGroupCount = m_wndPropGrid.GetPropertyCount(); for (int i = 0; i < nGroupCount; i++) { CMFCPropertyGridProperty* pGroup = m_wndPropGrid.GetProperty(i); if (pGroup == nullptr) continue; int nSubCount = pGroup->GetSubItemCount(); for (int j = 0; j < nSubCount; j++) { CMFCPropertyGridProperty* pSub = pGroup->GetSubItem(j); if (pSub != nullptr && pSub->GetData() == dwTarget) return pSub; } } return nullptr; }逻辑说明:按 ID 遍历查找,需要用的时候现找现用,用完即弃。GetProperty(i) 取的是第 i 个分组,GetSubItem(j) 取分组内第 j 个子属性。跨分组查找必须两层遍历,GetPropertyCount 和 GetSubItemCount 分别拿总数。这个函数值得单独抽出来,后面所有联动都会用到。
参数说明:GetProperty 的索引是分组索引,不是全局属性索引。如果属性还会嵌到第三层,查找函数还要再深一层,但实际项目中两层就够用。把查找写在回调里没问题,属性数量通常几十个以内,遍历开销可以忽略。
提示:任何时候拿到 CMFCPropertyGridProperty*,都假设它只在当前函数内有效。需要跨事件保存信息时,存值不存指针。
4.3 联动改值:改变一个属性,主动刷新另一个属性
场景很常见:协议类型从 TCP 切到串口后,“端口号”就没有意义了,应该置灰;或者“通道数”改小后,下面“字节偏移”的上限要跟着变。回调里改别的属性,核心是 Enable 控制可用性,SetValue 修正数值。
LRESULT CConfigDlg::OnPropChanged(WPARAM wParam, LPARAM lParam) { CMFCPropertyGridProperty* pProp = reinterpret_cast<CMFCPropertyGridProperty*>(lParam); if (pProp == nullptr) return 0; if (pProp->GetData() == ID_PROP_PROTOCOL) { CString strProtocol = pProp->GetOption(); CMFCPropertyGridProperty* pPort = FindPropertyByData(ID_PROP_PORT); if (pPort != nullptr) { pPort->Enable(strProtocol != _T("串口")); } m_wndPropGrid.Invalidate(); } return 0; }逻辑说明:Enable(FALSE) 会把属性行置灰,用户能看到但改不了,比直接隐藏更直观。如果确实需要直接修改另一个属性的值,回调里调用 SetValue 有可能再次触发 AFX_WM_PROPERTY_CHANGED,形成递归。常见做法是加一个 BOOL m_bUpdating 标志位,联动赋值期间置 TRUE,回调开头看到这个标志就跳过业务处理。
参数说明:联动完成后一定要 Invalidate 重绘,否则网格显示的还是旧值。如果只是改 Enable,重绘不及时会出现“属性看着是灰的,点一下又亮了”的怪现象。考究一点可以只刷新对应属性行,不要整表重绘,属性少时差别不大。
5. 避坑合集:刷新、颜色取值、文件对话框与嵌套层级的五条高频踩坑记录
5.1 改了值界面不刷新
现象:程序里 SetValue 改了某个属性的值,界面上显示的还是旧文本,点一下别的属性行才刷新过来。
原因:SetValue 只更新了属性对象内部的数据,不一定触发对应行重绘。属性网格没有收到绘制通知,旧文本就一直挂在界面上。
解决:批量修改属性时用 SetRedraw 配合 Invalidate,全部改完再统一重绘。
m_wndPropGrid.SetRedraw(FALSE); // 多次 SetValue m_wndPropGrid.SetRedraw(TRUE); m_wndPropGrid.Invalidate();如果单条修改后点了别的行才刷新,说明需要调 Invalidate;如果点了之后还是不刷新,那就要检查是不是改错了属性对象——拿到的是一个被替换前的旧指针,怎么刷新都不会有反应。
5.2 颜色属性取值翻车
现象:用 GetValue() 读颜色属性,拿到一个负数或者看起来完全不对的大数,和界面上选中的颜色对不上。
原因:颜色属性保存的是 COLORREF 类型,直接走 GetValue() 得到的 VARIANT 字段并不是预期的整数形式,有符号位参与解析后数值就乱了。
解决:对 CMFCPropertyGridColorProperty 类型单独处理,用 GetColor() 拿真正的 COLORREF。
CMFCPropertyGridColorProperty* pColorProp = DYNAMIC_DOWNCAST(CMFCPropertyGridColorProperty, pProp); if (pColorProp != nullptr) { COLORREF clr = pColorProp->GetColor(); CString strColor; strColor.Format(_T("#%06X"), clr & 0xFFFFFF); }用 DYNAMIC_DOWNCAST 做类型判断,比用属性名比较更可靠。转十六进制字符串时先做掩码,COLORREF 的高字节可能带着标志位,直接格式化会多出几位出来。
5.3 文件路径属性每次弹出都从默认目录开始
现象:CMFCPropertyGridFileProperty 点击后弹出文件选择框,每次都回到同一个固定目录,不记忆用户上一次选择的位置。
原因:文件属性对象构造时带了初始路径,但每次打开对话框时读到的还是这个初始值。用户选择完新路径后,新值没有及时写回属性对象,下次打开自然还是老目录。
解决:维护一个变量保存最近一次选择的完整路径。属性值变化时把新路径存下来,下次构造 CMFCPropertyGridFileProperty 时把这个路径作为初始值传入。如果是已存在的属性对象,选择完成后用 SetValue 写回新路径,下一次点开就会从上次的目录进入。
5.4 分组嵌套层级错乱
现象:一个组下面挂着子组,显示出来却像平级列表;折叠上一组时,子组纹丝不动。
原因:子组被 AddProperty 挂到了根节点,而不是用 AddSubItem 挂到父组下面。根节点上的每个属性都是独立分组,自然不存在父子关系。
解决:只有根级分组才用 AddProperty,所有下级子树一律用父属性的 AddSubItem。检查层级时看折叠箭头:分组名左侧有箭头说明它是根级或可展开节点;没有箭头大概率是挂错了。还有一个排查技巧是分别注释 AddProperty 和 AddSubItem 看显示差异,很快能定位是哪一层挂错。
5.5 高 DPI 下文字裁切
现象:高分屏把缩放比例调到 150%,属性文本被上下裁掉,行高明显不够。
原因:属性网格的行高按默认字体和固定行高计算,没有随 DPI 缩放。
解决:创建后按 DPI 重新计算行高,调用 SetRowHeight 设置,同时给网格设置一个按 DPI 缩放过的字体。
CClientDC dc(this); int nLogPixelsY = dc.GetDeviceCaps(LOGPIXELSY); int nRowHeight = MulDiv(16, nLogPixelsY, 96) + 4; m_wndPropGrid.SetRowHeight(nRowHeight); LOGFONT lf = { 0 }; lf.lfHeight = -MulDiv(9, nLogPixelsY, 72); m_fontProp.CreateFontIndirect(&lf); m_wndPropGrid.SetFont(&m_fontProp);行高基数 16 是按默认字体来的,实际字体偏大时可以往上加。不同主题下默认字体有差异,这个数值属于视觉微调,多试几个基数,找到看着不裁切的阈值。
6. 进阶:动态重建属性组和带单位自定义属性
6.1 配置项随状态变化:RemoveAll 后重建
有些模块,比如“设备类型”一换,参数表就完全不同。这时候不用维护复杂的一套显隐逻辑,直接 RemoveAll 全部清掉再重建,反而最不容易留脏属性。RemoveAll 会释放网格持有的所有属性对象,旧指针全部悬垂,重建前一定要先把需要的旧值拷出来。
void CConfigDlg::RebuildPropertyGrid(int nDeviceType) { m_wndPropGrid.RemoveAll(); if (nDeviceType == 1) { AddGroupNetwork(); } else { AddGroupSerial(); } m_wndPropGrid.ExpandAll(TRUE); m_wndPropGrid.Invalidate(); }重建后用户之前的修改会丢,所以一般只在整组切换的场景用。如果属性数量少,也可以保留公共组、只重建差异组。RemoveAll 会触发一次大面积重绘,界面有轻微闪烁,属正常现象,不用额外处理。
6.2 继承 CMFCPropertyGridProperty 做带单位的数值属性
属性名里写单位会把名称拉得很长,比如“采样间隔(ms)”。更干净的做法是继承一个带单位的数值属性,显示时自动拼接单位,编辑结束后校验数值。自定义属性类主要覆盖 FormatProperty 控制显示文本,OnUpdateValue 在编辑结束时校验并保留新值。具体接口签名以手头版本的头文件为准,思路是通用的。
class CUnitNumberProperty : public CMFCPropertyGridProperty { public: CUnitNumberProperty(const CString& strName, int nValue, const CString& strUnit) : CMFCPropertyGridProperty(strName, (LONG)nValue), m_strUnit(strUnit) { } protected: virtual CString FormatProperty() override { CString str; str.Format(_T("%d %s"), static_cast<int>(GetValue().lVal), m_strUnit); return str; } virtual BOOL OnUpdateValue() override { if (!CMFCPropertyGridProperty::OnUpdateValue()) return FALSE; // 在这里从编辑框拿到文本并做范围校验 // 不合法时回弹旧值或 SetValue 修正 return TRUE; } private: CString m_strUnit; };显示时走 FormatProperty 拼接单位,编辑结束时走 OnUpdateValue 做范围检查。这样“采样间隔(ms)”可以写成“采样间隔”,单位在值后面自然显示,表格宽度和阅读体验都会好。如果设备进了生产模式,还可以让某些单位属性直接拒收修改——在 OnUpdateValue 里返回 FALSE 即可,比外部 Enable(FALSE) 更内聚。
从那以后我每次给这种属性网格加新类型,都强制走一遍“构造 → 上树 → 改值 → 取回”四步自测,颜色、文件路径这类特殊子类单独验证取值接口,不再等到联调时才发现读出来的是负数。这套流程帮我挡掉过好几次返工,希望帮到你。
本文还有配套的精品资源,点击获取