近期关注 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 quick
CONFIG += c++17
SOURCES += \
main.cpp \
graphstest.cpp
HEADERS += \
graphstest.h
FORMS += \
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_NAMESPACE
class graphsTest : public QDialog
{
Q_OBJECT
public:
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 Graph
QVariant 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()));
});
//Add
QQmlListProperty<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.