ARTICLE DETAIL

资讯详情

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

ArcGIS Pro批量出图动态表格:属性表随图幅自动更新的完整方案

ArcGIS Pro批量出图动态表格:属性表随图幅自动更新的完整方案 干过批量出图的人大概率都遇到过这种需求领导看完样图说了一句“每个图幅右下角加一张表显示当前图幅范围内各地块的属性”于是你开始了手动复制Excel、调行列、摆位置的噩梦。尤其是图幅一多一百多张图每张表内容还不一样靠人工十有八九要加班到深夜。所谓动态表格本质就是让布局页面里的属性表内容跟着当前图幅自动变化不需要每张图手动改。这篇文章我就把完整思路和可复现的代码流程拆开讲包括当时我尝试过的几条技术路线、踩过的坑以及最终跑稳定的批量出图方案。我的需求背景很典型手头是一个村庄规划项目每个自然村出两张图——现状图和规划图每张图右下角都要挂一张当前图幅范围内地块的属性表字段包括地块编号、地类名称、面积等。整套图加起来一百多页。手动插入Excel不是不行但每页都要复制对应图幅的数据、调整行高列宽、对齐页面位置少说二十分钟一页一百页就是三十多个小时起步这还不算中途领导让你加字段、改排序的返工成本。所以在这个标题里“动态”两个字才是真正的核心表格不仅要出现还要跟着页面走内容自动匹配当前图幅。1. 手动插Excel插到怀疑人生之后我决定让表格跟着图幅自己变1.1 这类需求到底长什么样从每个图幅挂一张表说起先把这个需求的常见形态说清楚。大家做的项目类型不同但“属性表上布局”的需求在测绘、规划、国土等领域基本长得差不多按村级出图每村一个图幅表格显示该村范围内的所有图斑信息。按标准分幅出图每幅按图幅号裁剪表格显示该幅内全部要素的属性。按要素逐个出图比如每个用地图斑出一张单图表格显示这个图斑自身的属性。不管是哪种形态规律都一样地图范围一变表格数据就要跟着变而且要精确对应不能出现“这张图展示的是A村表里却混着B村的数据”这种低级错误。我当时的做法走了一段弯路先一个个把图幅导出然后在布局里手动贴一张Excel表格截图。头几个图幅还好到第20页的时候人就麻了因为表头、列宽、行数、字号稍微差一点整张图的观感就不统一更别提中途换过一次字段顺序前面二十几页全部返工。那段时间我才真正理解“动态表格”的价值它解决的不是“往页面里塞一张表”这么简单而是“一百张页面里的表长得一模一样、内容一一对应、改字段只改一次”。1.2 为什么先导出Excel再贴进布局这个老办法在批量场景走不通很多刚接触布局出图的同学第一反应是属性表可以用ArcGIS自带的“导出Excel”或者“表转Excel”工具导成Excel然后在布局里插入Excel对象。单张图没问题因为你可以手动调整位置和格式。但批量场景下它有四个非常致命的短板第一Excel插入布局本质是OLE嵌入对象更新数据需要刷新链接。一旦你换了数据源或者改了字段布局里的表格不会自动同步你必须手动“更新链接”或重新插入这在循环出图里几乎没有自动化接口。第二Excel表格的样式控制非常有限。行高、列宽、字体、边框在批量场景下很难做到统一的程序化控制尤其是合并单元格、自动换行这些操作用arcpy去操作Excel还要再走一遍win32com或者openpyxl链路越长越容易出幺蛾子。第三OLE对象在导出PDF时偶尔会“飘”。我自己实测过插入的Excel对象在某些PDF打印驱动下会显示不全或者被压扁这个问题排查起来非常痛苦因为它不是每次必现属于“偶发性抽风”。第四也是最核心的一条Excel表格内容没办法根据当前图幅范围自动筛选。你可以用数据驱动页面里的“页面定义”让某个动态表格视图跟着页面走但那是ArcMap时代的玩法操作繁琐且兼容性差后面我在第2节详细说。所以当我意识到“每页表格数据都是由当前地图范围决定的”这个规律之后思路就变了与其在布局里折腾可编辑的表格对象不如把属性表当成一张“图”来生成每次循环时重新画一张当前范围的表格图片替换掉布局里的旧图片。这就是整篇博文的核心方案也是我把这个需求的技术重心拆成了两段的原因一段是把属性数据画成表格图片另一段是把图片按固定位置塞进布局页面。2. 三条路摆在我面前表格框、动态文本、还是图转插2.1 ArcMap时代的ITableFrame和它的致命短板先说老路子。ArcMap的布局里有一个“表格框/Table Frame”的概念它可以直接绑定一个图层或独立表配合数据驱动页面可以实现表格内容跟着页面定义走。当时我在网上搜到最多就是这个方案很多人用ITableFrame接口写ArcMap插件做动态属性表。我试用之后的感觉是思路是对的但控制力太差。第一表格框能显示的字段、排序方式、行数控制都不直观很多格式调整要通过接口的细碎属性去设置开发调试成本很高。第二ArcMap的ITableFrame在批量导出时偶尔会出现表格刷新不及时的问题明明是新的页面范围导出的表格还是上一页的数据必须手动调用刷新接口。第三也是最现实的问题——ArcMap已经进入维护期新项目基本都是ArcGIS ProPro里的arcpy.mp并没有完整开放Table Frame的动态操作接口至少在我写插件的时候这条路对Pro用户来说基本是断的。所以如果你现在还在用ArcMap做这类需求ITableFrame可以用但也仅限于老的MXD工程新项目我建议直接看我的“图转插”方案。2.2 为什么最终选属性数据→渲染成图片→插入布局图片元素ArcGIS Pro的arcpy.mp虽然没有开放动态表格框但它有一个很灵活的布局元素类型——图片元素Picture Element。图片元素有一个其他元素不具备的优点它的内容就是一个外部图片文件你只要在批量循环里反复替换这个图片文件的内容布局元素本身不用删了重建整体流程就自动化了。所以我的技术链路就设计成了三段式拿到当前图幅的地图范围。根据这个范围从图层属性表里筛选出对应的要素属性数据。把这些数据用绘图库我用的是Pillow/PIL渲染成一张表格图片导出成PNG。在布局里固定一个图片元素每次循环把新PNG替换进去然后导出当前页面。这个方案最让我满意的一点是渲染表格的代码完全在我自己手里。字段要哪几列、列宽怎么分配、超长文本怎么截断、数值保留几位小数、表头背景用深色还是浅色全都可以通过几行参数控制。领导想改我改参数重新跑一遍就行不用一张张手动调。而且因为表格内容是被“画”出来的最终输出是一张规规矩矩的像素图哪怕原图只有几十分之一的精度插入布局后再导出PDF清晰度也完全可控。不用像Excel对象那样担心导出时变换渲染导致表格变形。2.3 这套方案的适用边界与硬件要求说完了优点也得把条件讲清楚。这套方案有三个前提一是你必须在Python脚本环境下运行也就是说ArcGIS Pro要开着Python环境或者你把自己的工具做成了ArcGIS插件/脚本工具来跑。ArcMap自带Python 2.7也支持但代码细节要稍微调整。二是图层属性数据的读取要通过arcpy.da.SearchCursor或类似接口不要在布局页面里直接依赖UI操作毕竟批量出图没人愿意手动点一百遍。三是对绘图库的要求。ArcGIS Pro自带的Python环境默认装了Pillow直接import PIL就能用不需要额外安装任何东西这点非常省心。如果你非要自己另装一个干净的Python环境那还需要pip install pillow。硬件方面说实话这类操作对电脑最大的压力不在绘图而在ArcGIS Pro打开工程和刷新地图范围。建议内存16GB起步固态硬盘否则一百多页循环跑起来中途等得人能睡着。3. 表格图片生成这一段才是全篇技术含量最高的部分3.1 数据读取先从图层把属性拿干净在画表格之前数据整理是决定表格质量的关键。我见过不少人的表格内容是一股脑把所有字段都往图上怼结果图面密密麻麻根本没法看。正常做法是先定好需要展示的字段清单然后读取字段别名、格式化字段值。这里有一个细节特别值得说表头一定要用字段别名不要直接用字段名。比如你的字段名是“DLMC”没有GIS基础的人根本看不懂但字段别名是“地类名称”放在表头里就一目了然。读取别名的代码很简单import arcpy fc rD:\data\gj.gdb\ghyd fields [DLMC, DKBH, MJ] field_aliases {f.name: f.aliasName for f in arcpy.ListFields(fc)} # 结果类似 {DLMC: 地类名称, DKBH: 地块编号, MJ: 面积}另外一个坑是字段值的格式化。属性表里的值什么类型都有面积可能是浮点数日期可能是datetime对象有些字段还可能是None空值。这些值如果不格式化直接丢给绘图库轻则显示成“2023-01-01 00:00:00”重则直接报错。我习惯在读取阶段就统一处理def format_value(val, field_name): if val is None: return if isinstance(val, float): return f{val:.2f} if isinstance(val, datetime.datetime): return val.strftime(%Y-%m-%d) return str(val)如果你图层里有几何字段参与了查询读取时别把SHAPE也塞进来那家伙一串坐标文字画出来能把表格撑爆。记住表格显示的是属性信息几何信息是用来做空间筛选的用途要分开。3.2 Pillow画表格列宽、行高、中文字体一个都不能少Pillow在ArcGIS Pro环境里叫PIL导入方式是from PIL import Image, ImageDraw, ImageFont。画表格的原理其实很简单先在内存里创建一张空白图片然后用ImageDraw在图片上逐格画矩形框和文字。但这个“简单”背后有三个细节不看实测根本发现不了第一个是中文字体。Pillow默认字体不支持中文如果你直接ImageFont.load_default()画出来的中文全是方框。必须指定系统中文字体文件Windows下我常用C:/Windows/Fonts/msyh.ttc微软雅黑或者C:/Windows/Fonts/simhei.ttf黑体。规划类图件一般用黑体或宋体比较端庄我习惯用微软雅黑加粗做表头。font_header ImageFont.truetype(rC:\Windows\Fonts\msyhbd.ttc, 14) font_body ImageFont.truetype(rC:\Windows\Fonts\msyh.ttc, 12)第二个是列宽计算。Pillow画文字没有自动换行如果某一列的单元格内容超长文字会直接溢出到边框外非常难看。所以必须先遍历所有单元格算出每列内容的最大像素宽度再统一加边距作为列宽。计算方法在Pillow 8.0之后推荐用draw.textlength(text, fontfont)老版本可以用font.getsize(text)[0]。兼容写法可以这样def text_width(draw, text, font): try: return draw.textlength(text, fontfont) except AttributeError: return font.getsize(text)[0]然后对每一列比较表头文字宽度和该列所有值里最宽的取最大值加前后边距。这样算出来的表格图片宽度才刚好。不提前算的话要么文字被截断要么整张表宽窄不齐逼死强迫症。第三个是行高。固定行高最简单但要注意字体字号变了行高也要跟着调。我实践中的经验是正文12号字行高至少28像素表头14号字加粗行高32像素这样文字上下留白才舒服。如果表格里还有换行文本行高就要动态按内容算了。3.3 完整代码把属性表变成一张规规矩矩的PNG下面这段是我在项目里实际用过的核心画表代码去掉工程细节后大概长这样import arcpy import datetime from PIL import Image, ImageDraw, ImageFont def make_table_image(rows, field_names, field_aliases, out_png, font_pathrC:\Windows\Fonts\msyh.ttc, font_header_pathrC:\Windows\Fonts\msyhbd.ttc, header_size14, body_size12, row_height28, header_h32, padding10): rows: list of list每行是字段值列表 field_names: 字段名列表用于格式化 field_aliases: 字段名字典用于表头 out_png: 输出图片路径 font_header ImageFont.truetype(font_header_path, header_size) font_body ImageFont.truetype(font_path, body_size) # 临时画布仅用于量文字宽度 tmp_img Image.new(RGB, (10, 10), white) draw_tmp ImageDraw.Draw(tmp_img) col_widths [] for ci, fn in enumerate(field_names): header_w text_width(draw_tmp, field_aliases.get(fn, fn), font_header) max_w header_w for row in rows: val format_value(row[ci], fn) w text_width(draw_tmp, val, font_body) if w max_w: max_w w col_widths.append(max_w padding * 2) table_w sum(col_widths) 1 table_h header_h row_height * len(rows) 1 img Image.new(RGB, (table_w, table_h), white) draw ImageDraw.Draw(img) # 表头 x 0 for ci, fn in enumerate(field_names): draw.rectangle([x, 0, x col_widths[ci], header_h], fill#E8E8E8) draw.text(x padding, (header_h - header_size) // 2, field_aliases.get(fn, fn), fontfont_header, fillblack) x col_widths[ci] # 数据行 y header_h for row in rows: x 0 for ci, fn in enumerate(field_names): draw.rectangle([x, y, x col_widths[ci], y row_height], outline#CCCCCC) draw.text(x padding, y (row_height - body_size) // 2, format_value(row[ci], fn), fontfont_body, fillblack) x col_widths[ci] y row_height img.save(out_png) return table_w, table_h这段代码有两个细节可能有人会问。一是为什么先用一个10x10的临时图片来量宽度因为ImageDraw创建后需要绑定图像对象而最终图片尺寸又依赖文字宽度这是个先有鸡还是先有蛋的问题所以先建一个临时小图画布用来测字宽再正式创建表格图片。二是画完边框再写字顺序一定不能反否则文字会被边框线盖住字号小的时候特别明显。4. 把图片稳稳放进布局坐标单位、元素更新和循环替换4.1 arcpy.mp操作图片元素的正确姿势图片生成好了接下来要解决的是往布局里放的问题。arcpy.mp里操作布局元素的核心入口是ArcGISProject和Layout对象图片元素属于PICTURE_ELEMENT类型查找、创建、更新的套路比较固定。第一步是找到目标布局和地图框aprx arcpy.mp.ArcGISProject(rD:\projects\xx.aprx) layout aprx.listLayouts(出图A3)[0] map_frame layout.listElements(MAPFRAME_ELEMENT)[0] # 假设只有一个地图框第二步是创建或查找图片元素。这里我强烈建议在布局里预先手动放置一个图片元素并取好名字比如叫“TABLE_IMG”然后在代码里通过listElements(PICTURE_ELEMENT)遍历查找。为什么不直接代码创建因为代码创建的图片元素位置和大小还得重新设置而手动放好的元素你可以在ArcGIS Pro界面里先把它摆到满意位置代码只要更新元素内容就行位置完全不用操心。pic_elm None for elm in layout.listElements(PICTURE_ELEMENT): if elm.name TABLE_IMG: pic_elm elm break if pic_elm is None: pic_elm layout.addPictureElement(rD:\temps\table.png, TABLE_IMG)第三步是更新图片内容。图片元素的sourceImage属性指向图片文件路径批量循环里你只要把sourceImage改成新的PNG路径再设置宽高和位置导出时布局里的图片就是新的内容了。pic_elm.sourceImage rD:\temps\table_latest.png pic_elm.elementWidth 16 # 单位是布局页面单位厘米 pic_elm.elementHeight 6 pic_elm.setPosition(2, 2) # x, y从布局左上角算起这里有一个非常容易踩的坑elementWidth和elementHeight的单位不是像素而是布局的页面单位。如果你布局单位是厘米那16就是16厘米如果布局单位是毫米那16就是16毫米图片会缩到看不见。所以在设置尺寸前一定要先确认布局单位或者先统一转换成你熟悉的一套。4.2 批量循环时的一次性创建、逐页替换套路批量出图的核心循环逻辑我总结成一个“四步走”套路每一页都走一遍这四个动作切换当前页面到这一页地图框范围跟着变化。获取地图框当前范围按范围筛选属性数据。生成当前页的表格图片更新图片元素的sourceImage。导出当前布局为PDF或JPG进入下一页。这里最值得注意的一点是图片元素在整个循环里只创建一次后面反复更新的其实只有sourceImage、elementWidth、elementHeight、setPosition这几个属性。这样布局内部不会因为反复创建删除元素而留下“幽灵对象”导出速度也更快。如果你用的是ArcGIS Pro的地图系列Map Series切换页面的代码非常优雅ms layout.mapSeries for page_num in range(1, ms.pageCount 1): ms.currentPageNumber page_num extent map_frame.camera.getExtent() # 当前页地图范围 # ... 筛选数据、画表、更新图片元素、导出如果项目工程里没有开地图系列而是按要素一个个缩放定位那就用map_frame.camera.setExtent()先设置地图范围再做同样操作。两种方式核心思路一致只是页面的“驱动源”不同。导出这一步我用的是layout.exportToPDF如果你需要JPGexportToJPEG也是现成的。注意导出PDF时文件路径千万不要复用同一个文件名不然后面覆盖前面最后只留下一张图。我在脚本里习惯用页码或FID拼文件名out_pdf os.path.join(out_folder, f图幅_{page_num:03d}.pdf) layout.exportToPDF(out_pdf, resolution300)4.3 页面单位和坐标换算的坑先说结论图片元素在布局上的最终显示尺寸由elementWidth和elementHeight决定和PNG图片本身的像素尺寸没有直接关系。也就是说我生成了一张2000x1200像素的PNG在布局里设成16x9.6厘米它就是16x9.6厘米设成8x4.8厘米它就是8x4.8厘米。所以“图片在布局里有多清晰”取决于你生成PNG时留了多少像素余量。我的习惯是先确定目标显示尺寸比如16厘米再按300DPI换算成像素来生成表格图片。换算公式是像素宽度 目标宽度(厘米) / 2.54 * 300这样做出来的表格图片插入布局刚好顶满目标宽度清晰度还高打印完全够用。如果你生成图片时直接用小尺寸比如800像素宽然后拉大到16厘米那导出PDF后表格文字会发虚凑近一看全是锯齿。另一个经常被忽略的坑是高度比例。你生成图片时是像素比例设置elementHeight时必须按同一比例换算。比如表格图片是1200像素高宽度2000像素那么宽度设16厘米时高度就应该是16 * 1200 / 2000 9.6厘米。写成代码就是aspect img_h_px / img_w_px pic_elm.elementWidth target_w_cm pic_elm.elementHeight target_w_cm * aspect如果不按比例设置表格会被拉伸变形字都变成了长脸非常难看。5. 与批量出图整合时我总结出的性能与稳定性清单5.1 性能优化别每页都去读一次数据第一批测试脚本跑下来我最大的感受就是“慢”。一百多页图每一页都重新查询数据库、重新读取要素属性整个流程慢得像蜗牛。后来优化之后速度提升了差不多三倍核心思路就一句话把能缓存的全缓存了每页只做最小必要操作。第一个优化点是字段信息。图层字段名、字段别名、字段类型这些元数据在整个循环里是不变的绝对不能在循环体里反复调用arcpy.ListFields。应该一次性在循环前面读取做成字典缓存。第二个优化点是属性数据。如果你的出图单位是“每个要素一页”那么完全可以一次性把这个图层的全部属性读入内存建一个以OBJECTID为键的字典循环里直接按当前要素的OID取数据连数据库查询都省了。# 循环前一次性读取 data_dict {} with arcpy.da.SearchCursor(fc, [OID] fields) as cur: for row in cur: data_dict[row[0]] row[1:]第三个优化点是表格图片。每页表格只有行数不一样列宽、表头样式、字体这些都是一样的。如果每次循环都重新计算列宽纯属浪费。我更常用的做法是先用所有页数据里最长的值算一次列宽然后所有页面用同一组列宽参数画表这样不仅快而且每页表格宽度一致布局上看非常整齐。如果你是按地图范围过滤要素每页查询不可避免但也尽量用arcpy.da.SearchCursor加where_clause不要每页都MakeFeatureLayer再SelectLayerByLocation后者每页多出好几个GP工具调用慢得惊人。我的建议是需要空间过滤时直接构造当前范围的四边形Polygon用arcpy.da.SearchCursor配合空间过滤参数来读语法如下polygon arcpy.Polygon(arcpy.Array([...四角点...]), sr) with arcpy.da.SearchCursor(fc, fields, spatial_filterpolygon) as cur: rows [row for row in cur]SearchCursor的spatial_filter参数可以直接传Polygon或Extent对象比MakeFeatureLayerSelectLayerByLocation那套组合拳快得多代码也更干净。5.2 稳定性防护临时文件、编码和异常兜底批量出图最怕什么最怕跑到第87页突然报错然后前面86页白跑或者更惨——报错之后你不知道是哪一页出了问题。所以稳定性设计一定要提前做。临时文件这块我在项目里用的是固定目录里的temp_table.png。刚开始我以为同一个文件反复覆盖没问题结果跑着跑着发现第N页导出的表格还是第N-1页的内容。后来排查发现Pillow的img.save()虽然会把文件写进去但ArcGIS Pro的图片元素可能还持有旧的缓存尤其当文件名完全相同的时候有些版本会命中缓存导致不刷新。解决办法很简单每页生成一个带序号或时间戳的临时文件名比如table_001.png、table_002.png更新sourceImage之后这个坑再也没有出现过。temp_png os.path.join(temp_dir, ftable_{page_num:03d}.png) # 画表、保存 make_table_image(rows, fields, aliases, temp_png) pic_elm.sourceImage temp_png编码这块也要专门说。如果你的属性表里有中文路径、中文图层名、中文字段值Python脚本文件一定要保存成UTF-8格式并且在脚本开头加# -*- coding: utf-8 -*-。另外ArcGIS Pro的Python环境默认是多语言环境但如果你在ArcMap的Python 2.7里跑这套代码字符串和路径的中文问题会更多建议直接用字符串前加u的方式写。异常兜底更是必须的。批量循环里我习惯用try/except包住每一页的处理逻辑一旦某页出错先记录日志然后继续跑下一页最后统一查看失败清单。不然辛辛苦苦跑一宿早上起来发现断在第27页那个心情真的很难形容。for page_num in range(1, ms.pageCount 1): try: # 当前页所有操作 except Exception as e: with open(os.path.join(out_folder, error_log.txt), a, encodingutf-8) as f: f.write(f第{page_num}页失败: {e}\n)5.3 和地图系列配合的小技巧最后分享一个和地图系列配合的实用技巧。ArcGIS Pro的地图系列有两种驱动方式一种是“空间地图系列”按要素范围逐页定位另一种是“页定义地图系列”多用于图集。不管你用的是哪种只要地图系列一激活地图框的camera.getExtent()返回的就是当前页的实际范围。我建议在批量出图前先用mapSeries.getPageSize()或布局页面属性确认页面尺寸一致然后先跑一个3-5页的小样把表格位置、列宽、行数都调好再全量跑。小样阶段记得重点检查中文字体、表格是否超出页面边界、图片是否变形这三个问题。全量跑的时候打开任务管理器看一眼内存占用如果内存稳步上涨不回落说明有资源泄漏要么是arcpy在大量操作后没有释放要么是循环里创建了太多没销毁的大对象需要定位优化。6. 我实测时踩过的坑按频率从高到低排了个队6.1 表格里的中文变成方框这是所有坑里出现频率最高的几乎每次新换一台电脑跑脚本都会遇到。原因通常就两个一是字体文件路径不对二是Pillow的默认字体不支持中文。排查方法很简单把字体路径打印出来看一眼文件是否存在import os font_path rC:\Windows\Fonts\msyh.ttc print(os.path.exists(font_path))如果返回False就把C:/Windows/Fonts/目录下的字体列出来找一个你认识的中文字体文件。另外有个容易忽略的点有些精简版Windows系统没有微软雅黑但宋体simsun.ttc基本都在改一下truetype的路径就能解决。还要注意一个细节字体文件是.ttcTrueType Collection时Pillow的truetype在读取时一般没问题但如果你在Linux服务器上跑比如发布成GP服务中文字体可能一个都没有那就得把字体文件一起打包部署。6.2 列太宽表格超出页面表格图片宽度超过布局可用宽度这是第二个高频坑。原因很直接画表的时候没有控制总宽度或者字段值里有超长字符串比如地块编号是“2023-ND-03-0125-ZX-001”这种一个值顶别人三个字符宽度。我的解决办法是在画表代码里加一个“总宽度兜底”逻辑。如果算出来的总宽度超过页面可用宽度就对列宽按比例缩减或者直接做省略号截断。截断逻辑不复杂就是先量完整文字宽度超过列宽上限就逐字往里加加到放不下时就加省略号def truncate_text(draw, text, font, max_width): if text_width(draw, text, font) max_width: return text ellipsis ... while text and text_width(draw, text ellipsis, font) max_width: text text[:-1] return text ellipsis另外在设计表格字段的时候也该提前做“减法”。布局页面右下角的空间本来就有限字段太多必然拥挤。我一般控制在3到6个字段以内字段种类优先保留编号、名称、面积、用途这类看图时最常被问到的信息。属性表如果实在放不下少一两个字段不失为一个明智的取舍。6.3 表格位置略偏离第一页和第二页对不齐这个问题一度让我怀疑人生第一页表格在右下角好好的第二页位置却偏了半厘米第三页又正常了。排查了半天发现跟图片元素本身没关系而是布局页面单位问题。我的布局模板在创建时默认单位是毫米但我在代码里写的是setPosition(20, 20)潜意识以为是厘米。如果layout.pageUnits返回的是PAGE_UNIT_MILLIMETER那这个20就是20毫米位置自然和别的页面不一致。正确的做法是循环前先读一次布局单位然后统一换算if layout.pageUnits PAGE_UNIT_CENTIMETER: pos_x 2.0 elif layout.pageUnits PAGE_UNIT_MILLIMETER: pos_x 20.0更稳妥的做法是固定用厘米思路先转成布局单位再设置。这个坑提醒我任何涉及坐标和尺寸的代码必须先确认单位再写数字否则看起来是小事跑全量的时候每页都可能跑偏。再说一个类似的坑图片元素的setPosition默认是从元素左上角开始算而elementWidth和elementHeight是从左上角向右下角延伸。如果你的表格位置是靠右下角对齐的最好先把位置和尺寸都确定好再一次性set不要在循环里频繁调整同一个元素的多个属性那样更容易出现“上一次设置还没生效又被下一次覆盖”的异步问题。6.4 每页表格内容没跟着图幅变这个坑最隐蔽一旦发生整个成果全部作废。我遇到过的情况是跑完一百页之后抽查发现第37页的表格和第36页内容一模一样但地图范围明显不同。排查了一圈发现是地图系列页面切换和数据读取的顺序问题。我的循环原来是这样写的获取当前地图框范围。读取属性数据。切到下一页。问题出在第一步获取范围时用的是旧的页面范围读完数据再切页当前页数据和页面就对不上了。正确的顺序必须是先切换页面再获取范围再读数据ms.currentPageNumber page_num # 1. 先切页 extent map_frame.camera.getExtent() # 2. 再取当前页范围 rows query_data(extent) # 3. 最后读数据这个顺序问题在单页测试时完全看不出来因为一页无所谓先后但批量循环里就会隔三差五出一次“串页”。所以我在最终脚本里加了一个断言每页把当前页号和获取到的范围中心点打印出来抽查时对一下就知道有没有串。除此之外如果你用了spatial_filter做范围过滤还要确认数据源的坐标系和地图框的范围坐标系一致。如果一个是地理坐标系一个是投影坐标系把Extent直接丢给spatial_filter会导致过滤结果完全不对查半天都查不出来。稳妥做法是先做坐标转换或者直接用投影坐标系的图层数据。最后再分享一个真实实用的心得这套“属性表转图片再插入布局”的方案我后来不止用在一个项目上不管是村庄规划的图斑属性表、土地调查的分类面积表还是业务上的管线段状要素表只要把数据读取和画表参数调一调全都能复用。说实话它不是什么高深技术Pillow画表格、arcpy.mp换图片源都是很基础的API但把这些基础操作组合成一条完整链路解决一个具体的批量出图痛点这才是经验值所在。如果你鱼塘里正好接了类似的需求我的建议是别急着写全量脚本先做三件事第一确认你用的是ArcGIS ProPro自带Pillow省去一堆环境问题第二先用两三个图幅手动验证画表效果和布局位置把字体、列宽、行数这些参数调到满意为止第三再跑全量并且全程保留错误日志。这样一步步来大概率不会翻车。我那时候如果早想清楚这些也不至于第一个版本的脚本反复折磨自己好几个晚上。
返回列表