简介围绕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开关CMFCPropertyGridPropertyVARIANT_TRUE / FALSEGetValue().boolVal颜色CMFCPropertyGridColorPropertyCOLORREFGetColor()文件路径CMFCPropertyGridFileProperty路径 过滤器GetValue().bstrVal下拉选择CMFCPropertyGridProperty AddOption先构造后逐条 AddOptionGetOption() / 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_castint(pProp-GetValue().lVal); BOOL bEnable pProp-GetValue().boolVal VARIANT_TRUE;逻辑说明bstrVal 是 BSTR 类型MFC 里赋给 CString 会自动转换但要注意空值判断。整数属性统一读 lVal布尔属性读 boolVal 后跟 VARIANT_TRUE 比较不要直接当 int 用。调试时可以打开 Watch 窗口看 VARIANT 的 vt 字段VT_I4 对应 lValVT_BSTR 对应 bstrValVT_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_castint(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_castCMFCPropertyGridProperty*(lParam); if (pProp nullptr) return 0; DWORD_PTR dwData pProp-GetData(); if (dwData ID_PROP_COUNT) { int nCount static_castint(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_castCMFCPropertyGridProperty*(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_castint(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) 更内聚。从那以后我每次给这种属性网格加新类型都强制走一遍“构造 → 上树 → 改值 → 取回”四步自测颜色、文件路径这类特殊子类单独验证取值接口不再等到联调时才发现读出来的是负数。这套流程帮我挡掉过好几次返工希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?