QCustomPlot 是 Qt 生态中轻量且强大的开源绘图库,相比 Qt 官方的 QCharts,它更轻量化、定制性更强,尤其适合实时数据可视化、工控界面、科研数据分析等场景。本文将从实战角度,系统讲解基于 QCustomPlot 的动态绘图实现、核心交互功能落地,并结合工业级应用场景优化性能,全文约 5000 字,覆盖从环境搭建到高级功能的完整链路。
一、QCustomPlot 基础环境搭建
1.1 库文件获取与集成
QCustomPlot 仅需两个核心文件( 和
qcustomplot.h)即可集成,无需复杂的编译链接,适合快速开发:
qcustomplot.cpp
下载地址:QCustomPlot 官方网站(建议下载最新稳定版,如 2.1.1);集成步骤:
将 和
qcustomplot.h 复制到 Qt 项目目录;在 Qt Creator 中右键项目 → “添加现有文件”,选中上述两个文件;在项目的
qcustomplot.cpp 文件中添加依赖(若需导出图片/打印功能):
.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 → 勾选 “全局包含” → 确认,将普通 Widget 提升为 QCustomPlot 控件,命名为
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 性能优化:减少重绘次数
频繁调用 会消耗性能,尤其当数据更新频率高(如 10ms 一次)时,可通过
replot() 让重绘入队,由 Qt 事件循环调度:
replot(QCustomPlot::rpQueued)
// 替换原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:启用鼠标跟踪
在 构造函数中启用 QCustomPlot 的鼠标跟踪:
MainWindow
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 更高效;开启 OpenGL 加速:QCustomPlot 2.x 支持 OpenGL 渲染,需在
setData 文件中添加
.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
六、常见问题与解决方案
动态绘图卡顿:
原因:频繁 、数据量过大、未启用 OpenGL 加速;解决:使用
replot()、降采样数据、开启 OpenGL。
replot(QCustomPlot::rpQueued)
鼠标交互失效:
原因:未启用 或控件未获取焦点;解决:确保调用
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 图表可视化需求,覆盖科研、工控、物联网等多个领域。
