Qt(C++) 使用 QChart 动态显示 3 个设备的温度变化曲线
- 2024-06-25 重庆
本文字数:10315 字
阅读完需:约 34 分钟
一、介绍
Qt 的 QChart 是一个用于绘制图表和可视化数据的类。提供了一个灵活的、可扩展的、跨平台的图表绘制解决方案,可以用于各种应用程序,如数据分析、科学计算、金融交易等。
QChart 支持多种类型的图表,包括折线图、散点图、柱状图、饼图等。它还支持多个数据系列(datasets)在同一个图表中显示,并且可以自定义各种图表属性和样式,如坐标轴标签、标题、图例等。
QChart 还支持多种数据源(data sources),可以来自 Qt 的数据模型(data models)、CSV 文件、JSON 文件等。数据源可以是任何支持迭代器(iterator)的类型,因此可以轻松地与其他 Qt 组件集成。
使用 QChart 可以轻松地创建交互式图表,如鼠标悬停提示(hover tooltip)、数据选择(data selection)等。此外,QChart 还支持多种主题(themes)和自定义 CSS 样式,使得图表外观可以灵活地定制。
二、实现代码(1)QMainWindow
以下是使用 Qt(C++)的 QChart 模块显示 3 个设备的动态温度曲线的代码实现:
【1】实现温度动态更新
mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QtCharts/QChart>
#include <QtCharts/QLineSeries>
#include <QTimer>
QT_CHARTS_USE_NAMESPACE
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr);
~MainWindow();
private slots:
void updateChartData(); // 更新数据槽函数
private:
Ui::MainWindow *ui;
QTimer *m_timer; // 定时器
QChart *m_chart; // 图表指针
QLineSeries *m_series1; // 设备1温度曲线
QLineSeries *m_series2; // 设备2温度曲线
QLineSeries *m_series3; // 设备3温度曲线
int m_timeCount; // 时间计数
};
#endif // MAINWINDOW_H
mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow),
m_timer(new QTimer(this)),
m_chart(new QChart()),
m_series1(new QLineSeries()),
m_series2(new QLineSeries()),
m_series3(new QLineSeries()),
m_timeCount(0)
{
ui->setupUi(this);
// 设置图表标题
m_chart->setTitle("Temperature Data");
// 创建温度曲线图1并设置属性
m_series1->setName(tr("Device 1"));
m_series1->setColor(Qt::red);
m_series1->setPen(QPen(Qt::red, 2));
m_chart->addSeries(m_series1);
// 创建温度曲线图2并设置属性
m_series2->setName(tr("Device 2"));
m_series2->setColor(Qt::green);
m_series2->setPen(QPen(Qt::green, 2));
m_chart->addSeries(m_series2);
// 创建温度曲线图3并设置属性
m_series3->setName(tr("Device 3"));
m_series3->setColor(Qt::blue);
m_series3->setPen(QPen(Qt::blue, 2));
m_chart->addSeries(m_series3);
// 设置横轴属性
QValueAxis *axisX = new QValueAxis;
axisX->setRange(0, 30);
axisX->setTitleText("Time (s)");
m_chart->addAxis(axisX, Qt::AlignBottom);
m_series1->attachAxis(axisX);
m_series2->attachAxis(axisX);
m_series3->attachAxis(axisX);
// 设置纵轴属性
QValueAxis *axisY = new QValueAxis;
axisY->setRange(0, 60);
axisY->setTitleText("Temperature (℃)");
m_chart->addAxis(axisY, Qt::AlignLeft);
m_series1->attachAxis(axisY);
m_series2->attachAxis(axisY);
m_series3->attachAxis(axisY);
// 定时更新数据
connect(m_timer, &QTimer::timeout, this, &MainWindow::updateChartData);
m_timer->start(1000); // 每隔1秒钟更新一次数据
// 将图表添加到ChartView中
ui->chartView->setChart(m_chart);
ui->chartView->setRenderHint(QPainter::Antialiasing);
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::updateChartData()
{
// 更新时间计数
m_timeCount++;
// 在温度曲线上增加一个点,模拟温度数据变化
QPointF p1(m_timeCount, qrand() % 10 + 20);
QPointF p2(m_timeCount, qrand() % 10 + 30);
QPointF p3(m_timeCount, qrand() % 10 + 40);
m_series1->append(p1);
m_series2->append(p2);
m_series3->append(p3);
// 清除多余的点,只保留最新的30个数据点
if (m_series1->count() > 30) {
m_series1->removePoints(0, 1);
}
if (m_series2->count() > 30) {
m_series2->removePoints(0, 1);
}
if (m_series3->count() > 30) {
m_series3->removePoints(0, 1);
}
}
在此代码中,定义了一个 QTimer 定时器对象,用于每隔一段时间更新温度曲线数据。在定时器的 timeout 信号触发时,调用 updateChartData()槽函数来更新温度曲线数据,同时控制数据量不超过 30 个点。
在 updateChartData()函数中,使用了 qrand()函数来生成随机的温度数据,模拟动态变化的效果。可以根据实际情况修改此函数的实现方式。
最后,将图表添加到 QChartView 控件中,并启用抗锯齿功能以提高显示质量。
【2】设置曲线可见范围
为了保证曲线显示一直在可见范围内,可以添加如下代码:
// 使图表自适应大小,确保曲线始终可见
m_chart->createDefaultAxes();
m_chart->axisX()->setRange(0, 30);
m_chart->axisY()->setRange(0, 60);
这段代码的作用是让图表自适应大小,并设置横轴范围为 0 到 30,纵轴范围为 0 到 60。这样当新数据点增加到图表之外时,图表会自动调整大小和范围,以确保曲线始终可见。
完整的mainwindow.cpp
代码如下所示:
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow),
m_timer(new QTimer(this)),
m_chart(new QChart()),
m_series1(new QLineSeries()),
m_series2(new QLineSeries()),
m_series3(new QLineSeries()),
m_timeCount(0)
{
ui->setupUi(this);
// 设置图表标题
m_chart->setTitle("Temperature Data");
// 创建温度曲线图1并设置属性
m_series1->setName(tr("Device 1"));
m_series1->setColor(Qt::red);
m_series1->setPen(QPen(Qt::red, 2));
m_chart->addSeries(m_series1);
// 创建温度曲线图2并设置属性
m_series2->setName(tr("Device 2"));
m_series2->setColor(Qt::green);
m_series2->setPen(QPen(Qt::green, 2));
m_chart->addSeries(m_series2);
// 创建温度曲线图3并设置属性
m_series3->setName(tr("Device 3"));
m_series3->setColor(Qt::blue);
m_series3->setPen(QPen(Qt::blue, 2));
m_chart->addSeries(m_series3);
// 设置横轴属性
QValueAxis *axisX = new QValueAxis;
axisX->setRange(0, 30);
axisX->setTitleText("Time (s)");
m_chart->addAxis(axisX, Qt::AlignBottom);
m_series1->attachAxis(axisX);
m_series2->attachAxis(axisX);
m_series3->attachAxis(axisX);
// 设置纵轴属性
QValueAxis *axisY = new QValueAxis;
axisY->setRange(0, 60);
axisY->setTitleText("Temperature (℃)");
m_chart->addAxis(axisY, Qt::AlignLeft);
m_series1->attachAxis(axisY);
m_series2->attachAxis(axisY);
m_series3->attachAxis(axisY);
// 使图表自适应大小,确保曲线始终可见
m_chart->createDefaultAxes();
m_chart->axisX()->setRange(0, 30);
m_chart->axisY()->setRange(0, 60);
// 定时更新数据
connect(m_timer, &QTimer::timeout, this, &MainWindow::updateChartData);
m_timer->start(1000); // 每隔1秒钟更新一次数据
// 将图表添加到ChartView中
ui->chartView->setChart(m_chart);
ui->chartView->setRenderHint(QPainter::Antialiasing);
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::updateChartData()
{
// 更新时间计数
m_timeCount++;
// 在温度曲线上增加一个点,模拟温度数据变化
QPointF p1(m_timeCount, qrand() % 10 + 20);
QPointF p2(m_timeCount, qrand() % 10 + 30);
QPointF p3(m_timeCount, qrand() % 10 + 40);
m_series1->append(p1);
m_series2->append(p2);
m_series3->append(p3);
// 清除多余的点,只保留最新的30个数据点
if (m_series1->count() > 30) {
m_series1->removePoints(0, 1);
}
if (m_series2->count() > 30) {
m_series2->removePoints(0, 1);
}
if (m_series3->count() > 30) {
m_series3->removePoints(0, 1);
}
}
【3】实现鼠标交互拖动
要实现折线图的横坐标可以拖动,可以设置 QChartView 的交互模式为拖拽,在构造函数中添加如下代码:
// 设置 ChartView 交互模式为拖拽
ui->chartView->setRubberBand(QChartView::HorizontalRubberBand);
ui->chartView->setRenderHint(QPainter::Antialiasing);
ui->chartView->setDragMode(QGraphicsView::ScrollHandDrag);
这样用户就可以通过鼠标左键在横轴上拖拽来改变曲线图的可见范围。同时,还需要在mainwindow.cpp
中添加横坐标的范围更新函数updateAxisRange()
,用于在拖拽时更新横坐标的范围。
完整的mainwindow.cpp
代码如下所示:
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow),
m_timer(new QTimer(this)),
m_chart(new QChart()),
m_series1(new QLineSeries()),
m_series2(new QLineSeries()),
m_series3(new QLineSeries()),
m_timeCount(0)
{
ui->setupUi(this);
// 设置图表标题
m_chart->setTitle("Temperature Data");
// 创建温度曲线图1并设置属性
m_series1->setName(tr("Device 1"));
m_series1->setColor(Qt::red);
m_series1->setPen(QPen(Qt::red, 2));
m_chart->addSeries(m_series1);
// 创建温度曲线图2并设置属性
m_series2->setName(tr("Device 2"));
m_series2->setColor(Qt::green);
m_series2->setPen(QPen(Qt::green, 2));
m_chart->addSeries(m_series2);
// 创建温度曲线图3并设置属性
m_series3->setName(tr("Device 3"));
m_series3->setColor(Qt::blue);
m_series3->setPen(QPen(Qt::blue, 2));
m_chart->addSeries(m_series3);
// 设置横轴属性
QValueAxis *axisX = new QValueAxis;
axisX->setRange(0, 30);
axisX->setTitleText("Time (s)");
m_chart->addAxis(axisX, Qt::AlignBottom);
m_series1->attachAxis(axisX);
m_series2->attachAxis(axisX);
m_series3->attachAxis(axisX);
// 设置纵轴属性
QValueAxis *axisY = new QValueAxis;
axisY->setRange(0, 60);
axisY->setTitleText("Temperature (℃)");
m_chart->addAxis(axisY, Qt::AlignLeft);
m_series1->attachAxis(axisY);
m_series2->attachAxis(axisY);
m_series3->attachAxis(axisY);
// 使图表自适应大小,确保曲线始终可见
m_chart->createDefaultAxes();
m_chart->axisX()->setRange(0, 30);
m_chart->axisY()->setRange(0, 60);
// 设置 ChartView 交互模式为拖拽
ui->chartView->setRubberBand(QChartView::HorizontalRubberBand);
ui->chartView->setRenderHint(QPainter::Antialiasing);
ui->chartView->setDragMode(QGraphicsView::ScrollHandDrag);
// 定时更新数据
connect(m_timer, &QTimer::timeout, this, &MainWindow::updateChartData);
m_timer->start(1000); // 每隔1秒钟更新一次数据
// 将图表添加到ChartView中
ui->chartView->setChart(m_chart);
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::updateChartData()
{
// 更新时间计数
m_timeCount++;
// 在温度曲线上增加一个点,模拟温度数据变化
QPointF p1(m_timeCount, qrand() % 10 + 20);
QPointF p2(m_timeCount, qrand() % 10 + 30);
QPointF p3(m_timeCount, qrand() % 10 + 40);
m_series1->append(p1);
m_series2->append(p2);
m_series3->append(p3);
// 清除多余的点,只保留最新的30个数据点
if (m_series1->count() > 30) {
m_series1->removePoints(0, 1);
}
if (m_series2->count() > 30) {
m_series2->removePoints(0, 1);
}
if (m_series3->count() > 30) {
m_series3->removePoints(0, 1);
}
// 更新横轴范围
updateAxisRange();
}
void MainWindow::updateAxisRange()
{
// 获取横轴范围
qreal minX = std::numeric_limits<qreal>::max();
qreal maxX = std::numeric_limits<qreal>::min();
foreach (QAbstractSeries *series, m_chart->series()) {
QXYSeries *xySeries = static_cast<QXYSeries*>(series);
QPointF p1 = xySeries->at(0);
QPointF p2 = xySeries->at(xySeries->count() - 1);
if (p1.x() < minX) {
minX = p1.x();
}
if (p2.x() > maxX) {
maxX = p2.x();
}
}
// 更新横轴范围
m_chart->axisX()->setRange(minX, maxX);
}
为了更新横坐标的范围,需要在MainWindow
中添加了一个新函数updateAxisRange()
。该函数会在数据更新时被调用来计算最新的横轴范围,以更新折线图的显示。
三、实现代码(2)QWidget
当前这份完整代码实现了一个动态折线图的绘制,是一个典型的 Qt Charts 应用程序。通过使用 QLineSeries 类、QValueAxis 类和 QChart 类来创建并显示温度随时间变化的折线图。
第一步:先创建了主窗口,其中包含一个 QChartView 控件,用于显示温度曲线图。在构造函数中,设置了一些基本属性,比如标题、横纵坐标的范围和名称等,并为每个设备创建了一个 QLineSeries 对象,用于存储温度数据。
第二步:将这些 QLineSeries 对象添加到 QChart 对象中。接着,将 QValueAxis 对象添加到 QChart 中,设置其范围和名称,并将 QLineSeries 对象与其关联。最后,将 QChart 对象添加到 QChartView 中,以便在界面上显示折线图。
第三步:在 updateChartData()函数中,定时器每隔 1 秒钟触发一次,用于更新温度数据,并通过调用 QLineSeries 类的 append()函数向 QLineSeries 对象中添加新的温度数据点。同时,使用 removePoints()函数删除旧的数据点,以保持折线图中显示的数据点不超过 30 个。在添加或删除数据时,使用 updateAxisRange()函数更新横坐标的范围,以便将折线图自适应地缩放到当前数据范围内。
第四步:重载了 updateAxisRange()函数,根据 QLineSeries 对象的数据点计算出横坐标的最小值和最大值,并通过调用 QChart 类的 axisX()->setRange()函数更新 QValueAxis 对象的范围。
【1】widget.cpp 代码
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
this->setWindowTitle("温度数据可视化采集系统");
m_timer=new QTimer(this);
m_chart=new QChart();
m_series1=new QLineSeries();
m_series2=new QLineSeries();
m_series3=new QLineSeries();
m_timeCount=0;
// 创建温度曲线图1并设置属性
m_series1->setName(tr("设备1"));
// m_series1->setColor(Qt::red);
// m_series1->setPen(QPen(Qt::red, 2));
m_chart->addSeries(m_series1);
// 创建温度曲线图2并设置属性
m_series2->setName(tr("设备2"));
// m_series2->setColor(Qt::green);
// m_series2->setPen(QPen(Qt::green, 2));
m_chart->addSeries(m_series2);
// 创建温度曲线图3并设置属性
m_series3->setName(tr("设备3"));
// m_series3->setColor(Qt::blue);
// m_series3->setPen(QPen(Qt::blue, 2));
m_chart->addSeries(m_series3);
// 设置横轴属性
QValueAxis *axisX = new QValueAxis;
axisX->setRange(0, 30);
axisX->setTitleText("时间 (s)");
m_chart->addAxis(axisX, Qt::AlignBottom);
m_series1->attachAxis(axisX);
m_series2->attachAxis(axisX);
m_series3->attachAxis(axisX);
// 设置纵轴属性
QValueAxis *axisY = new QValueAxis;
axisY->setRange(0, 60);
axisY->setTitleText("温度 (℃)");
m_chart->addAxis(axisY, Qt::AlignLeft);
m_series1->attachAxis(axisY);
m_series2->attachAxis(axisY);
m_series3->attachAxis(axisY);
// 使图表自适应大小,确保曲线始终可见
m_chart->createDefaultAxes();
m_chart->axisX()->setRange(0, 30);
m_chart->axisY()->setRange(0, 60);
// 定时更新数据
connect(m_timer, &QTimer::timeout, this, &Widget::updateChartData);
// 将图表添加到ChartView中
QChartView* chartView = new QChartView(m_chart);
// 设置 ChartView 交互模式为拖拽
chartView->setRubberBand(QChartView::HorizontalRubberBand);
chartView->setRenderHint(QPainter::Antialiasing);
chartView->setDragMode(QGraphicsView::ScrollHandDrag);
chartView->setRenderHint(QPainter::Antialiasing);
//将视图添加到布局
ui->view_verticalLayout->addWidget(chartView);
}
Widget::~Widget()
{
delete ui;
}
void Widget::updateChartData()
{
// 更新时间计数
m_timeCount++;
// 在温度曲线上增加一个点,模拟温度数据变化
QPointF p1(m_timeCount, qrand() % 10 + 20);
QPointF p2(m_timeCount, qrand() % 10 + 30);
QPointF p3(m_timeCount, qrand() % 10 + 40);
m_series1->append(p1);
m_series2->append(p2);
m_series3->append(p3);
// 清除多余的点,只保留最新的30个数据点
if (m_series1->count() > 30) {
m_series1->removePoints(0, 1);
}
if (m_series2->count() > 30) {
m_series2->removePoints(0, 1);
}
if (m_series3->count() > 30) {
m_series3->removePoints(0, 1);
}
// 更新横轴范围
updateAxisRange();
}
void Widget::updateAxisRange()
{
// 获取横轴范围
qreal minX = std::numeric_limits<qreal>::max();
qreal maxX = std::numeric_limits<qreal>::min();
foreach (QAbstractSeries *series, m_chart->series()) {
QXYSeries *xySeries = static_cast<QXYSeries*>(series);
QPointF p1 = xySeries->at(0);
QPointF p2 = xySeries->at(xySeries->count() - 1);
if (p1.x() < minX) {
minX = p1.x();
}
if (p2.x() > maxX) {
maxX = p2.x();
}
}
// 更新横轴范围
m_chart->axisX()->setRange(minX, maxX);
}
//开始采集
void Widget::on_pushButton_start_clicked()
{
m_timer->start(1000); // 每隔1秒钟更新一次数据
}
//停止采集
void Widget::on_pushButton_stop_clicked()
{
m_timer->stop(); //停止定时器
}
【2】widget.h 代码
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
// 包含line chart需要的头文件
#include <QtCharts/QChart>
#include <QtCharts/QLineSeries>
#include <QtCharts/QChartView>
#include <QtCore/QRandomGenerator>
#include <QValueAxis>
#include <QScatterSeries>
#include <QTimer>
// 引用命名空间
QT_CHARTS_USE_NAMESPACE
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private slots:
void updateChartData(); // 更新数据槽函数
void updateAxisRange();
void on_pushButton_start_clicked();
void on_pushButton_stop_clicked();
private:
Ui::Widget *ui;
QTimer *m_timer; // 定时器
QChart *m_chart; // 图表指针
QLineSeries *m_series1; // 设备1温度曲线
QLineSeries *m_series2; // 设备2温度曲线
QLineSeries *m_series3; // 设备3温度曲线
int m_timeCount; // 时间计数
};
#endif // WIDGET_H
【3】UI 文件代码
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Widget</class>
<widget class="QWidget" name="Widget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>600</width>
<height>444</height>
</rect>
</property>
<property name="windowTitle">
<string>Widget</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QWidget" name="widget" native="true">
<property name="minimumSize">
<size>
<width>100</width>
<height>100</height>
</size>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<layout class="QVBoxLayout" name="view_verticalLayout"/>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QWidget" name="widget_2" native="true">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>40</height>
</size>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QPushButton" name="pushButton_start">
<property name="text">
<string>开始采集</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_stop">
<property name="text">
<string>停止采集</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
【4】pro 工程文件
QT += core gui
QT += charts
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
CONFIG += c++11
# The following define makes your compiler emit warnings if you use
# any Qt feature that has been marked deprecated (the exact warnings
# depend on your compiler). Please consult the documentation of the
# deprecated API in order to know how to port your code away from it.
DEFINES += QT_DEPRECATED_WARNINGS
# You can also make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
# You can also select to disable deprecated APIs only up to a certain version of Qt.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
SOURCES += \
main.cpp \
widget.cpp
HEADERS += \
widget.h
FORMS += \
widget.ui
# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target
四、个人信息
版权声明: 本文为 InfoQ 作者【DS小龙哥】的原创文章。
原文链接:【http://xie.infoq.cn/article/d65e386bf223efb53f00680b6】。文章转载请联系作者。
DS小龙哥
之所以觉得累,是因为说的比做的多。 2022-01-06 加入
熟悉C/C++、51单片机、STM32、Linux应用开发、Linux驱动开发、音视频开发、QT开发. 目前已经完成的项目涉及音视频、物联网、智能家居、工业控制领域
评论