目录
Qt Charts介绍
自从 Qt 发布以来,给广大跨平台界面研发人员带来了无数的福利。但是Qt自己却一直没有提供自带的图表库,这就使得 QWT、QCustomPlot 等第三方图表库有了巨大的生存空间,为了降低开发成本,大家都涌向了这些第三方库。这种情况一直持续到 Qt5.7 版本后 Qt Charts 的发布。Qt Charts 是 Qt 自带的组件库,包含折线、曲线、饼图、棒图、散点图、雷达图等等各种常用的图表。协议的约束:GPLV3。
视图-QChartView
负责 QChart 的展示。QChart 本身只负责图表内容的组织、管理。图表的展示由视图负责,这个视图就是 QChartView。QChartView 派生自 QGraphicsView,只是它专门提供了几个面向 QChart 的接口,比如 setChart(QChart*)等。
在
QtCreator
中使用QChartView
可以放置一个QWidget
,然后升级为QChartView
。
QChartView
的内容很少,建议直接过一遍文档 QChartView
QChartView继承关系如下:
图表-QChart
QChart
管理不同类型的序列和其他与图表相关的对象,例如坐标轴及图例。QChart 承担了一个组织、管理的角色。QChart 派生自 QGraphicsObject,因此它实际上是一个图元 item。我们可以从 QChart 获取到坐标轴对象、数据系列对象、图例等等,并且可以设置图表的主题、背景色等样式信息。
QChart
继承关系如下:
系列-QAbstractSeries
不论是曲线、饼图、棒图还是其他图表,其中展示的内容本质都是数据。一条曲线是一组数据,一个饼图也对应一组数据。在 Qt Charts 中,这些一组组的数据被称作系列。对应不同类型的图表 Qt 提供了不同的系列。系列除了负责存储、访问数据,应该还提供了数据的绘制方法,比如常用的折线图和曲线图分别对应 QLineSerie 和 QSPLineSerie。我们可以用不同的系列达到不同的展示目的。
常见的系列如下:
QBarSeries
(柱状图)
QHorizontalBarSeries
(水平柱状图)
QHorizontalPercentBarSeries
(水平百分比柱状图)
QHorizontalStackedBarSeries
(水平层叠图)
QPercentBarSeries
(百分比柱状图)
QStackedBarSeries
(层叠图/堆叠的条形图)
QAreaSeries
(面积图)
QBoxPlotSeries
(形图/盒须图)
QPieSeries
(饼图)
QXYSeries
(线性图、曲线图、散点图的基类)
QLineSeries
(折线图)
QSplineSeries
(曲线图)
QScatterSeries
(散点图)
系列继承关系如下:
坐标轴-QAbstractAxis
图表中,一般都有 X、Y 坐标轴,复杂一些的还带有 Z 轴。对应到 Qt 的图表也有 X、Y 轴对象。坐标轴封装了刻度,标签,网格线,标题等属性。
坐标轴有以下几种:
QBarCategoryAxis
类别坐标轴,用字符串作为坐标轴的可读,用于图表的非数值坐标轴QDateTimeAxis
时间坐标轴,用作时间数据的坐标轴QLogValueAxis
对数数值坐标轴,作为数值类数据的对数坐标轴QValueAxis
数值坐标轴,用作数值型数据的坐标轴QCategoryAxis
分组数值坐标轴,可以为数值范围设置标签
坐标轴继承关系如下:
图例-Legend
图例是对图表上系列的补充说明。我们可以设置序列颜色及其文字说明、并控制序列显示的位置。
此外,图例中还有一个
QLegendMarker
类,可为每个序列的图例生成一个类似QChrckedBox
的组件;单击序列的标记,可以控制序列是否显示。
下面是官方例程 chartthemes
的运行效果。
上述内容,建议通过官方例程配合类文档学习。
创建GUI界面
需要在安装 Qt 时带上了 charts,否则后面工作无法开展。
- 对于编译方式安装的 Qt,需要注意在 configure 时不要跳过 charts。
- 对于安装包方式安装的 Qt,需要注意在安装时,确保 charts 组件被选中。
新建一个 Qt Widgets Application 项目。
在绘制 ui 窗体时,从 designer 的工具箱中选择一个 “Widget” 类型的控件,然后在它上面单击鼠标右键,选择 “提升为”。
在弹出的界面中,填写 "提升的类名称" 为: QChartView,头文件名称会自动生成,我们不用关心。然后单击“添加”按钮即可。
界面布局
完整代码
在 .pro 文件添加
QT += charts
QT += serialport serialbus
在 .h 文件添加
#include <QtCharts> // QChart 所需的头文件
#include <QModbusClient> // 发送 Modbus 请求
#include <QModbusReply> // 访问服务器后的回复
#include <QTimer>
// 使用串行总线与 Modbus 服务器进行通信的 Modbus 客户端
#include <QModbusRtuSerialMaster>
#include <QVariant> // 这个类型充当着最常见的数据类型的联合
#include <QSerialPort> // 串口IO操作
#include <QDebug>
QT_CHARTS_USE_NAMESPACE //声明 Qt Charts 的命名空间
widget.cpp 文件
#include "widget.h"
#include "ui_widget.h"
const uint8_t xMax = 10;
const uint8_t yMax = 70;
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
modbus_init();
charts_init();
my_time = new QTimer ();
my_time->setInterval(1000);
connect(my_time, &QTimer::timeout, this, &Widget::timeout);
}
Widget::~Widget()
{
delete ui;
}
void Widget::charts_init() // 配置图表
{
// 构建图表对象
chart = new QChart ();
chart->setTitle("温度曲线"); //图表的名字
ui->widget->setChart(chart); //将图表绑定到视图
ui->widget->setRenderHint(QPainter::Antialiasing);//设置渲染:抗锯齿
// 创建曲线系列
mSeries = new QSplineSeries ();
// mSeries->setColor(QColor(255, 0, 0));//曲线颜色
mSeries->setName("温度"); //曲线名字
chart->addSeries(mSeries);//将曲线系列添加到图表
chart->setTheme(QtCharts::QChart::ChartThemeBrownSand);
// 坐标轴
mAxisY = new QValueAxis ();
mAxisX = new QValueAxis ();
mAxisX->setTitleText(QString(tr("时间(s)")));
mAxisY->setTitleText(QString(tr("温度(℃)")));
mAxisY->setRange(-20, yMax);// 坐标轴范围
mAxisY->setTickCount(10);// 坐标轴分等份
mAxisX->setRange(0, xMax);
mAxisX->setTickCount(10);
mAxisX->setLabelFormat("%.0f"); //坐标轴标签格式:每个单位保留几位小数
mAxisX->setTitleFont(QFont("宋体"));
// mAxisX->setLabelsVisible(false); //隐藏坐标轴标签
// mAxisX->setMinorTickCount(4); //每个单位之间绘制了多少虚网线
// mAxisX->setGridLineVisible(false); // 隐藏网格
// mAxisX->setLabelsAngle(45);//字体倾斜角度
// mAxisX->setLabelsColor(Qt::blue);
// mAxisX->setLabelsEditable(true);
// 图例
QLegend *mlegeng = chart->legend();
mlegeng->setAlignment(Qt::AlignBottom);// 图例在图表底部显示
mlegeng->show();
// 添加坐标轴(先添加轴到 Chart 上,再附加轴到 Series 上)
chart->addAxis(mAxisX, Qt::AlignBottom);
chart->addAxis(mAxisY, Qt::AlignLeft);
mSeries->attachAxis(mAxisX);
mSeries->attachAxis(mAxisY);
// 隐藏背景
// chart->setBackgroundVisible(false);
// 设置外边界全都为 0
chart->setContentsMargins(0, 0, 0, 0);
// 设置内部边界全都为 0
chart->setMargins(QMargins(0, 0, 0, 0));
// 设置背景区域无圆角
chart->setBackgroundRoundness(10);
// 突出曲线上的点
mSeries->setPointsVisible(true);
}
void Widget::modbus_init() // 配置传输协议
{
modbusClient = new QModbusRtuSerialMaster ();//创建一个ModbusRTU通信的主机对象
//设置通信的端口号、波特率、奇偶校验位、数据位、停止位
modbusClient->setConnectionParameter(QModbusDevice::SerialPortNameParameter, QVariant("/dev/ttySTM2"));
modbusClient->setConnectionParameter(QModbusDevice::SerialBaudRateParameter, QSerialPort::Baud9600);
modbusClient->setConnectionParameter(QModbusDevice::SerialParityParameter, QSerialPort::NoParity);
modbusClient->setConnectionParameter(QModbusDevice::SerialDataBitsParameter, QSerialPort::Data8);
modbusClient->setConnectionParameter(QModbusDevice::SerialStopBitsParameter, QSerialPort::OneStop);
modbusClient->setTimeout(1000);//如果在规定的超时内没有收到响应,则设置TimeoutError
modbusClient->setNumberOfRetries(3);//设置客户端在请求失败前执行的重试次数
bool ok = modbusClient->connectDevice();//连接设备
if(ok){
qDebug() << "Successful connect";
}else{
qDebug() << "Connect failure";
}
}
void Widget::ready_read() // 读取数据并输出到图表
{
auto reply = qobject_cast<QModbusReply *>(sender());
if(reply == nullptr){
qDebug() << "No response";
return;
}
if(reply->error() == QModbusDevice::NoError){
const QModbusDataUnit rev_data = reply->result();
quint16 tempartureDate = rev_data.value(0);
quint16 tempar = tempartureDate;
qreal temparture = 0;
if((tempar & 0x8000) == 32768){ //温度为负数
tempartureDate &= 0x7fff;
temparture = tempartureDate/10.0;
QString str1 = QString("-%1").arg(temparture); //将任何类型的数据转化为字符型数据
ui->wendu_label->setText(str1); //读取数据到 ui 界面
}else{
temparture = tempartureDate/10.0;
QString str1 = QString("%1").arg(temparture); //将任何类型的数据转化为字符型数据
ui->wendu_label->setText(str1); //读取数据到 ui 界面
}
// 系列添加数值
static int count = 0;
if(count > mAxisX->max()){
mSeries->removePoints(0, mSeries->count() - xMax);
chart->axisX()->setMin(count - xMax);
chart->axisX()->setMax(count);
}
mSeries->append(count, temparture);
count++;
}else if(reply->error() == QModbusDevice::ProtocolError){
qDebug() << "Data read protocol error" << reply->errorString();
}else{
qDebug() << "Other error" << reply->errorString();
}
}
void Widget::timeout() // 定时器超时操作
{
//0x04:要读取寄存器的起始地址; 2:表示读取几个寄存器
QModbusDataUnit data(QModbusDataUnit::HoldingRegisters, 0x04, 2);
//0x01:表示设备地址
QModbusReply *reply = modbusClient->sendReadRequest(data, 0x01);
if(reply == nullptr){
qDebug() << "Read data failure: " << modbusClient->errorString();
}
if(!reply == isFullScreen()){
connect(reply, &QModbusReply::finished, this, &Widget::ready_read);
}
}
void Widget::on_time_btn_clicked()
{
my_time->start();
}
void Widget::on_stop_btn_clicked()
{
my_time->stop();
}
补充:
实现图表缩放、移动,点击鼠标右键返回
Widget_Chart.h
#ifndef WIDGET_CHART_H
#define WIDGET_CHART_H
#include <QWidget>
#include <QtCharts>
#include "chartview.h"
#include "dw_param.h"
namespace Ui {
class Widget_Chart;
}
class Widget_Chart : public QWidget
{
Q_OBJECT
public:
// 获取单实例对象
static Widget_Chart *getInstance();
// 填充图表数据
void charts_data(int numFormal);
protected:
void paintEvent(QPaintEvent *event)override;
private slots:
void on_pushButton_widget_chart_clear_clicked();
private:
explicit Widget_Chart(QWidget *parent = nullptr);
~Widget_Chart();
// 禁止外部拷贝构造
Widget_Chart(const Widget_Chart &widget_chart) = delete;
// 禁止外部赋值操作
const Widget_Chart &operator=(const Widget_Chart &widget_chart) = delete;
void widget_chart_init();
void charts_create(QChart *chart, QValueAxis *mAxX, QValueAxis *mAxY, \
QSplineSeries *mSeries_0, QSplineSeries *mSeries_1, QSplineSeries *mSeries_2);
QChart *chart_1 = nullptr;
QValueAxis *mAxX_1 = nullptr;
QValueAxis *mAxY_1 = nullptr;
QSplineSeries *mSeries_1_0 = nullptr;
QSplineSeries *mSeries_1_1 = nullptr;
QSplineSeries *mSeries_1_2 = nullptr;
ChartView *chartview_1 = nullptr;
private:
Ui::Widget_Chart *ui;
};
#endif // WIDGET_CHART_H
Widget_Chart.cpp
#include "widget_chart.h"
#include "ui_widget_chart.h"
const uint16_t xMax = 20;
const uint16_t yMax = 1100;
const uint16_t num = 3000; //每条曲线保留 num 点的数据
static int count = 0; //图表 X 轴坐标
Widget_Chart::Widget_Chart(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget_Chart)
{
ui->setupUi(this);
this->setWindowFlags(Qt::WindowMinimizeButtonHint|Qt::WindowCloseButtonHint); //取消最大化按钮
setFixedSize(this->width(), this->height()); //禁止调节窗口大小
widget_chart_init();
}
Widget_Chart::~Widget_Chart()
{
delete ui;
}
Widget_Chart *Widget_Chart::getInstance()
{
Widget_Chart *widget_chart = new Widget_Chart();
return widget_chart;
}
void Widget_Chart::charts_data(int numFormal)
{
for(int i = 0; i < numFormal; i++)
{
mSeries_1_0->append(count+i, dw_param.fwp[i]);
mSeries_1_1->append(count+i, dw_param.bwp[i]);
}
count += numFormal;
if(count > mAxX_1->max()){
chart_1->axisX()->setMin(count-xMax);
chart_1->axisX()->setMax(count);
}
if(count > num)
{
mSeries_1_0->removePoints(0, mSeries_1_0->count()-num);
mSeries_1_1->removePoints(0, mSeries_1_1->count()-num);
}
}
void Widget_Chart::paintEvent(QPaintEvent *event)
{
chartview_1->resize(ui->widget_chart->width(), ui->widget_chart->height());
}
void Widget_Chart::widget_chart_init()
{
//图表
chart_1 = new QChart();
//坐标轴
mAxX_1 = new QValueAxis();
mAxY_1 = new QValueAxis();
//图例
mSeries_1_0 = new QSplineSeries();
mSeries_1_1 = new QSplineSeries();
mSeries_1_2 = new QSplineSeries();
charts_create(chart_1, mAxX_1, mAxY_1, mSeries_1_0, mSeries_1_1, mSeries_1_2);
chartview_1 = new ChartView(chart_1, ui->widget_chart);
chartview_1->setRenderHint(QPainter::Antialiasing); //抗锯齿
//注:这里需要设置大小
chartview_1->resize(ui->widget_chart->width(), ui->widget_chart->height());
}
void Widget_Chart::charts_create(QChart *chart, QValueAxis *mAxX, QValueAxis *mAxY, QSplineSeries *mSeries_0, QSplineSeries *mSeries_1, QSplineSeries *mSeries_2)
{
mSeries_0->setName("前向功率");
mSeries_1->setName("反向功率");
//mSeries_2->setName("PSU_V");
mAxX->setRange(0, xMax);
mAxX->setLabelFormat("%.0f");
mAxX->setTickCount(20);
//mAxX->setLineVisible(false); //轴线显示
mAxX->setLabelsVisible(false); //轴刻度标签显示
mAxY->setRange(0, yMax);
mAxY->setLabelFormat("%d");
mAxY->setTickCount(12);
//mAxY->setLineVisible(false);
mAxY->setLabelsVisible(true);
QLegend *mlegend = chart->legend();
mlegend->setAlignment(Qt::AlignBottom);
//mlegend->hide();
chart->addSeries(mSeries_0);
chart->addSeries(mSeries_1);
//chart->addSeries(mSeries_2);
chart->setTheme(QtCharts::QChart::ChartThemeBrownSand); //图表主题
// mAxX->setTitleText(name);
mAxY->setTitleText("功率(W)");
chart->addAxis(mAxX, Qt::AlignBottom);
chart->addAxis(mAxY, Qt::AlignLeft);
mSeries_0->attachAxis(mAxX);
mSeries_0->attachAxis(mAxY);
mSeries_1->attachAxis(mAxX);
mSeries_1->attachAxis(mAxY);
// mSeries_2->attachAxis(mAxX);
// mSeries_2->attachAxis(mAxY);
chart->setContentsMargins(0, 0, 0, 0);
chart->setMargins(QMargins(0, 0, 0, 0));
chart->setBackgroundRoundness(10);
//ui->charts_widget->setChart(chart);
//ui->charts_widget->setRenderHint(QPainter::Antialiasing); //抗锯齿
mSeries_0->setPen(QPen(QColor(0, 35, 245), 3)); //设置颜色和线条粗细
mSeries_1->setPen(QPen(QColor(255, 0, 0), 3));
//mSeries_2->setPen(QPen(QColor(251, 160, 251), 3));
}
void Widget_Chart::on_pushButton_widget_chart_clear_clicked()
{
mSeries_1_0->clear();
mSeries_1_1->clear();
count = 0;
chart_1->axisX()->setMin(count);
chart_1->axisX()->setMax(xMax);
chart_1->axisY()->setMin(0);
chart_1->axisY()->setMax(yMax);
chartview_1->saveAxisRange(false);
}
ChartView.h
#ifndef CHARTVIEW_H
#define CHARTVIEW_H
#include <QChartView>
#include <QMouseEvent>
#include <QGraphicsSimpleTextItem>
#include <QChart>
#if (QT_VERSION <= QT_VERSION_CHECK(6,0,0))
QT_CHARTS_USE_NAMESPACE
#endif
class ChartView : public QChartView
{
Q_OBJECT
using AxisRange = std::array<qreal, 2>;
public:
explicit ChartView(QWidget* parent = nullptr);
ChartView(QChart *chart, QChartView *parent = nullptr);
~ChartView();
void saveAxisRange(bool save=true); //保存缩放或移动前的坐标轴,以便重新设置
protected:
void mousePressEvent(QMouseEvent *event);
void mouseMoveEvent(QMouseEvent *event);
void mouseReleaseEvent(QMouseEvent *event);
void wheelEvent(QWheelEvent *event);
void keyPressEvent(QKeyEvent *event);
void keyReleaseEvent(QKeyEvent *event);
private:
QPoint prevPoint_;
bool leftButtonPressed_;
bool ctrlPressed_;
bool alreadySaveRange_;
AxisRange xRange_;
AxisRange yRange_;
QGraphicsSimpleTextItem* coordItem_;
};
#endif // CHARTVIEW_H
ChartView.cpp
#include "chartview.h"
#include <QApplication>
#include <QValueAxis>
#include <QDebug>
ChartView::ChartView(QWidget* parent)
: QChartView( parent )
, prevPoint_{}
, leftButtonPressed_{}
, ctrlPressed_{}
, alreadySaveRange_{}
, xRange_{}
, yRange_{}
, coordItem_{}
{
setDragMode(QGraphicsView::RubberBandDrag);
setMouseTracking(false);
setCursor(QCursor(Qt::PointingHandCursor));
}
ChartView::ChartView(QChart *chart, QChartView *parent)
: ChartView(parent) // C++11 Delegating constructors
{
setChart(chart);
}
ChartView::~ChartView()
{
}
void ChartView::mousePressEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton) {
prevPoint_ = event->pos();
leftButtonPressed_ = true;
}
}
void ChartView::mouseMoveEvent(QMouseEvent *event)
{
//实时显示鼠标在界面上的坐标
if (!coordItem_) {
coordItem_ = new QGraphicsSimpleTextItem{ chart() };
coordItem_->setZValue(5);
coordItem_->setPos(100, 50);
coordItem_->show();
}
const QPoint curPos{ event->pos() };
const QPointF curVal{ chart()->mapToValue(QPointF(curPos)) };
coordItem_->setText(QString("X = %1, Y = %2").arg(curVal.x()).arg(curVal.y()));
if (leftButtonPressed_) {
const auto offset = curPos - prevPoint_;
prevPoint_ = curPos;
if (!alreadySaveRange_) {
saveAxisRange();
alreadySaveRange_ = true;
}
chart()->scroll(-offset.x(), offset.y());
}
}
void ChartView::mouseReleaseEvent(QMouseEvent *event)
{
leftButtonPressed_ = false;
if (event->button() == Qt::RightButton) {
if (alreadySaveRange_) {
chart()->axisX()->setRange(xRange_[0], xRange_[1]);
chart()->axisY()->setRange(yRange_[0], yRange_[1]);
}
alreadySaveRange_ = false;
}
}
void ChartView::wheelEvent(QWheelEvent *event)
{
#if (QT_VERSION <= QT_VERSION_CHECK(6,0,0))
const auto pos = QPointF(event->pos());
const auto isZoomIn = event->delta() > 0;
#else
const auto pos = event->position();
const auto isZoomIn = event->angleDelta().y() > 0;
#endif
const QPointF curVal = chart()->mapToValue(pos);
if (!alreadySaveRange_) {
saveAxisRange();
alreadySaveRange_ = true;
}
auto zoom = [](QValueAxis* axis, double centre, bool zoomIn) {
constexpr auto scaling{ 1.5 }; //缩放倍率
const double down = axis->min();
const double up = axis->max();
double downOffset{};
double upOffset{};
if (zoomIn) {
downOffset = (centre - down) / scaling;
upOffset = (up - centre) / scaling;
}
else {
downOffset = (centre - down) * scaling;
upOffset = (up - centre) * scaling;
}
axis->setRange(centre - downOffset, centre + upOffset);
};
if (ctrlPressed_) {
auto axis = qobject_cast<QValueAxis*>(chart()->axisY());
zoom(axis, curVal.y(), isZoomIn);
} else {
auto axis = qobject_cast<QValueAxis*>(chart()->axisX());
zoom(axis, curVal.x(), isZoomIn);
}
}
void ChartView::keyPressEvent(QKeyEvent *event)
{
if (event->key() == Qt::Key_Control)
ctrlPressed_ = true;
}
void ChartView::keyReleaseEvent(QKeyEvent *event)
{
if (event->key() == Qt::Key_Control)
ctrlPressed_ = false;
}
void ChartView::saveAxisRange(bool save)
{
alreadySaveRange_ = save;
const auto axisX = qobject_cast<QValueAxis*>(chart()->axisX());
xRange_ = { axisX->min(), axisX->max() };
const auto axisY = qobject_cast<QValueAxis*>(chart()->axisY());
yRange_ = { axisY->min(), axisY->max() };
}
参考鸣谢
Qt Chart之绘制折线图:图表以及坐标轴设置_qt绘制折线图 图标 坐标轴设置_好奇松鼠的博客-优快云博客