☰
TerraExplorer二次开发实战:用C#操控三维场景与COM互操作
2026/10/21 4:31:47 网站建设 项目流程

简介:TerraExplorer是基于Skyline平台的三维地球可视化软件,广泛用于GIS与虚拟现实场景。这份示例代码主要面向需要在C#环境中开展TerraExplorer二次开发的GIS工程师,通过完整工程展示SDK调用、地图控件管理、图层加载与3D场景交互等典型任务。压缩包内共21个文件,体积仅42KB,包含6个C#源码文件、Visual Studio解决方案及项目文件,并附带Shapefile、Fly等示例数据,便于直接编译运行与对照学习。已有483人学习使用。通过研读代码,可快速掌握TerraExplorer SDK的事件驱动编程、空间数据集成、3D模型放置等关键开发思路,也能为自定义界面、网络服务对接和性能优化提供参考起点,适合从入门到进阶的开发者参考。

1. TerraExplorer二次开发的正确打开方式:别把C#项目当三维场景控制器

刚开始做TerraExplorer二次开发的人,容易把它当成三维引擎来用——自己建窗口、自己画场景。真去搭框架之后才明白,TerraExplorer是Skyline三维GIS生态里的浏览与制作平台,你在C#里操作它,本质是通过COM接口驱动一个已经跑起来的原生程序。想通这一点,整个开发方向就清晰了:你写的不是渲染引擎,而是指挥系统,控制场景打开、视角漫游、要素标注、工程数据读取这一整套动作。

做过某跨平台数字化项目的开发者应该有体会,当"在三维场景里定位设备、按路线飞行漫游、把后台属性表和场景标注联动"这类需求压过来时,TerraExplorer二次开发是最快的落地路径。适合手上有场景工程文件、需要从C#对它做自动化控制或功能扩展的人。入门门槛不算高,但COM互操作的水很深,下面这条线一处处拆开讲。

2. 环境准备与工程搭建:先把COM引用折腾明白

2.1 开发环境与依赖认知

TerraExplorer的官方开发工具链一般是"Pro端桌面工具 + TerraExplorerX.dll"这套组合。Pro端本身用来制作和编辑场景:你在里面打开.fly或.mpt工程文件,把地形、影像、三维模型一层层叠好,保存成工程。到了二次开发阶段,你不再手动操作这个窗口,而是从C#把API调出来,让程序按逻辑自动操作场景。

具体装什么,不同项目组差别很大。有的环境里Pro端装在某台专门的制图机器上,开发机上只装了运行时,这种情况下你也能拿到TerraExplorerX.dll做引用,只是程序跑起来时机器上必须有运行时或Pro本体,否则实例化SGWorld会直接失败。我一般建议开发机直接装完整Pro端,调试时省得来回倒腾。

版本匹配是另一个容易被忽略的点。TerraExplorer的版本号跟着大版本走,不同版本对应的TerraExplorerX.dll和主接口名字会有差异,接口名后缀常见有版本号数字。工程文件如果是高版本保存的,低版本运行时打开就会提示版本不支持。所以开工前先确认手上的.fly是哪个版本做的,再选对应API,别把时间浪费在排查加载失败上。

平台目标也需要现在就定好。TerraExplorerX的COM组件在多数版本里是32位进程,工程属性→生成→平台目标选x86,别选"Any CPU"或x64。这个配置在开发初期不显眼,等你在运行时引入更多依赖后再返工,改动面就大了。

2.2 创建C#工程并添加COM引用

在VS里新建一个.NET Framework的WinForms或控制台工程。目标框架建议选.NET Framework 4.6.1或4.8,原因第5章会详细讲,跟COM互操作和事件模型都有关。先别碰.NET Core/.NET 5+那套,除非你熟悉互操作调试,否则踩坑成本高。

