WPF无人机地面站开发实战:MVVM架构、串口遥测与性能优化指南
发布时间:2026/9/28 15:51:17来源:尧图网络
简介这是一份基于WPFWindows Presentation Foundation技术开发的无人机地面站控制系统完整工程源码面向无人机爱好者、本科毕业设计学生及桌面端上位机开发者。项目以C#和XAML构建界面借助MAVLink协议实现与无人机的通信覆盖飞行状态实时监控、GPS定位显示、任务规划执行及数据采集分析等核心功能模块。压缩包内含243个文件约34.83MB以cs源代码、xaml界面布局、dll依赖库及xml配置文件为主同时包含png/jpg图表资源、sqlite数据库文件与sln解决方案便于直接打开编译或二次开发。资源整体结构完整、目录清晰适合学习WPF数据绑定、MVVM分层以及无人机地面站软件的整体设计思路。已有83人学习下载对于需要快速上手WPF项目实战或完成无人机相关毕设任务的读者具有较高的参考价值。1. 无人机地面站控制系统WPF版本到底在解决什么问题不是换皮肤是换数据链路接手过的地面站项目里绝大多数旧代码是 WinForm 写出来的。串口收一包数据就往文本框里塞一次地图控件一秒钟刷好几遍UI 线程忙到鼠标都发飘。后来我把其中一套重构成了 WPF 版本界面只是副产品真正的收益来自绑定、命令和消息分发这套机制。换句话说无人机地面站控制系统WPF版本解决的不是“界面好不好看”而是“遥测数据怎么流到界面上不乱、不卡、不错位”。这篇笔记写给两类人一类是要把老 WinForm 地面站改写成 WPF 的另一类是从零做飞控配套上位机的。我会按工程顺序拆开讲——先立框架再做通信再画地图最后讲我实际踩过的坑和性能调优手段。跟着步骤走能把一个最小可用版本跑起来熟手可以直接跳到第 5 章看边界条件。2. 先把 WPF 地面站骨架立住MVVM 分层、控件库与工程结构2.1 四层骨架地面站为什么必须用 ViewModel 驱动而不是代码后置WPF 里最常见的错误是延续 WinForm 习惯把所有逻辑写进 MainWindow.xaml.cs。地面站的功能注定了这条路走不通一个飞手在界面上点“起飞”背后要经过命令封装、串口下发、遥测回报、状态回读四个环节这些环节如果直接操作控件代码很快就变成一团浆糊。我一般会把工程拆成四层View 层只放 XAML 和绑定表达式不写业务ViewModel 层一个主窗口对应一个主 VM负责把服务层的数据翻译成界面状态Service 层串口服务、协议解析、航迹管理都在这层与 UI 完全隔离Model 层遥测帧、飞机状态、航迹点这些纯数据类。页面绑定关系是MainWindow 的 DataContext 指向 MainViewModelMainViewModel 持有 FlightService 和 TelemetryModel。地面站里绝大多数字段是单向绑定遥测数据从 Model 流向界面只有参数配置项用 TwoWay。用单向来绑定有个隐形好处忘记给属性写通知时界面不会报错只是不更新这比在代码后置里到处撒赋值语句好排查得多。下面是 GroundStationService 的最小组装逻辑用到了依赖注入来解耦串口和协议解析避免 VM 里直接 new 一个 ConcreteService// App.xaml.cs 中的组装入口把服务注册到容器并构建主窗口 VM public partial class App : Application { protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); // 用微软扩展库的 ServiceCollection 做注册不是必须但比手工 new 清晰 var services new ServiceCollection(); services.AddSingletonILogService, LogService(); services.AddSingletonIUavProtocol, UavProtocolParser(); services.AddSingletonIFlightService, FlightService(); // 所有服务都在主线程外的后台线程启动界面只管订阅结果 var provider services.BuildServiceProvider(); var flightService provider.GetRequiredServiceIFlightService(); flightService.Start(); var vm new MainViewModel(flightService, provider.GetRequiredServiceILogService()); var window new MainWindow { DataContext vm }; MainWindow window; window.Show(); } }参数说明Startup 里组装的顺序决定了服务启动时机IFlightService.Start() 必须放在 MainWindow.Show() 之前否则遥测事件可能在绑定建立前就丢掉了。ServiceCollection 不引第三方容器也可以但单例生命周期是 WPF 桌面应用最省心的注册方式既不会重复建串口也不会在窗口关闭后残留后台线程。2.2 引入 HandyControl 和 FontAwesome.Sharp 的理由地面站界面有大量“状态指示灯、抽屉面板、告警弹窗”这类控件自己写模板不是不行但维护成本高。常见做法是直接 NuGet 引用 HandyControl 做基础控件库用 FontAwesome.Sharp 做图标。前者提供现成的 Window 样式和通知弹窗后者能直接用免费的 FontAwesome 图标不用自己切图标资源。在 XAML 里把窗口样式换成 HandyControl 之后自定义标题栏就能省掉一整套 mouse drag 逻辑Window x:ClassGs.Wpf.MainWindow xmlns:hchttps://handyorg.github.io/handycontrol xmlns:fahttp://schemas.fontawesome.com/icons/sharp Title无人机地面站 - WPF Width1280 Height800 WindowStyleNone AllowsTransparencyTrue BackgroundTransparent Grid Border Margin8 CornerRadius6 Background#FF20262E Grid RowDefinitions48,*,Auto !-- 标题栏按钮直接用字体图标省掉位图资源 -- StackPanel OrientationHorizontal HorizontalAlignmentRight VerticalAlignmentCenter Button Style{StaticResource IconButton} hc:IconElement.Width18 hc:IconElement.Height18 ForegroundWhite hc:IconElement.Data{fa:Sharp Regular GaugeHigh}/ Button Content#xE8BB; Command{Binding MinimizeCommand}/ Button Content#xE8BB; Command{Binding ShutdownCommand}/ /StackPanel !-- 主内容区域 -- ContentControl Grid.Row1 Content{Binding CurrentPage}/ /Grid /Border /Grid /Window说明WindowStyleNone 之后最小化、关闭按钮的 Command 要在 MainViewModel 里实现不能再靠系统标题栏。FontAwesome.Sharp 有几种字体家族图标 Data 写法用的是 Sharp 系列的 MarkupExtension。首次写界面时会比较折腾图标名称建议把常用图标先列一个常量类后面所有按钮统一引用不要每次都在 XAML 里写死十六进制字符。2.3 命令与消息管道按钮怎么变成“一键起飞”地面站里最核心的交互是“按钮触发飞行动作”如果还在用 Button.Click 事件然后事件里调 service那 MVVM 就白做了。标准做法是 DelegateCommand把“能不能点”和“点了干什么”都交给 ViewModel 判断。public class DelegateCommand : ICommand { private readonly Actionobject? _execute; private readonly Funcobject?, bool? _canExecute; public DelegateCommand(Actionobject? execute, Funcobject?, bool? canExecute null) { _execute execute; _canExecute canExecute; } public bool CanExecute(object? parameter) _canExecute?.Invoke(parameter) ?? true; public void Execute(object? parameter) _execute(parameter); public event EventHandler? CanExecuteChanged; public void RaiseCanExecuteChanged() CanExecuteChanged?.Invoke(this, EventArgs.Empty); }使用方式是在 MainViewModel 里把起飞动作封装成一个命令命令体内部走服务层public class MainViewModel : INotifyPropertyChanged { private readonly IFlightService _flightService; public MainViewModel(IFlightService flightService, ILogService logService) { _flightService flightService; TakeoffCommand new DelegateCommand(_ { _flightService.SendCommand(UavCommandType.Takeoff, altitude: 120); }, _ _flightService.IsConnected); } public ICommand TakeoffCommand { get; } }参数说明CanExecute 里判断 _flightService.IsConnected未连接时按钮自动置灰这比 WinForm 里手动 button.Enabled 靠谱因为连接状态变化时会主动 RaiseCanExecuteChanged。另一个细节不要把 Receive 事件里几十个字段的赋值散落在多个 VM 属性上常见做法是 VM 订阅一个 FlightUpdated 事件每次拿到完整遥测快照后统一刷新。后面第 6 章会讲这个快照怎么合并避免一秒 20 次刷新把 UI 线程打满。3. 通信层怎么写才不卡界面串口参数、UDP 遥测与协议解析3.1 串口参数设置不是填完波特率就完事无人机地面站最常见的链路是“数传电台 串口”和“飞控 UDP 网络”两种我都做过。先讲串口。参数设置界面里至少要有端口号、波特率、数据位、停止位、校验位。这里面最容易翻车的是 Handshake 和 ReadTimeout 这两个默认值。用 SerialPort 做地面站通信我会把参数显式写清楚不吃默认值public sealed class SerialPortOptions { public string PortName { get; set; } COM3; public int BaudRate { get; set; } 115200; public int DataBits { get; set; } 8; public StopBits StopBits { get; set; } StopBits.One; public Parity Parity { get; set; } Parity.None; } public SerialPort BuildPort(SerialPortOptions options) { var port new SerialPort(options.PortName, options.BaudRate, options.Parity, options.DataBits, options.StopBits) { Handshake Handshake.None, ReadTimeout 500, // 避免 Read 方法无限阻塞 WriteTimeout 500, ReceivedBytesThreshold 4, // 凑满一个最小包头就触发事件 }; return port; }参数说明Handshake 设为 None 是多数数传电台的标准用法如果开了 RequestToSend / XOnXOff电台端不配合时串口直接挂死ReadTimeout 一定要设因为读线程可能在电台掉线后还卡在 Read() 里ReceivedBytesThreshold 默认是 1设为 4 是为了减少高频空转让事件在至少收到 4 个字节后才触发。3.2 遥测帧协议解析从原始字节到业务字段多数飞控的串口协议是自定义二进制帧比如帧头 0x7E、消息 ID、长度、Payload、校验。解析的关键是要处理“一包数据被拆成两次收到”和“两包数据粘在一起”这两种情况。我的做法是用一个字节队列做缓冲每次收到新数据先入队再循环尝试从中解析出完整帧。public sealed class UavProtocolParser { private readonly Queuebyte _buffer new(); private readonly Listbyte _frame new(); private const byte FrameHeader 0x7E; public ListUavMessage ProcessChunk(byte[] chunk) { var result new ListUavMessage(); foreach (var b in chunk) { _buffer.Enqueue(b); } while (_buffer.Count 4) // 最小帧头 类型 长度 校验 { // 1. 找帧头 if (_buffer.Peek() ! FrameHeader) { _buffer.Dequeue(); continue; } var bytes _buffer.ToArray(); int payloadLen bytes[2]; // 假设消息类型占第1字节长度占第2字节 if (_buffer.Count 3 payloadLen 1) { break; // 帧没到齐等下一次 chunk } // 2. 取出完整帧并校验 _frame.Clear(); for (int i 0; i 3 payloadLen 1; i) { _frame.Add(_buffer.Dequeue()); } if (VerifyCrc(_frame) _frame[^1]) { result.Add(DecodePayload(_frame)); } else { // 校验失败丢弃一字节重新找帧头 _buffer.Dequeue(); } } return result; } // DecodePayload 中按消息 ID 把 payload 映射成 TelemetryModel }逻辑说明外层循环里最关键是“校验失败时丢弃一字节再继续”。很多地面站卡在错帧恢复上因为收到一个坏帧后如果直接清空缓冲区后续正常帧也会被丢掉。逐字节丢弃是串口协议容错里成本最低的办法代价是解析速度略慢但对地面站这种每秒几十帧的量级完全够用。参数说明payloadLen 放在索引 2不同飞控协议定义不同你要先读清协议文档再调整硬编码位置。CRC 用 CRC16-CCITT 或简单异或和都行但注意一定要在解析前校验否则一个位翻转可能让高度变成负数、GPS 坐标跳到非洲。3.3 串口与 UDP 的线程模型后台收、队列转、界面订阅在 WPF 里直接订阅 DataReceived 事件并改控件是必踩的坑因为 SerialPort 的事件回调线程不是 UI 线程。正确模型是后台线程把数据打进队列解析线程从队列消费解析结果通过事件发布给 VM。有了这个边界后面无论换串口、换 UDP 都不动 UI 代码。public class FlightService : IFlightService, IDisposable { private readonly Channelbyte[] _rxChannel Channel.CreateUnboundedbyte[](); private readonly CancellationTokenSource _cts new(); private SerialPort? _port; public void Start() { // 消费者线程持续从 Channel 读数据并解析 Task.Run(async () { var parser new UavProtocolParser(); await foreach (var chunk in _rxChannel.Reader.ReadAllAsync(_cts.Token)) { var messages parser.ProcessChunk(chunk); foreach (var msg in messages) { FlightUpdated?.Invoke(this, msg.ToTelemetrySnapshot()); } } }); } private void OnSerialDataReceived(object sender, SerialDataReceivedEventArgs e) { var port (SerialPort)sender; int len port.BytesToRead; var buffer new byte[len]; port.Read(buffer, 0, len); _rxChannel.Writer.TryWrite(buffer); // 生产者只入队不解析 } public event EventHandlerTelemetrySnapshot? FlightUpdated; }说明Channel 在这里的价值是让接收线程只做“读串口、写队列”这件事解析和 UI 通知放到消费者线程。UDP 场景区别不大把 SerialPort 换成 UdpClient.ReceiveAsync收到后同样 TryWrite 进通道即可。这样串口和 UDP 在业务层完全同构切换链路时只换收发器不动解析器。这也是地面站系统能长期维护的关键结构。4. 地图与航迹可视化把遥测画到 WPF 界面上的正确姿势4.1 地图控件选型GMap.NET for WPF 的常见做法与缓存配置地面站地图选型我 90% 的情况直接用 GMap.NET for WPF而不是自己 Integrated 一份地图渲染。原因是瓦片加载、投影换算、缓存管理这套东西自己写太费时间GMap.NET 把这些做完了而且它的抽象接口能切换 OpenStreetMap、高德等源。配置的关键点有两个一是地图源要在一开始固定二是本地缓存目录要设置成大一点的空间。WPF 版本还容易在 x64 模式下遇到找不到 gmap 程序集的问题检查目标平台是否为 x64不要用 AnyCPU 跑 Release。// MainWindow 初始化 GMapControl 的典型配置 private void InitGMap() { MainMap.Bearing 0; MainMap.CanDragMap true; MainMap.DragButton MouseButton.Left; // 使用 OpenStreetMap 瓦片源缓存到本地避免每次启动重新下载 MainMap.MapProvider GMapProviders.OpenStreetMap; MainMap.Position new PointLatLng(30.5731, 104.0668); MainMap.MinZoom 2; MainMap.MaxZoom 18; MainMap.Zoom 15; MainMap.CacheLocation D:\UavCache\GMapCache\; GMapProvider.WebProxy null; // 避免系统代理导致瓦片加载失败 }4.2 航迹线绘制Overlay、防抖动与下采样飞机每秒钟回传一次 GPS如果每包数据就往航迹列表里加一个点并刷新 Overlay飞半小时后地图会明显卡顿。常见做法是只在位置变化超过阈值时才更新飞机图标航迹线用“分段存点 定期下采样”。private readonly GMapOverlay _trackOverlay new(track); private readonly ListPointLatLng _trackPoints new(); private const double MinTrackPointIntervalMeters 5.0; public void UpdateTrack(TelemetrySnapshot snap) { var newPoint new PointLatLng(snap.Lat, snap.Lng); // 距离上次记录点太近则跳过减少冗余点 if (_trackPoints.Count 0) { var last _trackPoints[^1]; double dist CalcDistanceMeters(last, newPoint); if (dist MinTrackPointIntervalMeters) return; } _trackPoints.Add(newPoint); // 每 500 个点做一次下采样只保留奇数位置的点 if (_trackPoints.Count 500) { _trackPoints _trackPoints.Where((_, idx) idx % 2 0).ToList(); } var polyline new GMapPolyline(_trackPoints) { Stroke new Pen(Brushes.OrangeRed, 2), IsHitTestVisible false }; _trackOverlay.Clear(); _trackOverlay.Polygons.Add(polyline); }参数说明MinTrackPointIntervalMeters 设 5 米意味着飞机盘旋时不会无限堆积航迹点下采样条件里 500 这个阈值是按内存来定的每点约 16 字节500 个点不到 10KB保留最近 500 个点是安全的。另一个重点是每次刷新航迹时先调用 _trackOverlay.Clear() 清掉旧的 Polyline再添加新线而不是新建 overlay否则地图会因盖层数量失控而翻车。飞机图标单独放一个 marker不要和航迹线混在同一个 overlay这样定位和剔除都方便。4.3 表格显示遥测DataGrid 一行变两行的处理地面站的飞手监视页经常放一个全参数的 DataGrid字段包括经纬度、高度、速度、电量、IMU 姿态。这里热搜词“wpf datagrid一行变为两行显示”是典型问题列一多默认宽度分配不均字符串换行后行高被顶开。我处理这问题分三步给数字列定死宽度、给经纬度列加 StringFormat 限制长度、打开横向滚动条。DataGrid ItemsSource{Binding TelemetryLog} AutoGenerateColumnsFalse IsReadOnlyTrue VirtualizingStackPanel.IsVirtualizingTrue VirtualizingStackPanel.VirtualizationModeRecycling HorizontalScrollBarVisibilityAuto DataGrid.Columns DataGridTextColumn Header时间 Width80 Binding{Binding Timestamp, StringFormatHH:mm:ss} / DataGridTextColumn Header经度 Width90 Binding{Binding Lng, StringFormat0.000000} / DataGridTextColumn Header纬度 Width90 Binding{Binding Lat, StringFormat0.000000} / DataGridTextColumn Header高度(m) Width70 Binding{Binding Alt} / DataGridTextColumn Header空速 Width60 Binding{Binding AirSpeed} / /DataGrid.Columns /DataGrid宽度设计这里最关键的是经纬度必须保留 6 位小数但一列宽度只有 90px 时StringFormat 直接把 106.123456 截断显示为 106.123456列宽不会再撑爆。如果哪一列内容真的长把它单独放出来固定宽度其他列用 MinWidth不要把“自动宽度”交给 DataGrid——它一自动就把行搞成两行了。这个表用上虚拟化之后即使日志区累计了几千条记录也不至于拖动时卡顿。5. 地面站 WPF 开发高频踩坑跨线程、内存膨胀与控件黑屏的现场排查5.1 现象串口一收数据界面抛“调用线程无法访问此对象”原因这是 WPF 新手最经典的翻车点。SerialPort.DataReceived 回调运行在线程池线程上代码里直接把收到的数据赋值给了某个控件的 Text 属性触发了跨线程 UI 访问。绑定到属性时未必抛异常但只要碰了控件WPF 的 Dispatcher 校验就会拦截。解决不要在回调里碰任何控件。做法就是第 3 章那个 Channel 模型回调里只入队消费线程解析后通过事件把数据抛给 VM。如果项目已经写崩了临时用 Dispatcher.BeginInvoke 包一层也能救急但长期维护一定会再踩因为每个新人看到 Dispatcher 都忍不住往里面塞代码。5.2 现象航迹飞了两小时内存占用从 300MB 涨到 3GB原因热点基本不在 GMap.NET而在你自己往 List 里无限存点并且每次都新建 GMapPolyline 对象。旧对象没有被及时释放GDI 句柄和托管对象同时堆积GC 又因为对象在 UI 线程创建而回收不及时。解决航迹列表设置上限超出后做下采样按 5.2 里的逻辑抽稀。另外把 Overlay 的更新从“每次重建线”改成“复用同一个 polyline 对象直接赋值 Points”这样 GDI 句柄不会持续增长。private GMapPolyline? _cachedTrackLine; public void UpdateTrackWithCachedLine(ListPointLatLng points) { if (_cachedTrackLine null) { _cachedTrackLine new GMapPolyline(points) { Stroke new Pen(Brushes.OrangeRed, 2) { DashCap PenLineCap.Round } }; _trackOverlay.Routes.Add(_cachedTrackLine as GMapRoute); } else { _cachedTrackLine.Points.Clear(); _cachedTrackLine.Points.AddRange(points); } _trackOverlay.Markers.Clear(); // 清理 Marker 列表但不销毁 Overlay }说明这里复用了 Polyline 对象Points 是集合类型直接 Clear 再 AddRange 在多数 GMap 版本里能触发重绘如果版本不支持退而求其次重建 Polyline 但保证 Overlay 只加一次也可以避免 Overlay 数量无限增长。5.3 现象地图控件整个黑屏或者只有灰色背景原因这个坑我排查了很久最终定位是瓦片源没设置。GMap.NET 的 GMapControl 默认没有 Provider不指定地图源时只显示一个空背景。另一个原因是首次运行没有缓存瓦片开着代理或断网时 OpenStreetMap 加载失败。解决初始化时必须给 MainMap.MapProvider 赋一个具体值比如 GMapProviders.OpenStreetMap并设置 CacheLocation 指向本地磁盘目录。跑一次飞行之前先把目标区域缩放到要用的层级并拖动一遍让瓦片写进缓存之后离线也能显示。提示OpenStreetMap 在无网环境不能首次出图。如果你在野外作业起飞前务必在 PC 上做好区域瓦片预热否则地图黑屏会让飞手彻底丧失方向感。5.4 现象在 4K 屏上界面模糊在别人的电脑上布局整体错位原因WPF 默认按系统 DPI 缩放没有声明 PerMonitorV2 时高分屏上字体发虚控件间距失衡。地面站通常是不定期搬到不同设备上用的这问题比想象中常见。解决给项目加 app.manifest 并声明 PerMonitorV2。application xmlnsurn:schemas-microsoft-com:asm.v3 windowsSettings dpiAwareness xmlnshttp://schemas.microsoft.com/SMI/2016/WindowsSettings PerMonitorV2 /dpiAwareness /windowsSettings /application加了这一行后窗口在不同 DPI 的显示器上拖拽时会重新缩放字体不会糊。另一个隐藏坑是如果用了 WindowStyleNone 和 AllowsTransparencyTruePerMonitorV2 在高 DPI 切换时可能导致窗口黑边这种要不放弃透明效果要不把窗口内容放进带 CornerRadius 的 Border 模拟。5.5 现象经常“莫名其妙”少帧跑一会遥测就停 2 秒再跳原因这种玄学问题多数是丢字节不是界面卡。串口收到的是“半包”或“坏包”解析器把整包数据清空重来外部看起来就是遥测停跳。详细抓包才能定位是 CTS/RTS 信号问题还是发送端频率过高。解决先抓串口日志把裸字节写成 hex 文件对照协议看是头字节丢了还是校验字节不对。我遇到过一次是 USB 转串口线在系统休眠恢复后缓冲清空导致的最后用电源计划禁用 USB 选择性挂起解决。这种问题靠改代码没用属于硬件层干扰别把时间耗在改解析器上。6. 遥测刷新与绑定性能的最后一公里批量更新与验证清单6.1 用“快照合并”把每秒 20 帧的刷新收敛成每 200ms 一次如果你一个 TelemetryModel 有 35 个属性每帧都触发 INotifyPropertyChanged那一秒就是 700 次通知WPF 的绑定引擎要逐一更新目标属性。表面看不出问题但 CPU 占用常年在 15% 以上。更严重的是这些通知是乱序的你会在界面看到高度先变 120 再变 100 再变 120 的跳动。我的做法是协议解析线程只更新一个“原始快照”界面层用一个 200ms 的 DispatcherTimer 汇总并一次性刷新所有绑定属性。这样既保留了 WPF 绑定的便利又把通知次数压掉 80%。public class TelemetryViewModel : INotifyPropertyChanged { private TelemetrySnapshot _latest; private readonly DispatcherTimer _mergeTimer; public void Start() { _mergeTimer new DispatcherTimer { Interval TimeSpan.FromMilliseconds(200), }; _mergeTimer.Tick (_, _) RaiseAllProperties(); _mergeTimer.Start(); } // 串口事件里只更新快照不通知 UI public void OnFlightUpdated(object? sender, TelemetrySnapshot snapshot) { _latest snapshot; } private void RaiseAllProperties() { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(string.Empty)); } }参数说明string.Empty 作为属性名会通知所有绑定。200ms 刷新意味着 UI 每秒最多更新 5 次视觉状态对飞手来说完全流畅但 CPU 和绑定压力会大幅下降。如果你要在高度剧烈变化时更快响应把 Interval 调到 100ms 就够不建议低于这个值否则又回到高频通知的老路。6.2 地面站上线前的验证清单空载 CPU不接收遥测时主进程 CPU 低于 5%GMap 平移时低于 30%。高频遥测测试用串口模拟器按 50Hz 连续发包跑 1 小时内存增量不超过 200MB界面无卡顿累计。断线恢复拔掉数传线再插回飞行数据应在 2 秒内恢复刷新界面无异常弹窗。丢包率通过串口收发的连续序号统计真实丢包率小于 0.1% 才算通过。最后讲一个我自己的教训早年做地面站总想把每一帧数据都实时画出来结果画出来的航迹线又碎又卡。后来想明白了地面站不是飞行记录仪人眼对 5Hz 的刷新已经足够平滑系统要保的是“稳定”而不是“高频”。如果你正在被界面卡顿折磨先检查是不是把刷新频率定得太高而不是去优化绘图算法。希望这些经验能帮你绕开我当年踩过的坑。本文还有配套的精品资源点击获取
网站建设高端定制企业官网