为什么我坚持用Add-in来做ArcMap下拉菜单
如果你用过ArcMap,大概经历过这种场面:菜单栏和工具条上堆了几十个按钮,真正常用的就那么几个,大部分时间都浪费在“点自定义 → 翻命令 → 找工具”的路上。我之前在项目里被反复要求做同一批操作——打开属性表、按字段选择、裁剪影像、出图命名,做到后来实在忍不住,于是决定自己写一个ArcMap下拉菜单插件,把这些操作全部收进去,一键触发。
这个需求在ArcGIS体系里不是新话题,做起来也比想象中简单,关键是用对路子。今天这篇就完整拆解一遍:如何基于ArcGIS Add-in编程机制,在ArcMap里创建一个名副其实的下拉菜单插件。文章面向两类读者:一类是被重复操作折磨的制图内业人员,想把手头机械动作收进菜单;另一类是刚接触ArcObjects开发的程序员,需要一个能跑通全流程的入门项目范本。如果你两者都沾边,那这篇文章就是为你准备的。
说句题外话,虽然现在新项目都在转向ArcGIS Pro,但ArcMap 10.x在很多存量业务和生产环境里仍然是绝对主力。掌握Add-in插件的写法,不仅能让手头工作立刻轻松好几个量级,也会让你对ArcObjects这套对象模型的理解上一个台阶。
1. 为什么我坚持用Add-in来做ArcMap下拉菜单
1.1 先分清ArcMap里的“下拉菜单”到底指什么
在动手写代码之前,得先把概念掰扯清楚。ArcMap界面定制里,有三个长得有点像的东西经常被新手混在一起:工具条(Toolbar)、下拉菜单(Menu)、下拉框/组合框(ComboBox)。
这三者的开发现难度和用途完全不同。工具条是一排按钮的容器,比如“标准工具条”和“工具”工具条,按钮直接平铺显示。下拉菜单是一个按钮,点击后弹出子级命令列表,在ArcGIS里通常叫做Menu,比如“文件”菜单、“地理处理”菜单,核心特征是点开出一串二级选项。而下拉框(ComboBox)则是可以输入或选择一个值的控件,像个设置项,不是“菜单”。
标题里要做的下拉菜单,对应的是Menu这一层。很多人搜“下拉菜单插件”搜到了ComboBox的资料,跟着做出来发现界面里出现的是一个可输入的方块控件,根本不是自己想要的二级菜单,方向直接跑偏。先把这个概念锚定住,后面代码才不会写歪。
1.2 老式DLL插件和Add-in的差别
ArcGIS的插件机制经历过两个阶段。ArcGIS 10.0以前,做ArcMap扩展基本是写一个Class Library类库,实现ICommand接口,生成DLL,再用RegAsm注册到COM注册表,之后在ArcMap里通过命令GUID去找到它。这套流程最大的问题在于:注册表污染严重、卸载不干净、换一台机器部署步骤繁琐,而且ArcGIS版本一升级,COM引用断一截,整个插件可能直接打不开。
ArcGIS 10.0之后引入了Add-in机制,这段历史很多老开发都知道,但如果是新人,可能刚接触就是Add-in,反而不知道它到底解决了什么。简单说,Add-in把“插件”变成了一种有标准格式的安装包:一个后缀为.esriAddIn的文件。这个文件本质上是一个压缩包,里面包含编译好的DLL、XML清单文件、图片资源。用户在目标机器上双击这个文件,插件就会被复制到当前用户的ArcGIS Add-ins目录中,ArcMap启动时自动扫描加载。
两种方式对比如下,这也是我强烈建议Add-in路线的直接理由。
| 对比项 | 老式DLL插件 | Add-in插件 |
|---|---|---|
| 注册方式 | RegAsm写注册表 | 复制文件到AddIns目录 |
| 安装 | 手动注册命令GUID | 双击.esriAddIn |
| 卸载 | 反注册,易残留 | 删除目录或加载项管理器删除 |
| 跨版本兼容 | 基本不兼容 | 同大版本可换编译目标 |
| 开发模板 | 手写骨架代码 | VS模板一键生成 |
| 新人上手难度 | 较高 | 较低 |
对我个人来说,最直白的收益是:再也不用担心装插件把系统环境搞坏,给同事分发时只需要发一个文件。这在新老版本混杂的单位里尤其省心。
1.3 谁需要自己做一个下拉菜单
接触这个需求的,通常有三类人。第一类是测绘、国土、规划行业的内业人员,他们每天面对的是重复性极高的ArcMap操作,比如按地块导出图片、对比两期影像、批量调整符号。这类人不需要学很深的ArcObjects,只要能把常用操作打包成菜单项就已经受益巨大了。第二类是GIS二次开发新手,把Add-in当作理解ArcObjects对象模型的敲门砖——一个下拉菜单就是一个类,每个按钮的OnClick就是一次与ArcMap文档的交互,结构简单、反馈直观,比一上来就搞复杂扩展(Extension)友好得多。第三类则是需要给团队搭建内部工具集的开发,需要在ArcMap里统一入口、统一风格,下拉菜单不占屏幕空间,挂十几个工具也不显得乱。
无论属于哪一类,核心路径是一致的:创建Add-in工程,编写按钮类,在XML清单文件里把按钮组装成菜单,编译安装,调试运行。接下来我按这个顺序完整走一遍。
2. 环境准备:SDK、Visual Studio、License的顺序问题
Add-in开发踩坑率最高的环节,其实不是写代码,而是环境搭建。很多人下载了ArcGIS Desktop,也装了Visual Studio,结果创建工程时找不到Add-in模板,或者生成代码后启动ArcMap报授权错误。这些多半是版本搭配和授权初始化的问题。
2.1 版本怎么搭配
ArcGIS Add-in开发依赖ArcObjects SDK for .NET Framework,这个SDK并不是独立安装的软件,而是在ArcGIS Desktop安装时作为可选组件出现,或者通过安装包里的SDK目录单独安装。不同的ArcGIS Desktop版本对Visual Studio版本有明确的支持范围,这是我见过的最容易踩的坑。
以ArcGIS 10.2时代为例,官方对VS的支持主要集中在Visual Studio 2010和2012,如果强行用VS2013或更高版本创建Add-in工程,模板可能直接不出现,或者生成了但编译报一堆程序集版本冲突。到了ArcGIS 10.8时代,VS2015、VS2017、VS2019基本都能用。我的建议是:不要试图记忆复杂的官方兼容矩阵,最稳的方法是“先装ArcGIS,再看它自带的SDK文档里支持的VS版本,然后照着装对应版本的VS”。
还有一点容易被忽略:ArcGIS 10.x系列在安装时通常自带一个独立的License Manager组件,这个组件负责各种浮动授权。开发调试ArcMap插件时,本机运行ArcMap本身就需要有效的Desktop许可,如果License Manager没有启动,ArcMap根本进不到界面,更别提调试插件了。
2.2 License这块到底要不要管
这里展开说一下,因为很多新手在环境准备阶段卡住的不是代码,而是“arcgis license server点击启动后没反应”这一类问题。如果你碰到License Server Administrator里点了启动,但服务状态一直不变,或者提示“Unable to start”,优先从三个方向排查:第一,安装时是否勾选了License Manager组件,没装就服务缺失;第二,本机端口27000到27006是否被占用,某些安全软件会拦截License Manager的通信;第三,查看License Manager安装目录下的Server日志文件,大部分启动失败原因会直接写在日志里。
我这里不展开所有排查细节,但想强调一个观点:插件开发阶段,不要把License问题想得太复杂。如果你的ArcMap能正常打开、能正常加载数据,那开发环境就是可用的。Add-in工程调试时只是借助ArcMap这个宿主来运行我们的代码,本质上是在一个已经可用的Desktop环境里追加内容,而不是重新申请一套授权。
2.3 用哪个模板确认环境OK
环境是否真配好了,最直接的验证方式就是新建一个工程。在Visual Studio里新建项目,搜索“ArcGIS”,应该能看到“ArcGIS Desktop Add-in”这类模板(不同SDK版本模板名字略有差异)。如果能看到,说明SDK安装成功且VS版本匹配。
模板新建时可以选择目标宿主程序,比如ArcMap、ArcCatalog等,语言通常选C#。工程生成后不要急着改代码,先直接编译一次,再双击生成的.esriAddIn文件,ArcMap能正常加载这个空壳插件,就说明整个链路已经通了。从这个基础出发再去改菜单,才不会被环境问题干扰。这里有一个细节:模板生成的插件默认可能不包含任何可见菜单或按钮,所以打开ArcMap后不会在工具条里看到明显变化,这是正常的,说明插件已经被加载,只是还没定义界面内容。后面第四部分会告诉你怎么让它在界面上出现。
3. 工程骨架和Config.esriaddinx:插件的说明书
环境通了之后,先在工程里转一圈,搞清楚Add-in工程到底生成了什么,再开始写具体逻辑。很多人上来就删掉模板代码自己写,结果把关键配置文件改坏了还不自知。Add-in工程虽然小,五脏俱全,有一个文件是灵魂,那就是Config.esriaddinx。
3.1 Add-in工程里到底生成了什么
一个典型的ArcGIS Desktop Add-in工程包含几个核心部分:一个是项目主文件,通常叫ThisAddIn.cs,继承自ESRI.ArcGIS.Desktop.AddIns.Extension,这个类代表插件本身的生命周期入口,可以在其OnStartup和OnShutdown方法里写初始化和清理逻辑;一个是Config.esriaddinx文件,这是插件的XML清单,相当于插件的说明书,所有界面组件的声明都在这里;还有一些可选的Images文件夹,用来存放按钮图标资源。
模板还自动生成了AssemblyInfo.cs文件,里面有一个ArcGISAddIn特性(Attribute),插件的名称、描述、作者、版本、目标产品(ArcMap还是ArcCatalog)都在这里定义。有一点很多人会忽略:插件的唯一标识(通过Guid或名称组合)一旦发布给同事,后续更新版本时最好不要改动这个标识,否则ArcMap可能认为这是一个全新的插件,导致旧版本和新版本同时出现,界面上重复挂载。
3.2 Config.esriaddinx的组件关系
Config.esriaddinx是整个Add-in最核心也是最容易被低估的文件。它的内部结构直接决定插件在ArcMap中长什么样。最小化的结构可以这样理解:根节点是AddIn,下面会区分ArcMap、ArcCatalog等宿主环境节点。在ArcMap节点中,横向存在Toolbars、Menus、Buttons、ComboBoxes等并列区域,各自声明组件。
这几个区域不是孤立的,而是存在明确的“组装关系”:按钮(Button)是最小的功能单元,负责执行具体逻辑;多个按钮放进一个下拉菜单(Menu)就形成了二级菜单;而菜单本身不能独立悬浮在界面上,它需要放置在某个工具条(Toolbar)上,工具条才是用户可以在“自定义 → 工具条”里勾选显示的容器。
这个关系可以用一句话概括:工具条上放菜单,菜单里放按钮,按钮指向代码类。记不住这句话没关系,写XML的时候照着这个逻辑一层层套就能套对。
3.3 图片资源和ID的管理
在Config文件里,每个组件都有一个id属性,这是组件在ArcMap命令系统中的唯一标识。id不要用中文、不要有空格,建议用项目前缀加含义的驼峰命名,比如MyTools_ZoomToFullExtent。多个插件都叫ZoomToFullExtent也不会冲突,因为ArcMap内部会用插件的唯一标识组合成全局命令ID。
图片资源同样遵循“相对路径”规则。在XML中image属性写的路径,是相对于工程根目录的,编译时会把图片打包进.esriAddIn文件。图片格式上,ArcMap的命令按钮图标强烈建议使用PNG或BMP,控制在16x16或24x24像素左右,矢量图和超大尺寸图不仅会拉低ArcMap渲染性能,还会出现图标显示模糊或异常的问题。
4. 核心实现:下拉菜单的XML声明与前台按钮代码
接下来是重头戏,完整走一遍下拉菜单从声明到能用的过程。这一节我以一个真实项目中的“地图快捷工具”菜单为例:菜单里包含三个功能——缩放至全图、统计当前地图矢量要素数量、导出当前视图为JPEG图片。这三个功能分别对应ArcMap中的视图控制、数据查询和出图操作,覆盖了日常使用频次最高的几种交互。
4.1 在Config.esriaddinx中声明工具栏、菜单、按钮
先用一个工具条把一个Menu挂起来。下面的XML是我在工程里实际用过的结构,字段含义我已经标注清楚:
<AddIn> <Name>MapQuickTools</Name> <Description>ArcMap常用地图操作快捷菜单</Description> <Version>1.0</Version> <Image>Images\addin.png</Image> <ArcMap> <Toolbars> <Toolbar id="MapQuickToolbar" caption="地图快捷工具"> <Items> <Menu refID="MapQuickMenu" /> </Items> </Toolbar> </Toolbars> <Menus> <Menu id="MapQuickMenu" caption="快捷工具" captionEx="Quick Tools"> <Items> <Button refID="ZoomToFullExtentButton" /> <Button refID="CountFeaturesButton" /> <Button refID="ExportMapButton" /> </Items> </Menu> </Menus> <Buttons> <Button id="ZoomToFullExtentButton" caption="缩放至全图" className="MapQuickTools.ZoomToFullExtentButton" image="Images\zoom.png" /> <Button id="CountFeaturesButton" caption="统计矢量要素数量" className="MapQuickTools.CountFeaturesButton" image="Images\count.png" /> <Button id="ExportMapButton" caption="导出当前视图为图片" className="MapQuickTools.ExportMapButton" image="Images\export.png" /> </Buttons> </ArcMap> </AddIn>注意到Button节点里的className属性,它写的是“命名空间.类名”,必须和后面C#代码里的类完全对应。这里最容易出错的地方有两个:一是类名拼错,加载时ArcMap会找不到类,插件菜单直接空白;二是image路径写错,按钮会变成系统默认的空白图标。这两个问题在VS里编译阶段都不会报错,是在ArcMap加载时才暴露的,所以排查起来有一定隐蔽性。
4.2 Button类的基本写法
每个菜单按钮对应一个类,这些类统一继承ESRI.ArcGIS.Desktop.AddIns.Button基类。这个基类帮我们处理好了与ArcMap宿主之间的连接,我们只需要重写两个方法:OnClick和OnUpdate。
OnClick是点击按钮时执行的核心逻辑;OnUpdate是ArcMap周期性调用的刷新方法,通常用来控制按钮的Enabled属性,也就是按钮什么时候可用、什么时候置灰。不要在OnUpdate里做耗时计算或数据库查询,因为ArcMap会在空闲时频繁调用它,写重了会导致界面明显卡顿。
以缩放至全图按钮为例,代码如下:
using System; using System.Windows.Forms; using ESRI.ArcGIS.ArcMapUI; using ESRI.ArcGIS.Carto; using ESRI.ArcGIS.Desktop.AddIns; namespace MapQuickTools { public class ZoomToFullExtentButton : Button { public ZoomToFullExtentButton() { } protected override void OnClick() { IMxDocument mxDoc = ArcMap.Application.Document as IMxDocument; if (mxDoc == null) return; IActiveView activeView = mxDoc.ActiveView; if (activeView == null) return; activeView.Extent = activeView.FullExtent; activeView.Refresh(); } protected override void OnUpdate() { Enabled = (ArcMap.Application != null); } } }这段代码的骨干逻辑是:通过ArcMap.Application拿到当前正在编辑的文档对象(IMxDocument),再从文档里取出活动视图(IActiveView),把视图范围设置为全图范围并刷新。这就是ArcObjects开发最典型的“取文档 → 取视图 → 操作 → 刷新”四步套路。ArcMap.Application是一个静态入口,在Add-in环境下它指向当前宿主应用的Application对象,不需要手动传参,这也是Add-in比老式DLL方便的地方。
4.3 三个菜单项代码
统计矢量要素数量稍微进阶一点,会接触到图层遍历和ITable接口:
public class CountFeaturesButton : Button { protected override void OnClick() { IMxDocument mxDoc = ArcMap.Application.Document as IMxDocument; if (mxDoc == null) return; IMap map = mxDoc.FocusMap; int total = 0; int featureLayerCount = 0; for (int i = 0; i < map.LayerCount; i++) { ILayer layer = map.get_Layer(i); IFeatureLayer featureLayer = layer as IFeatureLayer; if (featureLayer != null && featureLayer.FeatureClass != null) { ITable table = featureLayer.FeatureClass as ITable; if (table != null) { total += table.RowCount(null); featureLayerCount++; } } } MessageBox.Show( string.Format("当前地图共 {0} 个图层,其中矢量要素图层 {1} 个,要素总数为 {2}", map.LayerCount, featureLayerCount, total), "统计结果"); } protected override void OnUpdate() { IMxDocument mxDoc = ArcMap.Application.Document as IMxDocument; Enabled = (mxDoc != null && mxDoc.FocusMap.LayerCount > 0); } }这里有个小的经验点:IFeatureClass本身继承自ITable,所以把要素类直接转成ITable调用RowCount,比自己写游标去数要素要快得多,尤其是面对几十万条记录的大图层时。RowCount(null)里的null表示不过滤条件,统计全表行数。
导出当前视图为图片会用到ArcObjects的输出模块:
public class ExportMapButton : Button { protected override void OnClick() { IMxDocument mxDoc = ArcMap.Application.Document as IMxDocument; if (mxDoc == null) return; SaveFileDialog dlg = new SaveFileDialog(); dlg.Filter = "JPEG图片|*.jpg"; dlg.FileName = "map_export.jpg"; if (dlg.ShowDialog() != DialogResult.OK) return; IActiveView activeView = mxDoc.ActiveView; IExport export = new ExportJPEGClass(); export.ExportFilePath = dlg.FileName; export.Resolution = 150; tagRECT rect = new tagRECT(); rect.left = 0; rect.top = 0; rect.right = (int)activeView.Extent.Width; rect.bottom = (int)activeView.Extent.Height; activeView.Output(export, export.Resolution, ref rect); export.Cleanup(); MessageBox.Show("图片已导出到:" + dlg.FileName); } protected override void OnUpdate() { Enabled = (ArcMap.Application != null); } }关于这段代码,要提醒两点:第一,ExportJPEGClass来自ESRI.ArcGIS.Output程序集,需要在项目引用里确认已经包含;第二,tagRECT是Windows的矩形结构体,在ESRI.ArcGIS.esriSystem命名空间下,引用别漏了。我这段代码在实际项目中运行稳定,不过它只是一个简化版,没有处理带透明度图层、地图整饰元素(指北针、比例尺)等更精细的需求,如果需要把这些也导进去,就要研究IExport接口的AdvancedOptions了。
4.4 编译、安装、挂载流程
三个按钮类写好、XML配置完毕之后,整个工程就可以编译了。生成后的输出目录里会有一个后缀为.esriAddIn的文件,这是Add-in的最终产物。双击它,系统会弹出ArcMap的加载项安装提示,确认后插件会自动安装到当前用户目录下。
安装完成并不代表它在界面上自动显示。打开ArcMap,点击菜单栏的“自定义 → 自定义模式”,在弹出的对话框切到“工具条”选项卡,找到我们刚才定义的工具条“地图快捷工具”,勾选它。关掉自定义对话框后,界面上会出现一个包含“缩放至全图”“统计矢量要素数量”“导出当前视图为图片”的下拉菜单“快捷工具”。
这一步是判断整个插件是否成功的最直观标志。如果工具条列表里看不到我们定义的工具条,优先检查插件是否被ArcMap正确加载:打开“自定义 → 加载项管理器”,确认插件出现在已加载列表中。如果没有,说明.esriAddIn文件安装没成功,或者XML配置有语法错误。
5. 调试、分发与常见故障
到这里,一个能跑通的下拉菜单插件已经完成了。但实际项目开发中,从“代码能编译”到“同事能顺畅使用”之间,还有一段路。这一节把调试和分发这两个环节单独拿出来讲,因为它们决定了插件能否从个人机器走向生产环境。
5.1 用VS断点调试ArcMap插件
Add-in调试方式和普通.NET桌面程序类似,区别在于启动的程序不是我们的DLL,而是ArcMap.exe。
在工程属性里找到“调试”选项卡,把启动外部程序设置成ArcMap.exe的安装路径(默认是C:\Program Files (x86)\ArcGIS\Desktop10.x\bin\ArcMap.exe)。设置之后按F5,Visual Studio会启动ArcMap并自动附加调试器,插件代码里的断点就能正常命中了。
实际调试中,比断点更常用的是在OnClick方法里先写日志或弹出MessageBox,确定按钮是否被执行、执行到了哪一步。因为ArcMap是一个很大的COM宿主程序,有些时候断点能命中,但调用的COM对象抛出异常时,错误信息在VS的“输出”窗口里只显示一行“External exception 0x...”之类,比较难定位。我的习惯是:先保证数据对象不为空,再往下调。也就是所谓的“防御式编程”在ArcObjects开发中极其重要,ArcMap对象模型里到处是可能为null的引用,尤其涉及文档、图层、游标时,不判空很容易在运行时突然崩溃。
5.2 给同事装插件:esriAddIn分发
双击.esriAddIn的安装方式,默认安装目录是当前用户的“文档\ArcGIS\AddIns\Desktop10.x”下,不需要管理员权限,也不需要重启电脑。给同事分发时,直接把.esriAddIn文件发过去,让对方双击即可。
这里有个容易踩的坑:如果同事机器上装的ArcMap版本和我们开发的版本不一致,例如我们用的是10.8,同事用的是10.2,插件很可能加载异常。Add-in虽然解决了注册表问题,但并没有解决程序集版本兼容问题。项目引用的ESRI.ArcGIS.Desktop.AddIns等程序集是有版本号的,生成的DLL会绑定版本。如果两边版本不一致,稳妥的做法是:用目标版本的SDK重新编译一份再分发,而不是寄希望于“通用”。
如果单位里有大量电脑需要部署,也可以提前规划Add-ins目录的默认行为:ArcMap启动时会扫描该目录下的所有.esriAddIn文件。只要把文件放进对应版本目录,插件也会被识别。这种方式适合管理工具批量推送。
5.3 常见故障“菜单不显示”排查顺序
插件装上之后菜单不显示,是最常见的售后问题。排查时我的顺序固定不变,按这个顺序来基本能定位九成问题。
先检查加载项管理器里有没有这个插件,没有说明文件没有被ArcMap识别,重新双击安装,或者确认当前登录用户和安装插件的用户是不是同一个;再检查工具条选项里有没有我们定义的“地图快捷工具”,如果没有,说明XML里的ArcMap节点或Toolbars区域配置有问题,回看Config.esriaddinx的层级结构;再检查自定义对话框里的“命令”选项卡,如果直接在命令列表里能搜到按钮对应的caption文字,但工具条里不显示菜单,说明菜单的继承或refID引用出了问题;最后考虑版本问题,换目标机器对应版本的SDK重新编译。
还有一个常见到容易被忽略的点:ArcMap启动时如果插件内部抛出了异常,ArcMap可能选择静默跳过加载,也不给用户明确的提示。因此,如果菜单突然消失了,先看一眼Windows事件查看器里ArcMap进程的“.NET Runtime”日志,通常异常堆栈就在里面。
5.4 版本嫁移的坑
最后说版本迁移。ArcGIS 10.x系列从10.1到10.8,Add-in的基本机制没变,但底层程序集有时会有小幅调整。当你把一个10.2时代的Add-in工程拿到VS2019 + ArcGIS 10.8环境里打开时,最常见的问题是项目里引用的ArcGIS程序集版本号是10.2,需要全部更新到10.8版本。
更新的方法有两个:手动在项目引用里删掉旧版本,重新添加新SDK的程序集;或者用SDK自带的“升级”工具。更省事的做法是直接在目标环境新建工程,把旧代码文件复制过来,重新添加引用。我经验里后者往往更快,因为可以顺带清理掉老环境遗留的无效引用。
6. 踩坑记录:Add-in开发里最容易被忽略的细节
这一节不按步骤走,专门聊我实际开发过程中积累的几条心得。这些东西在官方文档和教程里很少被强调,但往往决定你开发的插件到底是一天就崩的玩具,还是能稳定跑几个月的内部工具。
6.1 OnUpdate里别做重活
前面提过OnUpdate会被ArcMap在空闲时频繁调用,这里再展开一点。有段时间我写的工具条加载了某个自定义图层集合,每次OnUpdate里都去遍历图层并做空间查询,结果ArcMap整个界面变得非常卡,点击工具条按钮要等好几秒才响应。后来把重逻辑从OnUpdate里挪到OnClick里——也就是按钮点击时才去执行,OnUpdate里只做简单的是否可用的判断,卡顿立刻消失了。
这条经验可以扩展成一句话:OnUpdate里只做“这个小按钮现在能不能点的判断”,不要做任何涉及地理计算、数据库访问、文件IO的操作。如果需要定期刷新菜单状态,也建议用System.Timers.Timer来控制刷新频率,而不是依赖ArcMap的OnUpdate机制。
6.2 COM对象释放与内存
ArcObjects是建立在COM基础上的对象模型。虽然有.NET运行时帮我们管理一部分生命周期,但ArcMap这种大型COM宿主里,GC(垃圾回收)并不能及时释放所有底层COM对象。长时间运行一个反复调用图层遍历、游标、几何操作的插件,内存占用会持续上涨,最终导致ArcMap越用越慢。
解决思路不需要做得很彻底,但有三个习惯建议养成:第一,用完的非托管COM对象,尤其是ITable、IFeatureCursor、IGeometry这类重对象,显式调用System.Runtime.InteropServices.Marshal.ReleaseComObject;第二,使用FeatrueCursor时,务必用try-catch-finally,在finally里把它释放掉;第三,不要静态地持有IMxDocument、IApplication这类的全局引用,ArcMap文档切换、关闭时,这类引用会导致“无法释放当前文档”的诡异问题。
6.3 图片路径与DPI
按钮图标这一项看起来不起眼,翻车概率却不低。我见过一个同事的插件加载后,按钮图标全部变成红色小叉,查了一圈发现是工程里Images目录的文件已经被他误删,但XML里的image路径还在,ArcMap加载时找不到文件就只能显示异常状态。
图标尺寸也有讲究:ArcMap的工具条按钮标准尺寸是16x16像素(小图标)和24x24像素(大图标),如果素材本身是超大尺寸的截图,ArcMap会强行压缩,结果就是图标糊成一团。建议准备16x16和24x24两套PNG素材,命名清晰。另外,某些Windows系统在高DPI缩放下,ArcMap会出现图标被拉伸的情况,这属于软件自身的渲染问题,插件层面很难完全规避,只能说是兼容性瑕疵。
6.4 更进一步:动态菜单类的思路
如果你做一个下拉菜单只是为了放固定几个按钮,前面介绍的方式已经完全够用。但有些业务场景需要菜单内容随当前文档状态动态变化,比如“根据当前选中的图层类型,动态显示不同的处理命令”,那就需要用动态菜单(Dynamic Menu)的思路。
Add-in框架支持以一个类来代替XML里静态的Items列表。这个类继承ESRI.ArcGIS.Desktop.AddIns.Menu,在运行时重写它的内部逻辑来动态生成菜单项。实现起来比静态菜单复杂不少,需要对ArcObjects的ICommandItem、ICommandBars等接口有一定掌握。我的建议是:先把静态菜单跑通,等真的遇到动态需求时,再去翻ArcObjects SDK自带的示例代码,不要一开始就用动态方案把复杂度拉满。
做ArcMap插件做到后面,你会发现真正难的不是某一个按钮的功能怎么写,而是对整个ArcMap命令系统的理解:按钮、菜单、工具条之间如何组装,命令如何在界面中被找到和触发,插件如何跟随宿主程序的启动、关闭完成自己的生命周期。把前面这些流程走一遍,你对ArcToolbox、右键菜单里那些命令的实现原理,也会忽然有一种“原来是这么回事”的通透感。
最后分享一个我自己的习惯:每次写完一个菜单,我会建一个测试用的临时mxd文档,里面放上不同类型的图层——点、线、面、栅格、注记——然后在OnUpdate里确保菜单在每种图层状态下的启用、禁用表现都符合预期。反复点几十遍,看着导出的图片一张张生成,确认没有异常才敢把.esriAddIn分发给同事。这类插件应用场景通常都是高频替代人工的操作,越稳定,才越能真正解放生产力。