☰
Qt实时波形系统:万点不卡顿的工业级曲线绘制方案
2026/10/17 11:21:57 网站建设 项目流程

简介:本资源是一套基于Qt5与QCustomPlot开发的轻量级实时波形绘制控件,专为Qt初学者及嵌入式上位机开发者设计,有效降低实时曲线显示界面的开发门槛。压缩包共15个文件,含4个核心cpp源码(如widgetplot2d.cpp、qcustomplot.cpp)、3个头文件(含qcustomplot.h)、2个UI界面文件、2个PNG图标资源、1个pro工程配置文件及qrc资源定义等,整体仅459KB,结构精简、即插即用。已有9650人学习下载,反映出其在工业监控、传感器数据可视化等场景中的广泛实用性。使用者可直接实例化WidgetPlot2D控件,通过两步完成绘图:先调用initGraphName初始化多通道波形名称,再以addData持续注入数据;项目已预置QT += widgets printsupport配置,且完整集成QCustomPlot源码,无需额外编译依赖,开箱即用。

1. 实时波形绘制不是“刷帧动画”:一个毫秒级延迟、万点不卡顿的 Qt 曲线系统,专治工业传感器数据抖动、示波器级采样回放、嵌入式串口流可视化这类硬需求

你有没有试过用 QLabel+QPixmap 手动画折线图来显示每 5ms 一路进来的 ADC 采样值?结果是 CPU 占用飙到 80%,曲线跳变撕裂,放大后锯齿像被狗啃过——这不是你代码写得烂,是选错了工具链。这个项目就是为这类真实场景而生:它基于 Qt 6.5 + QCustomPlot 2.1.1 构建,不依赖 OpenGL 渲染管线,纯 CPU 绘制下仍能稳定维持 200Hz 刷新率(即 5ms 一帧),单图层支持 10 万点连续追加而不掉帧,且所有坐标轴缩放、平移、鼠标拖拽、双击复位全部响应在 16ms 内完成。它不是教学 Demo,而是某高校嵌入式实验室实测跑在 i.MX6ULL 开发板上的工业数据看板核心模块,已接入 RS485 温湿度阵列、CAN 总线电机转速流、USB 虚拟串口的高速示波采样数据。如果你正被“实时性”和“流畅度”反复背刺,又不想上 QML 或 WebAssembly 这类重方案,那这份开箱即用的 C++ 工程,就是你该立刻 clone 下来跑通的第一份可信基线。


2. 环境准备与最小可运行工程:从零构建一个带串口模拟源的 Qt 波形系统

2.1 依赖确认:Qt 版本、QCustomPlot 编译方式与头文件路径绑定逻辑

这个项目严格适配 Qt 6.5.x(官方测试版本为 6.5.3),不兼容 Qt 5.x 的信号槽语法(如SIGNAL()宏)和旧版QVector接口。QCustomPlot 必须以静态库方式编译并链接,而非头文件直连——这是避免运行时符号冲突和绘图线程安全问题的关键。常见翻车点在于:开发者直接把qcustomplot.h和.cpp拖进工程,导致QCPItemTracer在多线程追加数据时崩溃。正确做法是:

  1. 从 QCustomPlot 官方 GitHub Release 下载qcustomplot-2.1.1.tar.gz
  2. 解压后进入sources目录,用 Qt Creator 新建一个Library → Static Library工程,将qcustomplot.cpp加入源文件,qcustomplot.h加入头文件
  3. 在.pro文件中显式关闭 RTTI 和异常(工业环境刚需):
CONFIG += c++17 no_keywords QMAKE_CXXFLAGS += -fno-rtti -fno-exceptions
  1. 编译生成libqcustomplot.a(Linux/macOS)或qcustomplot.lib(Windows),并在主工程.pro中引用:
LIBS += -L$$PWD/../qcustomplot-build/lib -lqcustomplot INCLUDEPATH += $$PWD/../qcustomplot-build/include

提示:../qcustomplot-build/是你静态库工程输出目录,必须与主工程保持相对路径一致;若改用 CMake,请确保target_link_libraries(your_target PRIVATE qcustomplot)前已通过add_subdirectory()导入 QCustomPlot 子项目。

