
简介面向地理信息与三维可视化开发者的 TerraExplorer C# 二次开发示例资源基于 Skyline 平台旨在帮助需要快速掌握 TerraExplorer SDK 集成、地图控件操作、数据接入与三维场景构建的入门及进阶用户。包内共 21 个文件压缩后约 42KB涵盖 6 个 C# 源码文件、解决方案与工程文件、界面资源文件以及用于测试的 Shapefile 空间数据和 TerraExplorer 场景文件fly、shv结构紧凑便于对照学习。已有 483 人学习下载。借助这套示例代码可以梳理 C# 中 TerraExplorer 的窗体交互、图层加载、3D 模型放置与空间查询等实现思路也能直接复用其中地图对象与事件处理的写法减少从零查阅文档的弯路同时示例中涉及的 SDK 调用方式、事件驱动逻辑和多种数据格式加载方法对理解 GIS 二次开发的项目组织与调试流程同样有参考价值。1. TerraExplorer二次开发C#示例代码先搞清楚它能帮你干哪些活手里拿一个三维场景需求比如数字孪生园区、管网巡检、地质灾害可视化许多 C# 工程师的第一反应是自研渲染或接 Unity/WebGL。但当你需要在三天内把真实地形、影像叠加和 3DML 模型摆进一个 Windows 客户端时TerraExplorer二次开发C#示例代码是一条省很多事的路。TerraExplorer 是 Skyline 的三维 GIS 底座提供 COM 层 APIC# 通过互操作就能驱动它打开 fly 工程、加图层、放模型、绑鼠标事件。这篇文章写给两类人一类是从没碰过 C# COM 互操作的新手照着环境搭建和第一段示例就能跑起来另一类是已经在项目里被 API 版本、坐标、许可折磨过的熟手可以直接跳到避坑章节对号入座。2. 二次开发的底层逻辑COM互操作与TerraExplorerX程序集2.1 为什么C#二次开发绕不开COMTerraExplorer的API血缘TerraExplorer 的 SDK 从骨子里是 C/COM 写的后来才为脚本化二次开发提供了顶层入口。C# 调用 COM 组件这件事准确叫法是“互操作”InteropVisual Studio 添加 COM 引用时会自动生成 Interop.TerraExplorerX.dll把 COM 接口翻译成托管接口。理解这一点很重要——你不是在写一个插件塞进 TerraExplorer 里做扩展而是在写一个“遥控器”去操控已经能跑起来的三维 GIS 进程。这和 Creo二次开发、NX二次开发、CATIA二次开发那类 CAD 插件的路径完全不同。CAD 类的常见模式是编译出 DLL 挂进主进程主进程加载你的代码API 是进程内对象模型TerraExplorer 更像一个独立程序加 COM 自动化接口你的 C# 程序通过 ProgID 创建它的对象再发号施令。很多做过 C# 上位机的同行第一次拿这个 SDK 会习惯性地找托管 DLL 直接 new结果发现程序集里全是接口定义没有实现类就是这个心智模型没转过来。C# 能拿到的入口主要有两个一个是 ActiveX 控件 TE3DWindow可以直接拖进 WinForms界面交互都在里面另一个是 SGWorld 对象这是 TerraExplorer Pro 的脚本 API 演化来的JavaScript、Python、C# 都能用。实际项目里后台批量生成工程、数据预处理、自动化测试我基本都用 SGWorld 进程外模式因为不需要总挂一个可见窗口交互展示场景才用控件模式。2.2 进程内与进程外两种模式选错后面全白干模式常见入口UI 能力稳定性典型场景进程内TE3DWindow ActiveX 控件嵌入 WinForms鼠标交互、量测、选点受主进程线程模型影响UI 线程不能长时间阻塞数字看板、指挥大屏、窗口嵌入进程外SGWorld / COM 自动化无直接可见 UI可用主程序窗口辅助独立进程异常不拖垮你的主程序服务端生成 fly、批量建模、自动化测试进程内模式最舒服的地方是能够做完整的三维交互点击选对象、拖拽漫游、框选查询这些在指挥大屏和园区管理场景里几乎都是刚需。代价是它要求你的 WinForms 跑在 STA 线程控件创建和释放都有讲究主线程一旦被狠操作卡住三维窗口就掉帧甚至白屏。进程外模式则干净得多C# 程序通过 COM 启动 TerraExplorer 主程序批量打开工程、创建模型、保存退出全程不需要人盯着。画面验证可以靠主程序自带的渲染窗口也可以截屏。选型没有对错只有合不合适。如果目标是做看板类产品TE3DWindow 嵌入更讨喜如果目标是把客户给的二维管网表变成三维场景数据SGWorld 批处理模式能让你的交付路径短三分之一。我的习惯是先用进程外把数据流程跑通再决定要不要把这套逻辑接到 UI 控件里避免一开始就陷入控件生命周期和线程问题。3. 环境搭建与引用从Visual Studio到COM组件3.1 开发机准备TerraExplorer Pro和SDK缺一不可先说结论开发机上最好装完整版 TerraExplorer Pro不要只装运行库。原因很简单SGWorld 和 TE3DWindow 的 COM 注册信息由完整安装写入缺了主程序你的 Visual Studio 根本找不到类型库后面所有代码都无从谈起。版本和位数是第一个隐性门槛。TerraExplorer 的 COM 组件有 32 位和 64 位两种形态安装时选哪个你的 C# 工程就必须匹配哪个。最稳妥的做法是装 64 位 SDK项目平台目标锁 x64同时关闭“首选 32 位”。Visual Studio 默认启用了“首选 32 位”哪怕你显式写了 x64这个开关也会把程序跑成 32 位进程COM 调用直接报类未注册。先跑一句代码确认进程位数Console.WriteLine(当前进程位数: Environment.Is64BitProcess); Console.WriteLine(操作系统位数: Environment.Is64BitOperatingSystem);如果第一行输出 False说明项目配置有问题先修配置再往下写。框架方面老老实实用 .NET Framework 4.7.2 或更高版本。.NET Core/5 也能做 COM 互操作但要自己处理 ComWrappers还要为生命周期管理额外写代码对 TerraExplorer 这种老牌 COM 组件不划算。我做这类项目默认开一个 .NET Framework 的 WinForms 或控制台工程省事得多。注意TerraExplorer 的版本差异比想象中大。某些版本里 SGWorld 的 ProgID 带版本号某些不带。以安装后注册表HKEY_CLASSES_ROOT里实际存在的 ProgID 为准不要背死一个字符串。3.2 在Visual Studio里添加COM引用生成Interop.TerraExplorerX.dll项目右键添加引用COM 选项卡里找到 TerraExplorerX 类型库名称通常类似 TerraExplorerX 1.0 Type Library确定后 Visual Studio 会自动生成互操作程序集。这一步会在你的 bin 目录里出现 Interop.TerraExplorerX.dll里面就是接口定义。选中这个引用在属性面板里把“嵌入互操作类型”设为 False不然你可能会因为程序集版本错位遇到稀奇古怪的加载错误。代码里直接using TerraExplorerX就可以访问接口了。但有一个习惯我强烈建议保留实例化对象时别用new TerraExplorerX.SGWorld()改用 ProgID 动态创建。理由很简单TerraExplorer 的主版本升级后命名空间和 coclass 名称可能会变编译期写死会让你升级 SDK 时改一地代码。用 ProgID 加一层反射至少能让你把“版本问题”控制在配置层using System; using System.Runtime.InteropServices; class TeEntry { public static object CreateSgWorld() { // 老版本用 Skyline.SGWorld新版本可能用 TerraExplorerX.SGWorld Type t Type.GetTypeFromProgID(Skyline.SGWorld) ?? Type.GetTypeFromProgID(TerraExplorerX.SGWorld); if (t null) { throw new COMException(TerraExplorer SDK 未注册请检查安装); } return Activator.CreateInstance(t); } }这段代码里我用了两个候选 ProgID先后尝试。实际部署时可以在配置文件里写死你机器上验证通过的那个正式项目里我更倾向把它做成配置项而不是在代码里俩都试。Activator.CreateInstance 会触发 COM 对象的类工厂调用失败时抛 COMException后续的 HResult 能帮你定位问题。3.3 三步验证许可证别等运行时才报License not found许可证问题是我见过翻车率最高的启动阶段问题。TerraExplorer 的授权常见形态是加密锁和软授权两者都会在 COM 对象初次初始化时校验。不少人写完整套代码一按 F5 才发现初始化失败排查半天找不到原因。我一般先把许可证验证做成一个独立的启动自检三步走第一步手动启动一次安装好的 TerraExplorer Pro确认界面能正常打开这一步能排除加密锁驱动、服务没启动等环境问题。第二步用你的 C# 工程里那段动态创建代码实例化 SGWorld并触发一个最简单的属性读取让 COM 底层完成完整初始化。第三步捕获 COMException 并打印 HResult 和 Message留存日志。using System; using System.Runtime.InteropServices; class Program { static void Main() { try { object obj TeEntry.CreateSgWorld(); if (obj null) { Console.WriteLine(SDK 未注册请先安装 TerraExplorer Pro/SDK); return; } // 触发一次底层初始化让许可错误尽早暴露 dynamic sgWorld obj; string version sgWorld.Version; Console.WriteLine(TerraExplorer 版本: version); } catch (COMException ex) { Console.WriteLine($0x{ex.HResult:X8}: {ex.Message}); // 0x80040154 类未注册 // 0x8007007E 依赖 DLL 缺失 // 消息里带 license / dongle / HASP 是许可问题 } catch (Exception ex) { Console.WriteLine(ex.Message); } } }这里用dynamic是因为我们从反射拿到的对象类型不确定直接编译期调用接口方法需要强转成某个具体接口而版本一变接口名就可能对不上用 dynamic 至少能跑起来。正式项目里我建议在验证完具体版本后补一个强类型封装层把 dynamic 限制在这个入口处别满代码飞。4. 跑通第一个示例加载地形和3DML模型4.1 实例化SGWorld并打开一个fly工程飞行工程文件.fly是 TerraExplorer 的项目文件地形、影像、模型图层都挂在里面。二次开发的第一步不是创建工程而是打开一个已经存在的 fly 工程因为 TerraExplorer 的地球场景需要加载全球地形缓存空工程也能跑但加载速度、视角定位都会让你怀疑人生。先用 Pro 主程序建一个带基础地形和影像的工程保存成 base.fly开发时反复用。下面是打开工程的最小代码。注意我把 SGWorld 实例存成了类字段这是 COM 生命周期管理的关键局部变量容易被 GC 提前回收回收后事件不回、对象失效问题表现非常隐蔽。using System; using TerraExplorerX; class SceneBuilder { private SGWorld _sg; private IProject _proj; public bool OpenScene(string flyPath) { // 用 ProgID 创建 SGWorld避免编译期锁死具体版本 object obj TeEntry.CreateSgWorld(); if (obj null) return false; _sg (SGWorld)obj; _proj _sg.Project; // 打开工程失败时返回 false不一定抛异常 bool ok _proj.Open(flyPath, , true); if (!ok) { Console.WriteLine(打开工程失败检查路径或许可); } return ok; } }参数说明Open的第一个参数是 fly 文件全路径支持相对路径但批处理脚本里我只会用绝对路径第二个参数传空字符串表示使用默认覆盖策略第三个参数传true表示只读打开防止脚本误改原始工程。只读模式在调试阶段非常有用跑挂了也不担心把原工程弄脏。4.2 创建图层组先给场景一个归类的骨架很多示例代码急着加模型模型全堆在信息树根节点上后期做显隐、导出、按业务分类筛选时根本没法收拾。TerraExplorer 的信息树是场景的骨架分组Group既是结构节点也是图层容器。我习惯在建任何模型之前先按业务域建好分组。// 在信息树根节点下创建一个分组 IGroup group _proj.CreateGroup(业务模型); Console.WriteLine(分组ID: group.ID);CreateGroup默认挂在根节点下也可以在第二个参数指定父分组 ID形成树状结构。拿到分组 ID 后后面创建的所有对象都可以挂进来这样信息树的结构就是你的业务结构而不是一串随机堆叠的模型名。实际交付时客户经常会在信息树里自己勾选显隐结构理不清的项目连验收这关都难过。4.3 用经纬度定位相机并创建3DML模型打开工程、建好分组之后核心动作来了把视角飞到目标区域然后创建三维模型。这里最关键的是TEPosition64这个位置对象它承载了经纬度、高度和姿态信息TerraExplorer 里凡是涉及位置的接口几乎都以它为参数。// 用经纬度构造位置坐标 TEPosition64 pos new TEPosition64(); pos.X 121.4737; // 经度 pos.Y 31.2304; // 纬度 pos.Altitude 5.0; // 相对地表 5 米 pos.AltitudeType TEAltitudeType.TE_AT_TERRAIN_REL; // 飞行定位第二个参数是飞行秒数0 表示瞬间到达 _sg.Navigate.SetPosition(pos, 0); // 创建 3DML 模型最后一个参数指定父节点分组 ID string objId _sg.CreateObject(TEObjectType.TE_3DML, pos, D:\models\hydrant.3dml, group.ID); if (string.IsNullOrEmpty(objId)) { Console.WriteLine(创建失败检查模型路径和许可); } else { Console.WriteLine(创建成功对象ID objId); }这段代码有三个参数值得多说。第一X和Y的对应关系TerraExplorer 所有位置对象统一用X表示经度、Y表示纬度。中国区域内经度在 73 到 135 之间纬度在 18 到 53 之间。看到代码里X 121、Y 31上海一带基本就是对的一旦出现X 31、Y 121模型大概率被挂到海里或荒地里了。第二AltitudeType决定高度基准TE_AT_TERRAIN_REL是相对地表高度适合路灯、消防栓、摄像头这类贴地摆放的设备TE_AT_TERRAIN_ABS是绝对海拔高度适合管线、隧道这类需要真实高程信息的对象。第三CreateObject返回的对象 ID 是后续所有操作的钥匙改姿态、换模型、删对象都靠它务必存进业务数据表别用完就丢。提示TEPosition64 在某些版本里还带 Yaw、Pitch、Roll 三个姿态角。如果你发现创建出来的模型朝向不对别在创建时直接调这三个参数某些版本会有兼容问题。稳妥做法是创建完成后通过对象接口再 SetPosition 一次设置姿态。5. TerraExplorer二次开发避坑手册五个血泪现场5.1 32位与64位进程错位COM实例化直接崩现象代码在开发机一切正常部署到服务器就报 0x80040154 类未注册或者同一台机器上控制台程序能跑WinForms 程序却闪退。原因TerraExplorer 的 COM 组件注册是按位数分区的64 位注册表里的类32 位进程看不到。绝大多数服务器上只装了 64 位 SDK而你的 WinForms 项目如果没关“首选 32 位”即使平台目标写了 x64Visual Studio 仍可能以 32 位进程运行调试。解决打开项目属性生成选项卡平台目标选 x64同时把“首选 32 位”勾选去掉。改完在Main入口第一行打印Environment.Is64BitProcess确认输出为 True 再往下走。5.2 经纬度写反模型挂到海里现象模型创建成功信息树里看得到但相机飞到业务坐标后场景里空空如也把视图缩小一看模型出现在距离目标点几百公里的地方。原因TerraExplorer 的位置对象用X存经度、Y存纬度和很多人习惯的“纬度在前、经度在后”刚好相反。从数据库里读坐标时如果表结构是(lat, lon)你直接赋值就会反。解决赋值时统一写成pos.X lon; pos.Y lat;并且在创建完对象后立刻回读一次位置打印出来对比。回读接口随版本略有差异但思路一致拿到刚创建的 objId通过对象接口读它的 Position检查 X/Y 是否落在业务区域。这步做成日志输出部署到现场时能省大量沟通时间。5.3 相对高度与绝对高度混用模型要么陷地要么飞上天现象同一个模型放在 A 区域贴地正常放到 B 区域就陷进地里半截换成TE_AT_TERRAIN_ABS后模型又浮在空中。原因TERRAIN_ABS是绝对海拔高度模型会严格钉在海拔面上。地形数据精度不够时模型底座和地形表面之间就会有缝隙TERRAIN_REL是相对地表高度会跟随地形起伏适合贴地物体但它需要地形缓存就在当前场景里。解决地表设备、建筑白模这类“必须压在地面上”的对象一律用TE_AT_TERRAIN_REL高度给一个很小值比如 0.1 到 1 米避免模型和地形穿插导致的闪面。必须用绝对海拔的场景先确认 DEM 数据的精度和坐标系一致否则高程误差会被模型渲染放大得很明显。5.4 事件回调不触发不是玄学是订阅方式和生命周期现象订阅了鼠标点击事件点击三维场景里的模型断点始终不进来偶尔第一次能触发之后再不触发。原因两个坑叠加。第一COM 事件在 C# 里要绑定到事件插口接口类似 IEventEvents直接按普通 .NET 事件写编译器不报错但运行时收不到第二事件源对象如果被 GC 回收事件就静默失效。局部变量在方法返回后失去引用事件自然断掉。解决把 SGWorld 和事件对象都存成类字段或静态字段保证长期存活订阅时用事件插口接口。代码里这样写// 事件源对象存入字段避免被 GC 回收 IEventEvents events (IEventEvents)_sg.Event; events.OnLButtonClick (x, y, px, py) { Console.WriteLine($点击屏幕坐标: ({x}, {y})投影坐标: ({px}, {py})); };事件回调里不要做耗时操作它是跑在 TerraExplorer 的 COM 线程上的你在这里面执行数据库查询或网络请求会把三维窗口卡死。正确做法是把事件数据塞进队列由你自己的工作线程去处理。5.5 许可证被桌面端占用批处理半夜失败现象批处理程序白天运行一切正常凌晨定时任务跑起来偶尔失败日志里出现 license 相关错误人为重启任务后又正常。原因TerraExplorer Pro 桌面端如果开着会占用授权同一台机器上再启动一个 COM 实例时软授权或加密锁的并发限制会拒绝服务。批处理任务失败后无人干预看起来就像偶发故障。解决批处理启动前先检测并关闭 Pro 桌面进程任务里做启动自检遇到 license 错误就退避重试别在原进程上反复重试因为失败的 COM 实例可能残留了锁句柄。多次重试仍失败则告警让值班人员介入。这一步看着不起眼但对无人值守的交付项目来说是刚需。6. 进阶技巧与验证让示例代码真正能交付6.1 把Excel点表批量变成三维场景单个模型创建跑通后真正的业务场景往往是几百上千个点。从 Excel 或数据库读出经纬度、模型路径、分组信息循环创建即可。注意三个细节对象 ID 必须存下来用于后续更新创建操作隔一段时间让 COM 线程喘口气分组的 ID 在循环体外取好别每次重复创建。foreach (var row in points) { TEPosition64 p new TEPosition64 { X row.Lon, Y row.Lat, Altitude row.Height, AltitudeType TEAltitudeType.TE_AT_TERRAIN_REL }; string id _sg.CreateObject(TEObjectType.TE_3DML, p, row.ModelPath, groupId); if (string.IsNullOrEmpty(id)) { Console.WriteLine($创建失败: {row.Lon},{row.Lat}); continue; } // 每 50 个对象让 COM 线程处理一下消息避免积压 if (count % 50 0) { System.Threading.Thread.Sleep(100); } }Thread.Sleep不是玄学是给 COM 底层 UI 线程让出时间片。批量 500 个以上对象时不加这个 SLeep 有时会出现后续对象创建失败或场景卡死具体阈值和机器性能有关跑一次压测就能定下来。6.2 自动化冒烟验证不打开界面也能判断是否成功交付前我只相信自动化验证脚本。固定流程是创建完所有对象后保存工程再重新打开遍历信息树统计对象数量核对坐标落在预期范围内。这个脚本每次改完代码都跑一遍接口升级也不怕。// 保存工程供自动化验收 _sg.Project.SaveAs(D:\scene\out.fly); // 重新打开核对对象数 _sg.Project.Open(D:\scene\out.fly, , true); int total 0; foreach (IObject obj in _sg.Project.Objects) { total; } Console.WriteLine(场景对象总数: total);把“打开工程、创建模型、统计数量、保存退出”做成一个不依赖界面的人工检查脚本任何环境问题都会在这里暴露。我最开始做这个方向时因为没关“首选 32 位”浪费了整整半天后来每次新建项目都先写一个这样的冒烟脚本后面所有接口调整都不怕了。TerraExplorer 的 API 版本差异比文档里看起来更野与其背接口不如先把验证脚本固定下来希望帮到你。本文还有配套的精品资源点击获取