Qt Graphs 模块拟取代 charts 和 data visualization还有很长的路要走
近期关注 Qt 6.10 的分支进展, 发现了 Qt 6.10 的 charts 和 data visualization (以下简称 DV)已经被deprecated, 功能将会合并到 graphs 模块。如果后面 charts\ DV 被弃用,那算是很大的API变化了。从Qt 6.5 以后开始引入的 graphs 使用的是QML的渲染器,和之前的 Qt Widgets GraphicsView 完全不同。如果有依赖上述模块的应用,需要提前评估API的变化。
1. 可能遇到的主要问题
只要是和QML相关的东西,那问题无外乎就是老电脑的兼容性,以及跨语言的API。
1.1 老旧和低成本环境可能不再适用
QML的渲染器过度依赖OS的本地3D加速API,如 Windows的 DirectX。在VMWare/VirtualBox环境下,QML有概率无法正常渲染图形。在只有核心显卡,没有配置独立显卡的低成本计算机上,QML也非常卡顿,甚至可能无法启动(如果驱动比较老或者配置不正确)。
与之形成对比的,是Widgets的2D渲染,基本是在backend里自绘的,因此,不但可以支持老旧计算机,还能在 vnc、framebuf里使用。
1.2 跨语言API调用和性能问题
charts和DV是支持C++ Only模式的开发的。但是, graphs 却不是。官网的例子里,C++ Widgets 程序使用 graphs 必须要进行QML混合编程。这里就牵扯到三个场景:
- 从C++中动态向 QML 刷新曲线,特别是1秒刷新23次的这种动画。
- 从C++中精确获得QML图元的signal,如橡皮筋选择、有图元被双击等等。
- 从C++中查询各个图元的属性和状态。比如查询视图的宽度、高度,选中的状态等。
这些操作要达到高性能、易于使用,需要特别注意不出现内存深度拷贝,且管理好QObject对象树的生命周期(life-cycle)。
2. 初步试用
抱着试试看的态度,初步对 Qt Graphs的API进行了研究,并以最简单的动态折线图进行了开发测试。
pro文件
模块包括 quickwidgets graphs quick 等模块。
QT += core gui widgets quickwidgets graphs quickCONFIG += c++17SOURCES += \main.cpp \graphstest.cppHEADERS += \graphstest.hFORMS += \graphstest.ui
2.2 main.cpp
#include "graphstest.h"#include <QApplication>int main(int argc, char *argv[])
{QApplication a(argc, argv);graphsTest w;w.show();return a.exec();
}
2.3 graphstest.h
#ifndef GRAPHSTEST_H
#define GRAPHSTEST_H#include <QDateTimeAxis>
#include <QDialog>
#include <QLineSeries>
#include <QValueAxis>
#include <QVector>
QT_BEGIN_NAMESPACE
namespace Ui
{
class graphsTest;
}
QT_END_NAMESPACEclass graphsTest : public QDialog
{Q_OBJECTpublic:graphsTest(QWidget *parent = nullptr);~graphsTest();protected:void timerEvent(QTimerEvent *evt) override;
private slots:void on_pushButton_add_clicked();void on_checkBox_update_clicked();private:Ui::graphsTest *ui;QDateTimeAxis *m_ax;QValueAxis *m_ay;int m_timerEvent;QVector<QLineSeries *> m_lineSeries;
};
#endif // GRAPHSTEST_H
2.4 graphstest.cpp
#include "graphstest.h"
#include <QDateTime>
#include <QDebug>
#include <QQuickItem>
#include "ui_graphstest.h"
graphsTest::graphsTest(QWidget *parent): QDialog(parent), ui(new Ui::graphsTest), m_ax(new QDateTimeAxis(this)), m_ay(new QValueAxis(this)), m_timerEvent(-1)
{ui->setupUi(this);QDateTime dtmNow = QDateTime::currentDateTime();m_ax->setMin(dtmNow.addDays(-1));m_ax->setMax(dtmNow);m_ay->setRange(-100, 100);QList<QObject *> seriesList;ui->graphsView->setResizeMode(QQuickWidget::SizeRootObjectToView);ui->graphsView->setInitialProperties({{"seriesList", QVariant::fromValue(seriesList)},{"axisX", QVariant::fromValue(m_ax)},{"axisY", QVariant::fromValue(m_ay)},{"zoomAreaEnabled", true}});ui->graphsView->loadFromModule("QtGraphs", "GraphsView");
}graphsTest::~graphsTest()
{delete ui;
}void graphsTest::timerEvent(QTimerEvent *evt)
{if (evt->timerId() == m_timerEvent){QList<QPointF> data;QDateTime dtmNow = QDateTime::currentDateTime();const int N = m_lineSeries.size();for (int n = 0; n < N; ++n){for (int i = 0; i < 30; ++i){data << QPointF(dtmNow.addSecs(-3600 * 24.0 / 30 * (29 - i)).toMSecsSinceEpoch(),(rand() % 500 - 250) / 100.0 + n * 16 - 80);}m_lineSeries[n]->replace(data);}m_ax->setMin(dtmNow.addDays(-1));m_ax->setMax(dtmNow);if (!ui->checkBox_update->isChecked()){killTimer(m_timerEvent);m_timerEvent = -1;}}QDialog::timerEvent(evt);
}void graphsTest::on_pushButton_add_clicked()
{if (m_lineSeries.size() >= 10)return;//Add to GraphQVariant seriesListVariant = ui->graphsView->rootObject()->property("seriesList");if (seriesListVariant.canConvert<QQmlListProperty<QObject>>()){QLineSeries *newLine = new QLineSeries(this);newLine->setColor(QColor(rand() % 128, rand() % 128, rand() % 128));newLine->setHoverable(true);newLine->setName(QString("Testing %1").arg(m_lineSeries.size()));connect(newLine,&QAbstractSeries::hover,[this](const QString &seriesName, QPointF position, QPointF value) -> void{ui->lineEdit_msg->setText(QString("%1:%2 %3 = %4 %5").arg(seriesName).arg(position.x()).arg(position.y()).arg(value.x()).arg(value.y()));});//AddQQmlListProperty<QObject> prop = seriesListVariant.value<QQmlListProperty<QObject>>();prop.append(&prop, newLine);m_lineSeries.append(newLine);}if (m_timerEvent < 0)m_timerEvent = startTimer(500);
}void graphsTest::on_checkBox_update_clicked()
{if (ui->checkBox_update->isChecked())m_timerEvent = startTimer(500);
}
2.5 graphstest.ui
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0"><class>graphsTest</class><widget class="QDialog" name="graphsTest"><property name="geometry"><rect><x>0</x><y>0</y><width>579</width><height>332</height></rect></property><property name="windowTitle"><string>graphsTest</string></property><layout class="QVBoxLayout" name="verticalLayout_2"><item><layout class="QHBoxLayout" name="horizontalLayout"><item><widget class="QQuickWidget" name="graphsView"><property name="resizeMode"><enum>QQuickWidget::ResizeMode::SizeRootObjectToView</enum></property></widget></item><item><layout class="QVBoxLayout" name="verticalLayout"><property name="sizeConstraint"><enum>QLayout::SizeConstraint::SetMaximumSize</enum></property><item><widget class="QPushButton" name="pushButton_add"><property name="sizePolicy"><sizepolicy hsizetype="Fixed" vsizetype="Fixed"><horstretch>0</horstretch><verstretch>0</verstretch></sizepolicy></property><property name="text"><string>Add Serials</string></property></widget></item><item><widget class="QCheckBox" name="checkBox_update"><property name="sizePolicy"><sizepolicy hsizetype="Maximum" vsizetype="Fixed"><horstretch>0</horstretch><verstretch>0</verstretch></sizepolicy></property><property name="text"><string>Updating</string></property></widget></item><item><spacer name="verticalSpacer"><property name="sizePolicy"><sizepolicy hsizetype="Fixed" vsizetype="Expanding"><horstretch>0</horstretch><verstretch>0</verstretch></sizepolicy></property><property name="orientation"><enum>Qt::Orientation::Vertical</enum></property><property name="sizeHint" stdset="0"><size><width>20</width><height>40</height></size></property></spacer></item></layout></item></layout></item><item><widget class="QLineEdit" name="lineEdit_msg"><property name="sizePolicy"><sizepolicy hsizetype="Expanding" vsizetype="Fixed"><horstretch>0</horstretch><verstretch>0</verstretch></sizepolicy></property></widget></item></layout></widget><customwidgets><customwidget><class>QQuickWidget</class><extends>QWidget</extends><header location="global">QtQuickWidgets/QQuickWidget</header></customwidget></customwidgets><resources/><connections/>
</ui>
3. 运行效果
运行效果如下,主要问题包括(BUG):
- 新插入的对象的色彩,会影响到旧对象的色彩。
- 各个连线的收尾会连接起来。
4 评价
Qt 团队看起来对传统的 Widgets 已经不是很愿意维护了,这种大规模的API变化,以及完全不顾及兼容老计算机、集成显卡的环境的激进改进,要么是公司no zuo no die,要么说明QML技术的趋势已经势不可挡。
QML技术与C++的紧密互动,相当程度的缓解了API跨语言的问题,其实QML就是个调用C++ QObject 运行时的胶水语言,其和C++的联系非常紧密。对Qt Widgets而言,如果使用了 QML的功能,就会在硬件兼容性上面大打折扣,这对于很多桌面场景而言是不能接受的。对未来技术的发展,我们会保持持续的跟踪,并适时为我们使用 charts 的既有代码进行 graphs的适配,并在具备 charts 的环境下优先使用 charts.