全景扫描瀑布图实现
1. 全景扫描瀑布图1.AUIMSCANPluginSystemControl/Waterfall/waterfall.cpp的Waterfall类这是普通 QWidget核心不在 OpenGL而是QPixmapQImagepaintEvent拼出来。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——按列聚合 纵向滚动 伪彩色横轴像素列数colCount _dataImage.width()频谱点数_dataCnt。每列对应一段 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); }时间方向滚动把整个_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);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上画游标、时间文字等DrawCenterLine、DrawTime…。这就是一种典型的混合绘制离屏缓冲QImage里用 CPU 写像素paintEvent里用QPainter叠图和矢量 UI。1.B 扫描/单频里更常见的单频测向里waterfallCtrl用的是这套这里没有单独的paintEvent写瀑布像素而是QGraphicsViewWaterfallItem::drawCurve。数据线程WaterfallDataManager::run把频谱重采样到「屏幕宽度」列点多则按列max点少则线性插值再drawCurve()→WaterfallItem::handleData。颜色WaterfallItem::handleData同样memmove整幅图向下滚一行对每个水平位置id电平data[id]映射到100 档颜色COLOR_COUNTint 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_pMapColor在WaterfallView构造时由分段线性 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; // ...显示drawCurve里painter-drawImage(rect(), m_image)——仍是 Scene 的 item 绘制路径和 View 的drawForeground等可再叠鼠标十字线。「混合」子线程算每帧_maxData并调handleData写QImage主线程updateUi()→QGraphicsView刷新Scene典型生产者/消费者 离屏位图。2. Level FlowLevelStream控件在 UI 里是levelstreamCtrl数据链路例如Render(level)→SignalHandleData→SlotRender。void LevelStream::Render(float level) { emit SignalHandleData(level); }SlotRender不是直接画而是更新时间序列 屏幕上的折线点横轴每个新点x为leftMargin 样本序号满一行后整列左移所有点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对_useLevelStreamPoints做generateSmoothCurve样条描一条青色_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在持锁情况下对当前_drawLayer调DrawLayer内部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 轴量程线性映射到调色板WaterfallcolorId 100 * (dB - ymin)/(ymax-ymin)查 RGB 表。Level Flow画笔/渐变不是按像素读 palette。Render vs paintEventRender/handleData喂数据 改离屏图paintEvent把已有位图和矢量 UI 合成到屏幕。「混动/混合」统一特征是CPU 在QImage或 Item里改像素QPainter/Graphics 框架负责合成不少路径里再加独立线程降低 UI 阻塞WaterfallDataManager、LevelStream::DataThread。**LevelStream曲线层电平在主线程的 SlotRender 里进队列、改点坐标绘制主要靠 DataThread 约每 100ms 调一次 DrawLayer把曲线画进 _drawLayer再 emit 回主线程 SlotGetLayer → update()。是 「数据高频进、位图低频重画」。瀑布图SystemControl::WaterfallRender → Replot 每次来数据都会 DrawDataLayermemmove colorize再 PaintUiSignal → update()刷新与数据帧基本同频。WaterfallView数据在 子线程里算每行像素、handleData 里对 QImage 做 memmove 填色UI 用 定时器约 50ms 汇总 updateUi() 刷新视图。相同点都可能在 离屏 QImage/位图 上改像素最后用 update/paint 显示。不同点LevelStream 是 1D 时域折线 固定周期合成瀑布图是 2D 频谱热力滚屏且 UIMSCAN 的 Waterfall 与 Waterfall 的线程/节流策略也和 LevelStream 不一致。**