Qt C++ 图表可视化进阶:QCustomPlot 动态绘图 + 交互功能落地

内容分享3小时前发布
0 0 0

QCustomPlot 是 Qt 生态中轻量且强大的开源绘图库,相比 Qt 官方的 QCharts,它更轻量化、定制性更强,尤其适合实时数据可视化、工控界面、科研数据分析等场景。本文将从实战角度,系统讲解基于 QCustomPlot 的动态绘图实现、核心交互功能落地,并结合工业级应用场景优化性能,全文约 5000 字,覆盖从环境搭建到高级功能的完整链路。

一、QCustomPlot 基础环境搭建

1.1 库文件获取与集成

QCustomPlot 仅需两个核心文件(
qcustomplot.h

qcustomplot.cpp
)即可集成,无需复杂的编译链接,适合快速开发:

下载地址:QCustomPlot 官方网站(建议下载最新稳定版,如 2.1.1);集成步骤:

qcustomplot.h

qcustomplot.cpp
复制到 Qt 项目目录;在 Qt Creator 中右键项目 → “添加现有文件”,选中上述两个文件;在项目的
.pro
文件中添加依赖(若需导出图片/打印功能):


QT += core gui widgets printsupport
SOURCES += main.cpp 
           mainwindow.cpp 
           qcustomplot.cpp
HEADERS += mainwindow.h 
           qcustomplot.h

1.2 基础绘图框架搭建

首先创建一个包含 QCustomPlot 控件的主窗口,完成最基础的静态绘图,为后续动态绘图和交互打基础:

步骤 1:UI 设计

在 Qt Designer 中,拖入一个
QWidget
控件,右键选择 “提升为” → 输入
QCustomPlot
→ 勾选 “全局包含” → 确认,将普通 Widget 提升为 QCustomPlot 控件,命名为
customPlot

步骤 2:基础静态绘图示例

// mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "qcustomplot.h"

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    initCustomPlot(); // 初始化绘图控件
}

MainWindow::~MainWindow()
{
    delete ui;
}

// 初始化QCustomPlot基础配置
void MainWindow::initCustomPlot()
{
    // 1. 设置坐标轴标签
    ui->customPlot->xAxis->setLabel("X 轴(时间/s)");
    ui->customPlot->yAxis->setLabel("Y 轴(数值)");
    
    // 2. 设置坐标轴范围
    ui->customPlot->xAxis->setRange(0, 10);
    ui->customPlot->yAxis->setRange(0, 100);
    
    // 3. 创建曲线
    QCPGraph *graph = ui->customPlot->addGraph();
    graph->setName("示例曲线"); // 曲线名称(用于图例)
    
    // 4. 生成静态数据
    QVector<double> xData(100), yData(100);
    for (int i = 0; i < 100; ++i) {
        xData[i] = i * 0.1; // X轴:0~10s
        yData[i] = 50 + 20 * sin(xData[i]); // Y轴:正弦曲线
    }
    
    // 5. 绑定数据到曲线
    graph->setData(xData, yData);
    
    // 6. 显示图例
    ui->customPlot->legend->setVisible(true);
    ui->customPlot->legend->setFont(QFont("Arial", 9));
    
    // 7. 刷新绘图
    ui->customPlot->replot();
}

运行程序,可看到一条静态的正弦曲线,这是后续动态绘图和交互的基础。

二、动态绘图核心实现

动态绘图是 QCustomPlot 最常用的场景(如实时采集传感器数据、工控数据监控),核心思路是:通过定时器周期性生成/更新数据,调整坐标轴范围,并重绘曲线。

2.1 基础动态绘图(固定数据长度)

需求:实时生成随机数据,曲线仅显示最近 100 个数据点,超出部分自动左移。

步骤 1:定义全局变量


MainWindow.h
中添加:


#include <QTimer>
#include <QVector>

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private slots:
    void updateData(); // 定时器槽函数:更新数据

private:
    Ui::MainWindow *ui;
    QTimer *dataTimer; // 数据更新定时器
    QVector<double> xData, yData; // 存储X、Y轴数据
    int dataCount; // 数据计数
};
步骤 2:初始化定时器与数据

修改
initCustomPlot
函数,并初始化定时器:


// mainwindow.cpp
void MainWindow::initCustomPlot()
{
    // 基础配置(同静态绘图)
    ui->customPlot->xAxis->setLabel("X 轴(时间/s)");
    ui->customPlot->yAxis->setLabel("Y 轴(随机值)");
    ui->customPlot->legend->setVisible(true);
    ui->customPlot->addGraph()->setName("实时随机数据");

    // 初始化数据容器
    xData.clear();
    yData.clear();
    dataCount = 0;

    // 初始化定时器:100ms更新一次数据
    dataTimer = new QTimer(this);
    connect(dataTimer, &QTimer::timeout, this, &MainWindow::updateData);
    dataTimer->start(100); // 启动定时器
}
步骤 3:实现数据更新逻辑

