简介:这是一份面向GIS开发学习者的ArcGIS+C#二次开发实例资源包,围绕ArcEngine与ArcGIS Server环境下的地图编辑、空间分析、网络分析三大模块展开。压缩包共282个文件、约911KB,其中99个cs源码文件配合sln/csproj工程结构,构成了可编译的完整开发骨架;37组resources/resx资源、71张bmp按钮图标及光标、ico、exe等文件,覆盖了界面显示与运行所需的辅助资源,便于读者直接加载项目查看效果并二次修改。资源已有431人学习下载。包内示例涉及撤销、保存数据、删除要素、增加注记、视图缩放与平移等地图编辑工具,同时包含数据管理、空间分析与网络分析的关键实现,能够让开发者直观看到如何在C#中调用ArcGIS接口完成几何对象操作、图层维护与服务发布。对希望入门ArcEngine桌面端开发或尝试ArcGIS Server Web GIS的IT从业者来说,这是一份体量紧凑、结构清晰且极具参考价值的实战案例。
1. 拿到这个 rar 之前,先想清楚 ArcGIS + C# 到底在解决什么问题
做 GIS 桌面端开发的人,手里经常会攒着这样一个 rar:标题写着“ArcGIS+C#二次开发”,压缩包里是地图编辑、空间分析、网络分析三块功能。它解决的不是“怎么画一张图”,而是“怎么让业务人员在一个 Windows 程序里完成矢量数据的编辑、查询、缓冲区分析和路径规划”。这套东西在测绘、规划、管线、国土类项目里需求量很大,企业要的往往不是一个 ArcMap 的替代品,而是一个能把 GIS 能力嵌进自己业务系统、又不用用户去学 ArcGIS 操作的桌面工具。适合刚接手 GIS 开发任务、手头有 ArcGIS Engine 许可、想快速搭出可用原型的开发者。一个反直觉的事实是:真正难住大家的不是空间分析和网络分析的算法,而是 AO 接口的许可初始化、COM 生命周期和图层数据源那几步,这三步不踏实,后面写得再顺也是白搭。
2. 工程搭建与许可初始化:为什么第一步就决定后面能不能跑起来
2.1 环境选型:ArcGIS Engine 10.x + VS 的搭配逻辑
做 ArcGIS + C# 二次开发,首先要明确走的是 ArcGIS Engine 这条路,而不是 ArcGIS Pro 的 Add-in。Pro 的 Add-in 是 .NET 6 之上的新体系,用的 API 和地图模型都和 Engine 不同;而这个 rar 标题里写的 C# 二次开发,基本锁定在 ArcGIS Engine 10.x 的 AO(ArcObjects)体系。常见做法是用 ArcGIS Engine 10.2 或 10.8 开发包,配上 Visual Studio 2015/2017/2019 和 .NET Framework 4.x 做 WinForms 或 WPF 程序。开发时引用 ESRI.ArcGIS 系列程序集,部署时目标机器装 ArcGIS Engine Runtime 即可。
选择 ArcGIS Engine 而不是直接让用户装 ArcMap,原因是可控的:Engine 只提供运行库和组件,不提供桌面界面,你的程序就是唯一入口,不会再弹出一堆 ArcMap 的菜单和工具。代价是开发量上来了,尤其是图层符号化、比例尺、坐标系统这些交互,AO 接口的粒度比 Pro 的 Add-in 细很多,每条都要自己写回调。这套选型在企业里还有一个现实原因:很多项目甲方手里已经有 10.x 的许可,版本迁移成本高,新活接在老技术上反而是最稳的。
2.2 许可初始化的顺序:代码第一行就该 Bind
ArcGIS Engine 的程序有一个铁律:第一个 AO 对象创建之前,必须先绑定运行时版本和产品码。很多新手把new AOInitializeClass()写在项目入口第一行,然后就开始 new 各种控件,一运行就报 “Class not registered” 或 “Failed to create object”。这不是控件没装好,而是 Bind 的顺序不对。ArcGIS Engine 10.x 里,初始化代码标准写法是:
using ESRI.ArcGIS; using ESRI.ArcGIS.esriSystem; // 绑定运行时,必须放在 new 任何 AO 对象之前 RuntimeManager.Bind(ProductCode.Engine); // 初始化许可 IAoInitialize aoInit = new AoInitializeClass(); esriLicenseProductCode product = esriLicenseProductCode.esriLicenseProductCodeEngine; // 先检查许可是否可用,再初始化 if (aoInit.IsProductLicensed(product) == esriLicenseStatus.esriLicenseStatusAvailable) { aoInit.Initialize(product); } else { throw new Exception("Engine 许可不可用,请检查许可服务"); }这段代码的逻辑是:RuntimeManager.Bind负责把托管代码绑定到本机安装的 ArcGIS Engine Runtime 上,它不认识ProductCode.Engine以外的产品,如果你 bind 成了ProductCode.Desktop而机器上只装了 Engine Runtime,后面照样报错。IsProductLicensed是许可校验,返回值是esriLicenseStatus枚举,只有Available才能继续初始化。Initialize之后才允许创建 MapControl、FeatureClass 这类重量级对象。整个 App 生命周期里,这段初始化执行一次就够了,不需要每个窗体都重复调。
这里有个容易翻车的点:很多人用 ArcGIS Engine 开发包写代码,但部署的时候机器上只装了 Desktop 的许可服务,没有单独给 Engine Runtime 配许可,程序启动时IsProductLicensed就会返回NotLicensed。这个错误在开发机上出现频率极高,因为开发机往往既装了 Desktop 又装了 Engine,默认可用;换到干净的部署机上马上就露馅。所以这个检查分支一定不能省略,要写一个明确的异常提示,方便现场排查。
2.3 程序集引用与 x86 平台:一个决定生死的编译开关
ArcGIS Engine 10.x 的 ESRI.ArcGIS 系列程序集是 32 位 COM 组件包,在 Visual Studio 里新建工程后,默认的AnyCPU目标平台会让 64 位模式下加载失败,报错信息要么是 “BadImageFormatException”,要么是控件无法拖入设计器。常见的做法是把项目的目标平台强制改成 x86:项目属性 → 生成 → 目标平台 → x86。这个设置对 WinForms 和 WPF 都适用,别指望用 AnyCPU 碰运气,碰不过去的。
引用哪些程序集也有讲究。一个做地图编辑 + 空间分析 + 网络分析的工程,最少要引这几个:
| 命名空间 | 程序集 | 用途 |
|---|---|---|
| ESRI.ArcGIS.Controls | ESRI.ArcGIS.Controls.dll | MapControl、ToolbarControl、TOCControl |
| ESRI.ArcGIS.Carto | ESRI.ArcGIS.Carto.dll | Map、Layer、FeatureLayer 等地图对象 |
| ESRI.ArcGIS.Geodatabase | ESRI.ArcGIS.Geodatabase.dll | 访问要素类、工作空间、编辑会话 |
| ESRI.ArcGIS.Geometry | ESRI.ArcGIS.Geometry.dll | 点线面几何、空间关系计算 |
| ESRI.ArcGIS.Display | ESRI.ArcGIS.Display.dll | 符号、颜色、渲染 |
| ESRI.ArcGIS.SpatialAnalyst | ESRI.ArcGIS.SpatialAnalyst.dll | 空间分析工具 |
| ESRI.ArcGIS.NetworkAnalyst | ESRI.ArcGIS.NetworkAnalyst.dll | 网络分析工具 |
右键引用 → 添加引用 → 浏览到 Engine 安装目录下的com文件夹(比如C:\Program Files (x86)\ArcGIS\Engine\com),就能看到这些托管程序集。注意不要从 GAC 里乱引,版本号要跟开发包一致。引用完之后,还有个隐藏步骤:把每个引用的“复制本地”设为 true,发布的时候这些 dll 会一起考到输出目录,部署机就不用额外装开发包了。
3. 地图编辑功能实现:从编辑会话到增删改与撤销
3.1 编辑会话的开启:为什么不能直接改 Feature
ArcGIS 的矢量数据编辑跟普通数据库 UPDATE 完全不是一个套路。你在同一个工作空间里开多个编辑会话、或者在没开会话的情况下直接对要素类做增删改,轻则崩溃重则数据锁死。AO 体系下,编辑操作必须包在一个编辑会话里,IEngineEditor接口是核心,它管理当前编辑的工作空间、图层和操作栈。开启会话的代码:
using ESRI.ArcGIS.Carto; using ESRI.ArcGIS.Geodatabase; using ESRI.ArcGIS.Controls; // 地图里取第一层作为编辑图层 IFeatureLayer featureLayer = mapControl.Map.get_Layer(0) as IFeatureLayer; if (featureLayer == null) return; IEngineEditor engineEditor = new EngineEditorClass(); // 第二步:开始编辑会话,传入地图和可编辑图层 if (!engineEditor.EditSession) { engineEditor.StartEditing(mapControl.Map, featureLayer); // 开启一个操作步骤,后续的改动都在这一步里,可整体撤销 engineEditor.StartOperation(); }这里的逻辑有三层。第一,StartEditing的第一个参数是IMap,第二个参数可以是ILayer也可以是IWorkspace,传图层时编辑器只锁定该图层所在数据源的范围。第二,编辑器对象全局只需要一个实例,整个窗体生命周期复用,不要每编辑一次就 new 一次,否则之前的撤销栈就丢了。第三,StartOperation声明一个操作的开始,操作是编辑栈的基本单位,所有在这个操作里产生的数据改动,会被合并成一个待撤销项。
3.2 增删改要素:三种写法背后的适用场景
要素新增的核心代码是CreateFeature+ 赋值 +Store,但不开编辑会话直接写也能成功,区别在于没有撤销栈和冲突检测。在实际项目里,散点新增、手绘多边形、批量导入这三种场景要用三种写法:散点手绘用编辑会话里的IFeature.Store();批量导入数据用IFeatureCursor逐条Insert;图层叠加到目标图层时用IFeatureEdit做合并。散点新增的完整代码:
// 获取要素类,创建要素 IFeatureClass featureClass = featureLayer.FeatureClass; IFeature feature = featureClass.CreateFeature(); // 构造一个点几何,坐标来自鼠标点击或表格 IPoint point = new PointClass(); point.X = 116.3; point.Y = 39.9; point.SpatialReference = featureClass.SpatialReference; // 给字段赋值 feature.Shape = point; feature.set_Value(featureClass.FindField("NAME"), "测试点"); feature.Store();CreateFeature只是在内存里创建一个要素实例,Store才会真正写入数据源。赋值字段时不要用set_Value(0, ...)这种硬编码索引,数据表结构一变动索引就失效,先用FindField拿字段索引。SpatialReference的设置容易忽略,点没有坐标系就加入图层,ArcMap 可能会动态投影,显示位置偏差,尤其在多图层不同坐标系时非常明显。
要素删除的代码比较简单但有个细节:删除前要先Select拿要素 OID,不要用遍历集合边删边取的方式,因为 AO 的要素集合在删除过程中会锁定游标,正确的做法是先收集要删的要素 OID 列表,再统一删除。要素修改则要记住feature.Store()之后调用编辑器的StopOperation结束当前操作,否则撤销列表里永远只有一笔。
3.3 撤销与保存的边界:StopEditing(true) 不是后悔药
ArcGIS Engine 的撤销栈只存在于编辑会话中。StartOperation开始的操作在StopOperation后进入撤销栈,engineEditor.Undo()可以往前回退;回退完想要再前进用Redo()。但这里有一个常见的认知偏差:撤销栈的内存存储只在你调用StopEditing之前有效。如果你直接调用StopEditing(true)保存并结束会话,编辑操作已经落盘,栈被清空,没有任何后悔药可吃。遇到用户误删了一大片数据,你只能从备份或版本管理里捞。
所以一个稳妥的做法是在StopEditing前弹一个确认框,明确告诉用户“编辑内容将写回数据源,且撤销记录会清空”。同时,在会话外也可以手动备份一份数据,常见方案是IFeatureClassCopy复制原始要素类到内存工作空间,作为会话级后悔药。这个备份只在当前会话存活,会话结束释放,不会污染磁盘。
还有一个细节是StartEditing之后,图层如果被Remove或地图刷新(MapControl.Refresh()),编辑器持有的图层引用可能失效,再次编辑时报“未指定图层”。遇到这种情况检查engineEditor.EditState == esriEditState.esriEditStateNotEditing,如果不是就先把编辑会话关掉再刷新地图。
4. 空间分析与网络分析:两个功能一条路
4.1 空间分析:缓冲区和叠加分析的参数怎么设
标题里的空间分析,最常落地的功能是缓冲区分析、裁剪和叠加分析。AO 里空间分析有两个入口:一个是IBuffer接口的几何级操作,一个是Geoprocessor跑 GP 工具。几何级操作处理的是内存里的IGeometry,不依赖数据源,适合小数据量实时计算;GP 工具会调起后台进程处理要素类,适合大批量数据。写一个小规模缓冲区分析,用几何级操作就够了:
using ESRI.ArcGIS.Geometry; using ESRI.ArcGIS.Carto; IFeatureLayer sourceLayer = mapControl.Map.get_Layer(1) as IFeatureLayer; IFeatureCursor cursor = sourceLayer.FeatureClass.Search(null, false); IFeature feature = null; // 遍历要素,逐个生成缓冲区并合并 IGeometry unionGeometry = null; while ((feature = cursor.NextFeature()) != null) { ITopologicalOperator topoOperator = feature.Shape as ITopologicalOperator; // 缓冲半径 100 米,负号表示地理缓冲(相对坐标系),正数表示线性缓冲 IGeometry bufferGeometry = topoOperator.Buffer(100.0); if (unionGeometry == null) { unionGeometry = bufferGeometry; } else { ITopologicalOperator unionOp = unionGeometry as ITopologicalOperator; unionGeometry = unionOp.Union(bufferGeometry); } }这里最有迷惑性的参数是Buffer的输入值是 double 类型,单位取决于图层坐标系。如果图层是地理坐标系(经纬度),Buffer(100)意味着 100 度,结果会把整个地球圈进去;如果是投影坐标系,100 就是 100 米。这在真实项目里是常见翻车点:用 WGS84 的经纬度数据直接缓冲,出来的图形大得离谱。还有一个相关坑是代码里写的是“100米”,但数据源的坐标系是度,结果就是错的。稳妥做法是先检查layer.FeatureClass.SpatialReference是投影还是地理坐标系,再决定传入的数值要不要换算。
叠加分析(如裁剪)在 AO 里用ITopologicalOperator.Intersect实现,两个几何对象求交,用法和Buffer类似。大数据量的裁剪我一般直接用Geoprocessor跑Clip工具,因为几何级操作在百万级要素下内存开销相当大,容易卡死界面。做空间分析时还有一个必调参数是空间参考匹配:参与运算的两个几何必须处于相同的投影坐标系,否则 AO 没有自动重投影,结果为空或形状错乱。
4.2 网络分析:最短路径与网络数据集的调用链
网络分析和空间分析在 ArcGIS 里是两个体系。空间分析是几何图形的集合运算,网络分析是基于网络数据集的图论求解。要做最短路径,数据准备就必须先有一个网络数据集或几何网络,通常由 Shapefile 的线图层构建,在 ArcMap/Catalog 里右键建好Network Dataset之后,代码才能用。常见的IRouteSolver调用链:
using ESRI.ArcGIS.NetworkAnalyst; using ESRI.ArcGIS.Geometry; using ESRI.ArcGIS.Carto; // 前提:当前地图里有一个网络数据集图层 INetworkLayer networkLayer = mapControl.Map.get_Layer(2) as INetworkLayer; if (networkLayer == null) return; INetworkDataset networkDataset = networkLayer.NetworkDataset; // 创建路径求解器并绑定网络分析环境 IRouteSolver3 routeSolver = new RouteSolverClass(); INetworkAnalysisExt3 analysisExt = new NetworkAnalysisExtClass(); analysisExt.NetworkDataset = networkDataset as INetworkDataset; routeSolver.NetworkAnalysis = analysisExt as INetworkAnalysisExt; // 添加两个停靠点(起点和终点) INetworkAnalysisExt3 ext = routeSolver.NetworkAnalysis as INetworkAnalysisExt3; IStop stop1 = ext.Stops.CreateStop(); stop1.Point = startPoint; IStop stop2 = ext.Stops.CreateStop(); stop2.Point = endPoint; ext.Stops.AddItem(stop1, 0); ext.Stops.AddItem(stop2, 1); // 求解路径 ISolveContext solveContext = routeSolver.Solve(null);这段代码里参数的重点有四个。一是NetworkDataset不能为 null,如果图层是普通线图层而不是网络数据集图层,这里就直接空引用,所以要用上面的判空。二是两个停靠点的 Z 值和 M 值必须为 0,网络分析是平面拓扑求解,带上高程值会导致求解器认为两点不可达。三是Solve的返回值是ISolveContext,里面有求解状态和路径几何,不要直接跳过它去取routeSolver.Results,因为结果可能为空但Solve没有抛异常。四是网络分析需要一个单独的扩展许可esriLicenseExtensionCode.esriLicenseExtensionCodeNetwork,在初始化阶段就要InitializeExtension,忘了这步,运行时倒还不会报错,但Solve会静默返回失败,状态码写的是“未授权扩展”。
实际项目中网络分析还有两个很实用的分支:一是服务区分析,把RouteSolver换成ServiceAreaSolver,参数一般调“默认断点”为阻抗值(比如 10 分钟),输出是一个面状几何,表示从起点出发在阻抗范围内能覆盖的区域;二是最近设施分析,把 Stops 换成 Facilities 和 Incidents,可以求解“离我最近的 3 个加油站”。这些参数在 AO 里都能映射到ClosestFacilitySolver的DefaultCutoff上。
5. 常见问题与避坑:这 4 个坑我几乎每个项目都踩过
5.1 点启动没反应:许可服务静默失败的典型
现象:程序双击启动,进程存在但没有窗口,或者弹一个“产品未授权”的提示框后退出。
原因:绝大多数情况是目标机器的 ArcGIS License Manager 服务没启动,或者开发机装了 Desktop 但部署机只装 Engine Runtime,License 文件并没有正确绑定。另一个隐蔽原因是机器没装 .NET Framework 3.5,AO 10.x 的部分本地组件依赖 .NET 3.5 的 CLR,Win10/11 默认不装。
解决:部署机按顺序重装三件套——.NET Framework 3.5(控制面板 → 启用或关闭 Windows 功能 → 勾选 .NET Framework 3.5),再装 ArcGIS Engine Runtime,最后启动 License Manager(ArcGIS 安装目录下的LicenseManager.exe)。装完后不要急着跑程序,先检查服务状态里有没有ArcGIS License Manager这个服务,没有就手动启动。这一步 90% 的启动失败都能消掉。
5.2 初始化 AO 报 “Failed to create object”:x86 和 Bind 顺序的锅
现象:new AoInitializeClass()或new MapControlClass()抛COMException,说创建对象失败。
原因:两个层面。一是项目编译目标是 AnyCPU,程序跑在 64 位模式,而 AO 组件是 32 位 COM,加载不上;二是RuntimeManager.Bind没有在第一次 new AO 对象之前执行,导致 AO 不知道你要用哪个产品码。
解决:项目属性 → 生成 → 目标平台改成 x86,同时把RuntimeManager.Bind(ProductCode.Engine)提到Program.Main方法的第一行,注意是任何newAO 对象之前。如果已经写了 Bind 还在报错,检查 Bind 的参数是Engine还是EngineGeoDB,装的是 Engine 基础版就 bind Engine,装了 GeoDatabase 扩展就 bind EngineGeoDB,反了也会失败。把这两步做完,问题基本绝迹。
5.3 地图编辑无法保存:要素类没有版本化或锁冲突
现象:engineEditor.StopEditing(true)抛异常,提示“无法在编辑会话期间提交”;或编辑过程中界面卡死,再次操作时弹“要素类被锁定”。
原因:目标数据是 SDE 地理数据库里的非版本化数据,或者有其他程序(ArcMap、另一个窗体)已经打开了编辑会话。AO 的编辑会话是单例的,不允许多个IEngineEditor同时启动。非版本化数据的编辑其实也能做,但要求工作空间是独占锁,一旦有别的连接在访问,就会抛锁冲突。
解决:操作前先engineEditor.StopEditing(false)清理残留会话,再重新StartEditing。如果反复被锁,打开 ArcMap 里的图层属性 → 源,确认数据源是文件型 GDB 还是 SDE;文件 GDB 一般不存在锁冲突问题,SDE 就要检查当前有没有其他用户在连接。遇到“无法在编审期间提交”这个具体报错,多半是你对版本化图层做了非版本化的编辑操作,解决问题的路径是检查图层是否RegisteredAsVersioned,不是版本化图层就用IWorkspaceEdit手动提交。
5.4 网络分析求解结果为空:权值字段和连通性设置导致
现象:Solve执行成功,返回的路径几何为 null,或者路径只连接了两个点的直线,完全不顺着路网走。
原因:网络数据集没有设置阻抗属性(如长度、时间),或者网络数据的连通性策略设置成“端点连接”而路网数据在现实中有立交桥、跨线桥,物理上点不重合,导致网络不连通。求解器在没有阻抗的情况下会默认用一个恒定权值,结果就变成了两点直线。
解决:回到 ArcMap/Catalog 里打开网络数据集属性,检查“连通性”是不是“端点连接”,如果是,把道路在交叉口断开的逻辑处理掉,或者改成“端点连接并允许折点连接”。同时给网络数据集添加一个“长度(Length)”属性作为默认阻抗,成本字段选 Meters。改完重建网络数据集,重新加载到地图,再跑Solve就正常了。
5.5 部署版报 “Class not registered”:开发包与 Runtime 版本不一致
现象:开发机上跑程序一切正常,拷到部署机装完 Runtime 后,启动即报“未注册类”,定位到某一行new对象的代码。
原因:开发时引用的 ESRI.ArcGIS 程序集版本是 10.8,部署机装的 Runtime 是 10.2,COM 组件的 CLSID 不匹配。AO 的程序集强命名版本和 Runtime 版本强绑定,不能混用。另一个原因是“复制本地”没有把引用的 100 多个 ESRI dll 全部带过去,少了一些运行时才加载的组件。
解决:部署机的 Runtime 版本必须和开发包的大版本一致,10.8 开发包就装 10.8 Runtime,补丁级别最好也对上。发布时把“复制本地”全部设为 true,再检查一下发布目录里有没有ESRI.ArcGIS.System.dll和ESRI.ArcGIS.Version.dll这两个依赖最重的文件,没有就从开发机的com文件夹拷一份。最后在部署机上跑一次ESRI 许可管理员,确认 Engine 许可状态为“可用”,而不是“未检测到许可”。
6. 进阶技巧:用 Geoprocessor 交叉验证结果并记录编辑日志
功能都调通了之后,最容易被忽略的是验证和审计。我自己的习惯是:每个空间分析和网络分析的结果,都用 Geoprocessor 重跑一次同参数的 GP 工具做对拍。比如自己用ITopologicalOperator.Buffer算出来的缓冲区面,再调Geoprocessor.Execute("Buffer")跑一次,比对两个结果的面积和要素数,偏差在 1% 以内才算通过。这个验证步骤能发现坐标系单位选错、拓扑点序错误这些肉眼看不出来的细节。网络分析同理,用 ArcMap 的 Network Analyst 窗口手动求解一次,和程序的结果对比路径长度,能筛掉路网方向字段配错导致的绕路问题。
编辑日志的建议也值得做:在StartOperation和StopOperation之间,用ITransactionLogger或最简单的文件日志记录每一次操作的要素类型和 OID。做应急恢复时,这个日志比任何备份都好用。最后提醒一个老生常谈的细节:AO 的 COM 对象是托管包装的非托管资源,用完要释放,特别是IFeatureCursor,不释放会让数据源一直处于占用状态,后续编辑锁冲突十有八九是它引起的。写这么多年 ArcGIS Engine,我在这上面翻车最多次,希望帮到你。
本文还有配套的精品资源,点击获取