4. 绘图引擎优化:降低QGC/Mission Planner的刷新率、使用离屏渲染、合并绘图指令、避免QPainter频繁重绘

地面站的界面卡顿,很多时候不是CPU不够快,而是绘图引擎在“空转”。

我见过不少开发者,一上来就优化算法、压缩数据链路,结果发现瓶颈其实在QPainter上——每秒重绘60次,但屏幕根本不需要那么快。说白了,你画得再快,用户的眼睛也跟不上。

这一章,我们专门聊绘图引擎的优化。四个核心方向:降低刷新率、离屏渲染、合并绘图指令、避免QPainter频繁重绘。每个点我都会结合自己在QGC和Mission Planner上的实际踩坑经验来讲。

核心观点:绘图优化的本质不是“画得更快”,而是“画得更少、画得更聪明”。

4.1 降低刷新率:别让UI做无用功

先问一个问题:你的地面站地图、仪表盘、航点编辑器,真的需要每秒60帧吗?

我个人习惯把刷新率分成三个等级:

场景 推荐刷新率 说明
地图平移/缩放 30 FPS 用户交互时,流畅度够用
航点轨迹绘制 10-15 FPS 静态路径,变化慢
仪表盘数值更新 5-10 FPS 人眼对数字变化不敏感
后台日志/图表 2-5 FPS 非实时显示,够用就行

我在项目中遇到过一件事:某次给客户做定制版QGC,飞手反映“飞机姿态一变化,界面就掉帧”。查了半天,发现是高度计仪表盘每秒刷新60次,而那个仪表盘其实只需要每200ms更新一次。改成10FPS后,CPU占用直接降了40%。

技巧:用QTimer代替QWidget::update()的默认刷新。设置一个独立的定时器,按需触发重绘,而不是依赖系统事件循环的默认频率。

// 错误示范:每次数据更新都重绘
void onNewData() {
    update();  // 可能每秒触发60次
}

// 正确做法:用定时器控制刷新率
QTimer *refreshTimer = new QTimer(this);
refreshTimer->setInterval(100); // 10 FPS
connect(refreshTimer, &QTimer::timeout, this, QOverload<>::of(&QWidget::update));

4.2 离屏渲染:把复杂画面“缓存”起来

离屏渲染,说白了就是先画到一张“虚拟画布”上,再一次性贴到屏幕上。而不是每次都在屏幕上直接画。

你想想看,如果每次重绘都要重新计算所有航点、轨迹、地图瓦片,那CPU得多累?

我常用的做法是:

  • 静态图层离屏缓存:地图底图、固定航点、背景网格——这些几乎不变的内容,画一次存成QPixmap。
  • 动态图层单独绘制:飞机当前位置、实时轨迹线、临时标记——这些变化频繁的内容,单独画在另一个图层上。
  • 合并输出:最后把静态缓存和动态图层叠加,一次blit到屏幕上。

注意:离屏渲染不是万能的。如果画面内容几乎每帧都在变(比如视频流),离屏缓存反而浪费内存。要区分场景使用。

// 离屏渲染示例
QPixmap staticLayer(width(), height());
QPainter painter(&staticLayer);
// 只画一次:地图、航点、网格
drawMap(&painter);
drawWaypoints(&painter);
painter.end();

// 每次重绘时,只画动态内容
void paintEvent(QPaintEvent *) {
    QPainter painter(this);
    painter.drawPixmap(0, 0, staticLayer); // 贴缓存
    drawAircraft(&painter);                // 画飞机
    drawTrajectory(&painter);              // 画轨迹
}

4.3 合并绘图指令:减少QPainter的“上下文切换”

QPainter的绘图指令,每一条都有开销。尤其是设置画笔、画刷、变换矩阵这些操作。

我曾经在Mission Planner上调试一个航点编辑器,发现画100个航点居然要200ms。后来一分析,每个航点都单独设置了画笔颜色和线宽——这就是典型的“过度上下文切换”。

