简介:面向具备一定C#基础、希望基于Skyline平台开展三维GIS定制开发的开发者,这份TerraExplorer二次开发示例代码围绕SDK调用、事件驱动编程、地图对象与控件操作、多格式数据集成、3D模型加载、空间查询分析及自定义界面等核心知识点组织,可帮助读者理解如何将C#与TerraExplorer API结合,完成从地图加载到场景交互的完整开发流程。压缩包共21个文件,约42KB,以cs源码文件为主,辅以resx资源、csproj与sln工程文件,并附带shp、shx、dbf、prj等Shapefile数据及fly飞行文件,便于直接打开工程对照调试。目前已有484人学习下载,适合作为入门参考与功能验证的起点,读者可从中获取可运行的工程骨架、典型API调用范例与数据加载思路,减少自行摸索SDK的成本。
1. 从一份 C# 示例包说起:TerraExplorer 二次开发到底能做什么
如果你手头拿到的是一个三维地理信息平台的 C# 示例工程,第一反应大概率是“这堆文件从哪看起”。TerraExplorer 这类平台的核心价值不在它自带的浏览界面,而在于它对外暴露的 COM 接口——你能用 C# 把三维场景、图层、标注、飞行路径、动态对象全部用代码驱动起来。这份示例代码包就是干这个的:它把常见的二次开发动作拆成一个个可运行的小工程,覆盖场景加载、对象创建、事件响应、界面嵌入几条主线。适合谁?适合已经能打开平台、但不知道怎么把业务逻辑塞进三维场景里的开发者,也适合需要把三维视图嵌进自己 WinForms 或 WPF 桌面程序的团队。下面按“先看清结构、再跑起来、最后动手改”的顺序拆。
2. 工程结构与技术栈:先搞清楚这堆文件谁调谁
2.1 示例包的典型目录布局
拿到一个 C# 二次开发示例包,别急着双击 sln。先看目录,它基本决定了你后面改代码时该往哪放。这类示例通常长这样:
TerraExplorerSamples/ ├── Samples.sln ├── Common/ │ ├── ComInterop/ # 平台 COM 接口的互操作封装 │ └── Helpers/ # 坐标转换、路径拼接等工具类 ├── BasicScene/ # 场景加载与相机控制示例 ├── ObjectCreate/ # 三维对象、标注、模型创建示例 ├── EventDemo/ # 事件订阅与回调示例 ├── EmbedView/ # 把三维视图嵌入自有窗体示例 └── packages.config / *.csprojCommon是公共层,互操作封装和工具类都在这,改任何示例前先确认它引用的平台组件版本和你本机装的一致。BasicScene到EmbedView是按功能切的独立工程,每个都能单独启动,互不依赖。这种“一个功能一个工程”的组织方式对二次开发很友好——你要验证某个接口行为,直接跑对应工程,不用在巨型项目里翻。
2.2 技术栈与运行前提
这套示例的技术栈很集中:C# + .NET Framework(多数是 4.x)+ 平台 COM 组件 + WinForms 宿主。没有 Web 那一套,也没有跨平台诉求。运行前提有三条必须满足:本机装了对应版本的三维平台运行时;示例工程的目标平台(x86/x64)和平台组件位数一致;COM 组件已正确注册。第三条是最容易翻车的地方,后面避坑章会细说。
提示:位数不一致是这类 COM 互操作项目最常见的启动失败原因,先确认再调代码。
2.3 互操作层是怎么封装的
示例里对平台接口的调用不会直接裸写dynamic,而是走一层封装。典型写法是用[ComImport]声明接口,再用一个工厂类统一创建和释放:
// ComInterop/SceneFactory.cs [ComImport] [Guid("...")] // 平台接口的 GUID,示例里已填好 [InterfaceType(ComInterfaceType.InterfaceIsIDispatch)] public interface ISceneControl { void LoadScene(string filePath); // 加载场景文件 void SetCamera(double lon, double lat, double alt); // 相机定位 object CreateObject(string typeName); // 按类型创建对象 } public static class SceneFactory { public static ISceneControl Create() { // 通过平台提供的 ProgID 创建实例 var type = Type.GetTypeFromProgID("Platform.SceneControl"); if (type == null) throw new InvalidOperationException("平台组件未注册"); return (ISceneControl)Activator.CreateInstance(type); } }这段代码的关键在Type.GetTypeFromProgID:它按注册表里的 ProgID 找组件,找不到就说明运行时没装或没注册。Guid特性里的 GUID 必须和平台文档一致,示例里已经填好,你改的时候别动。CreateObject返回object,实际用的时候要按具体对象类型再转一次,这是 COM 互操作的常态,不是示例写得糙。
2.4 一个示例工程的启动链路
以BasicScene为例,启动链路是:Program.Main→ 创建主窗体 → 窗体Load事件里调SceneFactory.Create()→ 调LoadScene加载示例场景 → 订阅相机变化事件 → 界面就绪。这条链路里每一步都能断点,排查时按顺序看哪个环节返回空或抛异常,比盲猜快得多。示例把“创建—加载—订阅”三步拆得很清楚,你二次开发时新增功能,照着这个顺序往链路里插就行。
3. 把示例跑起来:从环境检查到第一个场景
3.1 环境检查清单
跑之前先过一遍清单,能省掉大半折腾:平台运行时是否安装、组件是否注册、示例目标平台位数、.NET Framework 版本、示例场景文件路径是否存在。前两项用平台自带的检测工具或注册表确认,后三项在工程属性里看。示例场景文件通常是相对路径引用,挪动工程目录后路径会失效,这是新手最常踩的坑之一。
3.2 编译与首次运行
用 Visual Studio 打开Samples.sln,先只编译BasicScene一个工程,别一上来全量编译。全量编译会把所有示例的依赖都拉一遍,任何一个工程配置不对都会报错,干扰判断。单工程编译通过后直接 F5,正常情况会弹出窗体并加载出三维场景。如果窗体出来了但场景是黑的,先看场景文件路径,再看相机初始位置是否落在有效范围内。
# 命令行编译单个工程(在解决方案目录下) msbuild BasicScene\BasicScene.csproj /p:Configuration=Debug /p:Platform=x86/p:Platform=x86要和平台组件位数一致,示例默认可能是AnyCPU,跑 COM 组件时AnyCPU在 64 位系统上会按 64 位加载,位数不匹配就起不来。这条命令的含义是显式指定目标平台,避免运行时位数错配。
3.3 相机控制示例怎么读
BasicScene里最值得先读的是相机控制那段,因为它把“坐标怎么传、视角怎么设”讲得最直白:
// 设置相机到指定经纬度和高度,并朝向某个方位角 scene.SetCamera(116.39, 39.90, 500); // 经度、纬度、高度(米) scene.SetCameraOrientation(45.0, -30.0); // 方位角、俯仰角(度)SetCamera三个参数单位分别是度、度、米,别把高度当成公里传,传大了相机直接飞到外太空,场景看着就是空的。SetCameraOrientation的俯仰角负值表示向下看,这个符号约定示例里没写注释,但试一次就能确认。读示例代码时遇到这种“没注释但能试出来”的参数,建议自己补一行注释再往下走,后面改代码时省得再试一遍。
3.4 对象创建示例的最小闭环
ObjectCreate演示的是往场景里加东西,最小闭环是“创建对象 → 设位置 → 设样式 → 加入场景”:
var marker = scene.CreateObject("Marker"); // 创建标注对象 marker.SetPosition(116.39, 39.90, 100); // 经纬度 + 高度 marker.SetProperty("Color", "#FF0000"); // 样式属性按字符串键设置 scene.AddObject(marker); // 真正加入场景才可见四步里最容易漏的是最后一步AddObject,对象创建了但没加入场景,界面上什么都不会出现,排查时会误以为创建失败。SetProperty的键名是平台约定的字符串,示例里用到的键可以直接抄,没用到的查平台接口文档。这套“创建—配置—加入”的模式在后续所有对象类型上都通用,读通一个就等于读通一类。
4. 核心模块拆解:事件、嵌入与数据交互
4.1 事件订阅与回调线程
三维场景是动态的,相机移动、对象点击、场景加载完成都会触发事件。示例里的事件订阅写法是标准的 .NET 事件模式:
// 订阅相机变化事件 scene.CameraChanged += OnCameraChanged; private void OnCameraChanged(double lon, double lat, double alt) { // 回调可能不在 UI 线程,更新控件前要切回来 if (InvokeRequired) { BeginInvoke(new Action(() => UpdateStatus(lon, lat, alt))); return; } UpdateStatus(lon, lat, alt); }关键点在InvokeRequired那段:COM 组件的回调线程不保证是 UI 线程,直接更新控件会抛跨线程异常。示例里加了线程切换,你二次开发时新增事件处理也要照这个模式写。漏了这一步,程序跑起来看着正常,一操作就崩,属于典型的“玄学崩溃”,其实是线程问题。
4.2 把三维视图嵌进自有窗体
EmbedView是这份示例里对二次开发最有价值的部分,它演示怎么把平台的三维视图作为控件嵌进你自己的 WinForms 界面,而不是单独弹一个平台窗口。核心步骤是创建宿主控件、设置父窗口句柄、通知平台完成嵌入:
var host = new Panel { Dock = DockStyle.Fill }; this.Controls.Add(host); // 把平台视图的父窗口设为宿主控件句柄 scene.SetParentWindow(host.Handle); scene.Resize(host.Width, host.Height); // 首次嵌入后手动同步尺寸SetParentWindow传的是 Win32 窗口句柄,所以宿主必须是已有句柄的控件(Panel在加入窗体后才有句柄,顺序不能反)。Resize首次要手动调一次,之后靠宿主控件的Resize事件转发给平台视图,否则拖动窗体时三维视图尺寸不跟着变,界面会很难看。这个“句柄 + 尺寸同步”的组合是嵌入类开发的标准套路,记住这两步就够用了。
4.3 场景数据与业务数据的桥接
示例里对象和业务数据的关联方式值得单独说:平台对象只负责“显示”,业务属性存在你自己的字典或数据表里,用对象 ID 做键关联。示例里能看到类似Dictionary<string, BizData>的结构,对象创建后把返回的 ID 存进去,点击事件里拿 ID 反查业务数据。这种“显示与数据分离”的做法比把业务字段全塞进对象属性里更可控,后面换数据源或加字段都不用动三维那层。
4.4 资源释放与生命周期
COM 对象用完要释放,示例里在窗体关闭时统一调ReleaseComObject并置空引用。漏了这步,反复打开关闭窗口会累积未释放的组件实例,表现为内存上涨、偶尔卡死。示例的释放逻辑集中在窗体FormClosing里,你新增的每个 COM 对象都要纳入同一套释放流程,别散落在各处。
5. 避坑与排查:这几处翻车我见过太多次
5.1 组件未注册导致创建实例失败
现象:SceneFactory.Create()抛InvalidOperationException,提示组件未注册。原因:平台运行时没装,或装了但 COM 组件没注册(换机器、重装系统后常见)。解决:确认运行时安装完整,用平台自带的注册工具重新注册组件,再跑一次。别急着重装整个平台,先确认注册表里 ProgID 是否存在。
5.2 位数不匹配导致进程直接退出
现象:编译通过,一运行进程就退出,没有明显异常信息。原因:示例目标平台是AnyCPU,在 64 位系统上按 64 位加载,而平台组件是 32 位。解决:把工程目标平台显式改成x86(或与组件一致的位数),重新编译。这个坑隐蔽在于它不报错,只是静默退出,第一次遇到很容易怀疑代码。
5.3 跨线程更新控件引发崩溃
现象:操作场景时程序偶发崩溃,堆栈指向控件更新。原因:事件回调在非 UI 线程执行,直接改了控件。解决:所有事件处理里更新界面前加InvokeRequired判断并切回 UI 线程,参照 4.1 的写法。偶发崩溃最难查,养成“回调里先判线程”的习惯能省很多时间。
5.4 场景文件路径失效导致黑屏
现象:窗体正常,三维区域全黑。原因:示例场景文件用相对路径,工程目录挪动后路径失效。解决:改成绝对路径或把场景文件随工程一起拷贝,并在加载前判断文件是否存在,不存在就给出明确提示,别让它静默黑屏。
5.5 嵌入后尺寸不同步
现象:三维视图嵌进自有窗体后,拖动窗口时视图尺寸不变,出现留白或裁切。原因:只调了一次Resize,没把宿主控件的Resize事件转发给平台视图。解决:在宿主控件的Resize事件里调scene.Resize,并注意首次嵌入后手动补一次。
6. 二次开发改造:从跑通示例到落地自己的功能
跑通示例只是起点,真正的工作是把示例里的模式套到自己的业务上。我的做法是先定一条最小改造路径:选一个最接近需求的示例工程(比如要做标注管理就选ObjectCreate),在它基础上加一个自己的业务模块,而不是从空工程重新搭。这样互操作封装、事件线程处理、资源释放这些容易出错的底层逻辑直接复用,你只专注业务。
改造时几个具体技巧。第一,把Common层当稳定层,业务代码不要往里塞,互操作封装一旦被业务逻辑污染,后面升级平台组件会很痛苦。第二,新增对象类型时先写一个最小验证:创建、设位置、加入场景、点击能拿到 ID,四步都通了再往上叠业务属性。第三,事件处理统一走一个分发器,别在每个窗体里各写一套InvokeRequired,集中处理线程切换和异常捕获,出问题时只有一个地方要看。
验证方法上,我一般给每个改造点配一个可独立运行的验证入口,比如一个只加载场景并创建单个对象的按钮,点一下看结果。三维开发的问题往往表现为“界面没反应”,没有独立验证入口就只能靠猜。下面这个表格是我常用的改造检查项:
| 检查项 | 验证方式 | 失败表现 |
|---|---|---|
| 组件创建 | 调SceneFactory.Create() | 抛异常或返回空 |
| 场景加载 | 加载后看相机是否就位 | 黑屏 |
| 对象可见 | 创建后调AddObject | 无显示 |
| 事件回调 | 操作后看状态栏更新 | 不更新或崩溃 |
| 资源释放 | 反复开关窗口看内存 | 内存持续上涨 |
有个教训值得说:早期我图省事,把业务数据直接塞进平台对象的自定义属性里,结果换数据源时发现属性键名和平台版本绑定,升级后一批键失效,返工量很大。从那以后我每次做对象与数据关联,都强制走“对象 ID + 外部字典”这一层,平台对象只当显示用。希望这些能帮你在拿到示例包时少走几步弯路。
本文还有配套的精品资源,点击获取