2.2 工程结构解析:为什么WaveformWidget是唯一需要继承的 UI 类

整个项目采用“数据-视图-控制器”轻量分层,不引入 MVC 框架增加复杂度。核心结构如下:

src/ ├── main.cpp # Qt 应用入口,仅创建 QApplication 和主窗口 ├── MainWindow.h/.cpp # 主窗口,含菜单栏、状态栏、中央 widget 容器 ├── WaveformWidget.h/.cpp # 【核心】继承自 QWidget,内嵌 QCustomPlot,封装全部绘图逻辑 ├── DataGenerator.h/.cpp # 模拟数据源:支持定时器触发 / 串口读取 / 文件回放三种模式 ├── SerialPortSimulator.h/.cpp # 虚拟串口实现(Linux: /dev/ttyV0, Windows: COM9),用于无硬件调试 └── utils/ # 辅助工具:环形缓冲区 RingBuffer、时间戳对齐器、浮点数精度截断器

WaveformWidget是唯一需要你二次开发的 UI 类——它不暴露QCustomPlot*指针给外部,所有交互(如缩放、拖拽、添加曲线)都通过其自有接口调用。例如,添加一条新曲线只需:

// WaveformWidget.h 中声明 public slots: void addCurve(const QString &name, const QColor &color = Qt::blue); // 使用时(在 MainWindow.cpp 中) waveWidget->addCurve("Motor_RPM", Qt::red);

这种封装杜绝了外部代码误操作QCustomPlot::xAxis导致坐标轴错乱的问题。我一般会把addCurve的底层实现拆成三步:创建QCPGraph、设置setPen()、调用addGraph()并缓存指针到QHash<QString, QCPGraph*>,后续appendData()直接查表,避免字符串比对开销。

2.3 启动第一个波形:5 行代码让模拟数据跑起来

在MainWindow.cpp的构造函数中,插入以下代码(位置在setCentralWidget(...)之后):

// 1. 创建波形控件 waveWidget = new WaveformWidget(this); setCentralWidget(waveWidget); // 2. 添加两条曲线(名称即图例文本) waveWidget->addCurve("CH1", Qt::cyan); waveWidget->addCurve("CH2", Qt::magenta); // 3. 启动模拟数据发生器(5ms 间隔,双通道正弦+噪声) DataGenerator *gen = new DataGenerator(this); gen->setIntervalMs(5); gen->setChannelCount(2); gen->start(); // 自动连接到 waveWidget 的 appendData slot // 4. 设置初始显示范围(X轴:最近 2 秒,Y轴:-5 ~ +5) waveWidget->setTimeRangeSeconds(2.0); waveWidget->setYRange(-5.0, 5.0);

编译运行后,你会看到两条平滑滚动的正弦波,鼠标滚轮可缩放 X 轴,左键拖拽可平移,双击恢复初始视图。注意:DataGenerator::start()内部使用QTimer::singleShot(0, ...)触发首帧,避免首次appendData被阻塞在事件循环外。

2.4 避坑:常见问题与排查(现象 → 原因 → 解决)

现象 1:曲线完全不刷新,或只闪一下就停住

原因:DataGenerator发出的dataReady(QVector<double>, QVector<double>)信号未正确连接到WaveformWidget::appendData,常见于 Qt 6 的新信号槽语法误用。
解决:检查连接语句是否为connect(gen, &DataGenerator::dataReady, waveWidget, &WaveformWidget::appendData);若用 lambda,必须捕获waveWidget为this或强指针,禁止[=]捕获(会导致waveWidget析构后 lambda 仍尝试调用)。

现象 2:X 轴时间刻度显示为科学计数法(如 1e+06),而非秒数

原因:QCustomPlot默认启用QCPAxis::sdLogarithmic缩放模式,而本项目所有时间轴均需线性。
解决:在WaveformWidget构造函数中显式设置:

ui->customPlot->xAxis->setScaleType(QCPAxis::stLinear); ui->customPlot->xAxis->setNumberFormat('g'); // 禁用科学计数 ui->customPlot->xAxis->setNumberPrecision(2); // 小数点后 2 位
现象 3:添加第三条曲线后,前两条消失或颜色错乱

