大家好,今天分享一个轻量级类示波器UI界面,基于Qt+C/C++实现,可轻松处理数十万甚至百万数据点绘制而不卡顿。
因一个项目需要显示高速AD采集的仿真波形,采用普通QChart和QCustomPlot实现时,若前端采集速率拉满,UI界面瞬间卡成PPT。
还要支持缩放、游标测量等功能,用现成控件改起来比较麻烦。
所以我自己基于QPainter + RingBuffer封装了一个轻量级waveWidget。
在Demo中并不是将数据简单地画出来,而是实现了一个接近示波器交互体验的波形显示控件:
支持实时刷新、滚动缓存
普通滚轮:Y轴缩放
Ctrl + 滚轮:X轴缩放
X轴索引、Y轴数值实时显示
Marker测量 ΔX / ΔY
双击窗口放大、缩小功能
支持右键菜单操作,可添加自定义功能
话不多说,先上图看效果:
主界面实时显示(可同时支持多通道)
普通滚轮:Y轴缩放
Ctrl + 滚轮:X轴缩放
双击放大缩小选中窗口
标记测量 ΔX / ΔY
支持右键菜单操作,可添加自定义功能
下面简单讲述下部分实现代码,文章底部提供完整实现源码,可直接拷贝到项目工程使用。
waveWidget继承自QWidget,整个控件可以分成三层:数据层(Buffer)、渲染层(Painter)、交互层(Mouse Event),这三个层次分离得比较清楚,后续扩展会很方便。
在数据处理时抛弃传统的 vector.push_back(),采用环形缓冲区设计,当缓冲区未满时正常写入,缓冲区满则自动覆盖最旧数据。
最大占用内存固定化,写入复杂度 O(1),避免大数据量下性能下降,非常适合高频采样场景。
数据写入显示逻辑:
void WaveWidget::appendData(float value) { m_buffer[m_writePos] = value; m_writePos = (m_writePos + 1) % m_capacity; if (m_count < m_capacity) m_count++; update(); }主体绘制分为四层:背景、网格、波形、交互元素,比全部塞进 paintEvent 可维护高很多。
drawGrid(p); drawWave(p); drawMarkers(p); drawCursor(p);对于底层的FPGA研发或信号处理工程师来说,光能看到波形是远远不够的,必须能精确地测量出时序。为此,还重写了鼠标与滚轮事件。
动态缩放:在 wheelEvent 中,监听修饰键,按下Ctrl + 滚轮,修改 m_viewSizeX 进行 X 轴时间基准缩放。纯滚轮则修改 m_yScale 控制 Y 轴幅度比例。所有的缩放都是通过纯数学映射完成,不涉及任何底层数据的重新拷贝。
void WaveWidget::wheelEvent(QWheelEvent *e) { if (e->modifiers() & Qt::ControlModifier) { setXView(m_viewSizeX * ((e->angleDelta().y() > 0) ? 0.8 : 1.25)); } else { setYScale(m_yScale * ((e->angleDelta().y() > 0) ? 1.1 : 0.9)); } }右键工程菜单与卡尺测量:通过重写 contextMenuEvent ,组件支持呼出原生右键菜单,可以随时在波形上打下两根 X 轴(黄色)或 Y 轴(青色)标记线。
void WaveWidget::contextMenuEvent(QContextMenuEvent *e) { if (!m_contextMenu) { m_contextMenu = new QMenu(this); m_contextMenu->addAction("添加 X 标记", [this]() { if(m_markX.size()>=2) m_markX.clear(); m_markX.append(m_selectedIndex); update(); }); m_contextMenu->addAction("添加 Y 标记", [this]() { if(m_markY.size()>=2) m_markY.clear(); m_markY.append((height()/2.0 - m_mousePos.y())/m_yScale); update(); }); m_contextMenu->addAction("清除标记", [this](){ m_markX.clear(); m_markY.clear(); update(); }); } m_contextMenu->exec(e->globalPos()); }实时差值计算:在drawMarkers函数中,代码会自动执行 abs( m_markX[1] - m_markX[0]) 计算出X轴的采样点差值,以及利用 fabs(m_markY[1] - m_markY[0])计算出Y轴的幅度差值,并高亮显示在左上角。
void WaveWidget::drawMarkers(QPainter &p) { double xStep = (double)width() / (m_viewSizeX - 1); p.setPen(QPen(Qt::yellow, 1)); for (int lx : m_markX) { double px = width() - (m_count - 1 - lx) * xStep; p.drawLine(px, 0, px, height()); } p.setPen(QPen(Qt::cyan, 1)); for (double vy : m_markY) { double py = height() / 2.0 - vy * m_yScale; p.drawLine(0, py, width(), py); } // 绘制结果文字 p.setPen(Qt::white); if (m_markX.size() == 2) p.drawText(10, 20, QString("ΔX: %1").arg(abs(m_markX[1] - m_markX[0]))); if (m_markY.size() == 2) p.drawText(10, 40, QString("ΔY: %1").arg(fabs(m_markY[1] - m_markY[0]), 0, 'f', 3)); }双击处理:重载mouseDouble ClickEvent(),当双击时发送信号,把业务逻辑交给外部处理。
void WaveWidget::mouseDoubleClickEvent(QMouseEvent *e) { if (e->button() == Qt::LeftButton) { emit doubleClicked(); // 发出双击信号 } QWidget::mouseDoubleClickEvent(e); }很多时候,真正好用的工具,不一定复杂,但一定贴近实际需求。在工业级的测控系统与高速数据采集中,性能的瓶颈往往就隐藏在一次不经意的深拷贝,或者一次冗余的屏幕擦除中。掌握了底层 UI 渲染的机制,即使面对 GB/s 的吞吐数据,也能稳如泰山。
嵌入式软硬件系统
专注于嵌入式软硬件相关经验分享、工程实践与技术探索,涵盖单片机、FPGA、PCIe、驱动开发、上位机软件以及测控系统设计等多方面内容。