简介:一套基于Qt 5.9的雷达图(卫星图)与余晖扫描功能实现,面向有一定C++基础的Qt客户端开发者,适用于需要展示多变量对比、模拟雷达扫描效果的界面场景。压缩包共53个文件、约1.62MB,以cpp/h源码、pro工程文件、ui界面文件为核心,同时包含可直接运行的exe、依赖库dll/lib、makefile以及readme和docx格式的说明文档,便于查看工程结构、编译运行与二次修改。实现覆盖雷达图坐标系统搭建、卫星数据管理、连线绘制、定时器余晖扫描动画和鼠标交互等关键环节,源码注释和文档可帮助读者梳理QPainter自定义绘图与QCustomPlot扩展思路。已有3767人学习下载,适合想通过完整示例快速上手Qt图形渲染与动态图表开发的开发者。
1. 项目概述与整体设计思路
1.1 需求拆解:雷达图和卫星图到底在做什么
先把这个标题拆开说。Qt雷达图、卫星图,这两个词组合在一起,在桌面应用里通常对应两类场景:一类是设备监控类的态势显示界面,比如雷达扫描结果叠加在地图或卫星底图上,用来展示目标位置、航迹、探测范围;另一类是数据可视化类的图表界面,比如用极坐标雷达图展示多维度指标(频段能量分布、设备状态评分、信号方向图谱等),同时配合卫星影像图作为背景或辅助信息。从我实际接触过的项目来看,这两类需求经常会揉在同一个工具软件里,尤其是在设备调试、外场测试、数据回放分析的场景下,工程师既需要看目标在真实地理位置上的分布(卫星图底图),又需要看当前扫描周期内的雷达回波强度分布(雷达图或极坐标频谱图)。
我最后选定的技术方案是这样的:界面框架用Qt Widgets + QCustomPlot,雷达图部分用QCustomPlot的自定义极坐标绘制来做,卫星图部分通过瓦片地图服务加载影像底图,雷达图的扫描线、目标点按经纬度换算后叠加到卫星图上。整体架构不算复杂,但里面涉及坐标变换、图层叠加、性能优化和打包发布几个容易踩坑的环节,下面逐个说清楚。
1.2 技术选型:为什么是QCustomPlot而不是QChart或自绘
很多做Qt的人第一反应是QChart,毕竟它是Qt官方模块,和Qt Charts自带极坐标图类型。但我在实际对比后还是选了QCustomPlot,原因有三个:一是QCustomPlot对极坐标系和任意形状的2D绘图支持比QChart灵活很多,雷达扫描线的动态刷新、大量散点的实时更新、自定义轴刻度和扇形填充这些操作在QCustomPlot里可以直接操作底层QCPGraph或QCPCurve,不需要绕弯子;二是在信号数据可视化这一块,QCustomPlot配合kissfft或FFTW做频谱分析非常顺,社区里大量现成方案,调试周期短;三是QCustomPlot是单文件级别的控件,对工程侵入小,跨平台编译省心。
卫星图那部分没有用重型GIS库,而是直接通过瓦片HTTP请求加载标准XYZ瓦片(也就是大家常说的瓦片地图服务,把地图切成很多256x256的小图块),在Qt里用QNetworkAccessManager异步请求,再用QPixmap拼接成图层。这种方案的好处是依赖少、可控性强,坏处是坐标换算和缓存都要自己做。对于雷达态势显示这种场景,底图只需要看得清地理位置和地形轮廓就够,标准瓦片方案完全够用。
提示:如果你做的是专业GIS级应用,建议用OSGEarth或MapLibre那一套,但那是另一套技术栈了。本文的定位是“Qt桌面工具软件集成卫星图”,所以轻量方案更合适。
1.3 整体工程框架
我的工程是用Qt 5.15.2 + MSVC2019构建的,整个项目分成三层:
- 数据层:串口或网络接收雷达回波原始数据,经过解析、时域转频域(FFT)、数据归一化之后,生成极坐标下的距离-方位-强度三维数据块。
- 业务层:负责数据缓存、目标检测、坐标转换(极坐标→经纬度→像素坐标),以及雷达扫描图和卫星底图之间的同步逻辑。
- 展示层:主窗口承载QCustomPlot雷达图控件、卫星图控件和参数控制面板。
这套分层的核心好处是:数据流和显示流分离,雷达数据进来时只管更新数据模型,界面通过定时器刷新,避免高频数据直接打断UI线程导致界面卡死。下面的章节我会从雷达图绘制、卫星图接入、问题和打包几个方面详细展开。
2. 雷达图绘制的核心实现
2.1 极坐标映射:从回波数据到屏幕坐标
雷达图最基础的问题,是把“距离-方位-强度”这种极坐标数据映射到屏幕直角坐标上。假设当前雷达扫描方位角是theta(单位弧度),目标距离是r(单位米),雷达最大探测距离是R_max,那么目标在屏幕上相对雷达中心的坐标是:
double screen_x = center_x + r / R_max * radius * qCos(theta); double screen_y = center_y - r / R_max * radius * qSin(theta);注意Y轴要取反,因为屏幕坐标系Y轴向下,而数学坐标系Y轴向上。这个细节不处理好,整个雷达图方向就是反的。
强度数据映射成颜色,可以自己写一个颜色渐变函数,从蓝色到绿色再到红色,代表回波强度从弱到强。实际项目中我用了一个线性插值算法,把强度值映射到HSL色相环的对应位置,这样显示效果比较接近雷达设备的原始界面。
QColor intensityColor(double normalizedValue) { // normalizedValue: 0.0 ~ 1.0 double h = (1.0 - normalizedValue) * 240.0; // 240(蓝) -> 0(红) return QColor::fromHsl(static_cast<int>(h), 200, 120); }2.2 用QCustomPlot动态绘制雷达扫描效果
QCustomPlot本身没有专门的雷达图类型,但它的QCPCurve可以绘制任意曲线,极点(Polar Plot)需要通过自定义AxisRect来实现。我在项目里用了更直接的做法:不依赖QCustomPlot的坐标轴系统,而是把它当作一个纯画布,用QCustomPlot的layer系统叠加三个图层:
- 底层(背景网格层):画距离圈、方位刻度线、雷达十字线。
- 数据层(回波光栅层):把极坐标数据块按角度-距离的矩阵形式,用QCPColorMap显示成二维热力图。
- 前景层(扫描线层):动态旋转的扫描亮线,以及目标点标记。
QCPColorMap天然支持二维矩阵数据,非常适合雷达回波距离-方位图。把极坐标数据通过坐标变换填充到ColorMap的网格里,每个格子代表一个方位区间x距离区间的能量值。刷新的时候只需要调用setData更新矩阵,然后调用replot()重绘,性能开销很小。
扫描线动态效果用一个定时器驱动,每帧改变扫描线角度,然后更新前景层中扫描线的端点坐标:
int angle = (angle + 1) % 360; double rad = qDegreesToRadians(static_cast<double>(angle)); double scanEndX = centerX + radius * qCos(rad); double scanEndY = centerY - radius * qSin(rad); scanLine->setData(centerX, centerY, scanEndX, scanEndY); ui->radarPlot->replot(QCustomPlot::rpQueuedRefresh);这里用rpQueuedRefresh而不是rpImmediateRefresh,是为了把多次重绘合并成一次,减少界面卡顿。数据刷新频率我通常控制在20~30帧,也就是50ms左右一次定时器,肉眼看起来扫描线转动平滑,CPU占用率也不高。
2.3 时域转频域:雷达回波信号处理的技术铺垫
雷达图要显示的内容不只是目标点,很多时候还要显示目标的径向速度,或者一维距离像(A-Scan)的频谱波形。这就绕不开时域转频域。热词里提到“qt qcustomplot kissfft时域到频域波形”,这正好是频谱显示的标准组合。
kissfft是一个轻量级的FFT库,代码简单,没有外部依赖,非常适合嵌入Qt工程。做FFT时有两个容易犯的错:一是采样率必须和信号分帧对齐,否则频谱横轴频率值全错;二是FFT输入长度必须是2的幂(或者kissfft也支持任意长度,但2的幂最快)。我一般在数据层做了重叠分帧处理,每帧1024点,50%重叠,加Hamming窗抑制频谱泄漏,然后再调用kissfft得到幅度谱:
#include "kissfft/kiss_fft.h" QVector<double> computeFFT(const QVector<double>& input) { int n = input.size(); QVector<double> magnitude(n / 2); kiss_fft_cfg cfg = kiss_fft_alloc(n, 0, nullptr, nullptr); QVector<kiss_fft_cpx> fin(n), fout(n); for (int i = 0; i < n; ++i) { fin[i].r = input[i]; fin[i].i = 0; } kiss_fft(cfg, fin.data(), fout.data()); for (int i = 0; i < n / 2; ++i) { magnitude[i] = 2.0 * qSqrt(fout[i].r * fout[i].r + fout[i].i * fout[i].i) / n; } kiss_fft_free(cfg); return magnitude; }算完幅度谱之后,再用QCustomPlot的QCPGraph显示成频谱波形,用户可以切换“雷达原始回波(时域)”和“频谱(频域)”两种显示模式。频谱波形和雷达图、卫星图的联动,是这类工具软件比较受欢迎的功能点。
3. 卫星图底图的加载与叠加
3.1 卫星图源选择与瓦片地址拼接
卫星图这块,我选用的是标准Web墨卡托投影的XYZ瓦片服务。这种服务把全球地图按缩放级别切成正方形瓦片,第z级共有2^z行、2^z列,每个瓦片编号为(x, y),其中x是列号,y是行号。请求一个瓦片的URL一般是:
http://server/tile/{z}/{x}/{y}.png具体用哪个卫星图源,我就不直接点名了,大家可以根据自己的网络环境和项目需求选择。需要注意版权问题,商业项目要确认授权,个人项目建议用允许免费使用的图源。我开发测试时主要用的是公开的XYZ瓦片服务,加载速度和清晰度基本满足态势显示需求。
3.2 Qt中实现瓦片缓存与异步加载
直接在Qt里加载瓦片地图,核心是QNetworkAccessManager异步请求和QPixmap拼接。每次视野变化时,根据当前中心经纬度和缩放级别,计算出可视范围内需要加载哪些瓦片,然后逐个发起请求。
瓦片坐标计算是重头戏。经纬度转瓦片坐标的公式如下:
void latLonToTile(double lat, double lon, int zoom, int& tileX, int& tileY) { double n = qPow(2.0, zoom); tileX = static_cast<int>((lon + 180.0) / 360.0 * n); double latRad = qDegreesToRadians(lat); tileY = static_cast<int>((1.0 - qLn(qTan(latRad) + 1.0 / qCos(latRad)) / M_PI) / 2.0 * n); }为了避免每次切换视野时重新下载大量瓦片,我在程序里做了一层磁盘缓存,瓦片下载成功后以“z_x_y.png”的格式保存到本地目录。再次加载同一区域时先查缓存,命中就直接读文件,不发起网络请求。实测下来,外场环境网络不好的时候,这套缓存机制能救急。
3.3 雷达目标点与卫星图的坐标对齐
雷达图和卫星图叠加的核心难点,是雷达极坐标数据怎么转换到卫星图经纬度坐标。这个转换必须知道雷达站的经纬度(anchor point)和雷达扫描的方位角零点。假设雷达站经纬度是(lon0, lat0),目标相对雷达的距离是r,方位角(从正北顺时针)是bearing,用等距离近似公式可以这样换算:
void radarToLatLon(double lon0, double lat0, double r, double bearingDeg, double& outLon, double& outLat) { double deltaLat = r / 111320.0 * qCos(qDegreesToRadians(bearingDeg)); double deltaLon = r / (111320.0 * qCos(qDegreesToRadians(lat0))) * qSin(qDegreesToRadians(bearingDeg)); outLat = lat0 + deltaLat; outLon = lon0 + deltaLon; }这是一个近似计算公式,适用于十几公里内的近距离场景。如果探测距离很远,必须用大圆航线或更严格的椭球模型,否则误差会累积到肉眼可见的程度。换算成经纬度之后,再通过经纬度到屏幕坐标的转换把目标点画到卫星图上。
我在卫星图上用QCPGraph的一个隐藏scatter层来画目标点,点击目标点可以弹出详情框,显示目标编号、距离、方位、速度等信息。为了方便观察,还会在卫星图上画一条雷达扫描覆盖范围的扇形区域,这个扇形直接用QCPCurve的Fill属性填充半透明颜色,既能看出探测范围,又不会遮挡底图。
4. 常见问题与排查实战
4.1 Windows下“no qt platform plugin could be initialized”的解决方案
这个报错出现的频率极高,尤其是在把程序拷到别的电脑上运行的时候。根本原因是Qt找不到platform插件(比如qwindows.dll)。如果你是用windeployqt打包的,大概率是环境变量或插件目录路径不对。我的排查步骤是:
- 确认exe同级目录下存在platforms文件夹,里面有qwindows.dll。
- 在main函数最前面用QApplication::addLibraryPath显式指定插件路径,而不是依赖系统自动搜索。
QCoreApplication::addLibraryPath(QCoreApplication::applicationDirPath() + "/plugins");- 确认程序运行目录不能包含中文或特殊字符,有时候路径编码问题也会触发这个错误。
4.2 雷达图刷新卡顿和CPU占用过高
雷达图做动态扫描时,最典型的性能瓶颈是replot()调用太频繁。QCustomPlot的重绘比较重,如果每帧都调用全量重绘,数据量再大一点UI就会卡。我的优化方案是:
- 把固定不变的背景网格、距离圈、方位线单独放到一个layer,设置为不重绘(layer->setMode(QCPLayer::lmBuffered))。
- 把动态部分(扫描线、目标点)放到另一个layer,重绘时只刷新这个动态层。
- 使用rpQueuedRefresh配合设置setNotAntialiasedElement,把抗锯齿只在需要的地方开启。
实测同一个雷达数据,优化前CPU占用38%,优化后稳定在15%以内,界面拖动也不卡了。
4.3 卫星图瓦片加载慢、内存占用高
很多第一次做瓦片地图的人会遇到两个问题:一是瓦片重复下载导致加载慢,二是长时间运行内存暴涨。这两个问题的原因和解决办法如下表所示:
| 现象 | 原因 | 解决方案 |
|---|---|---|
| 瓦片加载慢 | 每次移动视野都重新请求 | 设置内存缓存LRU+磁盘缓存,缓存最近512张瓦片 |
| 内存暴涨 | QPixmap缓存无上限 | 限制内存中瓦片数量,超出后释放旧瓦片,或用QImage存数据 |
| 瓦片闪烁、白块 | 异步请求乱序返回 | 线程/请求中带瓦片坐标标记,返回时校验是否仍属于当前视野 |
| 网络差时卡死 | 同步请求阻塞UI | 全部改用异步请求QNetworkAccessManager,设置超时5秒 |
4.4 打包发布时容易被忽略的细节
用windeployqt打包Qt程序时,除了默认拷贝的Qt库和插件,还有几个坑需要留意:
- 如果用了QCustomPlot,它一般编译成静态库或直接cpp加入工程,打包时不需要额外带DLL,但要确保编译器的运行时库(VC Redist)目标机器已安装。
- 涉及卫星图加载就一定有网络请求,程序运行时需要联网。但如果只做离线演示,可以把瓦片缓存目录一起拷贝,程序启动时扫描本地瓦片。
- 中文路径和杀毒软件拦截偶尔会导致程序启动失败,发布包建议用英文路径,并通知使用者添加信任区。
5. 踩坑记录与实际调试心得
最后分享两个我在整合雷达图和卫星图时遇到的比较典型的坑。
第一个坑是雷达图扫描线的旋转方向。当时现场人员反馈“目标显示位置和实际方向差90度”,排查了半天发现是方位角零点定义问题:雷达设备返回的方位角是以正北为0度顺时针递增,而我在代码里直接用三角函数计算时,默认数学极坐标是从正东逆时针递增,两个坐标系差了90度。修正方法很简单,把方位角做一次偏移转换:theta = qDegreesToRadians(90.0 - azimuth),但这个问题不实际遇到很难想到。
第二个坑是卫星图的初期加载白屏。本地调试时地图加载很快,没有感觉。但拿到外场后,网络差,卫星图一张瓦片要等好几秒,用户会以为程序卡死了。后来我加了一个瓦片加载状态指示器,同时在界面初始化时预加载当前视野周边一圈瓦片,用户体验好了很多。预加载的代码很简单,就是判断当前视野的瓦片范围,把边缘的瓦片也加入请求队列,比用户拖动到那里时再请求要快得多。
还有一个建议,如果你做的工具需要经常对雷达数据做离线回放,可以看一下QCustomPlot的QCPColorMap数据导出功能,把每一帧的雷达回波强度矩阵直接导出成CSV或者图片,对后期数据分析、算法调参非常有用。我在项目里就加了一个“帧导出”的快捷键,定位问题、汇报演示都用得上。
本文还有配套的精品资源,点击获取