原因:QCustomPlot::addGraph()返回的QCPGraph*指针被重复delete,或WaveformWidget内部QHash缓存未同步更新。
解决:在addCurve实现中,务必检查QHash是否已存在同名 key,若存在则先removeGraph(graphPtr)并delete graphPtr;新增时用new QCPGraph(ui->customPlot)显式构造,勿用ui->customPlot->addGraph()的返回值直接存入哈希(该函数内部可能复用指针)。

现象 4:程序退出时崩溃,报QCustomPlot: Attempted to delete a graph that is still in the plot

原因:WaveformWidget析构时未手动清理QCPGraph,QCustomPlot析构器尝试二次释放。
解决:在WaveformWidget::~WaveformWidget()中添加:

for (auto graph : m_graphs) { ui->customPlot->removeGraph(graph); delete graph; } m_graphs.clear();

其中m_graphs是QHash<QString, QCPGraph*>成员变量。


3. 核心模块拆解:WaveformWidget 的实时追加、坐标系管理与交互响应链路

3.1appendData函数:毫秒级低延迟数据注入的三重缓冲设计

WaveformWidget::appendData(const QString &name, const QVector<double> &x, const QVector<double> &y)是整个系统的数据入口。它不直接调用QCPGraph::addData(),而是采用“生产者-消费者”模型,通过QMutex保护的环形缓冲区(RingBuffer<QPair<QVector<double>, QVector<double>>>)解耦数据接收与绘图线程。关键逻辑如下:

