一种高效绘制余晖波形的方法Qt/C++

一、什么是"余晖波形"?

"余晖波形"(afterglow / persistence waveform)是指在实时波形显示中保留上一帧或多帧的轨迹并使其逐渐衰退的视觉效果。类似示波器上"痕迹逐渐消失"的效果:

  • 能看到最新信号的同时,也能看见短期历史(轨迹),比单纯把历史点清掉再画更直观。

  • 常见于音频可视化、示波器、频谱可视化、监控仪表盘等场景。

实现上通常有两种思路:把历史数据一直保留并每帧重绘(昂贵),或者在画布上"叠加透明层"使旧像素逐帧衰减(高效且视觉平滑)。

二、QCustomPlot 的性能瓶颈

在一个包含 4 个通道、每个通道 5000 点(总计 20,000 点)的场景中,如果每帧都把数据传给 QCustomPlot 并调用完整重绘,会遇到以下问题:

  1. 整体重绘开销大:每次刷新都触发全窗口的完整绘制,哪怕只有部分数据更新,也会重新计算和渲染全部曲线。

  2. 频繁的内存分配和拷贝:每帧都要构造并传递大量点数据,增加了内存管理开销。

  3. 高层抽象带来的额外消耗:QCustomPlot 提供了丰富的数据和绘图管理功能,但这些便利在高频实时刷新时会成为性能负担。

结果就是 CPU 占用率高、界面刷新不流畅、容易出现掉帧。而且对于"余晖"这种需要保留历史痕迹的效果,每次都重绘所有历史数据本身就是多余的。

三、使用离屏缓冲 + alpha 淡化 + 增量绘制实现

3.1 本方法的主要优点

1)性能高效:避免每帧重建和绘制全部点数据,显著降低 CPU 与内存开销。

2)视觉流畅:通过淡化处理保留历史轨迹,波形变化连续自然,观察体验更接近示波器。

3)实现简洁:基于缓冲区叠加和局部更新即可实现,无需引入复杂的数据结构。

3.2 关键实现原理

1) 离屏缓冲

绘制波形时,并不是直接在控件上重绘所有历史数据,而是使用一张缓冲图像(离屏画布)来保存已经绘制过的波形。这样,每一帧只需要把缓冲图像整体"贴"到窗口上即可,开销非常小。

2) 淡化实现

为了产生"余晖"效果,每一帧在缓冲图像上覆盖一层半透明的颜色,让旧的轨迹逐渐变浅。这样既能保留短时间的历史痕迹,又不会无限叠加造成画面污染。

3) 增量更新

每个波形通道都只生成和绘制最新的数据点,而不是每次都把整个通道的所有点重新绘制一遍。这样可以大幅减少绘制调用次数,提高帧率。

3.3 关键代码展示

cpp 复制代码
void WaveformPlot::paintEvent(QPaintEvent* event) {

  QPainter p(this);
  p.setRenderHint(QPainter::Antialiasing);

  // 绘制背景
  p.fillRect(rect(), Qt::white);

  // 绘制缓冲图像中的波形
  p.drawImage(0, 0, m_buffer);

  // 绘制坐标轴
  drawAxes(p);
}

void WaveformPlot::updateWaveform() {

  // 对旧波形做淡化
  QPainter fadePainter(&m_buffer);

  // alpha 衰减
  fadePainter.fillRect(m_buffer.rect(), QColor(255, 255, 255, 30));
  fadePainter.end();

  QColor baseColors[4] = {
      QColor(0, 0, 255),   // 蓝
      QColor(255, 0, 0),   // 红
      QColor(0, 255, 0),   // 绿
      QColor(255, 128, 0)  // 橙
  };

  double newYMin = m_yMin;
  double newYMax = m_yMax;

  for (int i = 0; i < m_layers.size(); ++i) {
    auto wf = m_layers[i].waveform;

    // 生成最新波形数据
    wf->generate(m_layers[i].index, i);

    // 获取 Y 范围
    wf->getminmax(newYMin, newYMax);

    m_layers[i].index = (m_layers[i].index + 1) % m_maxCount;
  }

  // 如果数据超出原范围,就调整 Y 轴
  if (newYMin < m_yMin || newYMax > m_yMax) {
    m_yMin = newYMin;
    m_yMax = newYMax;

    // ==== 计算扩展后的 yMin/yMax ====
    double yRange = m_yMax - m_yMin;
    m_yMinExt = m_yMin - 0.2 * yRange;  // 下扩展 20%
    m_yMaxExt = m_yMax + 0.2 * yRange;  // 上扩展 20%
  }

  for (int i = 0; i < m_layers.size(); ++i) {
    auto wf = m_layers[i].waveform;
    // 绘制最新波形
    wf->draw(m_buffer, this, baseColors[i]);
  }

  // 触发 paintEvent
  update();
}

3.4 资源占用展示

绘制余晖波形过程中,资源占用情况如下图所示

四、总结

本文介绍的余晖波形绘制方法,最突出的优点如下:

1、性能高效:避免每帧重建和绘制全部点数据,显著降低 CPU 与内存开销。

2、视觉流畅:通过淡化处理保留历史轨迹,波形变化连续自然。

但是也有一些缺点,比如:

1、完全依靠painter绘制,交互性不如QCustomPlot

2、由于历史波形数据通过离屏缓冲 + alpha 淡化实现,窗口大小变化时历史波形无法自适应变化,适合窗口大小固定的场景。

相关推荐
小小龙学IT11 分钟前
DuckDB 深度实战:用 C++ 在进程内跑一个「分析型数据库」
数据库·c++
NineData23 分钟前
DTCC 2026 预告|NineData CEO& 创始人叶正盛:面向 AI Agent 的数据库 DevOps 与数据复制实践
数据库·人工智能·数据库开发·devops·ninedata·数据库技术·dtcc
J_bean1 小时前
MySQL 事务是否必须手动开启?
数据库·mysql·数据库事务·自动提交·手动提交
J_bean1 小时前
MySQL InnoDB 如何检测死锁、判定死锁、处理死锁
数据库·mysql·死锁·死锁检测·处理死锁·死锁判定
知无不研2 小时前
lambda表达式的使用(3)
开发语言·c++·lambda
浪子明X2 小时前
从 MongoDB 文档到关系模型:构建可重跑、可对账的数据迁移流水线
数据库·mongodb·oracle
杜子不疼.2 小时前
国产数据库撑起固井软件自主化:金仓 × 中海油服「海恒 Cemsol」落地解析
数据库
隔窗听雨眠2 小时前
GBase 8s并发控制深度解析:封锁机制、隔离级别与死锁处理全攻略
服务器·数据库·oracle
刀客Doc2 小时前
即时零售不只是多一个渠道,品牌开始重做终端生意
大数据·数据库
就叫飞六吧3 小时前
防抖、幂等、唯一约束三件套 ;
数据库