合并绘图指令的核心原则:

  • 同类元素一起画:所有航点用同一个画笔一次性画完,再换画笔画轨迹线。
  • 减少状态切换:把setPen()、setBrush()、setTransform()这些操作集中到循环外面。
  • 批量绘制:能用drawPoints()就别用drawPoint()循环。
// 低效做法:每个航点单独设置画笔
for (auto &wp : waypoints) {
    painter.setPen(wp.isSelected ? QPen(Qt::red) : QPen(Qt::blue));
    painter.drawEllipse(wp.pos, 5, 5);
}

// 高效做法:先分组,再批量绘制
painter.setPen(QPen(Qt::blue));
painter.setBrush(Qt::NoBrush);
QVector<QPointF> normalPoints, selectedPoints;
for (auto &wp : waypoints) {
    if (wp.isSelected) selectedPoints.append(wp.pos);
    else normalPoints.append(wp.pos);
}
// 一次画完所有普通航点
for (auto &p : normalPoints) painter.drawEllipse(p, 5, 5);
// 换画笔,一次画完所有选中航点
painter.setPen(QPen(Qt::red));
for (auto &p : selectedPoints) painter.drawEllipse(p, 5, 5);

实测数据:在QGC的航点编辑器中,合并绘图指令后,100个航点的绘制时间从200ms降到了35ms。提升接近6倍。

4.4 避免QPainter频繁重绘:用“脏区域”代替全屏刷新

这是最容易被忽视的一点。很多人一收到新数据,就直接调用update()——结果整个窗口都重绘一遍。

其实,大部分时候只有一小块区域需要更新。比如飞机位置变了,只需要重绘地图上飞机周围那一小块。

QPainter提供了update(const QRect &)方法,只重绘指定矩形区域。配合QRegion,甚至可以合并多个脏区域。

// 全屏重绘(不推荐)
void onAircraftMoved() {
    update(); // 整个窗口重绘
}

// 脏区域重绘(推荐)
void onAircraftMoved() {
    QRect dirtyRect = getAircraftBoundingRect(); // 只计算飞机周围区域
    update(dirtyRect); // 只重绘这一块
}

嗯,这里要注意:脏区域不能太小,否则频繁触发小区域重绘,反而增加调度开销。我一般把脏区域扩大20-30像素,作为缓冲。

避坑指南:我曾经在Mission Planner上遇到过一个问题——用了脏区域重绘后,地图边缘出现“残影”。原因是脏区域外的内容没有刷新,而地图瓦片在后台加载完了。解决办法:在脏区域重绘时,连带周围一圈瓦片一起刷新。

4.5 知识体系总览

下面这张图,把本章四个优化方向的关系梳理清楚了。你可以把它当作一个检查清单:

绘图引擎优化知识体系 绘图引擎优化四大方向 绘图引擎优化 降低刷新率 离屏渲染 合并绘图指令 避免频繁重绘 地图: 30 FPS 仪表: 10 FPS 日志: 5 FPS 静态图层缓存 动态图层分离 合并输出 同类元素一起画 减少状态切换 批量绘制 脏区域重绘 合并脏区域 缓冲边界 四个方向独立可叠加,建议按顺序逐一实施

4.6 实战建议:从哪个方向开始?

如果你现在面对一个卡顿的地面站,我的建议顺序是:

  1. 先降刷新率——改动最小,效果最明显。把非交互区域的FPS砍到10以下。
  2. 再搞离屏渲染——把静态内容缓存起来,动态内容单独画。
  3. 然后合并绘图指令——检查代码里有没有“每个元素单独设画笔”的坏习惯。
  4. 最后优化脏区域——这个需要仔细分析重绘范围,适合做精细调优。

警告:不要一次性全改。每改一步,就测试一次性能。我曾经一次性改了四个方向,结果画面出现奇怪的闪烁,排查了整整两天才发现是离屏渲染和脏区域重绘冲突了。