全景扫描瀑布图实现

1. 全景扫描瀑布图

1.A UIMSCANPluginSystemControl/Waterfall/waterfall.cppWaterfall

这是 普通 QWidget ,核心不在 OpenGL,而是 QPixmap + QImage + paintEvent 拼出来

Render(数据入口)

复制代码
void Waterfall::Render(float* fData, int iDataCount) {
    if(!_fData) {
        return ;
    }
    memcpy(_fData, fData, iDataCount * sizeof(float));
    _dataCnt = iDataCount;
    this->Replot();
    _frame++;
}

Replot:先画「底图」,再在 _dataImage 里叠一层「瀑布」

像素与颜色(DrawDataLayer)------按列聚合 + 纵向滚动 + 伪彩色

  1. 横轴像素列数 colCount = _dataImage.width()频谱点数 _dataCnt

  2. 每列对应一段 bin ,取该段 最大值(类似 max-hold 降采样):

    复制代码
     double dRgbFactor = (double)_dataCnt / colCount;  //颜色因子
     static double dstRgb[4096];
     for(int i = 0; i < colCount; ++i) {
         int iBeinIdx = i * dRgbFactor;
         int iEndIdx = i * dRgbFactor + dRgbFactor - 1;
         iEndIdx = std::max(iBeinIdx, iEndIdx);
         dstRgb[i] = *std::max_element(_fData + iBeinIdx, _fData + iEndIdx);
     }
  3. 时间方向滚动 :把整个 _dataImage 的像素 向下挪一行memmove),再在 第一行写新颜色:

    复制代码
     uchar* pDataSrc = _dataImage.bits();   // pointer first pixel data
     memmove(pDataSrc + _dataImage.bytesPerLine(), pDataSrc, (rowCount - 1) * _dataImage.bytesPerLine());
     _gradient->colorize(dstRgb, mDataRange, (QRgb*)pDataSrc, colCount);
  4. colorize 如何把「电平 → 颜色」**(线性映射到调色板下标):
    posToIndexFactor = (mLevelCount-1) / range.size(),对每个 i

    \text{index} = \text{clamp}\bigl(\lfloor (\text{data}_i - \text{range.lower}) \cdot \text{posToIndexFactor} \rfloor\bigr)

    然后 scanLine[i] = mColorBuffer[index]

mDataRange 用的是 SetYRange 设的 _amplitudeBegin/_amplitudeEnd(例如 -20~120)。渐变预设为 QCPColorGradient::gpSpectrum(构造函数里)。

paintEvent:多图层混合显示

复制代码
void Waterfall::paintEvent(QPaintEvent* event) {
    Q_UNUSED(event)

    QPainter painter(this);

    _mutex.lock();
    painter.drawPixmap(0, 0, _backgroundPixmap);
    painter.drawImage(QRect(_margin.left()  + _dataImage.width() + 10, _margin.top(), _colorRangeImage.width(), _colorRangeImage.height()), _colorRangeImage);
    if(0 == _dataCnt) {
        _dataImage.fill(_backgroundColor);
    }
    painter.drawImage(QRect(_margin.left(), _margin.top(), _dataImage.width(), _dataImage.height()), _dataImage);

顺序:全图背景 QPixmap右侧色标条 _colorRangeImageDrawBackgroundLayer 里按纵向扫渐变填的)→ 瀑布主体 _dataImage → 再在 没有锁住的 painter 上画游标、时间文字等(DrawCenterLineDrawTime...)。

这就是一种典型的 混合绘制离屏缓冲(QImage)里用 CPU 写像素paintEvent 里用 QPainter 叠图和矢量 UI


1.B 扫描/单频里更常见的单频测向里 waterfallCtrl 用的是这套

这里没有单独的 paintEvent 写瀑布像素,而是 QGraphicsView + WaterfallItem::drawCurve

数据线程 WaterfallDataManager::run:把频谱 重采样到「屏幕宽度」列 (点多则按列 max ,点少则 线性插值 ),再 drawCurve()WaterfallItem::handleData

