ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ArcGIS Engine专题图自动生成:C#二次开发从渲染器到批量出图

ArcGIS Engine专题图自动生成:C#二次开发从渲染器到批量出图 简介这套源码面向需要在 ArcGIS Engine 环境中实现专题图批量自动生成的 C# 开发者。使用 Visual Studio 配合 ArcGIS 10.2 与 ArcGIS Engine 10.2 开发支持多张专题图自动出图并可在图中添加地震烈度信息及其他地震灾害相关数据适合 GIS 二次开发进阶、地震应急信息可视化与灾情快速制图类项目参考。压缩包共 57 个文件其中包含 18 个 C# 源码文件、项目解决方案与配置文件、界面资源文件、编译产物及说明文档另有 7 张系统使用截图整体大小仅 1.33MB结构清晰便于定位代码。目前已有 174 人学习下载。源码覆盖地震烈度、灾害调查、灾情点、余震点等专题图模块并包含要素绘制、线对象创建、数据清理、通用数学计算等辅助类项目说明 README 也对运行环境与结构做了介绍。直接打开 WinForm.sln 即可编译运行开发者既能对照研究专题图自动出图的完整流程也可基于源码继续扩展新专题图层与输出规则。1. 专题图自动生成系统这套 ArcGIS Engine C# 源码到底拿来干什么做自然资源、规划出图、统计汇报的 GIS 从业者应该都有过这种周四下午要交十几张专题图ArcMap 里一张张选字段、选渲染方式、拖颜色条、调整图例再导出设定分辨率的 JPG一天基本就交代了。我最早接触 ArcGIS Engine 的 C# 二次开发就是被这种重复劳动逼的。这套基于 ArcGIS Engine 二次开发的专题图自动生成系统思路是把「选字段、选渲染、配色、出图」四步变成参数你把行政区面数据和统计数据丢进去源码自动生成唯一值、分级设色或统计图表专题图再按模板批量导出。适合想在 C/S 系统里内嵌一键出图能力、或者需要批量交付专题图集的项目方。专题图自动生成在 ArcObjects 里不算新鲜事但能跑通全流程、还带项目说明的 C# 源码包值得拆开看看。2. 先别急着写 Renderer三类专题图渲染器与选型依据很多新手拿到这套源码第一反应是去找「生成专题图」的函数结果在 ESRI.ArcGIS.Carto 命名空间里绕晕了。实际 ArcObjects 里根本没有「专题图」这个对象它只有渲染器Renderer——一份规则挂到图层上地图按规则重新画一遍。理解了这个映射关系后面所有代码都是顺理成章的。2.1 专题图的本质从字段到视觉符号的一次映射在 ArcMap 里手工做专题图你实际上干的事是告诉某个图层「用哪个字段、按什么规则、配什么颜色」。这套操作落到 ArcGIS Engine 里就是设置图层的 Renderer 属性。图层对象必须转成 IGeoFeatureLayer 接口因为只有它暴露了 Renderer 读写能力。// 把普通FeatureLayer转成GeoFeatureLayer专题图渲染器只能挂在这个接口上 IGeoFeatureLayer geoLayer featureLayer as IGeoFeatureLayer; // renderer 可以是唯一值、分级设色、统计图表等任意渲染器对象 geoLayer.Renderer renderer; // 渲染器挂上后必须刷新视图否则界面上看不到任何变化 (axMapControl1.Map as IActiveView).Refresh();逻辑说明这段代码是专题图自动生成的最小闭环。第一行做接口转换第二行写入渲染规则第三行让地图重绘。这里有个容易忽略的点——IFeatureLayer 接口也有 Renderer 属性吗有但 IGeoFeatureLayer 更完整而且后续设置透明、标注、符号级别都要靠它。源码里如果看到featureLayer as IGeoFeatureLayer的写法就是这个用途。参数说明featureLayer可以来自 Map 里已有的图层也可以是你新 new 出来的 FeatureLayerClass 实例。renderer是 IRenderer 类型后面三节讲的 UniqueValueRenderer、ClassBreaksRenderer、ChartRenderer 都实现了这个接口。Refresh 一定要放在 Renderer 赋值之后顺序反了偶尔也能出效果但那是靠地图别的事件触发的重绘属于不可控的玄学不可依赖。2.2 选型依据唯一值、分级设色、统计图表各管哪类数据这套源码里内置了三类渲染器选哪个不由喜好决定由字段类型和业务语义决定。我用一个表格把它们的适用关系先说清楚渲染器C# 接口字段类型典型业务唯一值IUniqueValueRenderer字符串/枚举型如地类代码、权属性质土地利用分类图、权属分布图分级设色IClassBreaksRenderer连续数值如 GDP、人口、地价普查数据分布、经济发展水平统计图表IChartRenderer多个数值字段的横向对比产业结构、三次产业占比选型理由很直白分类字段做唯一值是「一类一色」做分级设色就完全没意义因为字符串没有大小关系连续数值字段用唯一值更糟每个值一个颜色几千个图斑就是几千种颜色图面彻底报废。统计图表是给「一个面元素上要表达多个数值」准备的比用两个图层叠加更直观。实际项目里我还常用到 IProportionalSymbolRenderer按数值大小缩放符号做环境监测点位这种点数据时很好用但面数据专题图用得少这套源码里也没有展开读者知道有这个东西就行。选型后的产出差异很大唯一值适合「查询式」专题图分级设色适合「分布式」专题图统计图表适合「结构式」专题图。先定业务要表达什么再选渲染器顺序不能反。2.3 颜色条ArcGIS 专题图渲染的黑匣子专题图难看还是好看九成在颜色条。ArcObjects 里颜色条是 IColorRamp常见两个实现AlgorithmicColorRamp渐变色和 RandomColorRamp随机色。我在这套源码里看到的是 RandomColorRamp因为它拿来配分级设色很方便一段深红一段浅黄自动排好。但这个类的坑也最多。// 随机色条必须设置起止色、个数和种子 IRandomColorRamp ramp new RandomColorRampClass(); ramp.StartColor GetRgb(255, 255, 235); // 浅黄代表低值 ramp.EndColor GetRgb(140, 20, 20); // 深红代表高值 ramp.Size 5; // 生成几个颜色通常与分级数一致 ramp.Seed 2024; // 固定随机种子否则每次运行颜色都不一样 // 关键不调用CreateRamp直接取颜色取到的是空对象 bool created ramp.CreateRamp(true);逻辑说明RandomColorRamp 的 StartColor 和 EndColor 只是给随机算法一个范围不是精确的渐变端点。Seed 这个参数非常关键批量出图时如果不固定种子同一套数据跑两次出来的颜色顺序和色值都不同汇报时两张同字段的图配色不一致很难跟甲方解释。CreateRamp(true)的 true 表示允许部分生成即颜色数不足时也返回已有颜色生产环境建议保持 true。参数说明Size要和后面 ClassBreaksRenderer 的 BreakCount 保持一致多或少都会导致某些分级拿不到颜色。GetRgb是自定义的辅助函数通常封装成下面这样避免到处 new 颜色对象IRgbColor GetRgb(int r, int g, int b) { IRgbColor color new RgbColorClass(); color.Red r; color.Green g; color.Blue b; return color; }这里再说一个血泪经验很多人以为设置了 StartColor 和 EndColor 就能直接用 ramp.Next() 取色结果 Next() 返回 null程序不报错但图斑全是默认黑色。原因就是漏了 CreateRamp。这个调用是把颜色真正计算出来必须写在遍历取色之前。3. 把源码跑起来License 初始化、目录结构与第一个出图流程3.1 源码包里应该先看的三个文件解压这套源码先别急着双击解决方案我一般按下面的优先级把文件过一遍文件作用建议项目说明文档环境要求、版本依赖、部署注意事项第一时间通读重点看 SDK 版本Solution.sln解决方案入口决定编译目标框架确认 .NET Framework 版本与 Engine 位数匹配FormMain.cs 或 ThemeGenerator.cs主界面与专题图生成核心类先搜 License 初始化位置再读渲染器代码这套包的文件组织方式是典型的「界面 逻辑」两层主窗体负责加载地图和展示结果专题图生成逻辑单独封装在一个类里。读的时候只要分清哪个文件在碰 UI、哪个文件在碰 ArcObjects就能快速定位。3.2 License 初始化先绑定运行时再申请许可ArcGIS Engine 二次开发的程序运行时初始化顺序是第一名顺序错了程序直接闪退或者报「未绑定运行时」。常见做法是在 Program.cs 的 Main 方法最前面完成两步操作// Main方法第一行绑定运行时必须在new任何ArcObjects对象之前 ESRI.ArcGIS.RuntimeManager.Bind(ProductCode.Engine); // 第二步申请Engine许可 IAoInitialize aoInit new AoInitializeClass(); esriLicenseStatus status aoInit.IsProductCodeAvailable( esriLicenseProductCode.esriLicenseProductCodeEngine); if (status esriLicenseStatus.esriLicenseAvailable) { aoInit.Initialize(esriLicenseProductCode.esriLicenseProductCodeEngine); }逻辑说明RuntimeManager.Bind 是 ArcGIS 10.x 以后必须做的程序集绑定它负责加载和当前版本匹配的 ArcObjects 原生程序集。很多人把 License 初始化写在某个窗体的构造函数里平时能跑但偶尔报错就是因为 Main 函数在执行时已经初始化了其他 COM 对象绑定顺序乱了。AoInitialize 则是向本机 License Manager 申请可用许可独立安装的 Engine 运行时通常是 Engine 许可没有 GeoDatabase 扩展时不要尝试初始化 esriLicenseProductCodeArcInfo会失败。参数说明ProductCode.Engine对应独立安装的 ArcGIS Engine 运行时如果你的机器装的是 Desktop也可以试试 ProductCode.Desktop。esriLicenseProductCodeEngine与前面的 ProductCode 是两个层面的东西前者是运行时组件后者是功能许可配合使用。初始化成功后程序退出前最好调用aoInit.Shutdown()避免 COM 对象残留在进程里导致二次启动异常。3.3 数据加载链路从 Shapefile 到 Map 再到 ActiveView专题图生成前必须先有图层。这套源码里我比较喜欢它处理 Shapefile 的方式——没有用 Map 控件自带的添加数据对话框而是直接用工作空间工厂打开这样才能把数据路径做成参数方便批量换数据源。// 打开存放shapefile的目录 IWorkspaceFactory wsf new ShapefileWorkspaceFactoryClass(); IFeatureWorkspace fws wsf.OpenFromFile(D:\GisData, 0) as IFeatureWorkspace; // 注意这里写的是不带.shp的文件名 IFeatureClass fc fws.OpenFeatureClass(City_Population); IFeatureLayer layer new FeatureLayerClass(); layer.FeatureClass fc; layer.Name 人口数据; // 挂到地图上并获取ActiveView刷新 IMap map axMapControl1.Map; map.AddLayer(layer); (map as IActiveView).Refresh();逻辑说明OpenFromFile 的第二个参数 0 是 hWnd 句柄传 0 表示不弹任何交互对话框。OpenFeatureClass 的参数是文件名去掉扩展名这点和直接用 File 路径不一样。添加图层用 map.AddLayer内部会自动排序——不想要默认排序的话可以用 map.AddLayer 后手动调map.MoveLayer但专题图系统里通常不讲究图层顺序数据图层就是最底层。参数说明D:\GisData是 shapefile 所在目录不是文件路径写错了会抛找不到工作空间的 COM 异常。layer.Name会显示在 TOC 控件里建议用业务名而不是物理文件名。数据加载后因为有 Refresh地图窗口会立即显示矢量底图如果你想骨架更清晰可以先在地图上放个行政区边界底图再挂业务数据图层做专题渲染。4. 核心代码骨架唯一值、分级设色、统计图表的自动化参数4.1 唯一值专题图把字符串字段变成一张分类图唯一值渲染器是三类里最好写的也是分类字段的标准解。核心逻辑是告诉渲染器两个列表——值列表和符号列表两者按下标一一对应。// 新建唯一值渲染器设置参与分类的字段 IUniqueValueRenderer uvRenderer new UniqueValueRendererClass(); uvRenderer.FieldCount 1; uvRenderer.set_Field(0, LUSE); // 地类代码字段 // 手工枚举出要渲染的值并给每个值配一个符号 uvRenderer.ValueCount 3; uvRenderer.set_Value(0, 01); uvRenderer.set_Value(1, 02); uvRenderer.set_Value(2, 03); // 定义一个浅色符号实际项目通常是循环创建多个Symbol IFillSymbol fill1 new SimpleFillSymbolClass(); fill1.Color GetRgb(255, 220, 180); uvRenderer.set_Symbol(0, fill1 as ISymbol); uvRenderer.set_Symbol(1, fill1 as ISymbol); uvRenderer.set_Symbol(2, fill1 as ISymbol); // 挂到图层 (geoLayer as IGeoFeatureLayer).Renderer uvRenderer as IRenderer;逻辑说明set_Value 和 set_Symbol 按下标成对出现ValueCount 是总对数写少了有的值没符号写多了会越界抛 COM 异常。这里三个值共用了一个符号实际使用时当然要建三个不同颜色的 FillSymbol我只是在骨架代码里演示结构。每个符号的 Label 如果不设置图例里显示的就是原始值要显示中文业务名用uvRenderer.set_Label(0, 耕地)这种写法补充。参数说明set_Field 的字段名必须是图层属性表里真实存在的字段名写错不会编译报错运行时渲染器会忽略并回退到默认符号。建议在改代码前先用 ArcMap 打开属性表确认字段名大小写。这种渲染器的缺点是每个值都要手工加如果字段有 20 个值代码会很长所以生产环境一般先查属性表的唯一值列表再循环生成颜色而不是手写。4.2 分级设色专题图断点与颜色条是关键参数分级设色的核心是「断点」也就是把数值字段切成几段。这套源码里最有价值的部分就是对断点的处理——它把断点数组从界面参数传入这样你可以用外部统计软件算好自然断点再赋进来而不是每次在代码里硬编码。// 指定要分级的字段和分段数 IClassBreaksRenderer cbRenderer new ClassBreaksRendererClass(); cbRenderer.Field GDP; cbRenderer.BreakCount 5; // 从颜色条取5个颜色按下标挂给每一段 IEnumColors colors ramp.CreateRamp(true); // ramp已在前面配置好 ISymbol symbol; for (int i 0; i 5; i) { symbol new SimpleFillSymbolClass(); (symbol as IFillSymbol).Color colors.Next(); cbRenderer.set_Symbol(i, symbol); } // 设置每一段的数值上下界注意区间要连续 cbRenderer.set_Break(0, 0, 500); cbRenderer.set_Break(1, 500, 1000); cbRenderer.set_Break(2, 1000, 2000); cbRenderer.set_Break(3, 2000, 5000); cbRenderer.set_Break(4, 5000, 10000); geoLayer.Renderer cbRenderer as IRenderer;逻辑说明分段数 BreakCount 和颜色条 Size 必须一致和 set_Break 的条目数也必须一致否则会出现某一段没有符号或某一档没有断点。set_Break 的区间是左闭右开所以上一段的上界要等于下一段的下界中间留了空档或者重叠图面上就会出现某些要素没有渲染颜色。参数说明Field 只接受数值型字段遇到文本字段会直接渲染失败。断点值应该由业务分析确定常见做法是等间距或分位数如果数据里有空值NULL这段要素不会进入任何分级图面上会以默认符号显示——这是生产环境最容易漏掉的一类问题。建议在生成断点前先统计字段的最小值、最大值和空值数量避免把最小值直接用于第一段下界时留下一个空区间。4.3 统计图表专题图饼图与柱状图的 ChartRenderer统计图表专题图适合表达一个区域内部的结构对比比如某个行政区的一产、二产、三产比例。它的渲染逻辑不像前两者直接配整个图层而是给每个要素单独配一个饼图或柱状图符号符号大小默认一样。IChartRenderer chartRenderer new ChartRendererClass(); // 设置饼图符号Size决定图上符号基础大小 PieChartSymbolClass pieSymbol new PieChartSymbolClass(); pieSymbol.Color GetRgb(100, 150, 255); // 基础颜色 pieSymbol.Size 25; // 饼图直径单位是页面单位 chartRenderer.ChartSymbol pieSymbol as IChartSymbol; chartRenderer.FieldCount 3; chartRenderer.set_Field(0, 一产); chartRenderer.set_Field(1, 二产); chartRenderer.set_Field(2, 三产); // 允许符号避让避免相邻图斑的饼图叠压 chartRenderer.UseOverposter true; geoLayer.Renderer chartRenderer as IRenderer;逻辑说明ChartRenderer 和前面两个最大的区别是它不改变面要素本身的填充颜色而是在面中心画统计图。set_Field 的字段顺序决定了饼图扇区的排列顺序业务上通常把占比最小的放前面、占比最大的放最后阅读顺序更自然。PieChartSymbol 的 Size 以页面坐标为单位如果图层范围是经纬度25 这个值可能小到看不见也可能大到覆盖全图我在项目里一般把 Size 换算成当前地图范围宽度的十分之一再赋进去。参数说明UseOverposter打开后系统会做符号冲突检测相邻图斑的饼图不会互相压盖但会增加渲染耗时数据量大时我会先关掉它测试性能上线时再按需打开。如果要做柱状图把 PieChartSymbolClass 换成 BarChartSymbolClass 即可接口不变。5. 避坑指南专题图自动生成最常见的五个翻车点5.1 代码写对了地图一片白数据源没加载成功现象运行程序后 MapControl 里什么都没有菜单和工具栏正常但地图区空白日志里偶尔能看到「文件不存在」或 COM 异常。原因九成是 Shapefile 路径问题。OpenFromFile 的参数是工作空间目录不是文件路径目录里有中文字符、空格或权限不足时OpenFromFile 会静默失败返回 null。还有一种是图层要素在别的程序里被占用Windows 下文件被锁定后Engine 打开也会失败。解决在 OpenFromFile 后面加一个判空检查返回 null 时直接抛异常避免把空对象传给图层IFeatureWorkspace fws wsf.OpenFromFile(path, 0) as IFeatureWorkspace; if (fws null) { throw new Exception(无法打开工作空间检查目录路径和文件权限); }如果路径确认无误还失败检查目录下有没有同名 .lock 文件有的话删掉。从那以后我每次加载数据都会先把路径打印到调试输出框再进业务逻辑这个习惯省了很多排查时间。5.2 颜色变成一块块黑色ColorRamp 没 CreateRamp现象专题图生成成功地图显示也有分类但所有图斑都是黑色或者只有少数几个有颜色。原因代码里创建了 ColorRamp设置完 StartColor、EndColor 后直接调用ramp.Next()取色没有先调CreateRamp(true)。这是这个类最迷惑人的地方——不报错但取回来的 IColor 是空实现赋值给符号后变成默认黑色。解决养成固定三段式配置 ColorRamp 参数、调用 CreateRamp(true)、再遍历取色。检查代码时把 CreateRamp 放在循环取色的前一行不要放在循环体内避免重复创建。还有一个隐性因素用 AlgorithmicColorRamp 时如果 FromColor 和 ToColor 完全同色CreateRamp 可能只生成一个有效颜色其他全部报错起止色之间要有足够的色差。5.3 图例和地图对不上渲染器换了但界面没刷新现象地图上专题图已经更新了但 TOC 控件里的图例还是旧的分类、旧的颜色或者反过来地图没变图例先变了界面状态完全错乱。原因专题图渲染器是挂在图层对象上的而 TOC 控件显示的逻辑图层树有自己的缓存。直接改 Renderer 后只刷新了视图没有通知 TOC 重建树。这个问题最常见于「打开程序后连续切换两次字段」的场景第一次正常第二次图例就乱了。解决换完 Renderer 后同时强制刷新两个地方——地图视图和图例树IActiveView activeView axMapControl1.Map as IActiveView; activeView.Refresh(); axTOCControl1.Update(); // 强制重构TOC树注意axTOCControl1.Update()如果找不到用(axTOCControl1.Object as ITOCControl).Update()两者作用一样。每次改完渲染器都调这一对能避免很多莫名其妙的界面问题。5.4 换电脑就报「未绑定运行时」License 初始化顺序与生命周期现象本机编译运行都正常把整个 Release 目录拷到另一台装了 Engine Runtime 的机器上程序启动直接崩溃异常信息指向 RuntimeManager.Bind 或 License 初始化。原因License 初始化没有放在 Main 入口而是放在某个窗体的Load事件或类的静态构造函数里。程序启动时WinForm 主线程先创建了窗口句柄ArcObjects 的 COM 组件在这个时机已经被部分初始化顺序就乱了。还有一种情况AoInitialize 对象被当作局部变量函数退出后被 GC 回收许可又被释放了。解决把初始化的两步放在 Program.cs 的 Main 方法最前面使用静态字段持有 AoInitialize 实例static IAoInitialize _aoInit; [STAThread] static void Main() { ESRI.ArcGIS.RuntimeManager.Bind(ProductCode.Engine); _aoInit new AoInitializeClass(); // ... 初始化许可 ... Application.Run(new FormMain()); }_aoInit不要局部化保持静态引用到程序结束在进程退出前 Shutdown 即可。换机器部署时还有一点要注意目标机器必须安装了对应版本的 Engine Runtime并且能连上 License Manager否则初始化返回 esriLicenseNotLicensed程序照样跑不起来。5.5 导出大片白边页面布局与地图范围不一致现象点击导出专题图后JPG 文件能打开但内容只占画面中央一小块四周全是白色背景图面小得像邮票。原因导出时设置了分辨率但导出范围用的是 IActiveView 的默认 Extent没有和地图当前显示范围同步。特别是用 PageLayout 模式时PageLayout 的页面大小和地图框的 Extent 是两套坐标直接导出就把页面空白也输出了。解决导出前先把地图缩放到要素范围或者直接把导出范围设成当前视图的 ClipBoundsIEnvelope exportExtent (axMapControl1.Map as IActiveView).Extent; export.PixelBounds exportExtent;从项目需求出发批量导出时通常要求每张图都完整包含所有要素我会在导出前先做一次map.ActiveView.Extent layer.AreaOfInterest之类的操作再拿到 Extent 配置 PixelBounds。注意如果你是 PageLayout 模式出图需要操作的是 pageLayout 的 IActiveView不要和 Map 的 IActiveView 弄混。6. 导出与验证从一张图到一套图的自动化6.1 用 IPageLayout 控制出图分辨率与页面尺寸专题图做完了最后一步是导出。这套源码的导出模块只做了基础能力——JPG 和 PDF 两种格式分辨率参数可调。我把核心方法抽出来说一下public static bool ExportMap(IActiveView activeView, string filePath, string format, int resolution) { IExport export null; if (format.Equals(JPG)) export new ExportJPEGClass(); else if (format.Equals(PDF)) export new ExportPDFClass(); // 目标文件已存在时先删除否则导出操作可能静默失败 if (File.Exists(filePath)) File.Delete(filePath); export.ExportFilePath filePath; export.Resolution resolution; export.PixelBounds activeView.Extent; ITrackCancel cancel new CancelTrackerClass(); export.StartExporting(); // 第三个参数0表示使用内存DCref参数是输出完成后返回的设备DC值 int hDC 0; int lDC 0; activeView.Output(activeView.Extent, (IDisplay)export, hDC, ref lDC); export.FinishExporting(); return true; }逻辑说明Export 对象实现了 IDisplay 接口所以能直接传给 ActiveView.Output。StartExporting 和 FinishExporting 必须成对出现中间夹 Output顺序错会导致文件损坏。分辨率 150 足够屏幕汇报用打印图集建议设 300PDF 导出时如果图上有中文注记记得在导出前配置字体嵌入否则换台机器打开 PDF 中文字体变方框。6.2 三个字段评审一张专题图我一般建议拿到输出结果后用三个维度快速验证一遍而不是只看「图出来了没有」验证项检查方法通过标准断点一致性对比源码断点数组与 ArcMap 自然断点结果同段边界差不超过 1%图例一致性查看导出图角落图例与实际渲染色块色块颜色、个数与渲染器完全对应范围完整性看地图四周边界是否完整包括全部要素无白边、要素没有被裁掉这三个维度里断点一致性最容易出问题——很多人代码里用的是等间距断点而业务方默认按 ArcMap 的自然断点做两者算出来的段边界有差异增长率类数据尤其明显。检查方法很简单在 ArcMap 里用同一字段做一次分类把断点数组抄出来和你的配置比对差值大就调参。如果跑批量大我习惯把导出文件的命名也做成参数比如「行政区代码_指标名_日期.jpg」这样后期核对输出文件时不用一张张打开看。再配合固定随机种子同一套数据两次出图配色完全一致汇报材料不会因为「这次颜色和上次不一样」被临时打回。这套源码里还有一个被我沿用至今的习惯所有渲染器参数集中在配置区界面代码永远不直接写死字段名。从那以后我每次交付专题图工具都强制自己拿三张图做回归再交活一张唯一值、一张分级设色、一张饼图。三张都能过这套系统的主链路就算站住了。希望帮到你。本文还有配套的精品资源点击获取
返回列表