在解决方案里右键"引用"→"添加引用"→"COM"选项卡,找到TerraExplorerX类型库。如果列表里没有,就点"浏览"去TerraExplorer安装目录找TerraExplorerX.dll和旁边的.tlb文件,手动指定。添加完成后,工程里会生成互操作程序集,代码中就可以using TerraExplorerX;了。

关键一步:选择引用项,在属性面板中把"嵌入互操作类型"设为False。这个设置在旧版VS里叫"Embed Interop Types"。如果保持默认的True,编译期通常不报错,运行期会抛类型加载异常,具体表现就是第5章的第一条。

如果你的业务不是单机演示,而是要集成到一套现成的业务系统里,建议把二次开发代码单独放到一个类库,界面工程引用这个类库。好处是COM相关初始化、资源释放都收敛在一个程序集里,后续做单测或者换UI框架时,改动面可以控制在类库内部。

2.3 第一个最小示例:创建SGWorld并打开工程

引用配好,先写一个最小demo验证链路。打开工程→显示窗口→执行成功,这条最短链路通了,后面所有模块才有意义。

using System; using TerraExplorerX; namespace TerraExplorerDemo { class Program { static void Main(string[] args) { // 实例化SGWorld,参数传null表示不传启动配置 SGWorld sgworld = new SGWorld(null); // 打开一个现有的TerraExplorer工程文件 string projectPath = @"D:\projects\demo.fly"; sgworld.Project.Open(projectPath); // 让主窗口显示出来 sgworld.Project.ShowWindow(true); } } }

逻辑说明:第一行new SGWorld(null)会启动TerraExplorer的ActiveX窗口并返回全局对象,Project.Open把.fly工程载入内存,ShowWindow(true)把窗口带出来。如果你做的是后台批处理场景,不需要弹出窗口,ShowWindow可以不调用,或者传false。打开失败时Open返回false,同时会在TerraExplorer运行日志里记录原因,建议先看日志再查代码。

参数说明:SGWorld构造函数接受一个启动配置字符串,可传窗口模式、启动画面、自动登录等参数,多数二次开发场景传null就够。日志文件一般落在TerraExplorer安装目录的Logs文件夹,文件名形如TE_yyyyMMdd.log,查看时按最新文件排序即可。

跑通之后,你应该能看到TerraExplorer窗口弹出,工程地形模型加载完毕。如果提示缺数据,检查工程里引用的是相对路径还是绝对路径——工程换机器后,相对路径的资源经常找不到,这是脚手架阶段的常见坑。

提示:如果new SGWorld(null)抛"类未注册"异常,说明TerraExplorerX.dll注册状态不对,或者你引用的是32位组件但进程跑在64位。优先检查运行库安装状态与工程平台目标位数。

这个例子还引出一个习惯:每次运行前确认工程文件路径存在。很多人一开始把路径写成字符串硬编码,运行时报找不到文件,第一反应是代码问题,其实只是路径失效。我习惯在Open之前先File.Exists检查,再决定是提示还是自动跳过,后面调试会顺很多。

3. 核心对象模型拆解:SGWorld、信息树与坐标体系

3.1 SGWorld是全局入口,但不只是一个工厂

在TerraExplorer二次开发里,SGWorld是你拿到手的一个全局对象,它底下挂着几乎所有子对象。我见过不少刚上手的开发者只把它当成"创建对象的工厂",调用完CreatePoint就扔到一边。其实SGWorld身上还有几个重要属性值得留意,搞清楚它们是理解整套API的关键。

比较常用的几个:Project管整个工程的打开、保存、关闭;Navigate管视角和漫游;Creator管创建各类要素对象;Terrain管地形相关操作;InformationTree管场景里所有图层的目录结构。有些版本还有Window和Draw,分别对应窗口交互和绘图覆盖层。写代码前先把自己这次要干的事对到对应模块上,能少走很多弯路。

// 拿全局对象 SGWorld sgworld = new SGWorld(null); // 分别拿到子模块 IProject project = sgworld.Project; INavigate navi = sgworld.Navigate; ICreator creator = sgworld.Creator; ITerrain terrain = sgworld.Terrain; IInformationTree tree = sgworld.InformationTree;

逻辑说明:这段代码展示了从SGWorld取子模块的标准写法。接口名在不同小版本会有细微差异,比如有的版本把Navigate声明成INavigate5或INavigate7,编译时报错就先在对象浏览器里核对全名,别硬记。

3.2 信息树:场景的目录骨架

信息树(InformationTree)是TerraExplorer场景里的"图层管理器",树上每一项节点代表场景里的一个对象,可以大到整个地形数据集,小到一个点标注。二次开发里增删改查都绕不开它,跟你在桌面工具里看到的信息树面板是同一个东西。

// 获取信息树中指定ID的节点 IInformationTree tree = sgworld.InformationTree; // 创建一个分组,并挂到树的根节点下 IGroup group = creator.CreateGroup("MyGroup", 0, 0); tree.AddGroup(group, tree.NodeCount + 1);

逻辑说明:CreateGroup创建一个分组对象,前两个参数分别是分组名称和父节点ID,0通常代表根节点;AddGroup把它挂进信息树,第二个参数传节点位置。分组建好之后,后续创建的点、线、模型都可以放进去,方便统一管理。

参数说明:分组名字会直接出现在TerraExplorer窗口的信息树面板里,起名规则尽量带上业务语义,比如设备点分组就叫"设备_厂区A",后期在信息树里点选排查时一眼能定位。

3.3 坐标系与位置:IPosition是一切定位的基础

在三维地球场景里定位,光给经纬度不够,还要海拔、朝向、俯仰角、翻滚角。TerraExplorer用IPosition对象封装这些参数。创建位置对象的API参数比较多,我一般会把参数名和顺序写在注释里,防止隔一个月回来自己都忘了。

// 创建位置对象:经纬度、海拔、高度模式、朝向、俯仰、翻滚 IPosition pos = creator.CreatePosition( "116.3913, 39.9075, 100", // 经度,纬度,海拔(字符串形式) AltitudeType.ALT_GROUND, // 高度模式:贴地 0, // 朝向(yaw),单位:度 0, // 俯仰角(pitch) 0 // 翻滚角(roll) );

逻辑说明:CreatePosition返回一个IPosition对象,它既可用于Navigate.FlyTo定位视角,也可用于给标注点、模型设置位置。这里用字符串传坐标比三个浮点参数更直观,特别是在调试时可以直接把度分秒字符串拼进来。

参数说明:海拔模式是关键参数:ALT_GROUND表示贴地,ALT_ABSOLUTE表示绝对海拔,ALT_RELATIVE_TERRAIN表示相对地形。做设备点位标注时习惯用ALT_GROUND让它吸附地表;做飞行漫游时往往用ALT_ABSOLUTE确保视角不会被地形挡住。

3.4 两种初始化方式:独立进程与控件嵌入

TerraExplorer二次开发还有一条重要的分叉路:你是要让TerraExplorer以独立窗口运行,还是把它当控件嵌到自己写的界面里。独立窗口模式下,你只需要new SGWorld然后控制它;嵌入模式则需要把TerraExplorer的ActiveX控件拖进WinForms/WPF窗口,再通过控件的对象引用拿SGWorld。

嵌入模式的本质是ActiveX互操作。WinForms里从工具箱拖入控件后,VS会自动生成AxTerraExplorerX类型的包装类,持有一个SGWorld引用。做法上,我会在窗体加载事件里先实例化SGWorld,再把它绑定到控件上,顺序反了经常拿不到窗口句柄。

// WinForms嵌入方式示例 private void Form1_Load(object sender, EventArgs e) { // 在控件容器中启动SGWorld,部分版本通过该方法绑定 sgworld = new SGWorld(null); axTerraExplorerX1.SetSGWorld(sgworld); }

逻辑说明:嵌入模式最大的价值是和现有业务系统融合,把三维场景塞进自己的管理界面里,旁边放设备列表、属性表格、操作按钮。代价是事件、消息循环和控件生命周期都要自己管理,复杂度比独立窗口高一档。第一次做嵌入的项目建议先跑通独立窗口,再往界面里缩。

4. 实战功能模块:加载数据、飞行漫游与要素标注

4.1 打开工程与追加数据源

工程打开是基础,但实际开发里经常碰到"程序启动后临时追加图层"的需求。比如设备巡检系统运行时,要根据数据库里的点位动态生成标注,而不是每次启动都重新打开工程。

// 打开工程 sgworld.Project.Open(@"D:\projects\site.fly"); // 追加一个影像或地形数据源 bool ok = sgworld.Project.AppendToProject(@"D:\data\tile.mpt");

逻辑说明:AppendToProject会把第二个工程文件的数据叠加到当前场景里,也就是说你可以把底图工程和业务数据工程分离:底图由制图人员维护,业务数据在运行时用代码追加。这个分离思路在项目里非常实用。

参数说明:追加失败时返回false,并不抛异常。很多新手在这里翻车——拿不到返回值以为成功了,结果场景里什么都没有。我习惯每步操作后都检查返回值,并配合信息树节点数量判断。

4.2 飞行漫游:视角控制与巡检路线

飞行是TerraExplorer最常用的交互之一。Navigate.FlyTo接收一个IPosition对象,视角会平滑飞过去;如果你想要一条固定的巡检路线,就需要沿途插值多个位置点。

// 定义视角起点和终点 IPosition posA = creator.CreatePosition("116.3913,39.9075,300", AltitudeType.ALT_ABSOLUTE, 0, -60, 0); // 俯仰角-60表示俯视地面 IPosition posB = creator.CreatePosition("116.3920,39.9068,300", AltitudeType.ALT_ABSOLUTE, 45, -60, 0); // 朝向45度,仍然俯视 // 采用Arc飞行模式(曲线插值) bool flyOk = navi.FlyTo(posA, 3000, FlightMode.ARC); flyOk = navi.FlyTo(posB, 3000, FlightMode.ARC);

逻辑说明:FlyTo的参数依次是目标位置、飞行时长(毫秒)、飞行模式。FlightMode.ARC是曲线飞行,看起来更平滑;FlightMode.LINEAR是直线飞。巡检场景用ARC更接近无人机航线。需要注意的是,FlyTo的时长是动画总时长,不是每帧步长。

参数说明:如果路线点比较多,比如上百个点,连续调用FlyTo会把中间过程省略,视觉上直接跳变。常见做法是先算出整条路线的关键点,再在每两个关键点间插入足够密的过渡位置,逐个FlyTo,达到匀速巡航的效果。点的密度取决于场景范围,我一般先按每100米一个点试,效果不行再加密。

4.3 给场景添加业务标注与图标

给场景加业务数据,核心是把数据库里的记录转成场景里的标注点或图标。常见做法是为每台设备创建一个"点+标注"的组合:点负责定位,标注负责展示文字。这里还要把颜色、图标这些业务属性穿进去。

// 创建位置对象 IPosition pos = creator.CreatePosition(lon, lat, alt, AltitudeType.ALT_GROUND, 0, 0, 0); // 创建圆点标注:绿色、半径10像素 var point = creator.CreatePoint(pos, 0xFF00FF00, 10); // 创建文本标注:白色文字,字号2 var label = creator.CreateLabel(pos, "设备A-1", 0xFFFFFFFF, 2);

逻辑说明:CreatePoint创建的是一个绘制点对象,第三个参数是点尺寸(像素);CreateLabel创建文本标注。两者共享同一个IPosition,所以文字和圆点会叠在一起。颜色统一用ARGB整数,注意是0xAARRGGBB的顺序,绿色是0xFF00FF00,白色是0xFFFFFFFF。

参数说明:文本标注的字号参数在不同版本里含义不太一致,有的是绝对像素,有的是相对场景比例。建议先用小字号试,再根据窗口缩放调整。如果要把标注批量创建几百个,注意给每个对象设置独立的ID或分组,否则后期信息树里不好区分。

4.4 鼠标拾取:从屏幕坐标到地理坐标

鼠标拾取本质上是采集鼠标在场景中的点击位置,再换算成地理坐标,然后查询该位置附近的对象。TerraExplorer的鼠标事件是通过SGWorld的事件接口注册的,遵循COM事件模型。先注册事件对象并挂上回调,再解析坐标。

// 事件接收类 public class AppEvents : ISGWorldEvents { public void OnMouseClick(int x, int y, int flags) { // 把屏幕坐标转成3D地理坐标 IPosition pos = sgworld.Window.GetPositionFromScreenCoords(x, y); // 在坐标点做进一步查询:拾取对象、查属性等 object obj = sgworld.Window.GetObjectAtPosition(x, y, 0); } }

逻辑说明:这段写法是COM事件处理的常规套路——新建一个类实现事件接口,事件触发回调里做坐标转换和对象拾取。屏幕上点击的(x,y)和地理坐标不是一回事,中间隔着一层投影换算,GetPositionFromScreenCoords就是做这件事的。

参数说明:flags里通常能读出Shift/Ctrl组合键以及左右键标记。这个方法本身没有双击事件,要想区分单击和双击,需要在回调里记录点击时间戳和坐标,两次点击间隔小于某个阈值且坐标接近时判定为双击。习惯Web地图事件模型的开发者最容易在这里不适应。

5. 避坑记录:COM互操作阶段最常见的几个翻车现场

5.1 现象:编译能过,运行时报"无法加载指定的程序集"

很多项目第一次跑TerraExplorer示例代码就崩在这儿。代码写得没错,编译也通过,但一运行就抛异常,异常信息里常带"无法加载指定的程序集"或者类似的说法。

原因:VS把引用项的"嵌入互操作类型"设成了True,运行时它尝试把COM互操作类型嵌入到你的程序集里,但TerraExplorerX的类型库版本和本机运行时不匹配,导致类型加载失败。这个设置在.NET Framework项目里默认行为因VS版本而异,很容易踩中。

解决:把引用项的"嵌入互操作类型"改为False,并在工程属性→生成→平台目标里设为x86。这个组合在.NET Framework下基本是标准答案,改完后重新编译运行,大部分类型加载问题会消失。

5.2 现象:相同代码,WinForms能跑,WPF里闪退

同一个工程代码在WinForms里正常,搬到WPF窗口后就闪退,而且往往不是启动时立刻闪退,而是初始化到一半消失。这种问题没有异常堆栈可查,排查起来非常难受。

原因:WPF自身是DirectX渲染技术,窗口句柄(HWND)的创建方式和WinForms完全不同,ActiveX控件不能直接被放进WPF的可视化树里。必须通过WindowsFormsHost做托管容器,很多第一次做的开发者忽略了这个中间层。

解决:在XAML里放一个WindowsFormsHost,然后把TerraExplorer的UserControl放进去,顺序是"WinForms宿主→Ax控件→SGWorld初始化"。另外,WPF启动线程也必须是STA,Program入口的Main方法要标[STAThread],否则COM组件会拒绝在MTA线程里创建。

5.3 现象:事件注册成功但一个回调都收不到

鼠标事件注册了,方法签名、委托类型都对,编译、运行都没报错,但点击场景就是没有任何反应。这是TerraExplorer二次开发里最玄学的一种情况,我见过有人排查了一整天。

原因:COM事件是连接点(Connection Point)机制,TerraExplorer会把事件接口指针保存下来,等事件发生时回调你的对象。如果你的事件接收对象是在某个方法里new出来的局部变量,方法结束后它就被托管GC回收了,TerraExplorer再想回调就找不到目标,事件自然丢失。

解决:事件对象必须保存为某个字段,保持强引用,同时不要手动释放。初始化时给字段赋值,就能保证整个生命周期内事件正常。这个坑的排查思路其实可以推广到所有COM事件类开发。

5.4 现象:程序退出后,工程文件仍被占用无法修改

在代码里调用场景关闭或者退出程序,但回到资源管理器里去删除或重命名.fly文件时提示文件被占用。有时候重启电脑才好,烦人程度很高。

原因:TerraExplorer的进程没有真正退出,或者COM对象的生命周期没有收尾。你只关掉了窗口,但SGWorld背后的原生进程仍然持有工程文件句柄。这类COM资源不透明性在Skyline系列组件里很常见,看不见进程,但文件锁确实在。

解决:在应用退出前显式调用SGWorld的关闭方法,再用Marshal.ReleaseComObject逐层释放COM引用,最后延迟几百毫秒再结束进程。我一般还会检查任务管理器里是否有残留的TE进程,有的话说明关闭流程没走完,需要补上进程结束逻辑。

6. 二次改造思路:把常用操作封装成一个小工具类

6.1 为什么封装

实际项目里不会只有一次飞行或一次加载,而是"打开工程→追加数据→生成标注→飞行巡线→拾取查询"这样一条完整链路。把这些操作散落在窗体代码里,后期维护会非常痛苦。封装思路是把SGWorld的所有访问集中在一个托管类里,业务代码只和这个类打交道。

6.2 封装实现

public class TerraExplorerHelper : IDisposable { private readonly SGWorld _sgworld; private readonly AppEvents _events = new AppEvents(); public TerraExplorerHelper() { _sgworld = new SGWorld(null); } public bool OpenProject(string path) { if (!System.IO.File.Exists(path)) return false; return _sgworld.Project.Open(path); } public bool AppendData(string path) { return _sgworld.Project.AppendToProject(path); } public bool FlyTo(double lon, double lat, double alt, int durationMs) { IPosition pos = _sgworld.Creator.CreatePosition( $"{lon},{lat},{alt}", AltitudeType.ALT_ABSOLUTE, 0, -60, 0); return _sgworld.Navigate.FlyTo(pos, durationMs, FlightMode.ARC); } public void AddLabel(double lon, double lat, string text) { IPosition pos = _sgworld.Creator.CreatePosition( $"{lon},{lat},0", AltitudeType.ALT_GROUND, 0, 0, 0); _sgworld.Creator.CreateLabel(pos, text, 0xFFFFFFFF, 2); } public void Dispose() { // 释放前先保存或关闭工程,顺序不能反 _sgworld.Project.Save(@"D:\projects\cache.fly"); Marshal.ReleaseComObject(_sgworld); } }

逻辑说明:这个工具类把前面所有核心操作收成一个个带参数的方法,业务层完全不需要知道IPosition这些COM对象怎么构造。Dispose里做了两件事:保存现场和释放COM引用,顺序不能反——先释放再拿对象引用必然崩。

参数说明:AddLabel里海拔固定为0配合ALT_GROUND,表示贴地标注;FlyTo用了ALT_ABSOLUTE保证飞行高度不随地形起伏,避免飞行路径被山体截断。如果你希望沿地形巡航,把海拔模式换成相对地形并传一个固定海拔即可。

6.3 验证方法

封装完之后,验证路径我一般按三步走:先跑一遍最小链路验证基础能力(打开工程→飞到一个坐标→加上一个标注);再跑一遍完整业务链路(追加数据→批量生成20个标注→执行巡线→随机点拾取);最后在窗体关闭事件里调用Dispose,观察进程是否干净退出。

有一次我把Dispose写在窗口关闭事件里,结果发现文件锁没释放,后来把Dispose放到进程退出事件里并加了200ms的延迟才彻底解决。从那以后,我每次做TerraExplorer二次开发都强制走一遍"最小链路→完整链路→退出验证"三步,再也不在COM上吃暗亏。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询