void WaveformWidget::appendData(const QString &name, const QVector<double> &x, const QVector<double> &y) { if (!m_graphs.contains(name)) return; QMutexLocker locker(&m_dataMutex); // 步骤1:将新数据打包为时间戳对,压入环形缓冲区 m_dataBuffer.enqueue({x, y}); // 步骤2:若缓冲区满(默认 32 帧),丢弃最老一帧(防内存暴涨) if (m_dataBuffer.size() > 32) { m_dataBuffer.dequeue(); } }

真正触发绘图的是QTimer驱动的updatePlot()槽函数(默认 16ms 一帧):

void WaveformWidget::updatePlot() { QMutexLocker locker(&m_dataMutex); while (!m_dataBuffer.isEmpty()) { auto frame = m_dataBuffer.dequeue(); QCPGraph *graph = m_graphs[name]; graph->addData(frame.first, frame.second); // 批量添加,非逐点 } // 步骤3:仅当数据量超阈值(如 1000 点)才重采样,否则直绘 if (graph->data()->size() > 1000) { graph->resample(1000); // 降采样至 1000 点,保轮廓不失真 } }

注意:QCPGraph::resample(int maxPoints)是 QCustomPlot 2.1.1 新增 API,它用线性插值压缩数据点,比手动QVector::mid()截断更平滑。参数maxPoints建议设为屏幕宽度像素数的 1.5 倍(如 1280px 屏幕设 1920),避免过采样造成 GPU 填充率浪费。

3.2 时间轴动态管理:如何让 X 轴永远显示“最近 N 秒”,且不随数据累积而漂移

工业场景要求 X 轴始终锚定“当前时刻”,而非绝对时间戳。WaveformWidget用QElapsedTimer记录启动时刻,并将所有传入的x坐标视为相对于该时刻的偏移量(单位:秒)。核心逻辑在setTimeRangeSeconds(double seconds)中:

void WaveformWidget::setTimeRangeSeconds(double seconds) { m_timeRange = seconds; // 更新 X 轴范围:[now - range, now] double now = m_timer.elapsed() / 1000.0; // 毫秒转秒 ui->customPlot->xAxis->setRange(now - m_timeRange, now); }

但m_timer.elapsed()在updatePlot()中调用时,若QTimer间隔不准(如系统负载高导致 16ms 变成 22ms),X 轴会“抖动”。解决方案是:每次updatePlot时,用QElapsedTimer::elapsed()获取真实流逝时间,再反推当前应显示的右边界:

void WaveformWidget::updatePlot() { double now = m_timer.elapsed() / 1000.0; double rightBound = now; double leftBound = qMax(0.0, rightBound - m_timeRange); // 强制设置范围,避免自动缩放干扰 ui->customPlot->xAxis->setRange(leftBound, rightBound, Qt::AlignRight); // ... }

Qt::AlignRight参数确保缩放中心在右边界,即新数据总在右侧“生长”,旧数据从左侧“退出”。

3.3 鼠标交互增强:双击复位、滚轮缩放、拖拽平移的底层事件拦截逻辑

QCustomPlot 默认的鼠标事件(mousePressEvent,wheelEvent)被WaveformWidget全部重写,以支持工业级操作习惯。例如,双击复位并非简单调用rescaleAxes(),而是精确还原初始时间窗和 Y 轴范围:

void WaveformWidget::mouseDoubleClickEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) { double now = m_timer.elapsed() / 1000.0; ui->customPlot->xAxis->setRange(now - m_timeRange, now, Qt::AlignRight); ui->customPlot->yAxis->rescale(); // 仅重算 Y 轴,不碰 X ui->customPlot->replot(); // 强制重绘 } }

滚轮缩放则限制在 X 轴方向(Y 轴通常需人工设定量程):

void WaveformWidget::wheelEvent(QWheelEvent *event) { if (event->modifiers() & Qt::ControlModifier) { // Ctrl+滚轮:仅缩放 X 轴 double factor = event->angleDelta().y() > 0 ? 0.8 : 1.25; double center = ui->customPlot->xAxis->range().center(); double width = ui->customPlot->xAxis->range().size() * factor; ui->customPlot->xAxis->setRange(center - width/2, center + width/2); ui->customPlot->replot(); } else { // 无修饰键:传递给父类处理(如 Y 轴缩放) QWidget::wheelEvent(event); } }

血泪经验:QCustomPlot::axisRect()->setupFullAxesBox(true)会禁用鼠标拖拽,必须在WaveformWidget构造函数中调用ui->customPlot->axisRect()->setupFullAxesBox(false)关闭此功能,否则mouseMoveEvent不会被触发。

3.4 避坑:常见问题与排查(现象 → 原因 → 解决)

现象 1:鼠标滚轮缩放时,X 轴范围突变(如从 0~2 秒跳到 0~200 秒)

原因:QCustomPlot::xAxis->scaleRange()在数据为空时计算出无穷大范围,setRange()接收 NaN 导致行为不可预测。
解决:在wheelEvent中添加空数据保护:

if (ui->customPlot->graphCount() == 0 || ui->customPlot->graph(0)->data()->size() == 0) { return; // 无数据时不缩放 }
现象 2:拖拽平移后,松手瞬间曲线“弹回”原位

原因:QCustomPlot的QCPAxis::setRangeDrag(Qt::Horizontal)与WaveformWidget的自定义mouseMoveEvent冲突,导致事件被双重处理。
解决:彻底禁用 QCustomPlot 内置拖拽,在WaveformWidget构造函数中:

ui->customPlot->xAxis->setRangeDrag(Qt::Horizontal); ui->customPlot->xAxis->setRangeDrag(Qt::NoDrag); // 关闭内置拖拽 ui->customPlot->setInteractions(QCP::iRangeDrag | QCP::iRangeZoom); // 仅保留缩放

自定义拖拽逻辑在mousePressEvent/mouseMoveEvent中实现,用QCPAxis::setRangeLower()/setRangeUpper()手动更新。

现象 3:多曲线叠加时,图例(legend)文字重叠或被裁剪

原因:QCustomPlot::legend->setVisible(true)后未设置QCPLayoutGrid的行列拉伸策略。
解决:在WaveformWidget初始化中:

ui->customPlot->legend->setVisible(true); ui->customPlot->legend->setBrush(QColor(255, 255, 255, 150)); // 半透明白底 ui->customPlot->axisRect()->insetLayout()->setInsetPlacement(QCPLayoutInset::ipFree); // 手动定位图例到右上角 ui->customPlot->legend->setOuterBounds(QRect(0, 0, 200, 100));
现象 4:replot()调用后界面卡顿 100ms 以上

原因:QCustomPlot::replot(QCustomPlot::rpQueuedReplot)未启用,导致同步重绘阻塞主线程。
解决:全局启用异步重绘:

// 在 main() 函数中,QApplication 创建后 QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); // 添加这行: QCustomPlot::setReplotTimeout(16); // 16ms 内合并多次 replot 调用

并在WaveformWidget::updatePlot()末尾调用ui->customPlot->replot(QCustomPlot::rpQueuedReplot)。


4. 数据源对接实战:从模拟发生器到真实串口/UDP/CAN 的无缝替换

4.1DataGenerator抽象基类:统一接口屏蔽硬件差异

DataGenerator定义了start()/stop()/setIntervalMs()三个公共接口,其子类只需实现generateData()纯虚函数。这种设计让你能在不修改WaveformWidget的前提下,切换数据源:

class DataGenerator : public QObject { Q_OBJECT public: virtual void start() = 0; virtual void stop() = 0; virtual void setIntervalMs(int ms) = 0; signals: void dataReady(const QString &channelName, const QVector<double> &x, const QVector<double> &y); protected: virtual void generateData() = 0; // 子类实现:填充 x/y 向量 };

SerialPortSimulator继承它,内部用QSerialPort读取真实设备;UdpDataReceiver则监听 UDP 端口;而FilePlayer从 CSV 文件流式读取。所有子类在generateData()中调用emit dataReady(...),信号自动连接到WaveformWidget::appendData。

4.2 真实串口接入:QSerialPort配置要点与粘包处理

SerialPortSimulator类中,QSerialPort初始化必须显式设置:

m_serial->setPortName("/dev/ttyUSB0"); // Linux // m_serial->setPortName("COM3"); // Windows m_serial->setBaudRate(115200); m_serial->setDataBits(QSerialPort::Data8); m_serial->setParity(QSerialPort::NoParity); m_serial->setStopBits(QSerialPort::OneStop); m_serial->setFlowControl(QSerialPort::NoFlowControl);

关键难点是串口数据粘包:单次readAll()可能返回多个采样点(如 10 个 4 字节 float)。解决方案是定义固定帧头(如0xAA 0x55)+ 长度字节 + 数据体 + CRC:

void SerialPortSimulator::handleReadyRead() { QByteArray data = m_serial->readAll(); m_buffer.append(data); while (m_buffer.size() >= 6) { // 最小帧长:2字节头 + 1字节长 + 2字节CRC + 1字节数据? if (m_buffer.at(0) == 0xAA && m_buffer.at(1) == 0x55) { quint8 len = m_buffer.at(2); if (m_buffer.size() >= 4 + len) { QByteArray frame = m_buffer.left(4 + len); parseFrame(frame); // 解析出 float 值,emit dataReady m_buffer.remove(0, 4 + len); } else { break; // 不足一帧,等待下次 read } } else { m_buffer.remove(0, 1); // 同步失败,跳过首字节重试 } } }

提示:parseFrame()中用qFromBigEndian<float>(frame.mid(3, 4))解析大端 float,避免memcpy导致的 strict-aliasing 警告。

4.3 UDP 数据接收:如何应对网络抖动导致的乱序与丢包

UdpDataReceiver使用QUdpSocket,但必须开启QUdpSocket::ShareAddress选项允许多实例绑定同一端口(便于调试时同时运行发送端和接收端):

m_socket->bind(QHostAddress::Any, 8080, QUdpSocket::ShareAddress); connect(m_socket, &QUdpSocket::readyRead, this, &UdpDataReceiver::handleUdpRead);

为应对乱序,每包 UDP 数据携带单调递增的序列号(uint32_t),UdpDataReceiver维护一个QMap<quint32, QByteArray>缓存,收到包后检查序列号是否连续。若发现缺口(如收到 1,2,4),则启动 50ms 定时器等待 3 号包;超时则丢弃 4 及之后所有包,重置窗口——这是工业协议常用策略,比 TCP 重传更可控。

4.4 避坑:常见问题与排查(现象 → 原因 → 解决)

现象 1:串口打开失败,报Permission denied

原因:Linux 下/dev/ttyUSB0权限不足,当前用户不在dialout组。
解决:执行sudo usermod -a -G dialout $USER,重启终端;或临时sudo chmod 666 /dev/ttyUSB0(不推荐生产环境)。

现象 2:UDP 接收数据时,readyRead信号频繁触发但pendingDatagramSize()返回 0

原因:QUdpSocket::bind()未指定QUdpSocket::DefaultForPlatform,在某些 Qt 版本下绑定失败但不报错。
解决:显式指定bind()第三个参数:

m_socket->bind(QHostAddress::Any, 8080, QUdpSocket::ShareAddress | QUdpSocket::DefaultForPlatform);
现象 3:CSV 文件回放时,曲线跳变剧烈,疑似采样率不匹配

原因:文件中时间戳列为绝对时间(如 Unix 时间戳),而WaveformWidget要求相对时间(从启动开始的秒数)。
解决:在FilePlayer::parseLine()中,首次读取时记录m_baseTime = timestamp,后续所有x值设为timestamp - m_baseTime。

现象 4:CAN 总线数据接入后,WaveformWidgetCPU 占用飙升

原因:CAN 帧率极高(如 10kHz),emit dataReady()频率超过WaveformWidget::updatePlot()处理能力,导致信号队列积压。
解决:在CanDataReceiver::generateData()中添加速率限制:

if (m_lastEmitTime.elapsed() < 5) return; // 强制最低 5ms 间隔 m_lastEmitTime.restart(); emit dataReady(...);

5. 二次开发指南:定制化曲线样式、添加测量光标、导出 PNG 与 CSV

5.1 曲线样式深度定制:从基础颜色到抗锯齿线宽的全参数控制

WaveformWidget::addCurve()支持传入QColor,但真正影响视觉质量的是QPen的setWidthF()和setCosmetic():

void WaveformWidget::addCurve(const QString &name, const QColor &color) { QCPGraph *graph = new QCPGraph(ui->customPlot); graph->setName(name); QPen pen(color); pen.setWidthF(1.5); // 线宽 1.5px,比默认 1px 更清晰 pen.setCosmetic(true); // 禁用线宽随缩放变化,保持视觉一致 graph->setPen(pen); // 启用抗锯齿(对高频曲线至关重要) graph->setAdaptiveSampling(true); // 自动简化密集点 graph->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssNone)); // 关闭散点,只画线 ui->customPlot->addGraph(); m_graphs[name] = graph; }

若需虚线效果,用pen.setStyle(Qt::DashLine);若要渐变色,需继承QCPGraph并重写drawLinePlot(),但会显著增加复杂度,建议优先用QCPGraph::setChannelFill()填充曲线下方面积。

5.2 添加测量光标:实现双光标读取 ΔX/ΔY 与实时数值显示

WaveformWidget内置QCPItemTracer实现单点追踪,但双光标需两个 tracer + 一条连接线。核心步骤:

  1. 创建两个QCPItemTracer,绑定到xAxis和yAxis:
m_tracerA = new QCPItemTracer(ui->customPlot); m_tracerA->setGraphKey(0.0); // 初始 X 位置 m_tracerA->setGraphValue(0.0); // 初始 Y 位置 m_tracerA->setStyle(QCPItemTracer::tsSquare); m_tracerB = new QCPItemTracer(ui->customPlot); m_tracerB->setGraphKey(1.0); m_tracerB->setGraphValue(0.0); m_tracerB->setStyle(QCPItemTracer::tsSquare);
  1. 用QCPItemLine连接两 tracer:
m_line = new QCPItemLine(ui->customPlot); m_line->start->setParentAnchor(m_tracerA->position); m_line->end->setParentAnchor(m_tracerB->position);
  1. 在mouseMoveEvent中,根据鼠标位置更新 tracer:
void WaveformWidget::mouseMoveEvent(QMouseEvent *event) { if (m_isDraggingTracer) { double x = ui->customPlot->xAxis->pixelToCoord(event->pos().x()); double y = ui->customPlot->yAxis->pixelToCoord(event->pos().y()); if (m_activeTracer == m_tracerA) { m_tracerA->setGraphKey(x); m_tracerA->setGraphValue(y); } else if (m_activeTracer == m_tracerB) { m_tracerB->setGraphKey(x); m_tracerB->setGraphValue(y); } // 更新状态栏显示 ΔX/ΔY double dx = qAbs(m_tracerB->graphKey() - m_tracerA->graphKey()); double dy = qAbs(m_tracerB->graphValue() - m_tracerA->graphValue()); emit cursorMoved(dx, dy); } }

cursorMoved(dx, dy)信号可连接到QStatusBar显示实时差值。

5.3 导出功能实现:PNG 截图与 CSV 数据导出的线程安全方案

导出 PNG 必须在 GUI 线程调用QCustomPlot::savePng(),但大图(如 3840x2160)会阻塞界面。解决方案是:先replot()到离屏QPixmap,再保存:

bool WaveformWidget::exportPng(const QString &path, int width, int height) { QPixmap pixmap(width, height); pixmap.fill(Qt::white); QPainter painter(&pixmap); // 临时调整绘图尺寸 QSize originalSize = ui->customPlot->size(); ui->customPlot->resize(width, height); ui->customPlot->render(&painter, QRect(), QRect(), QWidget::DrawChildren); ui->customPlot->resize(originalSize); bool success = pixmap.save(path, "PNG", 100); painter.end(); return success; }

CSV 导出则需遍历QCPGraph::data(),但QCPGraphDataContainer是私有类。安全做法是维护一份QVector<QPair<double, double>>的镜像数据(在appendData时同步写入),导出时直接遍历该镜像:

void WaveformWidget::exportCsv(const QString &path, const QString &name) { QFile file(path); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) return; QTextStream out(&file); out << "Time(s),Value\n"; for (const auto &point : m_dataMirror[name]) { out << QString::number(point.first, 'g', 6) << "," << QString::number(point.second, 'g', 6) << "\n"; } file.close(); }

m_dataMirror是QHash<QString, QVector<QPair<double, double>>>,在appendData中同步更新。

5.4 避坑:常见问题与排查(现象 → 原因 → 解决)

现象 1:导出 PNG 时,图片内容为空白或只有坐标轴

原因:QCustomPlot::render()调用时,QCustomPlot尚未完成布局(QLayout未生效),导致size()返回 0。
解决:强制触发布局更新:

ui->customPlot->update(); // 触发重绘 QApplication::processEvents(); // 确保事件循环处理完毕 ui->customPlot->render(&painter, ...);
现象 2:CSV 导出数据量巨大(10 万点)时,界面假死

原因:QTextStream写入是同步 IO,阻塞主线程。
解决:将导出逻辑移到QThread:

class CsvExporter : public QThread { Q_OBJECT public: void setData(const QVector<QPair<double, double>> &data) { m_data = data; } void setPath(const QString &path) { m_path = path; } protected: void run() override { QFile file(m_path); if (file.open(QIODevice::WriteOnly | QIODevice::Text)) { QTextStream out(&file); for (const auto &p : m_data) { out << p.first << "," << p.second << "\n"; } file.close(); } } private: QVector<QPair<double, double>> m_data; QString m_path; };

在WaveformWidget中创建CsvExporter实例,moveToThread()后启动。

现象 3:双光标移动时,QCPItemLine连接线闪烁

原因:QCPItemLine的start/end锚点未设置setParentAnchor(),导致每次replot()重新计算位置。
解决:确保setParentAnchor()在 tracer 创建后立即调用,且 tracer 的position类型为QCPItemPosition::ptPlotCoords(而非ptAxisRectRatio)。

现象 4:QCPItemTracer在缩放后位置偏移

原因:tracer 的setGraphKey()设置的是数据坐标,但QCustomPlot缩放时若未调用replot(),tracer 未重定位。
解决:在updatePlot()末尾添加:

m_tracerA->updatePosition(); m_tracerB->updatePosition();

6. 从“能跑”到“可靠”的最后一公里:日志埋点、性能压测与跨平台部署验证

6.1 关键路径日志埋点:用 `qInstallMessage

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询