void MainWindow::updateData()
{
    // 1. 生成新数据(X轴为时间,Y轴为0~100随机数)
    double x = dataCount * 0.1; // 每100ms一个点,X轴单位为秒
    double y = qrand() % 100; // 0~100随机数

    // 2. 添加新数据到容器
    xData.append(x);
    yData.append(y);
    dataCount++;

    // 3. 限制数据长度:仅保留最近100个点
    if (xData.size() > 100) {
        xData.removeFirst();
        yData.removeFirst();
    }

    // 4. 更新曲线数据
    ui->customPlot->graph(0)->setData(xData, yData);

    // 5. 调整X轴范围:始终显示最后100个点的X轴区间
    ui->customPlot->xAxis->setRange(x - 10, x); // 10秒的显示范围(100个点×0.1s)
    ui->customPlot->yAxis->setRange(0, 100); // Y轴固定范围

    // 6. 重绘曲线(关键:必须调用replot()刷新)
    ui->customPlot->replot();
}

运行程序,可看到曲线实时向右延伸,当数据超过 100 个点后,曲线自动左移,始终显示最近 10 秒的随机数据。

2.2 高级动态绘图优化

2.2.1 性能优化:减少重绘次数

频繁调用
replot()
会消耗性能,尤其当数据更新频率高(如 10ms 一次)时,可通过
replot(QCustomPlot::rpQueued)
让重绘入队,由 Qt 事件循环调度:


// 替换原replot()调用
ui->customPlot->replot(QCustomPlot::rpQueued);
2.2.2 多曲线动态绘图

需求:同时显示两条动态曲线(如温度、湿度),各自独立更新数据。

修改
updateData
函数:


// 初始化时添加第二条曲线
ui->customPlot->addGraph()->setName("温度");
ui->customPlot->addGraph()->setName("湿度");
ui->customPlot->graph(0)->setPen(QPen(Qt::red)); // 温度曲线红色
ui->customPlot->graph(1)->setPen(QPen(Qt::blue)); // 湿度曲线蓝色

// 更新数据时同时更新两条曲线
void MainWindow::updateData()
{
    double x = dataCount * 0.1;
    double temp = 20 + 5 * sin(x); // 温度:20±5℃
    double humi = 60 + 10 * cos(x); // 湿度:60±10%

    // 存储两条曲线的数据(需定义两个Y轴数据容器:yTemp、yHumi)
    xData.append(x);
    yTemp.append(temp);
    yHumi.append(humi);

    // 限制数据长度
    if (xData.size() > 100) {
        xData.removeFirst();
        yTemp.removeFirst();
        yHumi.removeFirst();
    }

    // 更新两条曲线
    ui->customPlot->graph(0)->setData(xData, yTemp);
    ui->customPlot->graph(1)->setData(xData, yHumi);

    // 调整Y轴范围(自动适配两条曲线)
    ui->customPlot->yAxis->setRangeLower(qMin(yTemp.last(), yHumi.last()) - 5);
    ui->customPlot->yAxis->setRangeUpper(qMax(yTemp.last(), yHumi.last()) + 5);
    ui->customPlot->xAxis->setRange(x - 10, x);

    ui->customPlot->replot(QCustomPlot::rpQueued);
}
2.2.3 动态Y轴自适应

无需固定 Y 轴范围,让 Y 轴自动适配当前显示数据的最大值和最小值:


// 替换固定Y轴范围的代码
double yMin = *std::min_element(yData.begin(), yData.end());
double yMax = *std::max_element(yData.begin(), yData.end());
ui->customPlot->yAxis->setRange(yMin - 5, yMax + 5); // 留5个单位的余量

三、核心交互功能落地

QCustomPlot 内置了丰富的交互接口,无需重复造轮子,可快速实现缩放、平移、数据点提示、曲线隐藏/显示等常用交互功能。

3.1 基础交互:缩放与平移

QCustomPlot 原生支持鼠标交互,只需启用对应的交互标志即可:


// 在initCustomPlot中添加
void MainWindow::initCustomPlot()
{
    // 启用鼠标交互:缩放(滚轮)、平移(鼠标左键拖动)
    ui->customPlot->setInteractions(QCP::iRangeDrag | QCP::iRangeZoom | QCP::iSelectPlottables);
    // 说明:
    // iRangeDrag:允许拖动坐标轴(平移曲线)
    // iRangeZoom:允许滚轮缩放
    // iSelectPlottables:允许选择曲线(用于后续隐藏/显示)
}

启用后,可实现:

