1. 全景扫描瀑布图
1.A UIMSCANPlugin:SystemControl/Waterfall/waterfall.cpp 的 Waterfall 类
这是 普通 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)------按列聚合 + 纵向滚动 + 伪彩色
-
横轴像素列数
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 → 右侧色标条 _colorRangeImage (DrawBackgroundLayer 里按纵向扫渐变填的)→ 瀑布主体 _dataImage → 再在 没有锁住的 painter 上画游标、时间文字等(DrawCenterLine、DrawTime...)。
这就是一种典型的 混合绘制 :离屏缓冲(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_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 Flow(LevelStream)
控件在 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(电平)。 |
| 颜色怎么算 | Waterfall:QCPColorGradient::colorize,按 Y 轴量程线性映射到调色板 ;CJJWaterfall:colorId = 100 * (dB - ymin)/(ymax-ymin) 查 RGB 表。Level Flow:画笔/渐变,不是按像素读 palette。 |
| Render vs paintEvent | Render/handleData:喂数据 + 改离屏图 ;paintEvent:把已有位图和矢量 UI 合成到屏幕。 |
| 「混动/混合」 | 统一特征是:CPU 在 QImage(或 Item)里改像素 + QPainter/Graphics 框架负责合成 ;不少路径里再加 独立线程 降低 UI 阻塞(CJJWaterfallDataManager、LevelStream::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 不一致。