颜色(WaterfallItem::handleData

  • 同样 memmove 整幅图向下滚一行

  • 对每个水平位置 id,电平 data[id] 映射到 100 档颜色COLOR_COUNT):

    复制代码
      int colorId = 0;
      for(int id = 0; id < m_iPointCount; ++id) {
          if(data[id] < m_fYStart) {
              continue;
          }
    
          colorId = COLOR_COUNT * (data[id] - m_fYStart) / (m_fYEnd - m_fYStart);
    
          if(colorId < 0) {
              colorId = 0;
          } else if(colorId >= COLOR_COUNT) {
              colorId = COLOR_COUNT - 1;
          }
    
          pPixColor[id] = (*m_pMapColor)[colorId];
      }

m_pMapColorWaterfallView 构造时由 分段线性 RGB(蓝→绿→黄→红)生成:

复制代码
QRgb WaterfallView::getColor(float fFactory) {
    int iR, iG, iB;
    for(int i = 0; i < s_lstColor.size() - 1; ++i) {
        if(fFactory >= s_lstColor[i].fPos &&
                fFactory <= s_lstColor[i + 1].fPos) {
            fFactory = (fFactory - s_lstColor[i].fPos) / (s_lstColor[i + 1].fPos - s_lstColor[i].fPos);
            iR = s_lstColor[i].color.red() + (s_lstColor[i + 1].color.red() - s_lstColor[i].color.red()) * fFactory;
            // ...

显示drawCurvepainter->drawImage(rect(), m_image) ------ 仍是 Scene 的 item 绘制路径 ,和 View 的 drawForeground 等可再叠鼠标十字线。

「混合」子线程 算每帧 _maxData 并调 handleData QImage主线程 updateUi()QGraphicsView 刷新 Scene(典型生产者/消费者 + 离屏位图)。


2. 单频测向 Level Flow(LevelStream

控件在 UI 里是 levelstreamCtrl,数据链路例如:Render(level)SignalHandleDataSlotRender

复制代码
void LevelStream::Render(float level) {
    emit SignalHandleData(level);
}

SlotRender:不是直接画,而是更新 时间序列 + 屏幕上的折线点

  • 横轴 :每个新点 xleftMargin + 样本序号,满一行后 整列左移 (所有点 x -= 1,删掉队列头),新点接在右端。
  • 纵轴GetLevelYScale(level) 把 dBuV/dBm 映射到像素行。

Y 像素(线性电压刻度)

复制代码
int LevelStream::GetLevelYScale(float level) {
    int maxY = _maxY;
    int minY = _minY;
    int gridHeight = this->height() - _ctrlMargin.top() - _ctrlMargin.bottom() + 1;
    float ampStep = (maxY - minY) / (gridHeight * 1.0);
    int scaleY = (maxY - level) / ampStep + _ctrlMargin.top();
    return scaleY;
}

即:电平高 → maxY - level 小 → y 靠上(与常见示波图一致)。

DrawLevel(在 工作线程 里画到 _drawLayer 这张 QImage

  • 默认 Curve :对 _useLevelStreamPointsgenerateSmoothCurve(样条) ,描一条 青色 _levelEnvelop 曲线;
  • 可选 火柴棒 Match梯度填充 Cube实填 SolidFill
  • _useSmoothing 时再画一条 平滑曲线_smoothPen)。

颜色 不是 按电平映射伪彩色,而是 固定笔颜色 + 可选 QLinearGradient 填充(Cube 模式里蓝→绿→黄→红)。

paintEvent:主线程只 叠图 + 交互层

复制代码
void LevelStream::paintEvent(QPaintEvent* evt) {
    QWidget::paintEvent(evt);

    QPainter painter(this);
    painter.setRenderHint(QPainter::TextAntialiasing);

    painter.setBrush(QBrush(QColor(Qt::black)));
    painter.drawRect(this->rect());

    painter.drawPixmap(0, 0, QPixmap::fromImage(_bottomLayer));
    painter.drawPixmap(0, 0, QPixmap::fromImage(_drawLayer));

    DrawTimeMarkers(painter);
    if(_isMouseHover) {
        DrawMouseMove(painter);
    }

    DrawClickLine(&painter);
}

工作线程 DataThread::run :约 每 100ms 在持锁情况下对当前 _drawLayerDrawLayer(内部 DrawLevel 等),再 emit SignalCompleteDraw 回主线程换缓存图:

复制代码
        if(_isDrawLayer && (_handleWidget != nullptr)) {
             QMutexLocker locker(&DataMutex);
            _layerMutex.lock();
            QImage levelLayer = _drawLayer;
            _handleWidget->DrawLayer(levelLayer);
            _layerMutex.unlock();
            emit SignalCompleteDraw(levelLayer);
        }

        msleep(100);

混合绘制在这里也很明确:

  • 主线程 :收电平、维护 _levelStream / 点坐标;paintEvent黑底 + 两张大位图 + 游标/时间/点击线
  • 子线程 :把 曲线、网格、图例 烤到 _drawLayer(以及 SlotDrawLayer 里还会重画 bottom/grid/legend,见 DataThread::SlotDrawLayer)。

3. 总结

问题 答案要点
像素怎么来 瀑布:memmove 纵移 + 首行 写新颜色(每列一个标量);Level Flow:维护 (x,y) 点列y = GetLevelYScale(电平)
颜色怎么算 WaterfallQCPColorGradient::colorize,按 Y 轴量程线性映射到调色板CJJWaterfallcolorId = 100 * (dB - ymin)/(ymax-ymin) 查 RGB 表。Level Flow:画笔/渐变,不是按像素读 palette。
Render vs paintEvent Render/handleData喂数据 + 改离屏图paintEvent把已有位图和矢量 UI 合成到屏幕
「混动/混合」 统一特征是:CPU 在 QImage(或 Item)里改像素 + QPainter/Graphics 框架负责合成 ;不少路径里再加 独立线程 降低 UI 阻塞(CJJWaterfallDataManagerLevelStream::DataThread)。

LevelStream(曲线层):电平在主线程的 SlotRender 里进队列、改点坐标;绘制主要靠 DataThread 约每 100ms 调一次 DrawLayer,把曲线画进 _drawLayer,再 emit 回主线程 SlotGetLayer → update()。是 「数据高频进、位图低频重画」。
瀑布图:SystemControl::Waterfall:Render → Replot 每次来数据都会 DrawDataLayer(memmove + colorize),再 PaintUiSignal → update(),刷新与数据帧基本同频。
CJJWaterfallView:数据在 子线程里算每行像素、handleData 里对 QImage 做 memmove + 填色,UI 用 定时器约 50ms 汇总 updateUi() 刷新视图。
相同点:都可能在 离屏 QImage/位图 上改像素,最后用 update/paint 显示。
不同点:LevelStream 是 1D 时域折线 + 固定周期合成;瀑布图是 2D 频谱热力滚屏,且 UIMSCAN 的 Waterfall 与 CJJWaterfall 的线程/节流策略也和 LevelStream 不一致。

相关推荐
加号33 小时前
【Qt】 应用程序发布:依赖库拷贝与部署指南
开发语言·qt
神仙别闹8 小时前
基于QT(C++)实现线性表的建立、插入、删除、查找等基本操作
java·c++·qt
LuDvei11 小时前
Windows x86 架构下 Ubuntu 虚拟机内打包 Qt 文件指南
windows·qt·ubuntu
江公望14 小时前
Qt Q_OBJECT宏,5分钟讲清楚
qt
小灰灰搞电子14 小时前
PyQt QtWaitingSpinner详解-为你的 Qt 程序带来丝滑等待动画
开发语言·qt·pyqt
史迪仔011215 小时前
[QML] Qt6/Qt5四大渐变效果实战指南
开发语言·前端·c++·qt
小短腿的代码世界15 小时前
QCefView深度解析:在Qt中嵌入Chromium的架构设计与性能优化实战
开发语言·qt·性能优化
小短腿的代码世界15 小时前
Qt反射机制深度解析:从QMetaObject到运行时类型推导的底层密码
开发语言·qt