简介:面向Skyline TerraExplorer三维地球平台的C#二次开发示例代码,专为GIS开发者、三维可视化项目技术人员及TerraExplorer SDK初学者设计,旨在解决从零搭建三维地球应用时遇到的工程模板、API调用与数据集成难题。资源以完整的Windows Forms工程为载体,围绕地图对象与控件操作、图层与标记管理、Shapefile等矢量数据加载、3D模型放置、场景交互等核心环节,给出可直接编译运行的C#源码,帮助读者对照API理解事件驱动程序、空间查询分析、多线程与异步编程、自定义UI等二次开发要点。压缩包共21个文件,以cs源代码和sln/csproj工程文件为主体,附带resx资源文件、shp/shx/dbf/prj等Shapefile数据文件、fly三维场景文件及gif演示图片,整体仅42KB,结构紧凑,适合直接打开工程逐项研读。已有483人学习下载,这份示例的代码组织方式、资源引用和工程配置,可帮助开发者快速搭建TerraExplorer功能扩展原型,缩短上手周期,也为后续集成WMS/WFS等网络地图服务提供了可复用的参考。1. 三维GIS二次开发为什么绕不开Skyline的TerraExplorer做三维GIS应用的人绕不开Skyline产品线里的TerraExplorer,这套平台把地形、影像、模型和业务图层封装成一套可在C#里直接调用的对象模型。你在业务系统里看到的数字沙盘、飞行漫游、鼠标拾取信息,其实都是用TE的API叠出来的。它解决的是基础三维能力与专业场景落地之间的问题,适合需要把三维场景嵌进管理信息系统的开发者,以及想从演示工程转型做二次开发的技术人员。我的经验是,TE的学习成本不低,但一旦摸透了SGWorld这条主链路,后面做业务需求的速度会快很多。2. TerraExplorer C#二次开发环境:组件选型与第一个三维窗口2.1 开发包构成:从Pro客户端到Interop DLLTerraExplorer按使用角色分为客户端和开发套件,真正能调API的是Pro版本。操作演示和查看工程用普通客户端就够了,但你要做C#调用、控件嵌入、事件响应,就必须有Pro的授权和开发SDK。安装Pro后,SDK目录里能找到两个关键东西:一个是TerraExplorerX.dll,另一个是对应不同.NET版本的Interop.TerraExplorerX.dll。VS里做C#开发,我更推荐走COM引用而不是直接手动拷贝DLL,因为COM引用会自动生成Ax宿主控件和事件包装类,你往窗体上拖一个TerraExplorerX控件即可使用。我最初遇到的问题是没有添加正确的引用。手动添加DLL虽然能编译通过,但运行时控件无法实例化,现象是窗体加载直接抛InvalidCastException,原因是类型库没有通过COM注册表向项目提供元数据。换成VS的“添加COM引用”后,在引用列表里选择TerraExplorerX,IDE自动生成Interop程序集,这个问题就消失了。所以环境搭建的第一步不是写代码,而是确认安装的是Pro版,并让COM组件正确注册到系统。开发机系统上建议先装全VC++运行时库,TE的底层引擎是C++写的,部分新版本安装脚本不会自动补全运行库,导致SDK装上后控件注册失败。判断依据很简单:Windows命令行输入regsvr32 TerraExplorerX.dll,如果提示找不到入口点或已加载但调用入口点失败,基本就是运行库或版本位数不一致。再一个细节是编译平台目标,64位系统上如果VS编译成AnyCPU但TE只注册了32位类型库,运行时会抛出正被调用的类型库格式不对这类COM错误。我一般直接把项目平台目标固定为x86或x64中的某一个,避免运行时试探环境。2.2 搭建项目:引用、控件与初始化顺序新建一个C# Window窗体应用,工具箱里右键选择项,在COM组件页勾选TerraExplorerX,拖到窗体上。这时候窗体设计器会生成一个axTerraExplorerX1成员,它才是后续所有SGWorld调用的入口。初始化代码放在窗体Load事件里最合适,但要保证控件已完全创建,Populate之后才能拿到SGWorld对象。下面的代码演示了一次完整的初始化并打开本地工程:public partial class MainForm : Form { public MainForm() { InitializeComponent(); } private void MainForm_Load(object sender, EventArgs e) { // 指定本地工程文件路径,统一用英文路径降低兼容性风险 string mptFile = @"C:\te\demo.mpt"; try { // 初始化TE全局对象,参数0表示使用默认加密方式 axTerraExplorerX1.SGWorld.Initialize(0); // 打开工程,工程里记录了地形、影像、图层树及显示配置 axTerraExplorerX1.SGWorld.Project.Open(mptFile); } catch (Exception ex) { MessageBox.Show("启动失败:" + ex.Message); } // 将窗口模式显式切到三维场景 axTerraExplorerX1.SGWorld.Window.WindowMode = WindowMode.WINDOW_MODE_3D; } }逻辑说明:SGWorld是TE全局根对象,它下面挂满了Project、Navigate、Creator等子系统,是一切的起点。Initialize只应该调用一次,参数0代表加密方式按默认配置走;如果重复调用,可能导致全局状态被重置。Project.Open里传入的是.mpt工程文件,这个文件并不直接包含三维模型和影像,它描述的是数据源如何加载、图层如何排序,真正的地形数据是后台按需读取的。参数说明:WindowMode的切换是把窗口从平面空白状态转为三维场景渲染状态。实际部署时,如果工程里的数据源指向网络地址,那么启动时间会比本地慢很多,严重时会黑屏数秒,所以建议开发初期先备一份纯本地的精简工程用来验证初始化链路。另外一个容易忽略的问题是Initialize的调用时机,放在控件被销毁之后或重新加载之前都可能无法生效。2.3 打开工程与坐标系基准:MPT文件的潜规则TerraExplorer的工程文件记录了图层树、地形缓存路径、影像覆盖顺序等,但未强约束坐标基准。TE对坐标的容忍度有限,工程坐标系与动态创建对象坐标系一旦不匹配,轻则模型悬浮,重则整个场景显示到错误位置。做二次开发前,最好先确认原始数据的坐标系,再统一所有动态创建的要素坐标。下表是我在项目里常用的判断维度:概念说明二次开发关注点工程文件(.mpt)记录图层树与显示配置用Project.Open直接打开地形缓存DEM和DTM处理后的切片数据加载缓慢时优先排查缓存是否完整矢量/模型图层附加的业务数据通过信息树查找节点并控制显隐如果影像数据是WGS84经纬度,而模型数据用地方坐标系,TE不会帮你自动转换。常见做法是提前用投影工具把动态创建的模型坐标通过数学换算成工程坐标,再调用Creator.CreatePosition写进API。千万不要依赖TE在运行时的隐式转换,这不仅慢,而且结果不可预测。我在某跨平台系统中吃过亏,一个管线模型在开发机上显示正常,换到另一个投影参数不匹配的机器上就整体偏移到几公里外。处理方式是在启动阶段读取工程坐标系元数据,与动态数据源做差值,然后统一加载。TE没有公开直接的坐标轴转换接口,大多数工程师是在自己的工具类里维护一套WGS84到工程坐标的转换函数。这个方案虽然笨重,却是维护成本最低的。2.4 授权初始化:一次注册,试过才稳授权问题几乎贯穿TE二次开发全周期。新装环境首次运行,最常见的是Initialize抛出COMException,错误码0x80040204,排查步骤基本固定在三条线上:授权文件是否放置、系统时间是否偏差、版本类型库是否匹配。授权文件通常放在安装目录下某个文件夹,升级TE版本后必须重新生成。我推荐的工程写法是把初始化保护成单例,避免多窗体重入:private static bool _teReady = false; private bool EnsureTerraExplorerReady() { if (_teReady) return true; try { axTerraExplorerX1.SGWorld.Initialize(0); _teReady = true; } catch (Exception ex) { // 记录日志后由上层决定是否重试 Console.WriteLine(ex); return false; } return true; }逻辑说明:因为Initialize执行的不确定性,后面所有业务代码都先走EnsureTerraExplorerReady(),成功后再调Project.Open。这样能快速定位是授权还是工程加载问题。注意_teReady是进程级变量,窗口关闭重新打开时不重复初始化,TE的许可连接保持在进程内。参数说明:如果预期部署在多台机器上,不要把授权判断写死;更稳妥的是启动时检测解码器环境,失败则给出安装提示。这比让用户看着黑屏去查日志要友好得多。3. 核心API操作三维场景:定位、漫游、模型加载一网打尽3.1 SGWorld根对象:整个三维世界的入口SGWorld既是全局对象,也是所有子系统的制造工厂。通常代码里会频繁遇到这几个属性:属性类型用途ProjectIProject管理工程与图层树NavigateINavigate相机飞行、视点控制CreatorICreator创建位置、模型、图形对象WindowITerraExplorerWindow窗口交互、屏幕坐标转换ProjectTreeIInformationTree图层节点查找、显隐控制理解它的归属关系,能少走很多弯路。Creator.CreatePosition创建的Position不会自动显示在场景中,它只是一个参数对象;Creator.CreateModel创建模型后自动回填到当前工程的信息树;Navigator控制的是观察相机而不是模型,所以做第一人称交互时要用Navigate,做业务物标展示时要用ProjectTree和Creator。我在第一次写TE代码时,把相机的Position和模型的Position混为一谈,结果模型和视点绑定到了一起,移动相机时模型也跟着跑了。原因是相机移动必须用Navigate下的方法,而不是让模型的Position去跟随当前视点。把这两条链路分清楚,后面的逻辑才不会拧巴。3.2 Position与地面定位:经纬度、海拔和朝向的排列组合Position是TE空间操作的最小单元,无论是定位模型、设置相机还是落放图标,都离不开它。主要参数逻辑:经度(lng)、纬度(lat)、海拔高度(altitude)、海拔类型(AltitudeType)、航向角(yaw)、俯仰角(pitch)、翻滚角(roll)。航向0度指正北,俯仰角为负数时表示视线向下。// 在东经116.39度、北纬39.90度区域创建一个绝对海拔2000米的位置 IPosition pos = axTerraExplorerX1.SGWorld.Creator.CreatePosition( 116.39, 39.90, 2000.0, AltitudeType.ALTITUDE_TYPE_ABSOLUTE, 0.0, -60.0, 0.0); // 读取当前海拔,用于后续相机对齐 double alt = pos.Altitude; // 修改航向角,让它朝东偏转45度 pos.Yaw = 45.0;逻辑说明:CreatePosition返回一个IPosition实例,它更像一个参数容器,可以被Navigate、Creator等多种接口引用。经纬度顺序固定是先经度后纬度,这与日常口语中的纬度在前习惯相反。用AltitudeType.ALTITUDE_TYPE_RELATIVE时,传入的高度是相对当地地面的距离;用ABSOLUTE时则是相对海平面的绝对高程。选错海拔类型,模型要么扎进地里,要么悬空几百米。参数说明:Yaw、Pitch、Roll的角度单位都是度,而不是弧度,这点与很多三维引擎不同。在应用层封装一个BuildPosition(double lng, double lat, double alt, double heading, double pitch)工具方法,能显著减少手写传参顺序导致的低级错误。3.3 FlyTo与相机控制:如何丝滑地飞到目标点飞行漫游是三维场景最常见的演示能力之一。TE的Navigate接口提供了FlyTo重载,支持传入目标Position和飞行时间。我通常把飞行时间封装成全局变量,这样不同业务模块都能复用同一套相机操作习惯。// 获取当前相机视点,便于做一些差值计算 IPosition curPos = axTerraExplorerX1.SGWorld.Navigate.GetPosition(); // 构建目标位置:在目标点上空5000米处,相机俯角-45度看向地面 IPosition target = axTerraExplorerX1.SGWorld.Creator.CreatePosition( 116.45, 39.95, 5000.0, AltitudeType.ALTITUDE_TYPE_ABSOLUTE, 0.0, -45.0, 0.0); // 3秒飞行到目标视点 axTerraExplorerX1.SGWorld.Navigate.FlyTo(target, 3.0);逻辑说明:FlyTo在飞行过程中会持续更新相机状态,接口本身是异步的。如果不希望用户在中途打断飞行,需要自己禁用导航工具条。飞行时间参数不用整数,确保动画平滑过渡,过短的时长(小于1秒)容易让人产生眩晕感。参数说明:目标Position里的俯仰角决定了落定后的视角方向。如果想让相机平滑地左转飞到目标点,可以在调用FlyTo前先短暂地设置Yaw角,或者在飞行结束后重新设置位置。3.4 模型加载与放置:从X文件到业务功能TE原生的三维模型格式是X文件,由3D建模工具导出后转换使用。X带纹理和动画,且CPU占用较低,适合承载业务模型。加载一个模型的完整步骤是先构建Position,再调用CreateModel,返回类型为IModel。// 相对地面0米放置一个模型 IPosition pos = axTerraExplorerX1.SGWorld.Creator.CreatePosition( 116.48, 39.92, 0.0, AltitudeType.ALTITUDE_TYPE_RELATIVE, 0.0, 0.0, 0.0); // 从本地X文件创建三维模型 IModel model = axTerraExplorerX1.SGWorld.Creator.CreateModel(pos, @"C:\models\building.X"); // 修改模型显示比例 model.Scale = 2.5f; // 加载完成后重新定位,适合做动态安放功能 model.SetPosition( axTerraExplorerX1.SGWorld.Creator.CreatePosition( 116.49, 39.93, 0.0, AltitudeType.ALTITUDE_TYPE_RELATIVE, 0.0, 0.0, 0.0));逻辑说明:CreateModel一旦执行,模型会立即加入当前工程图层树,如果连续多次调用,图层树里会多个同名节点,因此最好在创建后通过model.Name重命名或记录句柄。Scale是全局缩放比率,2.5代表放大到原始尺寸2.5倍。SetPosition可以将模型从起点挪到新的位置,适合做一些点位搬迁或状态演示。参数说明:如果模型文件路径包含中文,加载失败率会直线上升,这是TE底层编码导致的,跟代码逻辑没太大关系。较稳妥的做法是把模型统一放在项目根目录的英文名子目录下,并在代码里使用Application.StartupPath拼接路径。4. 交互与图层控制:三维场景如何接住用户的鼠标4.1 屏幕坐标与地理坐标互转:点哪个就查哪个业务系统里最常见的一个需求是点击某个模型查看对应属性。TE暴露了窗口级的鼠标事件,在C#里可以直接接控件事件。事件参数里的X和Y是屏幕像素坐标,必须先转换成地理坐标才能做后续查询。private void axTerraExplorerX1_OnMouseClick(object sender, AxTerraExplorerX_OnMouseClickEventArgs e) { // 将屏幕像素坐标转换为地理坐标 IPoint62 coord = axTerraExplorerX1.SGWorld.Window.ScreenToWorld(e.X, e.Y); // 显示当前点击点的经纬度与海拔 if (coord != null) { string tip = string.Format("经度:{0:F6}, 纬度:{1:F6}, 海拔:{2:F2}", coord.X, coord.Y, coord.Altitude); statusLabel.Text = tip; } }逻辑说明:ScreenToWorld接收的是控件客户区内的像素坐标,事件参数就是控件坐标系下的偏移量。若把TE控件嵌入到带有左右面板的复杂界面中,就要保证传给事件的坐标没有被外层容器二次偏移。参数说明:转换得到的IPoint62包含X、Y、Altitude三个分量。当鼠标点击到空白天空位置时,Altitude的值可能为一个很大负数或默认值,需要在业务层做边界判断,否则后续空间查询会出现异常值。4.2 信息树与图层管理:显隐、透明度和动态刷新TE工程的图层都挂在一棵信息树上。二次开发通常用节点名称去查找,然后修改它的显示属性。初学时容易踩坑的是直接遍历子节点,但不同版本的子节点接口命名有差异。// 获取信息树根节点 IInformationTree tree = axTerraExplorerX1.SGWorld.ProjectTree as IInformationTree; INode root = tree.Node; // 按名称查找图层 INode node = root.FindChildItem("兴趣点"); if (node is ILayer layer) { // 隐藏图层 layer.Visible = false; // 透明度设为40,参数范围0到100 layer.Opacity = 40; }逻辑说明:FindChildItem按节点名称查找,同一名称只返回第一个匹配。如果图层树里有重名节点,用循环遍历更保险。ILayer是多个图层的公共接口,栅格层、矢量层、三维模型层都继承它,所以通过接口类型判断后可以直接调用共有属性。参数说明:Opacity为0表示完全透明,100表示完全不透明。更新透明度后场景不会自动刷新,需要手动调用ProjectTree.Refresh()或者在当前图层上做一次小位移来触发重绘。这个刷新机制在不同版本上表现不同,写入代码时最好主动刷新一次。4.3 量测工具接入:内置命令与结果回读TerraExplorer Pro内置了距离、面积、高度等量测工具,在自定义界面上可以通过ExecuteCommand命令打开。真正的难点在量测结束后的结果回读,因为它绕过了Language API的常规对象树。// 打开距离量测工具条,命令名以Pro版本SDK为准 axTerraExplorerX1.SGWorld.Window.ExecuteCommand("MeasureDistance", ""); // 量测完成事件里接收结果 void OnMeasureDone(object sender, EventArgs e) { // 通过事件参数解析长度值,不同版本参数结构略有差异 double result = ParseMeasureResult(e); MessageBox.Show("量测结果:" + result.ToString("F2")); }逻辑说明:ExecuteCommand只是触发了界面端的交互流程,量测结束后结果是通过事件广播出来的。我在实际项目里通常把量测结果同时写入当前选中模型属性,方便后续制表。命令名的具体写法在TE版本间不统一,开发到新版本时优先查SDK示例里现成的命令常量,而不是盲目硬编码。参数说明:有些版本的ExecuteCommand需要传起始坐标和结束坐标两个参数,把这些信息提前装配好,能提升量测精度。如果只是打开面板而不传入坐标,用户需要手动在地图上点两次才能得到结果。5. 二次开发避坑:六个最常踩的TerraExplorer API问题5.1 黑屏不显示场景:初始化顺序的锅现象:代码编译通过,窗体显示但场景区域一片黑,也没有报错。原因:TE控件在实际渲染前需要完成窗口句柄创建。如果在Load里过早调用Project.Open,控件虽然存在但渲染未就绪,数据加载后无法绘制。另一个诱因是工程引用大量网络影像,短时间内无法完成首帧渲染。解决:在Shown事件或第一次Paint事件后再打开工程,并给Open操作加超时保护。开发期先用本地mpt验证渲染链路,再切换到联网数据源。我把Load里的初始化逻辑全部挪到Shown事件后,黑屏问题再没出现过。5.2 授权验证不过:0x80040204的三种变体现象:第一次调用Initialize直接抛COMException,错误码0x80040204。原因:授权文件未生效;授权过期;32位/64位组件注册不匹配。这三类原因表现几乎一样,排查时容易浪费时间。解决:第一步检查授权文件是否放在安装目录指定位置;第二步确认系统时间没有偏差超过24小时;第三步把项目平台目标切到与控件注册相同位数。做一次统一检查后,让运维把授权固定到机器上,而不是每次手工配置。5.3 中文路径加载失败:非ASCII字符全线崩现象:工程放在C盘根目录能加载,放在“D:\园区平台数据”下就加载失败,模型文件同理。原因:TE底层引擎在部分版本对Unicode路径处理不完整,COM接口收到带中文的路径时,路径解析直接失败。解决:开发约定所有数据文件复制到纯英文目录。业务上确实有中文名称,只改图层节点显示名,而不是改文件名。导入大量模型前,先写一个批量改名脚本把文件名转为拼音或短ID,再交付给场景端。5.4 坐标拾取漂移:高DPI缩放带来的视觉差现象:在100%缩放下拾取准确,把Windows显示比例调到150%,点击位置与实际拾取点无关。原因:TE控件的鼠标事件坐标与物理像素坐标不一致,系统DPI缩放后事件参数仍按原始像素计算,导致ScreenToWorld的结果整体偏移。解决:在接收事件前,先通过窗体的DeviceDpi除以96得到缩放系数,再对坐标做换算:float scale = (float)DeviceDpi / 96f; int realX = (int)(e.X / scale); int realY = (int)(e.Y / scale); IPoint62 coord = axTerraExplorerX1.SGWorld.Window.ScreenToWorld(realX, realY);逻辑说明:DeviceDpi是窗体实际的DPI值,96是系统默认缩放基准。将事件坐标除以缩放系数,就是在逻辑像素区内还原物理坐标。项目交付后,运维环境变化大,这类问题往往只在特定电脑上复现,所以提前加入这一段换算能减少很多返工。5.5 漫游飞错位置:经度纬度别写反,海拔别用错现象:FlyTo结果落到一片陌生区域,或者飞到地下或者高空。原因:代码里把纬度写在经度参数位,AltitudeType用相对类型但传了绝对高度,位置在场景里被错误解释。解决:把所有位置构造收敛到一个工具函数,函数内做两件事:一是判断经度值在180到-180、纬度在90到-90范围内,超出则告警;二是根据业务场景显式指定AltitudeType。从那以后团队内部约定,业务层永远不直接调用CreatePosition。5.6 COM对象泄漏:场景越操作越卡现象:反复打开工程、创建模型后,内存只增不降,最后整个控件卡死无响应。原因:C#调用COM接口后,托管对象没有及时释放底层引用。TE创建的大量模型对象虽然失去了托管引用,但COM计数器并未归零。解决:在批量操作结束后,调用Marshal.FinalReleaseComObject释放不再使用的对象:System.Runtime.InteropServices.Marshal.FinalReleaseComObject(model); model = null;逻辑说明:FinalReleaseComObject会强制将COM引用计数归零,让底层资源立即回收。注意不要在控件事件回调里释放正在使用的对象,否则下次事件回调会抛空引用异常。我的习惯是批量操作放进队列,结束后统一清理。6. 进阶技巧:批量对象池与LOD距离,把场景卡顿压回去TE在使用中最大的性能瓶颈不是渲染,而是大量COM对象的创建与销毁。每创建一次CreateModel,底层就要申请渲染资源、挂节点、更新空间索引,循环几百次必然卡死界面。我常用一种批量池方案组合来解决:把待创建的模型放进队列,启动一个后台线程按帧调度,每帧只创建少量对象,创建完成后立即释放COM引用。Queue<ModelTask> pendingTasks = new Queue<ModelTask>(); private void EnqueueModel(IPosition pos, string xFile, string taskId) { pendingTasks.Enqueue(new ModelTask { Pos = pos, XFile = xFile, TaskId = taskId }); } private void ProcessTask() { if (pendingTasks.Count == 0) return; int batchLimit = 5; for (int i = 0; i < batchLimit && pendingTasks.Count > 0; i++) { ModelTask task = pendingTasks.Dequeue(); IModel model = axTerraExplorerX1.SGWorld.Creator.CreateModel(task.Pos, task.XFile); model.Name = task.TaskId; model.Scale = 1.0f; System.Runtime.InteropServices.Marshal.FinalReleaseComObject(model); } }逻辑说明:Enqueue负责收拢所有待创建任务,ProcessTask在Timer或空闲回调中执行,每帧最多创建5个对象,避免UI线程被阻塞。关键在每批次处理完后立即FinalReleaseComObject,让COM计数归零。显示名称写入模型节点,业务层后续通过名称搜索,不影响拾取和属性挂接。参数说明:batchLimit按机器性能调整,低配机器建议降到3,高配可以到8。实际帧率超过35FPS时保持5是安全值。若要进一步提升流畅度,可以用LOD距离属性来控制远处模型的可见范围,TE允许在图层对象上设置最大显示距离,超出距离自动隐藏,这比单纯减少数量更能稳定帧率。另一个值得做的优化是把常用地形区域提前缓存到本地。TE在使用网络影像时首次加载慢得让人抓狂,预缓存方案是把目标区域的影像请求在后台跑一遍,让缓存目录积累切片。TE的缓存机制是透明执行的,我只需要启动场景后用一个Camera飞行序列把重点区域飞一遍,等于给数据源做了预热。飞行的视点高度和角度能控制缓存密度,想看得清就多飞一次低空视角。那以后我每次交付新系统,都会强制走一遍授权环境核对、DPI适配、对象池加载和重点区域预热这套流程,再进入业务逻辑开发。做三维GIS二次开发,真正的坑往往不在API文档里,而在这些看似不起眼的边界问题中,希望这些思路帮到你少走一次弯路。本文还有配套的精品资源,点击获取
简介:面向Skyline TerraExplorer三维地球平台的C#二次开发示例代码,专为GIS开发者、三维可视化项目技术人员及TerraExplorer SDK初学者设计,旨在解决从零搭建三维地球应用时遇到的工程模板、API调用与数据集成难题。资源以完整的Windows Forms工程为载体,围绕地图对象与控件操作、图层与标记管理、Shapefile等矢量数据加载、3D模型放置、场景交互等核心环节,给出可直接编译运行的C#源码,帮助读者对照API理解事件驱动程序、空间查询分析、多线程与异步编程、自定义UI等二次开发要点。压缩包共21个文件,以cs源代码和sln/csproj工程文件为主体,附带resx资源文件、shp/shx/dbf/prj等Shapefile数据文件、fly三维场景文件及gif演示图片,整体仅42KB,结构紧凑,适合直接打开工程逐项研读。已有483人学习下载,这份示例的代码组织方式、资源引用和工程配置,可帮助开发者快速搭建TerraExplorer功能扩展原型,缩短上手周期,也为后续集成WMS/WFS等网络地图服务提供了可复用的参考。1. 三维GIS二次开发为什么绕不开Skyline的TerraExplorer做三维GIS应用的人绕不开Skyline产品线里的TerraExplorer,这套平台把地形、影像、模型和业务图层封装成一套可在C#里直接调用的对象模型。你在业务系统里看到的数字沙盘、飞行漫游、鼠标拾取信息,其实都是用TE的API叠出来的。它解决的是基础三维能力与专业场景落地之间的问题,适合需要把三维场景嵌进管理信息系统的开发者,以及想从演示工程转型做二次开发的技术人员。我的经验是,TE的学习成本不低,但一旦摸透了SGWorld这条主链路,后面做业务需求的速度会快很多。2. TerraExplorer C#二次开发环境:组件选型与第一个三维窗口2.1 开发包构成:从Pro客户端到Interop DLLTerraExplorer按使用角色分为客户端和开发套件,真正能调API的是Pro版本。操作演示和查看工程用普通客户端就够了,但你要做C#调用、控件嵌入、事件响应,就必须有Pro的授权和开发SDK。安装Pro后,SDK目录里能找到两个关键东西:一个是TerraExplorerX.dll,另一个是对应不同.NET版本的Interop.TerraExplorerX.dll。VS里做C#开发,我更推荐走COM引用而不是直接手动拷贝DLL,因为COM引用会自动生成Ax宿主控件和事件包装类,你往窗体上拖一个TerraExplorerX控件即可使用。我最初遇到的问题是没有添加正确的引用。手动添加DLL虽然能编译通过,但运行时控件无法实例化,现象是窗体加载直接抛InvalidCastException,原因是类型库没有通过COM注册表向项目提供元数据。换成VS的“添加COM引用”后,在引用列表里选择TerraExplorerX,IDE自动生成Interop程序集,这个问题就消失了。所以环境搭建的第一步不是写代码,而是确认安装的是Pro版,并让COM组件正确注册到系统。开发机系统上建议先装全VC++运行时库,TE的底层引擎是C++写的,部分新版本安装脚本不会自动补全运行库,导致SDK装上后控件注册失败。判断依据很简单:Windows命令行输入regsvr32 TerraExplorerX.dll,如果提示找不到入口点或已加载但调用入口点失败,基本就是运行库或版本位数不一致。再一个细节是编译平台目标,64位系统上如果VS编译成AnyCPU但TE只注册了32位类型库,运行时会抛出正被调用的类型库格式不对这类COM错误。我一般直接把项目平台目标固定为x86或x64中的某一个,避免运行时试探环境。2.2 搭建项目:引用、控件与初始化顺序新建一个C# Window窗体应用,工具箱里右键选择项,在COM组件页勾选TerraExplorerX,拖到窗体上。这时候窗体设计器会生成一个axTerraExplorerX1成员,它才是后续所有SGWorld调用的入口。初始化代码放在窗体Load事件里最合适,但要保证控件已完全创建,Populate之后才能拿到SGWorld对象。下面的代码演示了一次完整的初始化并打开本地工程:public partial class MainForm : Form { public MainForm() { InitializeComponent(); } private void MainForm_Load(object sender, EventArgs e) { // 指定本地工程文件路径,统一用英文路径降低兼容性风险 string mptFile = @"C:\te\demo.mpt"; try { // 初始化TE全局对象,参数0表示使用默认加密方式 axTerraExplorerX1.SGWorld.Initialize(0); // 打开工程,工程里记录了地形、影像、图层树及显示配置 axTerraExplorerX1.SGWorld.Project.Open(mptFile); } catch (Exception ex) { MessageBox.Show("启动失败:" + ex.Message); } // 将窗口模式显式切到三维场景 axTerraExplorerX1.SGWorld.Window.WindowMode = WindowMode.WINDOW_MODE_3D; } }逻辑说明:SGWorld是TE全局根对象,它下面挂满了Project、Navigate、Creator等子系统,是一切的起点。Initialize只应该调用一次,参数0代表加密方式按默认配置走;如果重复调用,可能导致全局状态被重置。Project.Open里传入的是.mpt工程文件,这个文件并不直接包含三维模型和影像,它描述的是数据源如何加载、图层如何排序,真正的地形数据是后台按需读取的。参数说明:WindowMode的切换是把窗口从平面空白状态转为三维场景渲染状态。实际部署时,如果工程里的数据源指向网络地址,那么启动时间会比本地慢很多,严重时会黑屏数秒,所以建议开发初期先备一份纯本地的精简工程用来验证初始化链路。另外一个容易忽略的问题是Initialize的调用时机,放在控件被销毁之后或重新加载之前都可能无法生效。2.3 打开工程与坐标系基准:MPT文件的潜规则TerraExplorer的工程文件记录了图层树、地形缓存路径、影像覆盖顺序等,但未强约束坐标基准。TE对坐标的容忍度有限,工程坐标系与动态创建对象坐标系一旦不匹配,轻则模型悬浮,重则整个场景显示到错误位置。做二次开发前,最好先确认原始数据的坐标系,再统一所有动态创建的要素坐标。下表是我在项目里常用的判断维度:概念说明二次开发关注点工程文件(.mpt)记录图层树与显示配置用Project.Open直接打开地形缓存DEM和DTM处理后的切片数据加载缓慢时优先排查缓存是否完整矢量/模型图层附加的业务数据通过信息树查找节点并控制显隐如果影像数据是WGS84经纬度,而模型数据用地方坐标系,TE不会帮你自动转换。常见做法是提前用投影工具把动态创建的模型坐标通过数学换算成工程坐标,再调用Creator.CreatePosition写进API。千万不要依赖TE在运行时的隐式转换,这不仅慢,而且结果不可预测。我在某跨平台系统中吃过亏,一个管线模型在开发机上显示正常,换到另一个投影参数不匹配的机器上就整体偏移到几公里外。处理方式是在启动阶段读取工程坐标系元数据,与动态数据源做差值,然后统一加载。TE没有公开直接的坐标轴转换接口,大多数工程师是在自己的工具类里维护一套WGS84到工程坐标的转换函数。这个方案虽然笨重,却是维护成本最低的。2.4 授权初始化:一次注册,试过才稳授权问题几乎贯穿TE二次开发全周期。新装环境首次运行,最常见的是Initialize抛出COMException,错误码0x80040204,排查步骤基本固定在三条线上:授权文件是否放置、系统时间是否偏差、版本类型库是否匹配。授权文件通常放在安装目录下某个文件夹,升级TE版本后必须重新生成。我推荐的工程写法是把初始化保护成单例,避免多窗体重入:private static bool _teReady = false; private bool EnsureTerraExplorerReady() { if (_teReady) return true; try { axTerraExplorerX1.SGWorld.Initialize(0); _teReady = true; } catch (Exception ex) { // 记录日志后由上层决定是否重试 Console.WriteLine(ex); return false; } return true; }逻辑说明:因为Initialize执行的不确定性,后面所有业务代码都先走EnsureTerraExplorerReady(),成功后再调Project.Open。这样能快速定位是授权还是工程加载问题。注意_teReady是进程级变量,窗口关闭重新打开时不重复初始化,TE的许可连接保持在进程内。参数说明:如果预期部署在多台机器上,不要把授权判断写死;更稳妥的是启动时检测解码器环境,失败则给出安装提示。这比让用户看着黑屏去查日志要友好得多。3. 核心API操作三维场景:定位、漫游、模型加载一网打尽3.1 SGWorld根对象:整个三维世界的入口SGWorld既是全局对象,也是所有子系统的制造工厂。通常代码里会频繁遇到这几个属性:属性类型用途ProjectIProject管理工程与图层树NavigateINavigate相机飞行、视点控制CreatorICreator创建位置、模型、图形对象WindowITerraExplorerWindow窗口交互、屏幕坐标转换ProjectTreeIInformationTree图层节点查找、显隐控制理解它的归属关系,能少走很多弯路。Creator.CreatePosition创建的Position不会自动显示在场景中,它只是一个参数对象;Creator.CreateModel创建模型后自动回填到当前工程的信息树;Navigator控制的是观察相机而不是模型,所以做第一人称交互时要用Navigate,做业务物标展示时要用ProjectTree和Creator。我在第一次写TE代码时,把相机的Position和模型的Position混为一谈,结果模型和视点绑定到了一起,移动相机时模型也跟着跑了。原因是相机移动必须用Navigate下的方法,而不是让模型的Position去跟随当前视点。把这两条链路分清楚,后面的逻辑才不会拧巴。3.2 Position与地面定位:经纬度、海拔和朝向的排列组合Position是TE空间操作的最小单元,无论是定位模型、设置相机还是落放图标,都离不开它。主要参数逻辑:经度(lng)、纬度(lat)、海拔高度(altitude)、海拔类型(AltitudeType)、航向角(yaw)、俯仰角(pitch)、翻滚角(roll)。航向0度指正北,俯仰角为负数时表示视线向下。// 在东经116.39度、北纬39.90度区域创建一个绝对海拔2000米的位置 IPosition pos = axTerraExplorerX1.SGWorld.Creator.CreatePosition( 116.39, 39.90, 2000.0, AltitudeType.ALTITUDE_TYPE_ABSOLUTE, 0.0, -60.0, 0.0); // 读取当前海拔,用于后续相机对齐 double alt = pos.Altitude; // 修改航向角,让它朝东偏转45度 pos.Yaw = 45.0;逻辑说明:CreatePosition返回一个IPosition实例,它更像一个参数容器,可以被Navigate、Creator等多种接口引用。经纬度顺序固定是先经度后纬度,这与日常口语中的纬度在前习惯相反。用AltitudeType.ALTITUDE_TYPE_RELATIVE时,传入的高度是相对当地地面的距离;用ABSOLUTE时则是相对海平面的绝对高程。选错海拔类型,模型要么扎进地里,要么悬空几百米。参数说明:Yaw、Pitch、Roll的角度单位都是度,而不是弧度,这点与很多三维引擎不同。在应用层封装一个BuildPosition(double lng, double lat, double alt, double heading, double pitch)工具方法,能显著减少手写传参顺序导致的低级错误。3.3 FlyTo与相机控制:如何丝滑地飞到目标点飞行漫游是三维场景最常见的演示能力之一。TE的Navigate接口提供了FlyTo重载,支持传入目标Position和飞行时间。我通常把飞行时间封装成全局变量,这样不同业务模块都能复用同一套相机操作习惯。// 获取当前相机视点,便于做一些差值计算 IPosition curPos = axTerraExplorerX1.SGWorld.Navigate.GetPosition(); // 构建目标位置:在目标点上空5000米处,相机俯角-45度看向地面 IPosition target = axTerraExplorerX1.SGWorld.Creator.CreatePosition( 116.45, 39.95, 5000.0, AltitudeType.ALTITUDE_TYPE_ABSOLUTE, 0.0, -45.0, 0.0); // 3秒飞行到目标视点 axTerraExplorerX1.SGWorld.Navigate.FlyTo(target, 3.0);逻辑说明:FlyTo在飞行过程中会持续更新相机状态,接口本身是异步的。如果不希望用户在中途打断飞行,需要自己禁用导航工具条。飞行时间参数不用整数,确保动画平滑过渡,过短的时长(小于1秒)容易让人产生眩晕感。参数说明:目标Position里的俯仰角决定了落定后的视角方向。如果想让相机平滑地左转飞到目标点,可以在调用FlyTo前先短暂地设置Yaw角,或者在飞行结束后重新设置位置。3.4 模型加载与放置:从X文件到业务功能TE原生的三维模型格式是X文件,由3D建模工具导出后转换使用。X带纹理和动画,且CPU占用较低,适合承载业务模型。加载一个模型的完整步骤是先构建Position,再调用CreateModel,返回类型为IModel。// 相对地面0米放置一个模型 IPosition pos = axTerraExplorerX1.SGWorld.Creator.CreatePosition( 116.48, 39.92, 0.0, AltitudeType.ALTITUDE_TYPE_RELATIVE, 0.0, 0.0, 0.0); // 从本地X文件创建三维模型 IModel model = axTerraExplorerX1.SGWorld.Creator.CreateModel(pos, @"C:\models\building.X"); // 修改模型显示比例 model.Scale = 2.5f; // 加载完成后重新定位,适合做动态安放功能 model.SetPosition( axTerraExplorerX1.SGWorld.Creator.CreatePosition( 116.49, 39.93, 0.0, AltitudeType.ALTITUDE_TYPE_RELATIVE, 0.0, 0.0, 0.0));逻辑说明:CreateModel一旦执行,模型会立即加入当前工程图层树,如果连续多次调用,图层树里会多个同名节点,因此最好在创建后通过model.Name重命名或记录句柄。Scale是全局缩放比率,2.5代表放大到原始尺寸2.5倍。SetPosition可以将模型从起点挪到新的位置,适合做一些点位搬迁或状态演示。参数说明:如果模型文件路径包含中文,加载失败率会直线上升,这是TE底层编码导致的,跟代码逻辑没太大关系。较稳妥的做法是把模型统一放在项目根目录的英文名子目录下,并在代码里使用Application.StartupPath拼接路径。4. 交互与图层控制:三维场景如何接住用户的鼠标4.1 屏幕坐标与地理坐标互转:点哪个就查哪个业务系统里最常见的一个需求是点击某个模型查看对应属性。TE暴露了窗口级的鼠标事件,在C#里可以直接接控件事件。事件参数里的X和Y是屏幕像素坐标,必须先转换成地理坐标才能做后续查询。private void axTerraExplorerX1_OnMouseClick(object sender, AxTerraExplorerX_OnMouseClickEventArgs e) { // 将屏幕像素坐标转换为地理坐标 IPoint62 coord = axTerraExplorerX1.SGWorld.Window.ScreenToWorld(e.X, e.Y); // 显示当前点击点的经纬度与海拔 if (coord != null) { string tip = string.Format("经度:{0:F6}, 纬度:{1:F6}, 海拔:{2:F2}", coord.X, coord.Y, coord.Altitude); statusLabel.Text = tip; } }逻辑说明:ScreenToWorld接收的是控件客户区内的像素坐标,事件参数就是控件坐标系下的偏移量。若把TE控件嵌入到带有左右面板的复杂界面中,就要保证传给事件的坐标没有被外层容器二次偏移。参数说明:转换得到的IPoint62包含X、Y、Altitude三个分量。当鼠标点击到空白天空位置时,Altitude的值可能为一个很大负数或默认值,需要在业务层做边界判断,否则后续空间查询会出现异常值。4.2 信息树与图层管理:显隐、透明度和动态刷新TE工程的图层都挂在一棵信息树上。二次开发通常用节点名称去查找,然后修改它的显示属性。初学时容易踩坑的是直接遍历子节点,但不同版本的子节点接口命名有差异。// 获取信息树根节点 IInformationTree tree = axTerraExplorerX1.SGWorld.ProjectTree as IInformationTree; INode root = tree.Node; // 按名称查找图层 INode node = root.FindChildItem("兴趣点"); if (node is ILayer layer) { // 隐藏图层 layer.Visible = false; // 透明度设为40,参数范围0到100 layer.Opacity = 40; }逻辑说明:FindChildItem按节点名称查找,同一名称只返回第一个匹配。如果图层树里有重名节点,用循环遍历更保险。ILayer是多个图层的公共接口,栅格层、矢量层、三维模型层都继承它,所以通过接口类型判断后可以直接调用共有属性。参数说明:Opacity为0表示完全透明,100表示完全不透明。更新透明度后场景不会自动刷新,需要手动调用ProjectTree.Refresh()或者在当前图层上做一次小位移来触发重绘。这个刷新机制在不同版本上表现不同,写入代码时最好主动刷新一次。4.3 量测工具接入:内置命令与结果回读TerraExplorer Pro内置了距离、面积、高度等量测工具,在自定义界面上可以通过ExecuteCommand命令打开。真正的难点在量测结束后的结果回读,因为它绕过了Language API的常规对象树。// 打开距离量测工具条,命令名以Pro版本SDK为准 axTerraExplorerX1.SGWorld.Window.ExecuteCommand("MeasureDistance", ""); // 量测完成事件里接收结果 void OnMeasureDone(object sender, EventArgs e) { // 通过事件参数解析长度值,不同版本参数结构略有差异 double result = ParseMeasureResult(e); MessageBox.Show("量测结果:" + result.ToString("F2")); }逻辑说明:ExecuteCommand只是触发了界面端的交互流程,量测结束后结果是通过事件广播出来的。我在实际项目里通常把量测结果同时写入当前选中模型属性,方便后续制表。命令名的具体写法在TE版本间不统一,开发到新版本时优先查SDK示例里现成的命令常量,而不是盲目硬编码。参数说明:有些版本的ExecuteCommand需要传起始坐标和结束坐标两个参数,把这些信息提前装配好,能提升量测精度。如果只是打开面板而不传入坐标,用户需要手动在地图上点两次才能得到结果。5. 二次开发避坑:六个最常踩的TerraExplorer API问题5.1 黑屏不显示场景:初始化顺序的锅现象:代码编译通过,窗体显示但场景区域一片黑,也没有报错。原因:TE控件在实际渲染前需要完成窗口句柄创建。如果在Load里过早调用Project.Open,控件虽然存在但渲染未就绪,数据加载后无法绘制。另一个诱因是工程引用大量网络影像,短时间内无法完成首帧渲染。解决:在Shown事件或第一次Paint事件后再打开工程,并给Open操作加超时保护。开发期先用本地mpt验证渲染链路,再切换到联网数据源。我把Load里的初始化逻辑全部挪到Shown事件后,黑屏问题再没出现过。5.2 授权验证不过:0x80040204的三种变体现象:第一次调用Initialize直接抛COMException,错误码0x80040204。原因:授权文件未生效;授权过期;32位/64位组件注册不匹配。这三类原因表现几乎一样,排查时容易浪费时间。解决:第一步检查授权文件是否放在安装目录指定位置;第二步确认系统时间没有偏差超过24小时;第三步把项目平台目标切到与控件注册相同位数。做一次统一检查后,让运维把授权固定到机器上,而不是每次手工配置。5.3 中文路径加载失败:非ASCII字符全线崩现象:工程放在C盘根目录能加载,放在“D:\园区平台数据”下就加载失败,模型文件同理。原因:TE底层引擎在部分版本对Unicode路径处理不完整,COM接口收到带中文的路径时,路径解析直接失败。解决:开发约定所有数据文件复制到纯英文目录。业务上确实有中文名称,只改图层节点显示名,而不是改文件名。导入大量模型前,先写一个批量改名脚本把文件名转为拼音或短ID,再交付给场景端。5.4 坐标拾取漂移:高DPI缩放带来的视觉差现象:在100%缩放下拾取准确,把Windows显示比例调到150%,点击位置与实际拾取点无关。原因:TE控件的鼠标事件坐标与物理像素坐标不一致,系统DPI缩放后事件参数仍按原始像素计算,导致ScreenToWorld的结果整体偏移。解决:在接收事件前,先通过窗体的DeviceDpi除以96得到缩放系数,再对坐标做换算:float scale = (float)DeviceDpi / 96f; int realX = (int)(e.X / scale); int realY = (int)(e.Y / scale); IPoint62 coord = axTerraExplorerX1.SGWorld.Window.ScreenToWorld(realX, realY);逻辑说明:DeviceDpi是窗体实际的DPI值,96是系统默认缩放基准。将事件坐标除以缩放系数,就是在逻辑像素区内还原物理坐标。项目交付后,运维环境变化大,这类问题往往只在特定电脑上复现,所以提前加入这一段换算能减少很多返工。5.5 漫游飞错位置:经度纬度别写反,海拔别用错现象:FlyTo结果落到一片陌生区域,或者飞到地下或者高空。原因:代码里把纬度写在经度参数位,AltitudeType用相对类型但传了绝对高度,位置在场景里被错误解释。解决:把所有位置构造收敛到一个工具函数,函数内做两件事:一是判断经度值在180到-180、纬度在90到-90范围内,超出则告警;二是根据业务场景显式指定AltitudeType。从那以后团队内部约定,业务层永远不直接调用CreatePosition。5.6 COM对象泄漏:场景越操作越卡现象:反复打开工程、创建模型后,内存只增不降,最后整个控件卡死无响应。原因:C#调用COM接口后,托管对象没有及时释放底层引用。TE创建的大量模型对象虽然失去了托管引用,但COM计数器并未归零。解决:在批量操作结束后,调用Marshal.FinalReleaseComObject释放不再使用的对象:System.Runtime.InteropServices.Marshal.FinalReleaseComObject(model); model = null;逻辑说明:FinalReleaseComObject会强制将COM引用计数归零,让底层资源立即回收。注意不要在控件事件回调里释放正在使用的对象,否则下次事件回调会抛空引用异常。我的习惯是批量操作放进队列,结束后统一清理。6. 进阶技巧:批量对象池与LOD距离,把场景卡顿压回去TE在使用中最大的性能瓶颈不是渲染,而是大量COM对象的创建与销毁。每创建一次CreateModel,底层就要申请渲染资源、挂节点、更新空间索引,循环几百次必然卡死界面。我常用一种批量池方案组合来解决:把待创建的模型放进队列,启动一个后台线程按帧调度,每帧只创建少量对象,创建完成后立即释放COM引用。Queue<ModelTask> pendingTasks = new Queue<ModelTask>(); private void EnqueueModel(IPosition pos, string xFile, string taskId) { pendingTasks.Enqueue(new ModelTask { Pos = pos, XFile = xFile, TaskId = taskId }); } private void ProcessTask() { if (pendingTasks.Count == 0) return; int batchLimit = 5; for (int i = 0; i < batchLimit && pendingTasks.Count > 0; i++) { ModelTask task = pendingTasks.Dequeue(); IModel model = axTerraExplorerX1.SGWorld.Creator.CreateModel(task.Pos, task.XFile); model.Name = task.TaskId; model.Scale = 1.0f; System.Runtime.InteropServices.Marshal.FinalReleaseComObject(model); } }逻辑说明:Enqueue负责收拢所有待创建任务,ProcessTask在Timer或空闲回调中执行,每帧最多创建5个对象,避免UI线程被阻塞。关键在每批次处理完后立即FinalReleaseComObject,让COM计数归零。显示名称写入模型节点,业务层后续通过名称搜索,不影响拾取和属性挂接。参数说明:batchLimit按机器性能调整,低配机器建议降到3,高配可以到8。实际帧率超过35FPS时保持5是安全值。若要进一步提升流畅度,可以用LOD距离属性来控制远处模型的可见范围,TE允许在图层对象上设置最大显示距离,超出距离自动隐藏,这比单纯减少数量更能稳定帧率。另一个值得做的优化是把常用地形区域提前缓存到本地。TE在使用网络影像时首次加载慢得让人抓狂,预缓存方案是把目标区域的影像请求在后台跑一遍,让缓存目录积累切片。TE的缓存机制是透明执行的,我只需要启动场景后用一个Camera飞行序列把重点区域飞一遍,等于给数据源做了预热。飞行的视点高度和角度能控制缓存密度,想看得清就多飞一次低空视角。那以后我每次交付新系统,都会强制走一遍授权环境核对、DPI适配、对象池加载和重点区域预热这套流程,再进入业务逻辑开发。做三维GIS二次开发,真正的坑往往不在API文档里,而在这些看似不起眼的边界问题中,希望这些思路帮到你少走一次弯路。本文还有配套的精品资源,点击获取