鼠标左键拖动:平移曲线;鼠标滚轮:缩放曲线(X/Y轴同步缩放);按住 Ctrl 键 + 滚轮:仅缩放X轴;按住 Shift 键 + 滚轮:仅缩放Y轴。

3.2 数据点提示:鼠标悬停显示数值

需求:鼠标悬停在曲线上时,显示当前点的 X/Y 坐标值,提升数据可读性。

步骤 1:启用鼠标跟踪


MainWindow
构造函数中启用 QCustomPlot 的鼠标跟踪:


ui->customPlot->setMouseTracking(true);
步骤 2:重写鼠标移动事件


MainWindow.h
中声明事件处理函数:


protected:
    void mouseMoveEvent(QMouseEvent *event) override;
步骤 3:实现鼠标悬停提示逻辑

// mainwindow.cpp
void MainWindow::mouseMoveEvent(QMouseEvent *event)
{
    // 1. 将鼠标坐标转换为绘图区域的坐标
    QPointF mousePos = ui->customPlot->mapFromGlobal(event->globalPos());
    double x = ui->customPlot->xAxis->pixelToCoord(mousePos.x());
    double y = ui->customPlot->yAxis->pixelToCoord(mousePos.y());

    // 2. 查找距离鼠标最近的数据点
    int closestIndex = -1;
    double minDist = 1e9;
    QCPGraph *graph = ui->customPlot->graph(0); // 取第一条曲线
    const QVector<double> &graphX = graph->data()->keys();
    const QVector<double> &graphY = graph->data()->values();

    for (int i = 0; i < graphX.size(); ++i) {
        double dist = qSqrt(qPow(graphX[i] - x, 2) + qPow(graphY[i] - y, 2));
        if (dist < minDist) {
            minDist = dist;
            closestIndex = i;
        }
    }

    // 3. 若找到近点,显示提示
    if (closestIndex != -1 && minDist < 5) { // 距离阈值:5个像素
        QString tip = QString("X: %1
Y: %2").arg(graphX[closestIndex], 0, 'f', 2)
                                             .arg(graphY[closestIndex], 0, 'f', 2);
        QToolTip::showText(event->globalPos(), tip, ui->customPlot);
    } else {
        QToolTip::hideText(); // 远离数据点时隐藏提示
    }

    QMainWindow::mouseMoveEvent(event); // 调用父类事件处理
}

运行程序,鼠标悬停在曲线上时,会显示当前点的精确坐标,提升数据交互体验。

3.3 曲线隐藏/显示:图例点击交互

需求:点击图例中的曲线名称,可切换曲线的显示/隐藏状态,这是多曲线场景的必备功能。

步骤 1:连接图例点击信号


initCustomPlot
中添加:


// 连接图例点击信号
connect(ui->customPlot, &QCustomPlot::legendClick, this, &MainWindow::onLegendClick);
步骤 2:实现图例点击槽函数

// MainWindow.h 中声明槽函数
private slots:
    void onLegendClick(QCPLegendItem *item, QMouseEvent *event);

// mainwindow.cpp 实现
void MainWindow::onLegendClick(QCPLegendItem *item, QMouseEvent *event)
{
    // 忽略右键点击
    if (event->button() != Qt::LeftButton) return;

    // 获取点击的图例对应的曲线
    QCPPlottableLegendItem *plottableItem = qobject_cast<QCPPlottableLegendItem*>(item);
    if (!plottableItem) return;

    QCPGraph *graph = qobject_cast<QCPGraph*>(plottableItem->plottable());
    if (graph) {
        // 切换曲线的显示状态
        graph->setVisible(!graph->visible());
        // 刷新图例(更新显示状态)
        item->update();
        // 重绘曲线
        ui->customPlot->replot();
    }
}

3.4 数据导出:保存图表为图片/CSV

交互功能还需支持数据导出,满足用户存档需求:

3.4.1 导出图表为图片

// 槽函数:导出图片
void MainWindow::exportPlotAsImage()
{
    QString filePath = QFileDialog::getSaveFileName(this, "保存图片", "", "PNG图片 (*.png);;JPG图片 (*.jpg)");
    if (!filePath.isEmpty()) {
        // 保存图片(分辨率300dpi,尺寸为绘图区域大小)
        ui->customPlot->savePng(filePath, ui->customPlot->width(), ui->customPlot->height(), 300);
        QMessageBox::information(this, "成功", "图片导出完成!");
    }
}
3.4.2 导出数据为CSV

// 槽函数:导出CSV
void MainWindow::exportDataAsCSV()
{
    QString filePath = QFileDialog::getSaveFileName(this, "保存CSV", "", "CSV文件 (*.csv)");
    if (!filePath.isEmpty()) {
        QFile file(filePath);
        if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
            QTextStream out(&file);
            // 写入表头
            out << "X轴,温度,湿度
";
            // 写入数据
            for (int i = 0; i < xData.size(); ++i) {
                out << xData[i] << "," << yTemp[i] << "," << yHumi[i] << "
";
            }
            file.close();
            QMessageBox::information(this, "成功", "CSV数据导出完成!");
        } else {
            QMessageBox::warning(this, "失败", "文件打开失败!");
        }
    }
}

