ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

基于ArcObjects的ArcGIS二次开发:批量出图工具地图整饰实现

基于ArcObjects的ArcGIS二次开发:批量出图工具地图整饰实现 简介面向ArcGIS二次开发者的出图工具Demo基于ArcObjects SDK专门演示在地图布局中添加图名、比例尺、指北针、图例并实现导出图片或文件、打印地图的完整流程解决GIS开发中常见的地图整饰与输出问题该Demo虽小但覆盖出图要素常用配置整体轻量实用适合作为入门参考和模块开发启动模板。压缩包共49个文件约829KB以C#源代码.cs、资源配置.resx、解决方案.sln/.csproj、可执行程序.exe为主另含PDF报告与TXT说明并附配置文件与调试符号工程结构清晰代码注释详细可直接打开调试学习。已有5633人学习下载适合刚接触AEArcGIS Engine开发、需要快速实现地图打印导出的开发者。通过源码可了解ITextElement、IScaleBar、IPolarNorthLineElement、ILegend等核心接口的实现细节掌握图名、比例尺、指北针、图例的自定义方法同时可运行exe查看实际出图效果导出支持JPG、PNG、PDF等常见格式打印前还能设置纸张大小与方向参考报告中的设计思路可快速迁移到自己的GIS项目中。 先说我做这个出图工具的起因。前几年在做一个类似批量制图的活儿图层几十个图班一变更就是全图重新出。ArcMap里手动加图名、调比例尺、排图例每张图都得点十几下鼠标而且不同人调出来的风格完全不一样有人图例靠左有人图例靠右。后来我就想能不能把这些整饰要素全部交给代码去做一键把图名、比例尺、指北针、图例按照固定规则放到图面四周然后直接导出图片、PDF或者走打印机出图。这个想法落地之后就是现在要分享的这个ArcGIS二次开发出图工具。这套工具本质上是基于ArcObjects的桌面端二次开发核心使用ArcGIS Engine的Carto、Display、Output等库配合WinForm界面。它解决的是从数据调好到图能交付之间的效率问题。适合两类人参考一类是用ArcMap做批量出图但受够了手工排版的人另一类是刚接触ArcGIS二次开发、想搞清楚MapSurround、GraphicsContainer这些概念怎么落地的开发者。1. 出图工具的整体设计地图整饰到底在做什么1.1 出图工具解决的核心问题先说清楚出图和显示地图的区别。你看ArcMap里的数据视图地图是可以缩放平移的比例尺、指北针这些是跟着数据框动态变化的。但是到了交付阶段尤其是工程类项目要求的是固定图幅、固定比例、固定图面配置——这时候就必须切到布局视图LayoutView在PageLayout的纸张上放上图名、比例尺、指北针、图例这些整饰元素。这一步在ArcMap里就是纯手工挪元素框过程枯燥且极容易出错。工具要解决的就是三件事一致性、效率、可交付。一致性指每张图的图名字体字号、比例尺样式、图例位置都完全统一效率指程序自动排版手动操作变成批量跑数可交付指输出格式可控图片分辨率可设定需要打印时直接调用Printer对象。这三个目标直接决定了代码怎么组织。1.2 整饰要素背后的ArcObjects对象体系ArcObjects里所有能放到布局上的东西要么是GraphicElement图形元素要么是MapSurroundFrame地图环绕框要么是MapFrame地图框。出图工具里最关键的几个接口IActiveView代表当前视图有DataView和LayoutView两种模式。添加图名、比例尺这类操作全部要切到LayoutView因为只有布局视图才有页面坐标系统。IGraphicsContainer图形元素的容器PageLayout本身就是一个GraphicsContainer用AddElement方法把元素加到布局上。IMapSurroundFrame / IMapSurround比例尺、指北针、图例全是MapSurround它们不能独立存在必须挂在MapFrame上通过MapSurroundFrame放进布局。IExport / IExport2负责导出PDF、图片等格式。IPrinter / IPrintSettings负责走打印机输出。很多人第一次写的时候会犯一个错误试图在数据视图DataView里直接AddElement比例尺。ArcObjects的设计是这些元素必须属于某个MapFrame在数据视图里数据框就是MapFrame的某种表现真正往图面放元素、定位元素一定要在LayoutView里操作。1.3 为什么选ArcGIS Engine C#桌面端二次开发现在有两条路线老的ArcObjectsArcMap/ArcGIS Engine和新的ArcGIS Pro SDK。这个工具为什么用ArcObjects原因是我需要直接在ArcMap环境里跑给部门同事用。ArcGIS Pro虽然自带Python的arcpy.mp可以做类似事但当时项目里大量历史资源还是ArcMap的mxd同事也没切到Pro。ArcGIS Engine C#做WinForm工具可以脱离ArcMap进程运行用LicenseControl控件初始化环境相对靠谱。如果你新开项目Pro SDK或arcpy.mp可以优先考虑但概念原理是相通的。2. 核心细节解析图名、比例尺、指北针、图例的实现原理2.1 图名TextElement不是简单Add就完事图名看起来最简单因为它就是个TextElement。你创建一个TextElement给它一个TextSymbol设置字体、字号、颜色然后用IGraphicsContainer.AddElement加到布局上。但实际操作里有几个细节不能忽略。第一TextElement的位置是布局坐标不是屏幕坐标。布局视图的坐标单位是Page单位通常以厘米或英寸计。你要把图名放在纸张顶部居中就得先拿IPage的PrintableBounds算出纸张宽度再减去一半文本宽度。问题是文本宽度取决于字体和字号你只能先估算用ITextSymbol的GetTextSize方法拿到文本在指定DPI下的尺寸再做居中偏移。图名字长不同动态调整字号比固定字号要稳妥得多。第二TextSymbol的设置有个经典坑Color必须是IColor接口下的具体对象比如RgbColor。很多人上来new一个ColorClass发现赋不了RGB值因为ColorClass不是Color。正确做法是RgbColor rgb new RgbColor(); rgb.Red 255; rgb.Green 0; rgb.Blue 0; TextSymbol symbol new TextSymbol(); symbol.Color rgb; symbol.Size 24; // 字号单位是点1pt在300dpi下约等于一个相对固定的像素数第三字体问题。出图图名通常用中文字体比如宋体、黑体。在ArcObjects里设置TextSymbol.Font是IFontDisp对象直接来自stdole库。如果字体名不对不会报错但出图时显示的是默认字体之前排查过很久。2.2 比例尺ScaleBar的样式选择与单位换算比例尺是整个工具里最容易让新手翻车的地方因为它不是简单画一条线段而是一个MapSurround。MapSurround会绑定到Map根据数据框的实际比例尺自动计算分段。创建MapScaleBar的常用步骤是创建MapScaleBar对象设置它的MapSurround属性中的Map然后设置样式。ArcGIS内置了多种比例尺样式最常用的是Alternating Scale Bar交替黑白格和Single Division Scale Bar单线分割。样式设置通过IScaleBar的Style属性传进去的是IScaleBarStyle对象通常用new AlternatingScaleBarStyle()或者从样式库加载。单位换算是关键。MapScaleBar的MapUnits属性决定显示单位。如果数据框坐标系是WGS84经纬度单位度而图面要求比例尺显示米你必须把MapScaleBar.MapUnits设置为esriUnits.esriMeters同时设置Divisions、Subdivisions这些分段数。否则出来的比例尺长度和标注数值完全是乱的。MapScaleBar scaleBar new MapScaleBar(); scaleBar.Map map; scaleBar.MapUnits esriUnits.esriMeters; scaleBar.Divisions 4; scaleBar.Subdivisions 2; scaleBar.Style new AlternatingScaleBarStyle();这里还有一个经验如果数据框比例尺是1:5000比例尺条应该大约等于纸张上一条5cm长的线段。MapScaleBar会自动根据Map的Scale计算分段但你需要控制整个Surround Frame的宽度不然比例尺条可能溢出到图外。Frame的Width/Height用IPage的坐标设置一般建议宽度不超过页面宽度的1/4。2.3 指北针通常不出问题但一出问题就是方向指北针和比例尺在ArcObjects里的处理方式几乎一样都是创建MapNorthArrow或者MapMagneticNorthArrow设置Map放进MapSurroundFrame。指北针的问题主要集中在两点。一是方向敏感。指北针对象默认正北朝上如果数据框被旋转过指北针的方向会跟随数据框的Rotation属性变化但很多人的图框没有旋转所以一般看不出来。如果要做精确控制确保指北针始终指向页面正上方需要设置MapNorthArrow的NorthAlignment属性但要小心设成Fixed之后就不跟随数据框了得根据实际需求取舍。二是加载不出来。指北针在布局里显示不出往往不是因为指北针对象没创建而是MapSurroundFrame的MapSurround属性没正确赋值或者没把Frame加到MapFrame上。这个问题在下面统一讲。2.4 图例复杂程度最高也是最容易出乱子的环节图例的坑比前三个加起来都多。原因是图例内容不是一个固定元素它要把Map里所有图层至少是部分图层的符号化结果动态抽出来。代码层面的做法是创建MapLegend对象绑定Map然后往ILegendFormat的Items集合里加LegendItem。每个LegendItem对应一个图层包含图层名和图层符号。但实际项目中图层的显示状态是动态的。某个图层在内容列表里被勾掉或者图层在数据框可见范围之外被自动隐藏图例里可能就出现空白项或者缺失项。我的做法是用代码遍历TOC中所有图层把图例想展示的图层过滤一遍再手动把所有可见图层的LegendItem添加到图例中。另外ArcObjects里LegendItem默认还有一个AutoVisibility属性这个属性会让图例项在地图缩放后自动增删。听着很智能实际出图时往往导致图例内容每打开一次就变个样建议直接关闭。图例框的排版也值得注意。MapLegend本身是一个MapSurroundFrame你需要指定它的Columns属性图例列数来控制图例是单列还是多列。很多图例内容多到溢出页面设置两列能大幅减少高度。图例的标题图例两字默认不显示需要通过ILegendFormat的Title属性设置还有TitleSymbol的字体字号。2.5 MapSurroundFrame所有整饰要素的统一宿主统一讲一下MapSurroundFrame因为比例尺、指北针、图例这三个要素都要靠它才能出现在布局上。MapSurroundFrame是一个特殊的FrameElement它持有一个MapSurround对象同时这个MapSurround对象绑定到了某个MapFrame。创建顺序必须是先创建Surround对象 - 设置Surround的Map属性或MapSurround属性 - 创建MapSurroundFrame - 把Surround赋值给Frame的MapSurround属性 - 把Frame作为GraphicElement AddElement到PageLayout。顺序反了比如先把Frame加进布局再设置MapSurround大概率运行时不报错但页面不显示任何东西。IMapSurroundFrame surroundFrame new MapSurroundFrameClass(); surroundFrame.MapSurround surround; // surround是IMapSurround对象 surroundFrame.Map map; IGraphicsContainer graphicsContainer pageLayout as IGraphicsContainer; graphicsContainer.AddElement((IElement)surroundFrame, 0);我习惯把这几种要素的添加逻辑写到一个通用方法里传入surround类型和布局位置统一处理Frame的Width、Height和Envelope。这样代码干净新增要素也容易维护。3. 实操过程用C#写一个一键出图的小工具3.1 工程搭建与License初始化开发环境我用的是Visual Studio 2019.NET Framework 4.6.2。ArcGIS Engine 10.8对.NET Framework版本有要求太高版本反而可能出问题。工程类型选Windows窗体应用。添加引用时主要引用这几组DLLESRI.ArcGIS.Carto、ESRI.ArcGIS.Display、ESRI.ArcGIS.Geometry、ESRI.ArcGIS.Output、ESRI.ArcGIS.Controls、ESRI.ArcGIS.System。其中Controls里包含了LicenseControl放到窗体上可以简化License初始化。License初始化是二次开发最常见的坑热搜词里那句 You are not licensed for ArcGIS for Desktop Advanced 就是典型的Engine和Desktop版本不匹配或者License选错级别。Engine开发时要保证LicenseControl里勾选的ProductCode和可用许可匹配通常勾选EngineGeoDB和Engine高级版即可级别不能低于你要用的功能的限制。初始化建议用IAoInitializeIAoInitialize aoInit new AoInitializeClass(); ESRI.ArcGIS.esriSystem.esriLicenseStatus status aoInit.Initialize(esriLicenseProductCode.esriLicenseProductCodeEngineGeoDB); if (status ! esriLicenseStatus.esriLicenseCheckedOut) { MessageBox.Show(License初始化失败请检查ArcGIS Engine许可); }3.2 获取Map、Page和ActiveView工具运行时先让用户指定一个mxd文档路径或者打开当前活动文档如果作为ArcMap插件运行。这里以独立工具为例用IMapDocument打开mxd。需要拿到四样东西Page、PageLayout、Map、ActiveView的LayoutView。IMapDocument mapDoc new MapDocumentClass(); mapDoc.Open(mxdPath, ); IMap map mapDoc.get_Map(0); // 或者按名称取 IPageLayout pageLayout mapDoc.PageLayout; IActiveView activeView pageLayout as IActiveView; IPage page pageLayout.Page;操作Page的主要原因是要设置纸张大小和方向。IPage.FormID对应标准纸张A4就是esriPageFormA4A3是esriPageFormA3。如果没有合适的FormID可以手动设置Page.Width和Page.Height单位是PageUnits默认厘米。Page.Orientation有esriPageOrientationPortrait纵向和esriPageOrientationLandscape横向两种设置Orientation后必须调用activeView.Refresh()或PartialRefresh才生效。3.3 添加图名、比例尺、指北针、图例的完整实现下面给出核心代码。为了保证每种整饰要素的位置统一我定义了一个简单的布局规则图名放顶部居中比例尺放下方偏左指北针放右下图例放右侧中间。先用IPage拿到页面边界IEnvelope pageBounds page.PrintableBounds; double pageWidth pageBounds.Width; double pageHeight pageBounds.Height;添加图名ITextElement textElement new TextElementClass(); textElement.Text 某某项目土地利用现状图; IFontDisp font new StdFontClass() as IFontDisp; font.Name 宋体; font.Size 26; RgbColor rgbColor new RgbColorClass(); rgbColor.Red 0; rgbColor.Green 0; rgbColor.Blue 0; ITextSymbol textSymbol new TextSymbolClass(); textSymbol.Font font; textSymbol.Color rgbColor; textSymbol.HorizontalAlignment esriTextHorizontalAlignment.esriTHACenter; textElement.Symbol textSymbol; IElement element textElement as IElement; element.Geometry new EnvelopeClass(); // 图名位置顶部居中X范围预留20%边距Y取页面高度减去约2厘米的留白 IPoint anchorPoint new PointClass(); anchorPoint.X pageWidth / 2 pageBounds.XMin; anchorPoint.Y pageBounds.YMax - 2; element.Geometry anchorPoint; IGraphicsContainer graphicsContainer pageLayout as IGraphicsContainer; graphicsContainer.AddElement(element, 0);注意这里用了IPoint作为Geometry。TextElement对IElement.Geometry的语义本身就是一个定位点所以给它一个Point是合理的但如果要用对齐必须把TextSymbol的HorizontalAlignment和VerticalAlignment都设对。添加比例尺、指北针、图例走通用的MapSurroundFrame方法private void AddMapSurround(IMap map, IPageLayout pageLayout, IMapSurround surround, IEnvelope envelope) { IMapSurroundFrame surroundFrame new MapSurroundFrameClass(); surroundFrame.MapSurround surround; surroundFrame.Map map; IElement element surroundFrame as IElement; element.Geometry envelope; IGraphicsContainer graphicsContainer pageLayout as IGraphicsContainer; graphicsContainer.AddElement(element, 0); }创建比例尺MapScaleBar scaleBar new MapScaleBarClass(); scaleBar.Map map; scaleBar.MapUnits esriUnits.esriMeters; scaleBar.Divisions 4; scaleBar.Subdivisions 2; AlternatingScaleBarStyle style new AlternatingScaleBarStyleClass(); scaleBar.Style style; IEnvelope scaleBarEnv new EnvelopeClass(); scaleBarEnv.PutCoords(pageBounds.XMin 1, pageBounds.YMin 1, pageBounds.XMin 6, pageBounds.YMin 2.5, true); AddMapSurround(map, pageLayout, scaleBar as IMapSurround, scaleBarEnv);创建指北针MapNorthArrow northArrow new MapNorthArrowClass(); northArrow.Map map; // 指北针的样式比较有限可以设Color和Size IEnvelope arrowEnv new EnvelopeClass(); arrowEnv.PutCoords(pageBounds.XMax - 3, pageBounds.YMin 1, pageBounds.XMax - 1.5, pageBounds.YMin 2.5, true); AddMapSurround(map, pageLayout, northArrow as IMapSurround, arrowEnv);创建图例MapLegend legend new MapLegendClass(); legend.Map map; legend.Columns 2; ILegendFormat legendFormat legend.Format; legendFormat.Title 图例; legendFormat.TitleSymbol CreateTextSymbol(黑体, 16); // 清空默认图例Item再手动加入需要的图层 legend.ClearItems(); for (int i 0; i map.LayerCount; i) { ILayer layer map.get_Layer(i); if (layer.Visible layer is IFeatureLayer) { LegendItem item new LegendItemClass(); item.Layer layer; legend.AddItem(item); } } IEnvelope legendEnv new EnvelopeClass(); legendEnv.PutCoords(pageBounds.XMax - 6, pageBounds.YMax - 8, pageBounds.XMax - 1, pageBounds.YMin 6, true); AddMapSurround(map, pageLayout, legend as IMapSurround, legendEnv);这段代码里我最终只添加了可见的要素图层进图例。如果你希望图例包含栅格图层或者注记图层可以再叠加判断。曾经遇到过图例把不需要的辅助图层也列出来最后检查是LRPRLayers visibility设置的问题。3.4 导出图片/PDF与打印地图的代码实现全部元素添加完毕后切到布局视图刷新IActiveView activeView pageLayout as IActiveView; activeView.Refresh();导出PDF使用IExport2的ExportPDF核心是设置导出范围和分辨率。分辨率我通常分两套预览用150DPI打印用300DPI。设置分辨率的方法是IExport2.SetViewByDevice第一个参数传ActiveView第二个传输出栅格的大小ITrackCancel传null第三个传设备DPI第四个传导出范围模式IExport2 export new ExportPDFClass(); export.ExportFilePath outputPath; export.Resolution 300; export.ExportTOC false; // 不要导出内容列表 // 设置导出范围 IEnvelope exportBounds activeView.Extent; if (export is IExport2) { export.SetViewByDevice(activeView, null, export.Resolution, pageBounds, false); bool success export.StartExporting(); if (success) { IntPtr hDC export.DeviceContext; activeView.Output(hDC, (int)export.Resolution, ref exportBounds, null, null, null, 1, false); export.FinishExporting(); } }这里有个教训activeView.Output传入的resolution必须是整数而且导出的范围要用exportBounds如果不设置好图片可能是黑底或者只有一半内容。打印地图走IPrinter和IPrintSettings。流程是创建Printer对象设置Paper的FormID、方向然后通过IPrintSettings设置份数、打印质量和打印机名称最后调用activeView.Printer。核心代码IPrinter printer new PrinterClass(); printer.Paper.FormID esriPageFormID.esriPageFormA4; printer.Paper.Orientation esriPageOrientation.esriPageOrientationPortrait; printer.Orientation esriPageOrientation.esriPageOrientationPortrait; IPrintSettings printSettings new PrintSettingsClass(); printSettings.PrinterPrinter printer; printSettings.Copies 1; printSettings.OutputPrinter printer; activeView.Printer printer; activeView.OutputPrinter null;手动打印时需要注意ArcObjects的打印输出完全依赖Printer驱动不同驱动下的页边距处理不一样。如果打印偏移优先检查打印机驱动里的可打印区域设置不要只改代码里的边距参数。4. 常见问题与排查技巧实录我把实际操作中遇到的高频问题整理成了速查表基本都是代码能跑但结果不对的类型。问题现象可能原因解决方法图例整个不显示MapSurroundFrame创建后没有设置Map属性或MapSurround对象未正确绑定检查MapSurroundFrame.Map和.MapSurround两个属性确认在LayoutView下AddElement比例尺标注数值明显不对MapUnits与数据框坐标系单位不匹配经纬度数据框要显示米时把MapUnits设为esriMeters检查Divisions设置图例里有空白条目图层没有Renderer或者图层当前不可见但被强制加入遍历图层时判断layer.Visible和渲染器类型过滤空符号图层图例项随缩放不断增删LegendItem.AutoVisibility默认开启循环所有LegendItem把AutoVisibility设为false导出的PDF没有图名TextElement的Symbol设置失败TextSymbol没有赋值就给IElement确保TextElement.Symbol不为null且Symbol.Color是具体RgbColor对象导出图片黑底activeView.Output的resolution与IExport2.Resolution不一致或导出范围错用SetViewByDevice设过分辨率后Output时传相同的整数值检查exportBounds打印位置整体偏右下Page类可打印区域和打印机驱动设置不一致打印前校验printer.Paper.PrintableBounds手动调整页边距中文字体在导出PDF中乱码字体未嵌入PDF或字体名设置不正确ExportPDF设置EmbedAllFontstrue设置字体名时确保系统已安装该字体License初始化失败Desktop Advanced提示许可级别/产品码选择与安装的Engine许可不匹配改用IAoInitialize初始化检查esriLicenseProductCodeEngineGeoDB是否可用除了表格里的问题还有两个排查思路值得单独说。第一个是关于所有整饰元素都AddElement了但布局视图里看不到。这种问题大概率是Geometry没设置或者设置成了空Envelope。ArcObjects里AddElement会接受null不报错但元素不在页面范围内。你可以在添加后用IGraphicsContainer.get_Element(i)遍历已添加元素查看每个元素的Geometry范围排查哪个元素范围异常。第二个是动态出图时的坐标系问题。如果不同mxd的数据框坐标系不一样比例尺的MapUnits换算会出问题。我的做法是在工具初始化时统一读取数据框坐标系如果是经纬度坐标系但项目要求米制比例尺代码里就做一次坐标转换提示。这个属于更偏业务层的处理但实际项目中非常常见。最后分享一个小技巧如果你要批量出很多张图不要每张图都重新创建MapDocument而是打开一个模板mxd在代码里替换数据源或者更新图层内容再添加整饰要素、导出。这样一套模板反复用出图速度会快很多而且图面风格完全一致。我这个工具后期的优化方向就是把打开模板-换数据-出图整个流程串成一个批处理已经跑通了基本原型。如果你手头也面临几十上百个图幅的出图需求建议往这个方向走。本文还有配套的精品资源点击获取
返回列表