可在 UI 中添加两个按钮,分别绑定上述两个槽函数,实现一键导出。

四、高级功能与性能优化

4.1 双Y轴实现

场景:当两条曲线数值范围差异大(如温度:050℃,电压:05V),共用一个Y轴会导致其中一条曲线几乎不可见,需使用双Y轴:


// 初始化双Y轴
void MainWindow::initDualYAxis()
{
    // 主Y轴(左侧):温度
    ui->customPlot->yAxis->setLabel("温度 (℃)");
    ui->customPlot->addGraph(ui->customPlot->xAxis, ui->customPlot->yAxis);
    ui->customPlot->graph(0)->setPen(QPen(Qt::red));
    ui->customPlot->graph(0)->setName("温度");

    // 次Y轴(右侧):电压
    QCPAxis *yAxis2 = ui->customPlot->yAxis2;
    yAxis2->setVisible(true);
    yAxis2->setLabel("电压 (V)");
    ui->customPlot->addGraph(ui->customPlot->xAxis, yAxis2);
    ui->customPlot->graph(1)->setPen(QPen(Qt::blue));
    ui->customPlot->graph(1)->setName("电压");

    // 启用右侧Y轴交互
    ui->customPlot->setInteractions(QCP::iRangeDrag | QCP::iRangeZoom | QCP::iSelectPlottables);
}

4.2 性能优化策略

当数据量达到 10 万级以上时,需优化性能避免卡顿:

减少数据点数量:对高频数据做降采样(如每10个点保留1个);批量更新数据:避免每次仅添加一个点,可累积N个点后一次性调用
setData
禁用不必要的交互:如无需选择曲线,关闭
iSelectPlottables
使用
setData
而非
addData

addData
会频繁触发数据结构调整,批量更新时
setData
更高效;开启 OpenGL 加速:QCustomPlot 2.x 支持 OpenGL 渲染,需在
.pro
文件中添加
QT += opengl
,并调用:


ui->customPlot->setOpenGl(true);

五、完整工程实战:实时监控系统

整合上述所有功能,实现一个工业级的实时数据监控系统,功能包括:

双曲线动态绘图(温度、湿度);鼠标缩放/平移交互;鼠标悬停显示数据点数值;图例点击切换曲线显示;数据/图表导出;双Y轴适配不同数值范围。

5.1 工程结构


├── main.cpp
├── mainwindow.h
├── mainwindow.cpp
├── mainwindow.ui
├── qcustomplot.h
├── qcustomplot.cpp
└── monitor.pro

5.2 核心代码整合

关键代码已在上述章节中拆解,只需将动态绘图、双Y轴、交互功能、导出功能整合到
initCustomPlot
和定时器槽函数中,即可完成完整的监控系统。

六、常见问题与解决方案

动态绘图卡顿

原因:频繁
replot()
、数据量过大、未启用 OpenGL 加速;解决:使用
replot(QCustomPlot::rpQueued)
、降采样数据、开启 OpenGL。

鼠标交互失效

原因:未启用
setInteractions
或控件未获取焦点;解决:确保调用
setInteractions
并启用对应标志,设置
ui->customPlot->setFocusPolicy(Qt::StrongFocus)

双Y轴缩放不同步

原因:未单独设置次Y轴的交互;解决:启用
ui->customPlot->yAxis2->setRangeZoom(true)

setRangeDrag(true)

数据导出乱码(CSV)

原因:未设置编码;解决:在写入CSV时设置编码为 UTF-8:


file.setEncoding("UTF-8");

七、总结

本文从 QCustomPlot 基础集成出发,逐步实现了动态绘图、核心交互功能(缩放平移、数据点提示、图例交互、数据导出),并结合工业级场景做了性能优化。QCustomPlot 的优势在于轻量化、定制性强,相比 Qt Charts 更适合嵌入式、工控等资源受限的场景。

在实际项目中,可基于本文的基础框架扩展更多功能:如数据标注、网格线定制、背景渐变、实时报警(曲线超过阈值时变色)等。掌握 QCustomPlot 的核心 API 和交互逻辑,可快速落地各类 Qt 图表可视化需求,覆盖科研、工控、物联网等多个领域。

© 版权声明

相关文章

暂无评论

none
暂无评论...