普通视图

发现新文章,点击刷新页面。
昨天以前Qt进阶之路-涛哥的博客

玩转QtQuick(1)-SceneGraph场景图简介

作者 JaredTao
2021年1月20日 13:44

简介

这是《玩转QtQuick》系列文章的第一篇,主要是介绍Qt Quick Scene Graph “场景图”的关键特性、主要架构及实现原理等等。

(不是QWidget 框架中那个 QGraphicsView哦,是Qt Quick的Scene Graph,不一样)

Scene Graph 是QtQuick/Qml所依赖的渲染框架。

本文会涉及到一些图形学的基本概念,例如:材质、纹理、光栅化、图元等,建议参考相关资料,本文不做进一步的解释。

因为Qt官方文档写的比较全面,所以本文主要是对官方文档的翻译,同时会补充一些个人理解。

翻译主要参考Qt5.15的文档,适当做了一些调整,尽量信达雅,尽量说人话。

下面翻译开始

Qt Quick 中的“场景图”

Qt Quick 2 使用了专用的“场景图”,然后遍历并通过图形API(例如OpenGL、OpenGL ES、Vulkan、Metal 或Direct 3D)渲染该“场景图”。

将“场景图”用于图形渲染而不是传统的命令式绘图系统(QPainter之类的),意味着可以在帧之间保留要渲染的场景,并且在渲染开始之前就知道要

渲染的完整图元集。这为许多优化打开了大门,例如:通过批量渲染最大程度减少状态变化、丢弃被遮挡的图元。


再举个具体的例子,假设用户界面包含一个列表,列表有10个节点,其中每个节点都有背景色、图标和文本。

使用传统的绘图技术,这将导致30次DrawCall和30次状态更改。

而“场景图”可以重组原始图元进行渲染,以便在第一次DrawCall中渲染所有背景,第二次DrawCall渲染所有图标,第三次DrawCall渲染所有文本,

从而将DrawCall的总数减少到3次。这样可以显著提高硬件的性能。


“场景图”与Qt Quick 2.0 紧密相关,不能单独使用。“场景图”由QQuickWindow类管理和渲染,自定义Item类型

可以通过调用QQuickItem::updatePaintNode()将其图元添加到“场景图”中。

“场景图”是Item场景的图形表示,它是一个独立的结构,其中包含足以渲染所有节点的信息。

设置完成后,就可以独立于Item状态对其进行操作和渲染。

在许多平台上,“场景图”会在GUI线程准备下一帧状态时,在专用渲染线程上进行渲染。

注意:本文列出的许多信息特定于 Qt “场景图”的内置默认行为。如果使用替代的方案时,并非所有概念都适用。

Qt Quick “场景图”的结构

“场景图” 由许多预定义的节点类型组成,每种类型都有专门的用途。

尽管我们将其称为“场景图”,但更精确的定义是“节点树”。

该树根据Qml场景中的QQuickItem类型构建,然后在内部对该场景进行渲染,最终呈现该场景。

“节点” 本身不包含任何 绘制 或者 paint() 虚函数。

“节点树”主要由内建的预定义类型组成,用户也可以添加具有自定义内容的完整子树,包括表示3D模型的子树。

Scene Graph API / “场景图”接口

一般是指Qt Quick中 QSG开头的所有类。

节点

对用户而言,最重要的节点是QSGGeometryNode。它用来实现自定义图形中的几何形状和材质。

使用QSGGeometry可以定义几何坐标,并描述形状或者图元网格。它可以是直线,矩形,多边形,许多

不连续的矩形或者复杂的3D网格。材质定义如何填充此图形的每个像素。

一个节点可以有任意数量的子节点,并且几何节点将被渲染,以便它们以子顺序出现,且父级位于其子级之后。

注意:这并未说明渲染器中的实际渲染顺序,仅保证视觉顺序。

有效的节点如下:

节点名称描述
QSGNode“场景图”中所有节点的基类
QSGGeometryNode用于“场景图”中所有可渲染的内容
QSGClipNode“场景图”中实现“切割”功能
QSGOpacityNode用来改变透明度
QSGTransformNode实现旋转、平移、缩放等几何变换

自定义节点通过继承QQuickItem类,重写QQuickItem::updatePaintNode(),并且设置 QQuickItem::ItemHasContents 标志的方式,添加到“场景图”。

警告:至关重要的是, 原生图形(OpenGL,Vulkan,Metal等)操作以及与“场景图”的交互只能在渲染线程中进行,主要

updatePaintNode()调用期间进行。经验法则是仅在QQuickItem::updatePaintNode()函数内使用带有“QSG”前缀的类。

更多详细的信息,可以参考Qt文档: Scene Graph - Custom Geometry

预处理

节点具有虚函数QSGNode::preprocess(),该函数将在渲染“场景图”之前被调用。

节点子类可以设置标志QSGNode::UsePreprocess并重写QSGNode::preprocess()函数以对其节点进行预处理。

例如, 更新纹理的一部分, 或者将贝塞尔曲线划分为当前比例因子的正确细节级别。

节点所有权

节点的所有权归创建者,或者设置标志QSGNode::OwnedByParent后归“场景图”。

通常将所有权分配给“场景图”是可取的,因为这样可以简化“场景图”位于GUI线程之外时的清理操作。

材质

材质描述如何填充QSGGeometryNode中几何图形的内部。它封装了图形管线中顶点和片元阶段的着色器,并提供了足够的灵活性,

尽管大多数Qt Quick 项目本身仅使用了非常基本的材质,例如纯色和纹理填充。

想要对Qml中Item使用自定义着色的用户,可以直接在Qml中使用ShaderEffect

下面是一个完整的材质类列表:

材质名称描述
QSGMaterial封装了“着色器程序”的渲染状态
QSGMaterialRhiShader表示独立于图形API的“着色器程序”
QSGMaterialShader表示渲染器中的OpenGL“着色器程序”
QSGMaterialTypeQSGMaterial结合用作唯一类型标记
QSGFlatColorMaterial“场景图”中渲染纯色图元的便捷方法
QSGOpaqueTextureMaterial“场景图”中渲染不透明纹理图元的便捷方法
QSGTextureMaterial“场景图”中渲染纹理图元的便捷方法
QSGVertexColorMaterial“场景图”中渲染 逐顶点彩色图元的便捷方法

更多详细的信息,可以参考Qt文档: Scene Graph - Simple Material

便捷的节点

“场景图”API是一套 偏底层的接口,专注于性能而不是易用性。

从头开始编写自定义的几何图形和材质,即使是最基本的几何图形和材质,也需要大量的代码。

因此,“场景图”API包含了一些节点类,以使最常用自定义节点的开发更便捷。

节点名称描述
QSGSimpleRectNodeQSGGeometryNode的子类,定义了矩形图元和纯色材质
QSGSimpleTextureNodeQSGGeometryNode的子类,定义了矩形图元和纹理材质

“场景图”和渲染

“场景图”的渲染发生在QQuickWindow类的内部,并且没有公共API可以访问它。

但是,渲染管线中有一些地方可以让用户附加应用程序代码。

可通过直接调用“场景图”使用的图形API(OpenGL、Vulkan、Metal等)来添加自定义“场景图”内容,或插入

任意渲染命令。插入点由“渲染循环”定义。

有关“场景图”渲染器如何工作的详细说明,可以参考Qt文档: Qt Quick Scene Graph Default Renderer。

渲染循环

共有三种渲染循环变体: 基本渲染循环(basic),窗口渲染循环(windows)和线程渲染循环(threaded)。

其中,基本渲染循环和窗口渲染循环是单线程的,线程渲染循环在专用线程上执行“场景图”渲染。

Qt尝试根据平台及可能使用的图形驱动程序选择合适的渲染循环。如果这不能满足你的需求,或者处于测试的目的,可以使用环境变量

QSG_RENDER_LOOP强制使用指定的渲染循环。要验证使用哪个渲染循环,请启用qt.scenegraph.general日志类别。


注意:线程渲染循环和窗口渲染循环 依赖于图形API实现来进行节流,例如,在OpenGL环境下,“请求交换间隔”为1。

一些图形驱动程序允许用户忽略此设置并将其关闭,而忽略Qt的请求。

在不阻塞“交换缓冲区”操作(或其它位置)的情况下,渲染循环将以尽快的速度运行动画并使CPU 100%运转。

如果已知系统无法提供基于vsync的限制,请通过设置环境变量QSG_RENDER_LOOP = basic使用 基本渲染循环。

线程渲染循环

在许多环境中,“场景图”将在专用渲染线程上进行。这样做是为了增加多核处理器的并行度,并更好地利用停顿时间。

这可以显著提高性能,但是与“场景图”进行交互的位置和时间加了一些限制。

以下是关于OpenGL环境下如何使用线程渲染循环的简单概述。除了OpenGL上下文的特定要求外,其它图形API的步骤也是相同的。

  1. Qml场景中发生变化,触发调用QQuickItem::update(), 这可能是动画或者用户操作的结果。

    一个 事件会被post到渲染线程来启动新的一帧。

  2. 渲染线程准备渲染新的一帧,GUI线程会启动阻塞。

  3. 当渲染线程准备新的一帧时,GUI线程调用QQuickItem::updatePolish() 对场景中节点进行最终的“润色”,再渲染它们。

  4. GUI 线程阻塞。

  5. QQuickWindow::beforeSynchronizing()信号发出。应用程序可以对此信号进行直连(Qt::DirectConnection),

    以进行QQuickItem::updatePaintNode()之前所需的任何准备工作。

  6. 将Qml状态同步到“场景图”中。自上一帧以来,所有已更改的节点上调用QQuickItem::updatePaintNode()函数完成同步。

    这是Qml与“场景图”中的节点唯一的交互时机。

  7. GUI线程不再阻塞。

  8. 渲染“场景图”:

    a. QQuickWindow::beforeRendering() 信号发出。应用程序可以直连(Qt::DirectConnection)此信号,来

    调用自定义图形API,然后将其可视化渲染在Qml场景之下。

    b. 指定了QSGNode::UsePreprocess标志的节点将调用其QSGNode::preprocess()函数。

    c. 渲染器处理节点。

    d. 渲染器生成状态并记录使用中的图形API的绘制调用。

    e. QQuickWindow::afterRendering 信号发出。应用程序可以直连(Qt::DirectConnection)此信号,来

    调用自定义图形API,然后将其可视化渲染在Qml场景之上。

    f. 新的一帧准备就绪。交换缓冲区(OpenGL),或者记录当前命令,然后将命令缓冲区提交到图形队列(Vulkan,Metal)。

    QQuickWindow::frameSwapped()信号发出。

  9. 渲染线程正在渲染时,GUI可以自由地进行动画、处理事件等。

当前默认情况下,线程渲染循环工作在 带opengl32.dll的Windows平台,具有Metal的MacOS平台,移动平台,

具有EGLFS的嵌入式Linux,以及平台无关的Vulkan环境,但这可能会有所改变。

通过在环境变量中设置QSG_RENDER_LOOP=threaded,可以强制使用线程渲染器。

非线程渲染循环 (基本渲染循环和窗口渲染循环)

当前默认在使用非线程渲染循环的环境,包括使用ANGLE及非默认opengl32实现的windows平台,使用OpenGL的MacOS,

以及一些特殊驱动的linux环境。

这主要是一种预防措施,因为并非所有的OpenGL驱动和窗口系统的组合都经过测试。同时,诸如ANGLE 或

Mesa llvmpipe之类的实现根本无法在线程渲染中正常运行。因此,对于这些环境,不能使用线程渲染。


在MacOS OpenGL环境下,使用XCode 10 (10.14 SDK) 或更高版本进行构建时不支持线程渲染循环,因为这会选择在

MacOS 10.14上使用“基于图层的视图”。你可以使用XCode 9 (10.13 SDK)进行构建,以避开“基于图层的视图”,这种

情况下,线程渲染循环可以用并且默认会启用。

Metal没有这样的限制。


非线程渲染循环默认在使用ANGLE的windows平台,而“基本渲染循环”用于其它需要非线程渲染循环的平台。

即使使用非线程渲染循环,也应像使用线程渲染循环一样编写代码,否则将使代码不可移植。

以下是非线程渲染循环中帧渲染序列的简化图示。

使用QQuickRenderControl自定义渲染控制

使用QQuickRenderControl时,驱动渲染循环的责任将转移到应用程序中。

在这种情况下,不使用内置的渲染循环。

取而代之的是,由应用程序在适当的时候调用 polish synchronize rendering等渲染步骤,实现类似于上述

行为的线程渲染循环或非线程渲染循环。

“场景图”和原生图形API的混合使用

“场景图”提供了两种方法,来集成应用程序提供的图形命令:

直接发出OpenGL、Vulkan、Metal等命令,以及在“场景图”中创建纹理化节点。


通过连接到QQuickWindow::beforeRenderingQQuickWindow::afterRendering()信号,应用程序可以直接在“场景图”

渲染的同一上下文中进行OpenGL调用。

使用Vulkan或者Metal之类的API,应用程序可以通过QSGRendererInterface来查询本机对象,例如“场景图”的命令缓冲区,

并在认为合适的情况下,向其记录命令。

如信号的名称所示,用户随后可以在Qt Quick “场景图”下方或者上方渲染内容。

以这种方式集成的好处是不需要额外的帧缓冲区或者内存来执行渲染,并且消除了可能昂贵的纹理化步骤。

缺点是Qt Quick 决定何时调用信号,这也是唯一允许OpenGL应用程序绘制的时间点。


Qt提供了一些 “场景图”相关的示例,可在examples中找到:

例子名称描述
Scene Graph - OpenGL Under QML示例通过“场景图”的信号使用OpenGL
Scene Graph - Direct3D 11 Under QML示例通过“场景图”的信号使用Direct3D
Scene Graph - Metal Under QML示例通过“场景图”的信号使用Metal
Scene Graph - Vulkan Under QML示例通过“场景图”的信号使用Vulkan

另一个替代方式,是创建一个 QQuickFrameBufferObject (当前仅适用OpenGL),在这个FBO内部渲染,然后将其

作为纹理显示在“场景图”中。

“Scene Graph - Rendering FBOs” 示例如何完成此操作。


还可以组合多个渲染上下文和多个线程以创建要在“场景图”中显示的内容。

“The Scene Graph - Rendering FBOs in a thread” 示例如何完成此操作。


“Scene Graph - Metal Texture Import”示例直接使用基础API创建和渲染纹理,然后在自定义QQuickItem中的

“场景图”中包装和使用此资源。该示例适用了Metal,但是概念也适用于所有其它图形API。

尽管QQuickFrameBufferObject当前不支持,除OpenGL之外的其它图形API也可以采用这种方法。


警告:当在“场景图”中混合渲染OpenGL内容时,重要的一个点是应用程序不要使OpenGL上下文

处在缓冲区绑定状态,“属性启用”,特殊值处在z缓冲区或模板缓冲区等。这样做会导致无法预测的行为。

警告:自定义渲染代码必须具有多线程意识,它不应该假设应用程序在GUI线程中运行。

自定义Item使用QPainter

QQuickItem提供一个子类QQuickPaintedItem,它允许用户使用QPainter渲染内容。

警告: QQuickPaintedItem通过“间接2D 表面”渲染它的内容,“间接2D 表面”可以是软件光栅化,也可以是

“OpenGL帧缓冲对象(FBO)”。这种渲染包含2步操作。第一步是光栅化表面,第二步是渲染表面。

因此,直接使用“场景图” 接口渲染,速度比QQuickPaintedItem快。

日志支持

“场景图”支持很多种日志类别。这些日志除了对Qt贡献者有帮助之外,还可用于追踪性能问题和缺陷。

日志类别描述
qt.scenegraph.time.texture纹理上传的耗时
qt.scenegraph.time.compilation编译着色器耗时
qt.scenegraph.time.renderer渲染器不同步骤耗时
qt.scenegraph.time.renderloop渲染循环不同阶段耗时
qt.scenegraph.time.glyph准备字形的距离场耗时
qt.scenegraph.general“场景图”和图形栈中的常规信息
qt.scenegraph.renderloop渲染循环相关的信息。这个日志模式是Qt开发者主要使用的

旧版QSG_INFO环境变量也可以用。将其设置为非零值将启用qt.scengraph.general类别。

注意:遇到图形问题时,或不确定正在使用哪个渲染循环或图形API时,请至少启用qt.scenegraph.generalqt.rhi,或者

设置QSG_INFO=1的情况下启动应用程序。然后这将在初始化期间将一些基本信息打印到调试输出。

“场景图”后端

除了公共API外,“场景图”还具有适配层,该适配层用以实现特定硬件的适配。这是一个未公开的、内部的、私有实现的插件,

可以让硬件适配团队充分利用其硬件。这包括:

  • 自定义纹理; 特别是QQuickWindow::createTextureFromImage的实现以及Image和BorderImage类型使用的纹理的内部表示。

  • 自定义渲染器;适配层使插件可以决定如何遍历和渲染“场景图”,从而有可能针对特定硬件优化渲染

算法或 使用可提高性能的扩展。

  • 许多默认Qml类型的自定义“场景图”实现,包括其文本和字体渲染。

  • 自定义动画驱动程序;允许动画系统连接到低级“垂直同步”的显示设备,以获得平滑的渲染。

  • 自定义渲染循环;可以更好地控制Qml如果处理多个窗口。

QQuickWidget中文输入法问题的正确解法

作者 JaredTao
2020年11月30日 12:44

QQuickWidget中文输入法问题的正确解法

本文分享特定问题的解法,用不到的可以忽略。

Qt的bug

使用QQuickWidget的时候,遇到过这个问题:界面的TextInput 或者TextEdit, 鼠标点击聚焦后,切换为光标输入状态,此时切换系统中文输入法,会发现无法输入。

(系统任务栏的输入法状态是正确的,界面上输入字符,直接显示英文,无法显示输入法的候选框)

需要把界面切到其它软件,再切换回来,之后就能够输入了。

可以参考Qt官方bug报告:

https://bugreports.qt.io/browse/QTBUG-61475

旧的解法

这个Bug是2018年报告的,我们当时做项目,也被这个Bug坑到了。

当时我给出了一个弱化版本的解法,原理是在第一次聚焦的时候,清理掉QQuickWidget的焦点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
QuickWidget::QuickWidget(QWidget *parent)
: QQuickWidget(parent)
{
...
connect(quickWindow(), &QQuickWindow::activeFocusItemChanged, this, &QuickWidget::onClearFocus);
...
}
void QuickWidget::onClearFocus()
{
QQuickItem *pItem = quickWindow()->activeFocusItem();
if (pItem && (pItem->inherits("QQuickTextInput") || pItem->inherits("QQuickTextField")))
{
disconnect(quickWindow(), &QQuickWindow::activeFocusItemChanged, this, &QuickWidget::onClearFocus);
QuickWidget::clearFocus();
}
}

此方法勉强能用,一些细节上体验不太好。

当时找不到更好的方法,就这样用着了。

正确的解法

2020年Qt官方终于派出了资深的专家,在Qt5.15.2中,彻底解决了这个问题。

(看到有不少博客、论坛,还在流传我提供的旧版本,于心不忍)

于是我从新版本里面,提炼出来了代码,给使用旧版本的同学解决此问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
QuickWidget::QuickWidget(QWidget *parent)
: QQuickWidget(parent)
{
...

#if QT_VERSION < QT_VERSION_CHECK(5, 15, 2)
connect(quickWindow(), &QQuickWindow::focusObjectChanged, this, &QuickWidget::propagateFocusObjectChanged);
#endif

...
}

#if QT_VERSION < QT_VERSION_CHECK(5, 15, 2)
bool QuickWidget::event(QEvent *e)
{
switch (e->type())
{
case QEvent::FocusAboutToChange:
return QCoreApplication::sendEvent(quickWindow(), e);
default:
break;
}
return Super::event(e);
}
void QuickWidget::propagateFocusObjectChanged(QObject *focusObject)
{
if (QApplication::focusObject() != this)
return;
if (this->window()->windowHandle()) {
emit this->window()->windowHandle()->focusObjectChanged(focusObject);
}
}
#endif

玩转Qml(17)-树组件的定制

作者 JaredTao
2020年6月15日 23:44

简介

最近遇到一些需求,要在Qt/Qml中开发树结构,并能够导入、导出json格式。

于是我写了一个简易的Demo,并做了一些性能测试。

在这里将源码、实现原理、以及性能测试都记录、分享出来,算是抛砖引玉吧,希望有更多人来讨论、交流。

TreeEdit源码

起初的代码在单独的仓库

github https://github.com/jaredtao/TreeEdit

gitee镜像 https://gitee.com/jaredtao/Tree

后续会收录到《玩转Qml》配套的开源项目TaoQuick中

github https://github.com/jaredtao/TaoQuick

gitee镜像 https://gitee.com/jaredtao/TaoQuick

效果预览

看一下最终效果

预览

Qml实现的树结构编辑器, 功能包括:

树结构的缩进
节点展开、折叠
添加节点
删除节点
重命名节点
搜索
导入
导出
节点属性编辑(完善中)

原理说明

数据model的实现,使用C++,继承于QAbstractListModel,并实现rowCount、data等方法。

model本身是List结构的,在此基础上,对model数据进行扩展以模拟树结构,例如增加了 “节点深度”、“是否有子节点”等数据段。

view使用Qml Controls 2中的ListView模拟实现(Controls 1 中的TreeView即将被废弃)。

关键代码

model

基本model的声明如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
template <typename T>
class TaoListModel : public QAbstractListModel {
public:
//声明父类
using Super = QAbstractListModel;

TaoListModel(QObject* parent = nullptr);
TaoListModel(const QList<T>& nodeList, QObject* parent = nullptr);

const QList<T>& nodeList() const
{
return m_nodeList;
}
void setNodeList(const QList<T>& nodeList);

int rowCount(const QModelIndex& parent) const override;

QVariant data(const QModelIndex& index, int role) const override;
bool setData(const QModelIndex& index, const QVariant& value, int role) override;


bool insertRows(int row, int count, const QModelIndex& parent = QModelIndex()) override;
bool removeRows(int row, int count, const QModelIndex& parent = QModelIndex()) override;

Qt::ItemFlags flags(const QModelIndex& index) const override;
Qt::DropActions supportedDropActions() const override;

protected:
QList<T> m_nodeList;
};

其中数据成员使用 QList m_nodeList 存储, 大部分成员函数是对此数据的操作。

Json格式的model声明如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
const static QString cDepthKey = QStringLiteral("TModel_depth");
const static QString cExpendKey = QStringLiteral("TModel_expend");
const static QString cChildrenExpendKey = QStringLiteral("TModel_childrenExpend");
const static QString cHasChildendKey = QStringLiteral("TModel_hasChildren");
const static QString cParentKey = QStringLiteral("TModel_parent");
const static QString cChildrenKey = QStringLiteral("TModel_children");

const static QString cRecursionKey = QStringLiteral("subType");
const static QStringList cFilterKeyList = { cDepthKey, cExpendKey, cChildrenExpendKey, cHasChildendKey, cParentKey, cChildrenKey };
class TaoJsonTreeModel : public TaoListModel<QJsonObject> {
Q_OBJECT
Q_PROPERTY(int count READ count NOTIFY countChanged)
public:
//声明父类
using Super = TaoListModel<QJsonObject>;
//从json文件读入数据
Q_INVOKABLE void loadFromJson(const QString& jsonPath, const QString& recursionKey = cRecursionKey);
//导出到json文件
Q_INVOKABLE bool saveToJson(const QString& jsonPath, bool compact = false) const;
Q_INVOKABLE void clear();
//设置指定节点的数值
Q_INVOKABLE void setNodeValue(int index, const QString &key, const QVariant &value);
//在index添加子节点。刷新父级,返回新项index
Q_INVOKABLE int addNode(int index, const QJsonObject& json);
Q_INVOKABLE int addNode(const QModelIndex& index, const QJsonObject& json)
{
return addNode(index.row(), json);
}
//删除。递归删除所有子级,刷新父级
Q_INVOKABLE void remove(int index);
Q_INVOKABLE void remove(const QModelIndex& index)
{
remove(index.row());
}
Q_INVOKABLE QList<int> search(const QString& key, const QString& value, Qt::CaseSensitivity cs = Qt::CaseInsensitive) const;
//展开子级。只展开一级,不递归
Q_INVOKABLE void expand(int index);
Q_INVOKABLE void expand(const QModelIndex& index)
{
expand(index.row());
}
//折叠子级。递归全部子级。
Q_INVOKABLE void collapse(int index);
Q_INVOKABLE void collapse(const QModelIndex& index)
{
collapse(index.row());
}
//展开到指定项。递归
Q_INVOKABLE void expandTo(int index);
Q_INVOKABLE void expandTo(const QModelIndex& index)
{
expandTo(index.row());
}
//展开全部
Q_INVOKABLE void expandAll();

//折叠全部
Q_INVOKABLE void collapseAll();


int count() const;

Q_INVOKABLE QVariant data(int idx, int role = Qt::DisplayRole) const
{
return Super::data(Super::index(idx), role);
}
signals:
void countChanged();
...
};

TaoJsonTreeModel继承于TaoListModel,并提供大量Q_INVOKABLE函数,以供Qml调用。

view

TreeView的模拟实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
Item {
id: root
readonly property string __depthKey: "TModel_depth"
readonly property string __expendKey: "TModel_expend"
readonly property string __childrenExpendKey: "TModel_childrenExpend"
readonly property string __hasChildendKey: "TModel_hasChildren"

readonly property string __parentKey: "TModel_parent"
readonly property string __childrenKey: "TModel_children"
...
ListView {
id: listView
anchors.fill: parent
currentIndex: -1
delegate: Rectangle {
id: delegateRect
width: listView.width
color: (listView.currentIndex === index || area.hovered) ? config.normalColor : config.darkerColor
// 根据 expaned 判断是否展开,不展开的情况下高度为0
height: model.display[__expendKey] === true ? 35 : 0
// 优化。高度为0时visible为false,不渲染。
visible: height > 0
property alias editable: nameEdit.editable
property alias editItem: nameEdit
TTextInput {
id: nameEdit
anchors.verticalCenter: parent.verticalCenter
//按深度缩进
x: root.basePadding + model.display[__depthKey] * root.subPadding
text: model.display["name"]
height: parent.height
width: parent.width * 0.8 - x
editable: false
onTEditFinished: {
sourceModel.setNodeValue(index, "name", displayText)
}
}
TTransArea {
id: area
height: parent.height
width: parent.width - controlIcon.x
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
onPressed: {
//单击时切换当前选中项
if (listView.currentIndex !== index) {
listView.currentIndex = index;
} else {
listView.currentIndex = -1;
}
}
onTDoubleClicked: {
//双击进入编辑状态
delegateRect.editable = true;
nameEdit.forceActiveFocus()
nameEdit.ensureVisible(0)
}
}
Image {
id: controlIcon
anchors {
verticalCenter: parent.verticalCenter
right: parent.right
rightMargin: 20
}
//有子节点时,显示小图标
visible: model.display[__hasChildendKey]
source: model.display[__childrenExpendKey] ? "qrc:/img/collapse.png" : "qrc:/img/expand.png"
MouseArea {
anchors.fill: parent
onClicked: {
//点击小图标时,切换折叠、展开的状态
if (model.display[__hasChildendKey]) {
if( true === model.display[__childrenExpendKey]) {
collapse(index)
} else {
expand(index)
}
}
}
}
}
}
}
...
}

model层并没有扩展role,而是在data函数的role为display时直接返回json数据,

所以delegate中统一使用model.display[xxx]的方式访问数据。

性能测试

测试环境

CPU: Intel i5-8400 2.8GHz 六核

内存: 16GB

OS: Windows10 1909

Qt: 5.12.6

编译器: msvc 2017 x64

测试框架: QTest

测试方法

数据生成

使用node表示根节点的数量,depth表示每个根节点下面嵌套节点的层数。

例如: node 等于 100, depth 等于10,则数据如下:

预览

顶层有100个节点,每个节点下面再嵌套10层,共计节点 100 + 100 * 10 = 1100.

生成json数据的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
...
//单元测试类
class LoadTest : public QObject {
Q_OBJECT

public:
LoadTest();
~LoadTest();

static void genJson(const QPoint& point);

...
//私有槽函数会被QTest调用
private slots:
//初始化
void initTestCase();
//清理
void cleanupTestCase();
//测试导入
void test_load();
//测试导入前,准备数据
void test_load_data();
//测试导出
void test_save();
//测试导出前,准备数据
void test_save_data();
};
...
//节点最大值
const int nodeMax = 10000;
//嵌套深度最大值
const int depthMax = 100;

void LoadTest::genJson(const QPoint& point)
{
using namespace TaoCommon;
int node = point.x();
int depth = point.y();
QJsonArray arr;
for (int i = 0; i < node; ++i) {
QJsonObject obj;
obj["name"] = QString("node_%1").arg(i);
QVector<QJsonArray> childrenArr = { depth, QJsonArray { QJsonObject {} } };
//最后一个节点,嵌套层级最深的。
childrenArr[depth - 1][0] = QJsonObject { { "name", QString("node_%1_%2").arg(i).arg(depth - 1) } };
//从后往前倒推。
for (int j = depth - 2; j >= 0; --j) {
childrenArr[j][0] = QJsonObject { { cRecursionKey, childrenArr[j + 1] }, { "name", QString("node_%1_%2").arg(i).arg(j) } };
}
obj[cRecursionKey] = childrenArr[0];
arr.append(obj);
}
writeJsonFile(qApp->applicationDirPath() + QString("/%1_%2.json").arg(node).arg(depth), arr);
}

void LoadTest::initTestCase()
{
QList<QPoint> list;
for (int i = 1; i <= nodeMax; i *= 10) {
for (int j = 1; j <= depthMax; j *= 10) {
list.append({ i, j });
}
}
auto result = QtConcurrent::map(list, &LoadTest::genJson);
result.waitForFinished();
}

初始化函数initTestCase中,组织了一个QList,然后使用QtConcurrent::map并发调用genJson函数,生成数据json文件。

node和depth每次扩大10倍。

经过测试,嵌套层数在100以上时,Qt可能会崩溃。要么是QJsonDocument无法解析,要么是Qml挂掉。所以不使用100以上的嵌套级别。

测试过程

QTest十分好用,简单易上手,参考帮助文件即可

例如测试加载的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void LoadTest::prepareData()
{
//添加两列数据
QTest::addColumn<int>("node");
QTest::addColumn<int>("depth");
//添加行
for (int i = 1; i <= nodeMax; i *= 10) {
for (int j = 1; j <= depthMax; j *= 10) {
QTest::newRow(QString("%1_%2").arg(i).arg(j).toStdString().c_str()) << i << j;
}
}
}
void LoadTest::test_load_data()
{
//准备数据
prepareData();
}
void LoadTest::test_load()
{
using namespace TaoCommon;
//取数据
QFETCH(int, node);
QFETCH(int, depth);
TaoJsonTreeModel model;
//性能测试
QBENCHMARK
{
model.loadFromJson(qApp->applicationDirPath() + QString("/%1_%2.json").arg(node).arg(depth));
}
}

测试结果

预览

一秒内最多可以加载的数据量在十万级别,包括

10000 x 10耗时在 386毫秒,1000 x 100 耗时在671毫秒。

玩转Qt(14)-Qt与Web混合开发

作者 JaredTao
2020年3月4日 19:44

前言

这次讨论Qt与Web混合开发相关技术。

这类技术存在适用场景,例如:Qt项目使用Web大量现成的组件/方案做功能扩展,

Qt项目中性能无关/频繁更新迭代的页面用html单独实现,Qt项目提供Web形式的SDK给

用户做二次开发等等,或者是Web开发人员齐全而Qt/C++人手不足,此类非技术问题,

都可以使用Qt + Web混合开发。

(不适用的请忽略本文)

简介

这篇文章,会先整体介绍一下Qt的各种Web方案,再提供简单的Demo,并做一些简要的说明。

之后的一篇文章,会通过一个Web控制Qt端小车的案例,来做进一步讨论。

Qt的Web方案

Qt提供的Web方案主要包括 WebEngine/WebView、Quick WebGL Stream、QtWebAssembly三种。

Quick WebGL Stream

可以参考Qt官方的WebGL Stream介绍文档

https://resources.qt.io/cn/qt-quick-webgl-release-512

resources.qt.io
WebGL Stream在5.12中正式发布,其本质是一种通信技术,将已有的QtQuick程序中渲染指令和数据,通过socket传输给Web端,由WebGL实现界面渲染。

其使用方式非常的简单,无需修改源码,应用程序启动时,带上端口参数,例如:

./your-qt-application -platform webgl:port=8998
(相当于应用程序变成了一个服务器端程序)

这样程序就在后端运行,看不到界面了,之后浏览器打开本地网址 localhost:8998 或者内网地址/映射后的公网地址,就能在浏览器中看到程序页面。

WebGL Stream的应用不多,Qt官方给的案例是:欧洲某工厂的大量传感器监测设备,都以WebGL Stream的方式运行Qt 程序,本身都不带显卡和显示器,而在控制中心的显卡/显示器上,通过Web打开网页的方式,查看每个设备的运行状况。因此节约了大量显卡/显示器的成本。类比于网吧的无硬盘系统。

涛哥相信,未来结合5G技术会有不错的应用场景。

Qt WebAssembly

Qt WebAssembly技术,在5.13中正式发布。本质是把Qt程序编译成浏览器支持的二进制文件,由浏览器加载运行。

一方面可以将现有的Qt程序编译成Web,另一方面可以用Qt/C++来弥补Web程序的性能短板。

Qt WebAssembly在使用细节上还有一些坑的地方,需要踩一踩。后续我再写文章吧。

Qt WebEngine/WebView

Qt提供了WebEngine模块以支持Web功能。

Qt WebEngine基于google的开源浏览器chromium实现,类似的项目还有cefminiblink等等。

QtWebEngine可以看作是一个完整的chromium浏览器。

(WebView是同类的方案,稍微有些区别。后文再说。)

QtWebEngine的更新情况

浏览器技术十分的庞大,这里先不深入展开,先来关注一下Qt WebEngine对chromium的跟进情况。

数据来源于Qt wiki,Qt每个版本的change log

Qt版本chromium后端chromium安全更新
5.9.056
5.9.1-59.0.3071.104
5.9.2-61.0.3163.79
5.9.3-62.0.3202.89
5.9.4-63.0.3239.132
5.9.5-65.0.3325.146
5.9.6-66.0.3359.170
5.9.7-69.0.3497.113
5.9.8-72.0.3626.121
5.9.9-78.0.3904.108
5.12.069
5.12.171.0.3578.94
5.12.272.0.3626.121
5.12.373.0.3683.75
5.12.474.0.3729.157
5.12.576.0.3809.87
5.12.677.0.3865.120
5.12.779.0.3945.130
5.14.077
5.14.179.0.3945.117

可以看到Qt在WebEngine模块,一直持续跟进Chromium的更新。

当前(2020/3/4)最新的chromium版本是80。

WebEngine的架构

QtWebEngine提供了C++和Qml的接口,可以在Widget/Qml中渲染HTML、XHTML、SVG,

也支持CSS样式表和JavaScript脚本。

QtWebEngine的架构图如下

基于Chromium封装了一个WebEngineCore模块,在此之上,

WebEngine Widgets模块专门用于Widget项目,

WebEngine 模块用于Qml项目,

WebEngineProcess则是一个单独的进程,用来渲染页面、运行js脚本。

Web在单独的进程里,我们开发的时候知道这一点就好了,不需要额外关注,

只要在发布的时候,带上QTDIR目录下的可执行程序QtWebEngineProcess即可。

(这里提一下底层实现原理,使用了进程间共享OpenGL上下文的方式, 实现多个进程的UI混合在一起)

WebEngine的平台要求

(以Qt5.12为参考)

首先一条是:不支持静态编译 (因为依赖的chromium、chromium本身的依赖库 不能静态编译)

接下来再看看各平台的要求和限制:

Windows

编译器要 Visual Studio 2017 version 15.8 以上

系统环境要 Windows 10 SDK

默认只支持X64版本,如果要x86版本,要自己编译qt源码。

MacOS

  • MacOS 10.12以上

  • XCode 8.3.3以上

  • MacOS 10.12以上 SDK

不支持32-bit

不兼容 Mac App Store (chromium使用了私有api,App Sandbox和chromium Sandbox优先级问题)

Linux

编译器要 clang, 或者 gcc 5以上

需要pkg-config来探测依赖库,dbus-1和 fontconfig是必须的。

如果配置了xcb,还要额外配置相关库。

WebView

Qt还提供了一个WebView组件,可以用来将Web内容嵌入到Qml程序中。(这个没有提供Widget的接口)

WebView组件的实现,使用了平台原生api,在移动端意义重大,特别是在ios平台,使用

原生的web view,这样就能兼容App Store了。

在Windows/MacOS/Linux平台,则是渲染部分还是使用了WebEngine。

WebView的使用可以参考官方例子Minibrowser

WebEngine的使用

WebEngine Widget最简Demo

源代码

这里示例一个最简单的demo, 使用WebEngine Widget模块提供的QWebEngineView:

1
2
3
4
5
6
7
//Demo.pro
QT += core gui webenginewidgets

CONFIG += c++11

SOURCES += \
main.cpp

注意pro文件中包含的Qt模块

1
2
3
4
5
6
7
8
9
10
11
12
13
//main.cpp
#include <QApplication>
#include <QWebEngineView>
int main(int argc, char **argv)
{
QApplication app(argc, argv);

QWebEngineView view;
view.load(QUrl("https://www.zhihu.com/"));
view.show();

return app.exec();
}

运行结果

上面代码以打开知乎首页为例,运行结果如下

最小发布包

涛哥尝试了在Windows平台,做出可用的最小发布包:

尺寸在170M左右。这些依赖项中,除了常见的Qt必备项platforms、Qt5Core、Qt5Gui等,

Qt5WebEngineCore是最大的一个,有70M。QtWebEngineProcess.exe是新增加的一个exe程序,

前文说架构图时提到的单独进程就是这个程序实现。

resources/icudtl.dat在其它浏览器引擎中也常看到。

translations/qtwebengine_locales是WebEngine的翻译项,不带可能会发生翻译问题。

Qt5Positioning、Qt5PrintSupport一般不怎么用,但是不带这两个程序起不来。

同时发现Qml和Quick模块也是必须的,Qt5QuickWidgets也用上了。

涛哥查看源码后发现WebEngineCore模块依赖Quick和Qml模块。

WebEngine Qml最简Demo

再做一个纯Qml的Demo

源码

pro中增加webengine模块即可

1
2
3
4
5
6
7
8
9
10
//WebQml.pro
QT += core gui quick qml webengine

CONFIG += c++11

SOURCES += \
main.cpp

RESOURCES += \
Qrc.qrc

注意初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//main.cpp
#include <QGuiApplication>
#include <QQuickView>
#include <QtWebEngine>
int main(int argc, char *argv[])
{
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);

QGuiApplication a(argc, argv);
//初始化。时机在QApp之后,Window/View构造之前。
QtWebEngine::initialize();

QQuickView view;
view.setSource(QUrl("qrc:/main.qml"));
view.show();
return a.exec();
}

qml导入模块,填入url

1
2
3
4
5
6
7
8
9
10
11
//main.qml
import QtQuick 2.0
import QtWebEngine 1.8
Item {
width: 800
height: 600
WebEngineView {
anchors.fill: parent
url: "https://www.zhihu.com"
}
}

运行结果

运行结果和上一个Demo一样

最小发布包

这回可以去掉Widget模块

同时也去掉不必要的翻译文件。包大小160M左右,和前面的差别不大。

玩转Qt(12)-github-Actions缓存优化

作者 JaredTao
2019年12月4日 12:44

简介

在之前两篇文章《github-Actions自动化编译》《github-Actions自动化发行》中,

介绍了github-Actions的一些用法,其中有部分配置,已经有了缓存相关的步骤。

这里专门开一篇文章,来记录github-Actions的缓存优化相关的知识。

原理

缓存actions模板

github-Actions提供了缓存模板cache

缓存文档

官方文档也有说明 缓存文档

缓存大致原理就是把目标路径打包存储下来,并记录一个唯一key。

下次启动时,根据key去查找。找到了就再按路径解压开。

缓存大小限制

注意缓存有大小限制。对于免费用户,单个包不能超过500MB,整个仓库的缓存不能超过2G。

缓存运作流程

一般我们在任务步骤中增加一个cache

1
2
3
4
5
6
steps:
...
- use: actions/cache@v1
with:
...
...

那么在这个地方,缓存执行的操作是restore。

在steps的末尾,会自动增加一个PostCache,执行的操作是record。

Qt项目的缓存优化

Qt项目每次运行Actions时,都是先通过install-qt-action模板,安装Qt,之后再获取代码,编译运行。

安装Qt这个步骤,可快可慢,涛哥在windows平台测试下来,平均要1分30秒左右。

加上cache后,平均只有25秒。

无缓存的配置

先看一个Qt项目的编译配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
name: Windows
on: [push,pull_request]
jobs:
build:
name: Build
runs-on: windows-latest
strategy:
matrix:
qt_ver: [5.12.6]
qt_target: [desktop]
qt_arch: [win64_msvc2017_64, win32_msvc2017]
include:
- qt_arch: win64_msvc2017_64
msvc_arch: x64
- qt_arch: win32_msvc2017
msvc_arch: x86
# 步骤
steps:
# 安装Qt
- name: Install Qt
uses: jurplel/install-qt-action@v2.0.0
with:
version: ${{ matrix.qt_ver }}
target: ${{ matrix.qt_target }}
arch: ${{ matrix.qt_arch }}
# 拉取代码
- uses: actions/checkout@v1
with:
fetch-depth: 1
# 编译msvc
- name: build-msvc
shell: cmd
env:
vc_arch: ${{ matrix.msvc_arch }}
run: |
call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" %vc_arch%
qmake
nmake

加缓存

缓存步骤,一般尽量写steps最前面。

1
2
3
4
5
6
7
8
9
# 步骤
steps:
# 缓存
- name: cacheQt
id: WindowsCacheQt
uses: actions/cache@v1
with:
path: ../Qt/${{matrix.qt_ver}}/${{matrix.qt_arch_install}}
key: ${{ runner.os }}-Qt/${{matrix.qt_ver}}/${{matrix.qt_arch}}

install-qt-action有默认的Qt安装路径${RUNNER_WORKSPACE},不过这个环境变量不一定能取到。

涛哥实际测试下来,以当前路径的上一级作为Qt路径即可。

环境变量还原

缓存只是把文件还原了,环境变量并没有还原,我们还需要手动还原环境变量。

install-qt-action这个模板增加了一个环境变量Qt5_Dir,值为Qt的安装路径,并把对应的bin添加到了Path。

我们要做的,就是在缓存恢复成功后,重新设置这两个变量,并去掉install-qt的步骤。

1
2
3
4
5
6
7
8
9
- name: setupQt
if: steps.WindowsCacheQt.outputs.cache-hit == 'true'
shell: pwsh
env:
QtPath: ../Qt/${{matrix.qt_ver}}/${{matrix.qt_arch_install}}
run: |
$qt_Path=${env:QtPath}
echo "::set-env name=Qt5_Dir::$qt_Path"
echo "::add-path::$qt_Path/bin"

steps.WindowsCacheQt.outputs.cache-hit == ‘true’

是缓存模板的输出值,可以作为后续步骤的条件判断。

最终配置

写个伪配置,简单示例一下缓存流程

steps:

  • cache
  • setupQt
    if: cache-hit == ‘true’
  • installQt
    if: cache-hit = ‘false’

实际配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
name: Windows
on:
# push代码时触发workflow
push:
# 忽略README.md
paths-ignore:
- 'README.md'
- 'LICENSE'
# pull_request时触发workflow
pull_request:
# 忽略README.md
paths-ignore:
- 'README.md'
- 'LICENSE'
jobs:
build:
name: Build
# 运行平台, windows-latest目前是windows server 2019
runs-on: windows-latest
strategy:
# 矩阵配置
matrix:
qt_ver: [5.12.6]
qt_target: [desktop]
# mingw用不了
# qt_arch: [win64_msvc2017_64, win32_msvc2017, win32_mingw53,win32_mingw73]
qt_arch: [win64_msvc2017_64, win32_msvc2017]
# 额外设置msvc_arch
include:
- qt_arch: win64_msvc2017_64
msvc_arch: x64
qt_arch_install: msvc2017_64
- qt_arch: win32_msvc2017
msvc_arch: x86
qt_arch_install: msvc2017
env:
targetName: HelloActions-Qt.exe
# 步骤
steps:
- name: cacheQt
id: WindowsCacheQt
uses: actions/cache@v1
with:
path: ../Qt/${{matrix.qt_ver}}/${{matrix.qt_arch_install}}
key: ${{ runner.os }}-Qt/${{matrix.qt_ver}}/${{matrix.qt_arch}}
- name: setupQt
if: steps.WindowsCacheQt.outputs.cache-hit == 'true'
shell: pwsh
env:
QtPath: ../Qt/${{matrix.qt_ver}}/${{matrix.qt_arch_install}}
run: |
$qt_Path=${env:QtPath}
echo "::set-env name=Qt5_Dir::$qt_Path"
echo "::add-path::$qt_Path/bin"
# 安装Qt
- name: Install Qt
if: steps.WindowsCacheQt.outputs.cache-hit != 'true'
# 使用外部action。这个action专门用来安装Qt
uses: jurplel/install-qt-action@v2.0.0
with:
# Version of Qt to install
version: ${{ matrix.qt_ver }}
# Target platform for build
target: ${{ matrix.qt_target }}
# Architecture for Windows/Android
arch: ${{ matrix.qt_arch }}
# 拉取代码
- uses: actions/checkout@v1
with:
fetch-depth: 1
# 编译msvc
- name: build-msvc
shell: cmd
env:
vc_arch: ${{ matrix.msvc_arch }}
run: |
call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" %vc_arch%
qmake
nmake

玩转Qt(10)-github-Actions自动化编译

作者 JaredTao
2019年11月19日 12:44

前言

几个月前写过两篇持续集成的教程,当时使用的是travis和appveyor这两个第三方网址提供的服务。

由于配置比较复杂,劝退了很多同学……

2019年8月份,github正式上线了Actions功能,提供了十分强大的CI(持续集成)/CD(持续部署)服务,

使用非常简单、方便,再加上github的Marketplace(github的应用商店)有各路大神开源的Actions模板, 完全可以抛弃那些落后的第三方服务了。

注:Actions也能在私有仓库上用(微软良心)。

简介

这回涛哥将给大家提供一个简易的Qt项目的Action模板,让每一个有追求的Qter,都能轻松地用上强大的CI/CD功能。

(本文先说自动化编译,自动化发布下次说。)

代码仓库

我创建了一个新的代码仓库,地址在这:

https://github.com/jaredtao/HelloActions-Qt

效果预览

先来看看效果吧

这是github的Actions页面

图中可以看到,最后一次提交的代码,在Windows、Ubuntu、MacOS、Android、IOS五个平台都编译通过了(通过显示绿色的对勾✔,未通过显示红色的叉❌)。

涛哥是个徽章爱好者,把这些徽章都链接进了README文件中。别人在预览代码仓库的时候,很容易就能看到仓库的编译状态。

当然,在commit页面,还可以详细查看每一次commit的代码,是否都编译通过

使用方式

(这里假设各位读者会使用基本的git、github操作,不会的请去搜索相关教程)

  1. 下载涛哥的仓库HelloActions-Qt
1
git clone https://github.com/jaredtao/HelloActions-Qt
  1. 拷贝文件夹’.github’到你的代码仓库根目录

  2. 在你的仓库中commit并添加.github文件夹中的文件

  3. push你的仓库到github

push完就可以了,到你的github相应仓库页面-Actions子页面查看状态吧。

没错,复制、粘贴,就这么简单。

.github/workflows文件夹中包括写好的5个模板:

你也可以根据你的需要,只选择你需要的。

原理

授人以鱼,不如授人以渔

这里再来介绍一些基本的原理。

Actions官方文档

可以参考 github Actions官方文档

中文文档目前翻译不全面,建议优先看英文的。

Actions的默认环境

github-Actions 主要提供了windows server 2019、macos 10.15、ubuntu 18.04三个平台的docker环境,

并预装了大量开发者常用的软件,比如Java SDK、Android SDK、VisualStudio、python、golang、nodejs等,

可以在文档github Actions默认环境及预装软件 中看到详细的信息。

Actions语法

github-Actions和大部分docker环境一样,使用yaml/yml格式的配置文件。

同时github-Actions还提供了一些便利的功能、函数,可以参考

github Actions配置文件语法

更多细节请大家参考文档,这里就不赘述了。

Actions模板

每个github仓库,都有一个Actions页面,在这里可以创建、管理Actions

一般使用nodejs、python、golang等环境的项目,github提供了现成的Actions模板,可以

直接在Actions创建页面或者Marketplace(github的应用商店)进行搜索、引用。

有闲暇的开发者,也可以开发自己的Actions并提交到github商店,甚至可以赚点零花钱哦。

(Actions开发使用TypeScript)

Qt项目的编译流程

简单总结一下Qt项目的编译流程

  1. 安装Qt环境

    这一步用下文的Action模板:install-qt-action

  2. 获取项目代码

    这一步用Actions官方核心模板:actions/checkout@v1

  3. 执行qmake、make

    这一步用自定义脚本,可以换成qbs、cmake、gn、ninja等构建工具

  4. 执行test

    这一步可以引入单元测试、自动化UI测试等。以后再说。

  5. 执行deployment

    等我下一篇文章

Qt相关的Actions模板

install-qt-action

Qt项目暂时没有公开、完整的Actions模板,不过有一个安装Qt的Actions,解决了在不同平台安装不同版本Qt的问题。

install-qt-action

github的Actions有一个非常强大的功能,就是引用外部模板。

比如要引入这个install-qt-Actions模板,只要在配置文件中添加两行即可:

1
2
3
4
...
- name: Install Qt
uses: jurplel/install-qt-action@v2
...

Qt的安装路径、版本、目标平台、目标架构都有默认配置,当然你也可以手动配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
...
- name: Install Qt
uses: jurplel/install-qt-action@v2
with:
# 安装目录,默认当前路径
#dir: # optional
# 版本,默认最新的LTS(5.12.6)
version: 5.12.6
# 编译平台。一般不修改。
#host: # optional
# 目标平台。默认desktop,可选android、ios
target: desktop
# 架构
arch: win64_msvc2017_64
...

这个Actions模板的实现,是按照Actions的工作原理(TypeScript),调用另一个python仓库aqtinstall,

把配置参数传递过去,由该库完成Qt的安装。

aqtinstall由一位日本的程序员使用python开发,直接访问Qt官方的发布仓库

http://download.qt.io/online/qtsdkrepository/ , 下载指定平台的各模块压缩包,并解压到指定目录。

直接绕过了我们平常使用的Qt安装器。

aqtinstall没有实现‘只安装指定模块’,默认全安装。希望后续能做支持,毕竟Qt全安装太大了。

action-setup-qt

涛哥还发现一个开源的action,并没有进商店,功能是适配所有平台的Qt环境变量

https://github.com/Skycoder42/action-setup-qt

可以在该作者的’Json序列化库’中,看到实际应用

https://github.com/Skycoder42/QtJsonSerializer

目前是固定在Qt5.13.2版本,包含winrt、wasm等所有平台。

扩展

接下来,说一下涛哥提供的模板,对各平台的配置。

以方便那些,需要对模板做修改的同学。

Windows平台

涛哥在这个配置文件中,写了一些注释。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# windows.yml
name: Windows
on:
# push代码时触发workflow
push:
# 忽略README.md
paths-ignore:
- 'README.md'
- 'LICENSE'
# pull_request时触发workflow
pull_request:
# 忽略README.md
paths-ignore:
- 'README.md'
- 'LICENSE'
jobs:
build:
name: Build
# 运行平台, windows-latest目前是windows server 2019
runs-on: windows-latest
strategy:
# 矩阵配置
matrix:
qt_ver: [5.9.8,5.12.6]
qt_target: [desktop]
# mingw用不了
# qt_arch: [win64_msvc2017_64, win32_msvc2017, win32_mingw53,win32_mingw73]
qt_arch: [win64_msvc2017_64, win32_msvc2017]
# 从矩阵中除外的配置
exclude:
# 不存在5.9.8-win32_msvc2017的版本
- qt_ver: 5.9.8
qt_arch: win32_msvc2017
# mingw用不了
# - qt_ver: 5.9.8
# qt_arch: win32_mingw73
# - qt_ver: 5.12.6
# qt_arch: win32_mingw53
# 额外设置msvc_arch
include:
- qt_arch: win64_msvc2017_64
msvc_arch: x64
- qt_arch: win32_msvc2017
msvc_arch: x86
# 步骤
steps:
# 安装Qt
- name: Install Qt
# 使用外部action。这个action专门用来安装Qt
uses: jurplel/install-qt-action@v2.0.0
with:
# Version of Qt to install
version: ${{ matrix.qt_ver }}
# Target platform for build
target: ${{ matrix.qt_target }}
# Architecture for Windows/Android
arch: ${{ matrix.qt_arch }}
# 拉取代码
- uses: actions/checkout@v1
with:
fetch-depth: 1
# 编译msvc
- name: build-msvc
shell: cmd
env:
vc_arch: ${{ matrix.msvc_arch }}
run: |
call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" %vc_arch%
qmake
nmake

大部分配置都是显而易见的,这里对一些特殊情况做一些说明吧。

默认mingw不能用

windows平台优先推荐用msvc编译,不过有些情况不得不用mingw。

github-Actions提供的Windows Server 2019环境,预装Mingw为8.1.0,版本太高了。

Qt5.9需要的mingw版本是5.3,而5.12则需要7.3,涛哥试过简单的HelloWorld程序,都会报链接失败。

所以需要使用MinGW的同学,需要自己安装了。

Windows平台指定shell

github-Actions在Windows平台默认的shell是PowerShell,其它平台是bash。

使用msvc命令行编译项目时,一般要先调用’vcvarsxxx.bat’脚本来设置环境变量。

Powershell虽然强大,却不太方便直接调用这个bat。要么安装Powershell扩展Pcsx,要么

用一些取巧的方式:

https://stackoverflow.com/questions/2124753/how-can-i-use-powershell-with-the-visual-studio-command-prompt

github-Actions当然也可以直接指定使用cmd。

1
2
3
4
5
6
7
8
9
10
11
...
# 编译msvc
- name: build-msvc
shell: cmd
env:
vc_arch: ${{ matrix.msvc_arch }}
run: |
call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" %vc_arch%
qmake
nmake
...

Ubuntu平台

Ubuntu 平台看配置吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# ubuntu.yml
name: Ubuntu
# Qt官方没有linux平台的x86包
on:
push:
paths-ignore:
- 'README.md'
- 'LICENSE'
pull_request:
paths-ignore:
- 'README.md'
- 'LICENSE'
jobs:
build:
name: Build
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-16.04,ubuntu-18.04]
qt_ver: [5.9.8,5.12.6]
steps:
- name: Install Qt
uses: jurplel/install-qt-action@v2.0.0
with:
version: ${{ matrix.qt_ver }}
- name: ubuntu install GL library
run: sudo apt-get install -y libglew-dev libglfw3-dev
- uses: actions/checkout@v1
with:
fetch-depth: 1
- name: build ubuntu
run: |
qmake
make

MacOS平台

MacOS平台和Ubuntu差别不大

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# macos.yml
name: MacOS
on:
push:
paths-ignore:
- 'README.md'
- 'LICENSE'
pull_request:
paths-ignore:
- 'README.md'
- 'LICENSE'
jobs:
build:
name: Build
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [macos-latest]
qt_ver: [5.9.8,5.12.6]
steps:
- name: Install Qt
uses: jurplel/install-qt-action@v2.0.0
with:
version: ${{ matrix.qt_ver }}
- uses: actions/checkout@v1
with:
fetch-depth: 1
- name: build macos
run: |
qmake
make

Android平台

Android使用ubuntu编译,Windows那个ndk似乎没装,未尝试。

如果只使用Qt5.12.6,默认的配置可以直接用,编译前设置环境变量 ANDROID_SDK_ROOT

和ANDROID_NDK_ROOT就可以了。

Qt5.9.8要指定低版本的NDK、SDK才行,这里涛哥没有进一步尝试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# android.yml
name: Android
on:
push:
paths-ignore:
- 'README.md'
- 'LICENSE'
pull_request:
paths-ignore:
- 'README.md'
- 'LICENSE'
jobs:
build:
name: Build
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest]
# 5.9.8 版本低,需要额外设置工具链。这里暂不支持。
qt_ver: [5.12.6]
qt_target: [android]
# android_arm64_v8a 暂时不支持. install-qt-action 依赖的aqtinstall版本为0.5*,需要升级
# qt_arch: [android_x86,android_armv7,android_arm64_v8a]
qt_arch: [android_x86,android_armv7]
# exclude:
# - qt_ver: 5.9.8
# qt_arch: android_arm64_v8a
steps:
- name: Install Qt
# if: steps.cacheqt.outputs.cache-hit != 'true'
uses: jurplel/install-qt-action@v2.0.0
with:
# Version of Qt to install
version: ${{ matrix.qt_ver }}
# Target platform for build
target: ${{ matrix.qt_target }}
# Architecture for Windows/Android
arch: ${{ matrix.qt_arch }}
- uses: actions/checkout@v1
with:
fetch-depth: 1
- name: build android
run: |
export ANDROID_SDK_ROOT=$ANDROID_HOME
export ANDROID_NDK_ROOT=$ANDROID_HOME/ndk-bundle
qmake
make

IOS平台

ios只能使用MacOS编译。

qmake的时候要指定平台、release模式等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#ios.yml
name: IOS
on:
push:
paths-ignore:
- 'README.md'
pull_request:
paths-ignore:
- 'README.md'
jobs:
build:
name: Build
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [macos-latest]
qt_ver: [5.12.6]
qt_target: [ios]
steps:
- name: Install Qt
# if: steps.cacheqt.outputs.cache-hit != 'true'
uses: jurplel/install-qt-action@v2.0.0
with:
# Version of Qt to install
version: ${{ matrix.qt_ver }}
# Target platform for build
target: ${{ matrix.qt_target }}
- uses: actions/checkout@v1
with:
fetch-depth: 1
- name: build ios
run: |
qmake -r -spec macx-ios-clang CONFIG+=release CONFIG+=iphoneos
make

玩转Qt(7)-窥探信号槽的实现细节

作者 JaredTao
2019年8月30日 18:44

简介

这次讨论Qt信号-槽的实现细节。

上次的文章《认清信号槽的本质》中介绍过,信号-槽是一种对象之间的通信机制,是

Qt在标准C++之外,使用元对象编译器(MOC)实现的语法糖。

这次通过一个简单的案例,学习一些信号-槽的实现细节。

猫和老鼠的故事

预览

还是拿上次的设定来说明:Tom有个技能叫”喵”,就是发出猫叫,而正在偷吃东西的Jerry,听见猫叫声就会逃跑。

我们用信号-槽的方式写出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//Tom.h
#pragma once

#include <QObject>
#include <QDebug>
class Tom : public QObject
{
Q_OBJECT
public:
Tom(QObject *parent = nullptr) : QObject(parent)
{
}
void miaow()
{
qDebug() << u8"喵!" ;
emit miao();
}
signals:
void miao();
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//Jerry.h
#pragma once

#include <QObject>
#include <QDebug>
class Jerry : public QObject
{
Q_OBJECT
public:
Jerry(QObject *parent = nullptr) : QObject(parent)
{
}
public slots:
void runAway()
{
qDebug() << u8"那只猫又来了,快溜!" ;
}
};

以上面的代码为例,要使用信号-槽功能,先决条件是继承QObject类,并在类声明中增加Q_OBJECT宏。

之后在”signals:” 字段之后声明一些函数,这些函数就是信号。

在”public slots:” 之后声明的函数,就是槽函数。

接下来看看我们的main函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//main.cpp
#include <QCoreApplication>
#include "Tom.h"
#include "Jerry.h"
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);

Tom tom;
Jerry jerry;

QObject::connect(&tom, &Tom::miao, &jerry, &Jerry::runAway);
tom.miaow();


return a.exec();
}

信号-槽都准备好了,接下来创建两个对象实例,并使用QObject::connect将信号和槽连接起来。

最后使用emit发送信号,就会自动触发槽函数了。

运行结果:

预览

声明与实现

信号和槽的本质都是函数。

我们知道C++中的函数要有声明(declare),也要有实现(implement),

而信号只要声明,不需要写实现。这是因为moc会为我们自动生成。

另外触发信号时,不写emit关键字,直接调用信号函数,也是没有问题的。

Q_OBJECT宏

我们来看一下Q_OBJECT宏,展开如下:

(不同的Qt版本有些差异,涛哥这里用的是5.12.4,以此为例)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public: \
QT_WARNING_PUSH \
Q_OBJECT_NO_OVERRIDE_WARNING \
static const QMetaObject staticMetaObject; \
virtual const QMetaObject *metaObject() const; \
virtual void *qt_metacast(const char *); \
virtual int qt_metacall(QMetaObject::Call, int, void **); \
QT_TR_FUNCTIONS \
private: \
Q_OBJECT_NO_ATTRIBUTES_WARNING \
Q_DECL_HIDDEN_STATIC_METACALL static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **); \
QT_WARNING_POP \
struct QPrivateSignal {}; \
QT_ANNOTATE_CLASS(qt_qobject, "")

我们看到,关键的地方,是声明了一个只读的静态成员变量staticMetaObject,以及3个public的成员函数

1
2
3
4
5
6
7
static const QMetaObject staticMetaObject; 

virtual const QMetaObject *metaObject() const;

virtual void *qt_metacast(const char *);

virtual int qt_metacall(QMetaObject::Call, int, void **);

还有一个private的静态成员函数qt_static_metacall

1
static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **)

那么声明的这些成员变量/函数,在哪里实现?答案是moc生成的cpp文件。

信号的moc生成

预览

如上图所示目录结构,项目编译完成后,在build文件夹中,自动生成了moc_Jerry.cpp 和 moc_Tom.cpp两个文件

其中moc_Tom.cpp内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
/****************************************************************************
** Meta object code from reading C++ file 'Tom.h'
**
** Created by: The Qt Meta Object Compiler version 67 (Qt 5.12.4)
**
** WARNING! All changes made in this file will be lost!
*****************************************************************************/

#include "../../TomJerry/Tom.h"
#include <QtCore/qbytearray.h>
#include <QtCore/qmetatype.h>
#if !defined(Q_MOC_OUTPUT_REVISION)
#error "The header file 'Tom.h' doesn't include <QObject>."
#elif Q_MOC_OUTPUT_REVISION != 67
#error "This file was generated using the moc from 5.12.4. It"
#error "cannot be used with the include files from this version of Qt."
#error "(The moc has changed too much.)"
#endif

QT_BEGIN_MOC_NAMESPACE
QT_WARNING_PUSH
QT_WARNING_DISABLE_DEPRECATED
struct qt_meta_stringdata_Tom_t {
QByteArrayData data[3];
char stringdata0[10];
};
#define QT_MOC_LITERAL(idx, ofs, len) \
Q_STATIC_BYTE_ARRAY_DATA_HEADER_INITIALIZER_WITH_OFFSET(len, \
qptrdiff(offsetof(qt_meta_stringdata_Tom_t, stringdata0) + ofs \
- idx * sizeof(QByteArrayData)) \
)
static const qt_meta_stringdata_Tom_t qt_meta_stringdata_Tom = {
{
QT_MOC_LITERAL(0, 0, 3), // "Tom"
QT_MOC_LITERAL(1, 4, 4), // "miao"
QT_MOC_LITERAL(2, 9, 0) // ""

},
"Tom\0miao\0"
};
#undef QT_MOC_LITERAL

static const uint qt_meta_data_Tom[] = {

// content:
8, // revision
0, // classname
0, 0, // classinfo
1, 14, // methods
0, 0, // properties
0, 0, // enums/sets
0, 0, // constructors
0, // flags
1, // signalCount

// signals: name, argc, parameters, tag, flags
1, 0, 19, 2, 0x06 /* Public */,

// signals: parameters
QMetaType::Void,

0 // eod
};

void Tom::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
{
if (_c == QMetaObject::InvokeMetaMethod) {
auto *_t = static_cast<Tom *>(_o);
Q_UNUSED(_t)
switch (_id) {
case 0: _t->miao(); break;
default: ;
}
} else if (_c == QMetaObject::IndexOfMethod) {
int *result = reinterpret_cast<int *>(_a[0]);
{
using _t = void (Tom::*)();
if (*reinterpret_cast<_t *>(_a[1]) == static_cast<_t>(&Tom::miao)) {
*result = 0;
return;
}
}
}
Q_UNUSED(_a);
}

QT_INIT_METAOBJECT const QMetaObject Tom::staticMetaObject = { {
&QObject::staticMetaObject,
qt_meta_stringdata_Tom.data,
qt_meta_data_Tom,
qt_static_metacall,
nullptr,
nullptr
} };


const QMetaObject *Tom::metaObject() const
{
return QObject::d_ptr->metaObject ? QObject::d_ptr->dynamicMetaObject() : &staticMetaObject;
}

void *Tom::qt_metacast(const char *_clname)
{
if (!_clname) return nullptr;
if (!strcmp(_clname, qt_meta_stringdata_Tom.stringdata0))
return static_cast<void*>(this);
return QObject::qt_metacast(_clname);
}

int Tom::qt_metacall(QMetaObject::Call _c, int _id, void **_a)
{
_id = QObject::qt_metacall(_c, _id, _a);
if (_id < 0)
return _id;
if (_c == QMetaObject::InvokeMetaMethod) {
if (_id < 1)
qt_static_metacall(this, _c, _id, _a);
_id -= 1;
} else if (_c == QMetaObject::RegisterMethodArgumentMetaType) {
if (_id < 1)
*reinterpret_cast<int*>(_a[0]) = -1;
_id -= 1;
}
return _id;
}

// SIGNAL 0
void Tom::miao()
{
QMetaObject::activate(this, &staticMetaObject, 0, nullptr);
}
QT_WARNING_POP
QT_END_MOC_NAMESPACE

可以大致看出,生成的cpp文件中,就是变量staticMetaObject以及 那几个函数的实现。

staticMetaObject是一个结构体,用来存储Tom这个类的信号、槽等元信息,并把

qt_static_metacall静态函数作为函数指针存储起来。

因为是静态成员,所以实例化多少个Tom对象,它们的元信息都是一样的。

qt_static_metacall函数提供了两种“元调用的实现”:

如果是InvokeMetaMethod类型的调用,则直接 把参数中的QObject对象,

转换成Tom类然后调用其miao函数

如果是IndexOfMethod类型的调用,即获取元函数的索引号,则计算miao函数的偏移并返回。

而moc_Tom.cpp末尾的

1
2
3
4
5
// SIGNAL 0
void Tom::miao()
{
QMetaObject::activate(this, &staticMetaObject, 0, nullptr);
}

就是信号函数的实现。

信号的触发

miao信号的实现,直接调用了QMetaObject::activate函数。其中0代表miao这个函数的索引号。

QMetaObject::activate函数的实现,在Qt源码的QObject.cpp文件中,略微复杂一些,

且不同版本的Qt,实现差异都比较大,这里总结一下大致的实现:

先找出与当前信号连接的所有对象-槽函数,再逐个处理:

这里处理的方式,分为三种:

1
2
3
4
5
6
7
8
9
if((c->connectionType == Qt::AutoConnection && !receiverInSameThread)
|| (c->connectionType == Qt::QueuedConnection)) {
// 队列处理
} else if (c->connectionType == Qt::BlockingQueuedConnection) {
// 阻塞处理
// 如果同线程,打印潜在死锁。
} else {
//直接调用槽函数或回调函数
}

receiverInSameThread表示当前线程id和接收信号的对象的所在线程id是否相等。

如果信号-槽连接方式为QueuedConnection,不论是否在同一个线程,按队列处理。

如果信号-槽连接方式为Auto,且不在同一个线程,也按队列处理。

如果信号-槽连接方式为阻塞队列BlockingQueuedConnection,按阻塞处理。

(注意同一个线程就不要按阻塞队列调用了。因为同一个线程,同时只能做一件事,

本身就是阻塞的,直接调用就好了,如果走阻塞队列,则多了加锁的过程。如果槽中又发了

同样的信号,就会出现死锁:加锁之后还未解锁,又来申请加锁。)

队列处理,就是把槽函数的调用,转化成了QMetaCallEvent事件,通过QCoreApplication::postEvent

放进了事件循环, 等到下一次事件分发,相应的线程才会去调用槽函数。

关于事件循环,可以参考之前的文章《Qt实用技能3-理解事件循环》

槽和moc生成

slot函数我们自己实现了,moc不会做额外的处理,所以自动生成的moc_Jerry.cpp文件中,

只有Q_OBJECT宏的展开,和前面的moc_Tom.cpp是一致的,不赘述了。

第三方信号槽实现

信号-槽是非常优秀的通信机制,但Qt的moc实现方式,被一些人诟病,所以他们造了新的轮子,比如:

https://woboq.com/blog/verdigris-qt-without-moc.html

http://sigslot.sourceforge.net/

https://github.com/NoAvailableAlias/nano-signal-slot

https://github.com/pbhogan/Signals

玩转Qt(6)-认清信号槽的本质

作者 JaredTao
2019年7月23日 18:44

简介

这次讨论Qt信号-槽相关的知识点。

信号-槽是Qt框架中最核心的机制,也是每个Qt开发者必须掌握的技能。

网络上有很多介绍信号-槽的文章,也可以参考。

涛哥的专栏是《Qt进阶之路》,如果连信号-槽的文章都没有,将是没有灵魂的。

所以这次涛哥就由浅到深地说一说信号-槽。

猫和老鼠的故事

如果一上来就讲一大堆概念和定义,读者很容易读睡着。所以涛哥从一个故事/场景开始说起。

涛哥小时候喜欢看动画片《猫和老鼠》, 里面有汤姆猫(Tom)和杰瑞鼠(Jerry)斗智斗勇的故事。。。

预览

现在做个简单的设定:Tom有个技能叫”喵”,就是发出猫叫,而正在偷吃东西的Jerry,听见猫叫声就会逃跑。

预览

预览

我们尝试用C++面向对象的思想,描述这个设定。

先是定义Tom和Jerry两种对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//Tom的定义

class Tom
{
public:
//猫叫
void Miaow()
{
cout << "喵!" << endl;
}
//省略其它
...
};
//Jerry的定义
class Jerry
{
public:
//逃跑
void RunAway()
{
cout << "那只猫又来了,快溜!" << endl;
}
//省略其它
...
};

接下来模拟场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int main(int argc, char *argv[])
{
//实例化tom
Tom tom;

//实例化jerry
Jerry jerry;

//tom发出叫声
tom.Miaow();

//jerry逃跑
jerry.RunAway();

return 0;
}

这个场景看起来很简单,tom发出叫声之后手动调用了jerry的逃跑。

我们再看几种稍微复杂的场景:

场景一:

假如jerry逃跑后过段时间,又回来偷吃东西。Tom再次发出叫声,jerry再次逃跑。。。

这个场景要重复几十次。我们能否实现,只要tom的Miaow被调用了,jerry的RunAway就自动被调用,而不是每次都手动调用?

场景二:

假如jerry是藏在“厨房的柜子里的米袋子后面”,无法直接发现它(不能直接获取到jerry对象,并调用它的函数)。

这种情况下,该怎么建立 “猫叫-老鼠逃跑” 的模型?

场景三:

假如有多只jerry,一只tom发出叫声时,所有jerry都逃跑。这种模型该怎么建立?

假如有多只tom,任意一只发出叫声时,所有jerry都逃跑。这种模型又该怎么建立?

场景四:

假如不知道猫的确切品种或者名字,也不知道老鼠的品种或者名字,只要 猫 这种动物发出叫声,老鼠 这种动物就要逃跑。

这样的模型又该如何建立?

还有很多场景,就不赘述了。

对象之间的通信机制

这里概括一下要实现的功能:

要提供一种对象之间的通信机制。这种机制,要能够给两个不同对象中的函数建立映射关系,前者被调用时后者也能被自动调用。

再深入一些,两个对象都互相不知道对方的存在,仍然可以建立联系。甚至一对一的映射可以扩展到多对多,具体对象之间的映射可以扩展到抽象概念之间。

尝试一:直接调用

应该会有人说, Miaow()的函数中直接调用RunAway()不就行了?

明显场景二就把这种方案pass掉了。

直接调用的问题是,猫要知道老鼠有个函数/接口叫逃跑,然后主动调用了它。

这就好比Tom叫了一声,然后Tom主动拧着Jerry的腿让它跑。这样是不合理的。(Jerry表示一脸懵逼!)

真实的逻辑是,猫的叫声在空气/介质中传播,传到了老鼠的耳朵里,老鼠就逃跑了。猫和老鼠互相都没看见呢。

尝试二:回调函数+映射表

似乎是可行的。

稍微思考一下,我们要做这两件事情:

1 把RunAway函数取出来存储在某个地方

2 建立Miaow函数和RunAway的映射关系,能够在前者被调用时,自动调用后者。

RunAway函数可以用 函数指针|成员函数指针 或者C++11-function 来存储,都可以称作 “回调函数”。

(下面的代码以C++11 function的写法为主,函数指针的写法稍微复杂一些,本质一样)

我们先用一个简单的Map来存储映射关系, 就用一个字符串作为映射关系的名字

1
std::map<std::string, std::function<void()>> callbackMap;

我们还要实现 “建立映射关系” 和 “调用”功能,所以这里封装一个Connections类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Connections 
{
public:
//按名称“建立映射关系”
void connect(const std::string &name, const std::function<void()> &callback)
{
m_callbackMap[name] = callback;
}
//按名称“调用”
void invok(const std::string &name)
{
auto it = m_callbackMap.find(name);
//迭代器判断
if (it != m_callbackMap.end()) {
//迭代器有效的情况,直接调用
it->second();
}
}
private:
std::map<std::string, std::function<void()>> m_callbackMap;
};

那么这个映射关系存储在哪里呢? 显然是一个Tom和Jerry共有的”上下文环境”中。

我们用一个全局变量来表示,这样就可以简单地模拟了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
//全局共享的Connections。
static Connections s_connections;

//Tom的定义
class Tom
{
public:
//猫叫
void Miaow()
{
cout << "喵!" << endl;
//调用一下名字为mouse的回调
s_connections.invok("mouse");
}
//省略其它
...
};
//Jerry的定义
class Jerry
{
public:
Jerry()
{
//构造函数中,建立映射关系。std::bind属于基本用法。
s_connections.connect("mouse", std::bind(&Jerry::RunAway, this));
}
//逃跑
void RunAway()
{
cout << "那只猫又来了,快溜!" << endl;
}
//省略其它
...
};
int main(int argc, char *argv[])
{
//模拟嵌套层级很深的场景,外部不能直接访问到tom
struct A {
struct B {
struct C {
private:
//Tom在很深的结构中
Tom tom;
public:
void MiaoMiaoMiao()
{
tom.Miaow();
}
}c;
void MiaoMiao()
{
c.MiaoMiaoMiao();
}
}b;
void Miao()
{
b.MiaoMiao();
}
}a;
//模拟嵌套层级很深的场景,外部不能直接访问到jerry
struct D {
struct E {
struct F {
private:
//jerry在很深的结构中
Jerry jerry;
}f;
}e;
}d;

//A间接调用tom的MiaoW,发出猫叫声
a.Miao();

return 0;
}

看一下运行结果:

预览

RunAway没有被直接调用,而是被自动触发。

分析:这里是以”mouse”这个字符串作为连接tom和jerry的关键。这只是一种简单、粗糙的示例实现。

观察者模式

在GOF四人帮的书籍《设计模式》中,有一种观察者模式,可以比较优雅地实现同样的功能。

(顺便说一下,GOF总结的设计模式一共有23种,涛哥曾经用C++11实现了全套的,github地址是:https://github.com/jaredtao/DesignPattern)

初级的观察者模式,涛哥就不重复了。这里涛哥用C++11搭配一点模板技巧,实现一个更加通用的观察者模式。

也可以叫发布-订阅模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//Subject.hpp
#pragma once
#include <vector>
#include <algorithm>

//Subject 事件或消息的主体。模板参数为观察者类型
template<typename ObserverType>
class Subject {
public:
//订阅
void subscibe(ObserverType *obs)
{
auto itor = std::find(m_observerList.begin(), m_observerList.end(), obs);
if (m_observerList.end() == itor) {
m_observerList.push_back(obs);
}
}
//取消订阅
void unSubscibe(ObserverType *obs)
{
m_observerList.erase(std::remove(m_observerList.begin(), m_observerList.end(), obs));
}
//发布。这里的模板参数为函数类型。
template <typename FuncType>
void publish(FuncType func)
{
for (auto obs: m_observerList)
{
//调用回调函数,将obs作为第一个参数传递
func(obs);
}
}
private:
std::vector<ObserverType *> m_observerList;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
//main.cpp
#include "Subject.hpp"
#include <functional>
#include <iostream>

using std::cout;
using std::endl;

//CatObserver 接口 猫的观察者
class CatObserver {
public:
//猫叫事件
virtual void onMiaow() = 0;
public:
virtual ~CatObserver() {}
};

//Tom 继承于Subject模板类,模板参数为CatObserver。这样Tom就拥有了订阅、发布的功能。
class Tom : public Subject<CatObserver>
{
public:
void miaoW()
{
cout << "喵!" << endl;
//发布"猫叫"。
//这里取CatObserver类的成员函数指针onMiaow。而成员函数指针调用时,要传递一个对象的this指针才行的。
//所以用std::bind 和 std::placeholders::_1将第一个参数 绑定为 函数被调用时的第一个参数,也就是前面Subject::publish中的obs
publish(std::bind(&CatObserver::onMiaow, std::placeholders::_1));
}
};
//Jerry 继承于 CatObserver
class Jerry: public CatObserver
{
public:
//重写“猫叫事件”
void onMiaow() override
{
//发生 “猫叫”时 调用 逃跑
RunAway();
}
void RunAway()
{
cout << "那只猫又来了,快溜!" << endl;
}
};
int main(int argc, char *argv[])
{
Tom tom;
Jerry jerry;

//拿jerry去订阅Tom的 猫叫事件
tom.subscibe(&jerry);

tom.miaoW();
return 0;
}

任意类只要继承Subject模板类,提供观察者参数,就拥有了发布-订阅功能。

Qt的信号-槽

信号-槽简介

信号-槽 是Qt自定义的一种通信机制,它不同于标准C/C++ 语言。

信号-槽的使用方法,是在普通的函数声明之前,加上signal、slot标记,然后通过connect函数把信号与槽 连接起来。

后续只要调用 信号函数,就可以触发连接好的信号或槽函数。

预览

连接的时候,前面的是发送者,后面的是接收者。信号与信号也可以连接,这种情况把接收者信号看做槽即可。

信号-槽分两种

信号-槽要分成两种来看待,一种是同一个线程内的信号-槽,另一种是跨线程的信号-槽。

同一个线程内的信号-槽,就相当于函数调用,和前面的观察者模式相似,只不过信号-槽稍微有些性能损耗(这个后面细说)。

跨线程的信号-槽,在信号触发时,发送者线程将槽函数的调用转化成了一次“调用事件”,放入事件循环中。

接收者线程执行到下一次事件处理时,处理“调用事件”,调用相应的函数。

(关于事件循环,可以参考专栏上一篇文章《Qt实用技能3-理解事件循环》)

信号-槽的实现 元对象编译器moc

信号-槽的实现,借助一个工具:元对象编译器MOC(Meta Object Compiler)。

这个工具被集成在了Qt的编译工具链qmake中,在开始编译Qt工程时,会先去执行MOC,从代码中

解析signals、slot、emit等等这些标准C/C++不存在的关键字,以及处理Q_OBJECT、Q_PROPERTY、

Q_INVOKABLE等相关的宏,生成一个moc_xxx.cpp的C++文件。(使用黑魔法来变现语法糖)

比如信号函数只要声明、不需要自己写实现,就是在这个moc_xxx.cpp文件中,自动生成的。

MOC之后就是常规的C/C++编译、链接流程了。

moc的本质-反射

MOC的本质,其实是一个反射器。标准C++没有反射功能(将来会有),所以Qt用moc实现了反射功能。

什么叫反射呢? 简单来说,就是运行过程中,获取对象的构造函数、成员函数、成员变量。

举个例子来说明,有下面这样一个类声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Tom {
public:
Tom() {}
const std::string & getName() const
{
return m_name;
}
void setName(const std::string &name)
{
m_name = name;
}
private:
std::string m_name;
};

类的使用者,看不到类的声明,头文件都拿不到,不能直接调用类的构造函数、成员函数。

从配置文件/网络拿到了一段字符串“Tom”,就要创建一个Tom类的对象实例。

然后又拿到一段“setName”的字符串,就要去调用Tom的setName函数。

面对这种需求,就需要把Tom类的构造函数、成员函数等信息存储起来,还要能够被调用到。

这些信息就是 “元信息”,使用者通过“元信息”就可以“使用这个类”。这便是反射了。

设计模式中的“工厂模式”,就是一个典型的反射案例。不过工厂模式只解决了构造函数的调用,没有成员函数、成员变量等信息。

反射包括 编译期静态反射 和 运行期动态反射。。。

文章有点长了,这次先到这里,剩下的下次再讨论。

参考文献

[1] Qt帮助文档, 搜索关键词 Signals & Slots
[2] IBM文档库 https://www.ibm.com/developerworks/cn/linux/guitoolkit/qt/signal-slot/index.html

玩转Qml(16)-移植ShaderToy

作者 JaredTao
2019年7月4日 13:44

简介

这次涛哥将会教大家移植ShaderToy的特效到Qml

源码

《玩转Qml》系列文章,配套了一个优秀的开源项目:TaoQuick

github https://github.com/jaredtao/TaoQuick

访问不了或者速度太慢,可以用国内的镜像网站gitee

https://gitee.com/jaredtao/TaoQuick

效果预览

先看几个效果图

穿云洞

星球之光

蜗牛

超级马里奥

gif录制质量较低,可编译运行TaoQuick源码或使用涛哥打包好的可执行程序,查看实际运行效果。

可执行程序下载链接(包括windows 和 MacOS平台) https://github.com/jaredtao/TaoQuick/releases

关于ShaderToy

学习过计算机图形学的人,都应该知道大名鼎鼎的ShaderToy网站

用一些Shader代码和简单的纹理,就可以输出各种酷炫的图形效果和音频效果。

如果你还不知道,赶紧去看看吧https://www.shadertoy.com

顺便提一下,该网站的作者是IQ大神,这里有他的博客:

http://www.iquilezles.org/www/articles/raymarchingdf/raymarchingdf.htm

本文主要讨论图形效果,音频效果以后再实现。

关于ShaderEffect

Qml中实现ShaderToy,最快的途径就是ShaderEffect了。

上一篇文章《Qml特效-着色器效果ShaderEffect》已经介绍过ShaderEffect了, 本文重点是移植ShaderToy。

在涛哥写这篇文章之前,已经有两位前辈做过相关的研究。

陈锦明: https://zhuanlan.zhihu.com/p/38942460

qyvlik: https://zhuanlan.zhihu.com/p/44417680

涛哥参考了他们的实现,做了一些改进、完善。

在此感谢两位前辈。

下面正文开始

ShaderToy原理

OpenGL的可编程渲染管线中,着色器代码是可以动态编译、加载到GPU运行的。

而OpenGL又包括了桌面版(OpenGL Desktop)、嵌入式版(OpenGL ES)以及网页版(WebGL)

ShaderToy网站是以WebGL 2.0为基础,提供内置函数、变量,并约定了一些输入变量,由用户按照约定编写着色器代码。

只要不是太老的OpenGL版本,内置函数、变量基本都是通用的。

约定的变量

ShaderToy网站约定的变量如下:

1
2
3
4
5
6
7
8
9
10
11
vec3        iResolution             image/buffer        The viewport resolution (z is pixel aspect ratio, usually 1.0)
float iTime image/sound/bufferCurrent time in seconds
float iTimeDelta image/buffer Time it takes to render a frame, in seconds
int iFrame image/buffer Current frame
float iFrameRate image/buffer Number of frames rendered per second
float iChannelTime[4] image/buffer Time for channel (if video or sound), in seconds
vec3 iChannelResolution[4]image/buffer/soundInput texture resolution for each channel
vec4 iMouse image/buffer xy = current pixel coords (if LMB is down). zw = click pixel
sampler2DiChannel{i} image/buffer/soundSampler for input textures i
vec4 iDate image/buffer/soundYear, month, day, time in seconds in .xyzw
float iSampleRate image/buffer/soundThe sound sample rate (typically 44100)

Qml中的相应实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
ShaderEffect {
id: shader

//properties for shader

//not pass to shader
readonly property vector3d defaultResolution: Qt.vector3d(shader.width, shader.height, shader.width / shader.height)
function calcResolution(channel) {
if (channel) {
return Qt.vector3d(channel.width, channel.height, channel.width / channel.height);
} else {
return defaultResolution;
}
}
//pass
readonly property vector3d iResolution: defaultResolution
property real iTime: 0
property real iTimeDelta: 100
property int iFrame: 10
property real iFrameRate
property vector4d iMouse;
property var iChannel0; //only Image or ShaderEffectSource
property var iChannel1; //only Image or ShaderEffectSource
property var iChannel2; //only Image or ShaderEffectSource
property var iChannel3; //only Image or ShaderEffectSource
property var iChannelTime: [0, 1, 2, 3]
property var iChannelResolution: [calcResolution(iChannel0), calcResolution(iChannel1), calcResolution(iChannel2), calcResolution(iChannel3)]
property vector4d iDate;
property real iSampleRate: 44100

...

}

其中时间、日期通过Timer刷新,鼠标位置用MouseArea刷新。

同时涛哥导出了hoverEnabled、running属性和restart函数,以方便Qml中控制Shader的运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
ShaderEffect {
id: shader
...
//properties for Qml controller
property alias hoverEnabled: mouse.hoverEnabled
property bool running: true
function restart() {
shader.iTime = 0
running = true
timer1.restart()
}

Timer {
id: timer1
running: shader.running
triggeredOnStart: true
interval: 16
repeat: true
onTriggered: {
shader.iTime += 0.016;
}
}
Timer {
running: shader.running
interval: 1000
onTriggered: {
var date = new Date();
shader.iDate.x = date.getFullYear();
shader.iDate.y = date.getMonth();
shader.iDate.z = date.getDay();
shader.iDate.w = date.getSeconds()
}
}
MouseArea {
id: mouse
anchors.fill: parent
onPositionChanged: {
shader.iMouse.x = mouseX
shader.iMouse.y = mouseY
}
onClicked: {
shader.iMouse.z = mouseX
shader.iMouse.w = mouseY
}
}
...
}

glsl版本号

GLSL Versions

OpenGL VersionGLSL Version
2.0110
2.1120
3.0130
3.1140
3.2150
3.3330
4.0400
4.1410
4.2420
4.3430

GLSL ES Versions (Android, iOS, WebGL)

OpenGL ES VersionGLSL ES Version
2.0100
3.0300

glsl版本兼容

ShaderToy限定了WebGL 2.0,而我们移植到Qml中,自然是希望能够在所有可以运行Qml的设备上运行ShaderToy效果。

所以要做一些glsl版本相关的处理。

涛哥研究了Qt的GraphicsEffects模块源码,它的版本处理要么默认,要么 150 core,显然是不够用的。

glsl各个版本的差异,可以参考这里 https://github.com/mattdesl/lwjgl-basics/wiki/glsl-versions

涛哥总结出了如下的代码和注释说明:

注意”#version xxx”必须是着色器的第一行,不能换行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
    
// 如果环境是OpenGL ES2,默认的version是 version 110, 不需要写出来。
// 比ES2更老的版本是ES 1.0 和 ES 1.1, 这种古董设备,建议还是不要玩Shader了吧。
// ES2没有texture函数,要用旧的texture2D代替
// 精度限定要写成float

readonly property string gles2Ver: "
#define texture texture2D
precision mediump float;
"
// 如果环境是OpenGL ES3,version是 version 300 es
// ES 3.1 ES 3.2也可以。
// ES3可以用in out 关键字,gl_FragColor也可以用out fragColor取代
// 精度限定要写成float

readonly property string gles3Ver: "#version 300 es
#define varying in
#define gl_FragColor fragColor
precision mediump float;

out vec4 fragColor;
"
// 如果环境是OpenGL Desktop 3.x,version这里参考Qt默认的version 150。大部分Desktop设备应该
// 都是150, 即3.2版本,第一个区分Core和Compatibility的版本。
// Core是核心模式,只有核心api以减轻负担。相应的Compatibility是兼容模式,保留全部API以兼容低版本。
// Desktop 3.x 可以用in out 关键字,gl_FragColor也可以用out fragColor取代
// 精度限定抹掉,用默认的。不抹掉有些情况下会报错,不能通用。
readonly property string gl3Ver: "#version 150
#define varying in
#define gl_FragColor fragColor
#define lowp
#define mediump
#define highp

out vec4 fragColor;
"
// 如果环境是OpenGL Desktop 2.x,version这里就用2.0的version 110,即2.0版本
// 2.x 没有texture函数,要用旧的texture2D代替
readonly property string gl2Ver: "#version 110
#define texture texture2D
"
property string versionString: {
if (Qt.platform.os === "android") {
if (GraphicsInfo.majorVersion === 3) {
console.log("android gles 3")
return gles3Ver
} else {
console.log("android gles 2")
return gles2Ver
}
} else {
if (GraphicsInfo.majorVersion === 3 ||GraphicsInfo.majorVersion === 4) {
return gl3Ver
} else {
return gl2Ver
}
}
}
readonly property string forwardString: versionString + "
varying vec2 qt_TexCoord0;
varying vec4 vertex;
uniform lowp float qt_Opacity;

uniform vec3 iResolution;
uniform float iTime;
uniform float iTimeDelta;
uniform int iFrame;
uniform float iFrameRate;
uniform float iChannelTime[4];
uniform vec3 iChannelResolution[4];
uniform vec4 iMouse;
uniform vec4 iDate;
uniform float iSampleRate;
uniform sampler2D iChannel0;
uniform sampler2D iChannel1;
uniform sampler2D iChannel2;
uniform sampler2D iChannel3;
"

versionString 这里,主要测试了Desktop和 android设备,Desktop只要显卡不太搓,都能运行的。

Android ES3的也是全部支持,ES2的部分不能运行,比如iq大神的蜗牛Shader,使用了textureLod等一系列内置函数,就不能在ES2上面跑。

ShaderToy适配

本来是不需要写顶点着色器的。如果我们想把ShaderToy做成一个任意坐标开始的Item来用,就需要适配一下坐标。

涛哥写的顶点着色器如下,仅在默认着色器的基础上,传递qt_Vertex给下一阶段的vertex

1
2
3
4
5
6
7
8
9
10
11
12

vertexShader: "
uniform mat4 qt_Matrix;
attribute vec4 qt_Vertex;
attribute vec2 qt_MultiTexCoord0;
varying vec2 qt_TexCoord0;
varying vec4 vertex;
void main() {
vertex = qt_Vertex;
gl_Position = qt_Matrix * vertex;
qt_TexCoord0 = qt_MultiTexCoord0;
}"

片段着色器这里处理一下,适配出一个符合shaderToy的mainImage作为入口函数

1
2
3
4
5
6
7
8
9
10
11
12
readonly property string startCode: "
void main(void)
{
mainImage(gl_FragColor, vec2(vertex.x, iResolution.y - vertex.y));
}"
readonly property string defaultPixelShader: "
void mainImage(out vec4 fragColor, in vec2 fragCoord)
{
fragColor = vec4(fragCoord, fragCoord.x, fragCoord.y);
}"
property string pixelShader: ""
fragmentShader: forwardString + (pixelShader ? pixelShader : defaultPixelShader) + startCode

稍微说明一下,qyvlik大佬的Shader使用gl_FragCoord作为片段坐标传进去了,这种用法的ShaderToy坐标将会占据整个Qml的窗口,

而实际ShaderToy坐标不是整个窗口的时候,超出去的地方就会被切掉,显示出来的只有一小部分。

涛哥研究了一番后,顶点着色器把vertex传过来,vertex.x就是x坐标,vertex.y坐标从上到下是0 - height,而gl_FragCoord 从下到上是0 - height,

所以要翻一下。

TaoShaderToy

最后,看一下代码的全貌吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
//TaoShaderToy.qml
import QtQuick 2.12
import QtQuick.Controls 2.12
/*
vec3 iResolution image/buffer The viewport resolution (z is pixel aspect ratio, usually 1.0)
float iTime image/sound/bufferCurrent time in seconds
float iTimeDelta image/buffer Time it takes to render a frame, in seconds
int iFrame image/buffer Current frame
float iFrameRate image/buffer Number of frames rendered per second
float iChannelTime[4] image/buffer Time for channel (if video or sound), in seconds
vec3 iChannelResolution[4]image/buffer/soundInput texture resolution for each channel
vec4 iMouse image/buffer xy = current pixel coords (if LMB is down). zw = click pixel
sampler2DiChannel{i} image/buffer/soundSampler for input textures i
vec4 iDate image/buffer/soundYear, month, day, time in seconds in .xyzw
float iSampleRate image/buffer/soundThe sound sample rate (typically 44100)
*/
ShaderEffect {
id: shader

//properties for shader

//not pass to shader
readonly property vector3d defaultResolution: Qt.vector3d(shader.width, shader.height, shader.width / shader.height)
function calcResolution(channel) {
if (channel) {
return Qt.vector3d(channel.width, channel.height, channel.width / channel.height);
} else {
return defaultResolution;
}
}
//pass
readonly property vector3d iResolution: defaultResolution
property real iTime: 0
property real iTimeDelta: 100
property int iFrame: 10
property real iFrameRate
property vector4d iMouse;
property var iChannel0; //only Image or ShaderEffectSource
property var iChannel1; //only Image or ShaderEffectSource
property var iChannel2; //only Image or ShaderEffectSource
property var iChannel3; //only Image or ShaderEffectSource
property var iChannelTime: [0, 1, 2, 3]
property var iChannelResolution: [calcResolution(iChannel0), calcResolution(iChannel1), calcResolution(iChannel2), calcResolution(iChannel3)]
property vector4d iDate;
property real iSampleRate: 44100

//properties for Qml controller
property alias hoverEnabled: mouse.hoverEnabled
property bool running: true
function restart() {
shader.iTime = 0
running = true
timer1.restart()
}
Timer {
id: timer1
running: shader.running
triggeredOnStart: true
interval: 16
repeat: true
onTriggered: {
shader.iTime += 0.016;
}
}
Timer {
running: shader.running
interval: 1000
onTriggered: {
var date = new Date();
shader.iDate.x = date.getFullYear();
shader.iDate.y = date.getMonth();
shader.iDate.z = date.getDay();
shader.iDate.w = date.getSeconds()
}
}
MouseArea {
id: mouse
anchors.fill: parent
onPositionChanged: {
shader.iMouse.x = mouseX
shader.iMouse.y = mouseY
}
onClicked: {
shader.iMouse.z = mouseX
shader.iMouse.w = mouseY
}
}
// 如果环境是OpenGL ES2,默认的version是 version 110, 不需要写出来。
// 比ES2更老的版本是ES 1.0 和 ES 1.1, 这种古董设备,还是不要玩Shader了吧。
// ES2没有texture函数,要用旧的texture2D代替
// 精度限定要写成float
readonly property string gles2Ver: "
#define texture texture2D
precision mediump float;
"
// 如果环境是OpenGL ES3,version是 version 300 es
// ES 3.1 ES 3.2也可以。
// ES3可以用in out 关键字,gl_FragColor也可以用out fragColor取代
// 精度限定要写成float
readonly property string gles3Ver: "#version 300 es
#define varying in
#define gl_FragColor fragColor
precision mediump float;

out vec4 fragColor;
"
// 如果环境是OpenGL Desktop 3.x,version这里参考Qt默认的version 150。大部分Desktop设备应该都是150
// 150 即3.2版本,第一个区分Core和Compatibility的版本。Core是核心模式,只有核心api以减轻负担。相应的Compatibility是兼容模式,保留全部API以兼容低版本。
// 可以用in out 关键字,gl_FragColor也可以用out fragColor取代
// 精度限定抹掉,用默认的。不抹掉有些情况下会报错,不能通用。
readonly property string gl3Ver: "#version 150
#define varying in
#define gl_FragColor fragColor
#define lowp
#define mediump
#define highp

out vec4 fragColor;
"
// 如果环境是OpenGL Desktop 2.x,version这里就用2.0的version 110,即2.0版本
// 2.x 没有texture函数,要用旧的texture2D代替
readonly property string gl2Ver: "#version 110
#define texture texture2D
"

property string versionString: {
if (Qt.platform.os === "android") {
if (GraphicsInfo.majorVersion === 3) {
console.log("android gles 3")
return gles3Ver
} else {
console.log("android gles 2")
return gles2Ver
}
} else {
if (GraphicsInfo.majorVersion === 3 ||GraphicsInfo.majorVersion === 4) {
return gl3Ver
} else {
return gl2Ver
}
}
}

vertexShader: "
uniform mat4 qt_Matrix;
attribute vec4 qt_Vertex;
attribute vec2 qt_MultiTexCoord0;
varying vec2 qt_TexCoord0;
varying vec4 vertex;
void main() {
vertex = qt_Vertex;
gl_Position = qt_Matrix * vertex;
qt_TexCoord0 = qt_MultiTexCoord0;
}"
readonly property string forwardString: versionString + "
varying vec2 qt_TexCoord0;
varying vec4 vertex;
uniform lowp float qt_Opacity;

uniform vec3 iResolution;
uniform float iTime;
uniform float iTimeDelta;
uniform int iFrame;
uniform float iFrameRate;
uniform float iChannelTime[4];
uniform vec3 iChannelResolution[4];
uniform vec4 iMouse;
uniform vec4 iDate;
uniform float iSampleRate;
uniform sampler2D iChannel0;
uniform sampler2D iChannel1;
uniform sampler2D iChannel2;
uniform sampler2D iChannel3;
"
readonly property string startCode: "
void main(void)
{
mainImage(gl_FragColor, vec2(vertex.x, iResolution.y - vertex.y));
}"
readonly property string defaultPixelShader: "
void mainImage(out vec4 fragColor, in vec2 fragCoord)
{
fragColor = vec4(fragCoord, fragCoord.x, fragCoord.y);
}"
property string pixelShader: ""
fragmentShader: forwardString + (pixelShader ? pixelShader : defaultPixelShader) + startCode
}

玩转Qml(15)-着色器效果ShaderEffect

作者 JaredTao
2019年6月22日 13:44

简介

这次涛哥将会教大家一些ShaderEffect(参考QmlBook,译作:着色器效果)的相关知识。

前面的文章,给大家展示了进场动画,以及页面切换动画,大部分都使用了ShaderEffect,所以这次专门来说一下ShaderEffect。

源码

《玩转Qml》系列文章,配套了一个优秀的开源项目:TaoQuick

github https://github.com/jaredtao/TaoQuick

访问不了或者速度太慢,可以用国内的镜像网站gitee

https://gitee.com/jaredtao/TaoQuick

ShaderEffect

动画只能控制组件的属性整体的变化,做特效需要精确到像素。

Qml中提供了ShaderEffect这个组件,就能实现像素级别的操作。

ShaderEffect允许我们在Qml的渲染引擎SceneGraph上,利用强大的GPU进行渲染。

使用ShaderEffect,需要有一些图形学知识,了解GPU渲染管线,了解图形API如OpenGL、DirectX等,同时也需要一些数学知识。

图形学的知识体系还是非常庞大的,要系统的学习,需要看很多书籍。入门级的比如“红宝书”《OpenGL编程指南》、“蓝宝书”《OpenGL超级宝典》……

一篇文章是说不完的,涛哥水平也有限。所以本文从实用的角度出发,按照涛哥自己的理解,提炼一些必要的知识点,省略一些无关的细节,

让各位Qt开发者能了解GPU原理,能看懂、甚至于自己写一些简单的着色器代码,就大功告成了。说的不对的地方,也欢迎大佬来指点。

显示器如何显示色彩

先来了解一下,显示器是如何显示出各种色彩的。

假如我们把显示器的画面放大100倍,就会看到很多整齐排列的像素点。

继续放大,就会发现每个像素点,由三种发光的元件组成,这三种元件分别发出红、绿、蓝三种颜色的光。三种颜色的光组合在一起,

就是人眼看到的颜色。这就是著名的RGB颜色模型。

如果把这三种光的亮度分为255个等级,就能组合出16777216种不同颜色的光。

GPU的任务,就是通过计算,给出每一个像素的红、绿、蓝 (简称r g b)三种颜色的数值,让显示器去”发出相应的光”。

(这样说可能不太严谨、不太专业,只是方便大家理解。另一方面,本文的目的,

是让大家学习如何写特效,不是去造显卡/造显示器。所以请专业人士见谅!)

注:参考[1]

GPU渲染流程

我们以画一个填充色的三角形为例,来说明

渲染管线图

下图是一个简易的渲染管线,引用自 LearnOpenGL

画一个三角形,要经历顶点着色器、图元装配、几何着色器、片段着色器、光栅化等阶段。

其中蓝色部分是可以自定义的,自定义是指,按照图形API规范,写一段GPU能编译、运行的代码。

(这种代码就是着色器代码。可以自定义的这种渲染管线,就是可编程渲染管线,与之相对的是古老的固定渲染管线。)

这里各个阶段,分别引用一下,LearnOpenGL中的介绍(看不懂可以先跳过,看我画的图):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
1 管线的第一个部分是顶点着色器(Vertex Shader),它把一个单独的顶点作为输入。顶点着色器主要的目的是

3D坐标转为另一种3D坐标(后面会解释),同时顶点着色器允许我们对顶点属性进行一些基本处理。

2 图元装配(Primitive Assembly)阶段将顶点着色器输出的所有顶点作为输入(如果是GL_POINTS,那么就是一个顶点),

并所有的点装配成指定图元的形状;本节例子中是一个三角形。

3 图元装配阶段的输出会传递给几何着色器(Geometry Shader)。几何着色器把图元形式的一系列顶点的集合作为输入,

它可以通过产生新顶点构造出新的(或是其它的)图元来生成其他形状。例子中,它生成了另一个三角形。

4 几何着色器的输出会被传入光栅化阶段(Rasterization Stage),这里它会把图元映射为最终屏幕上相应的像素,

生成供片段着色器(Fragment Shader)使用的片段(Fragment)。在片段着色器运行之前会执行裁切(Clipping)。

裁切会丢弃超出你的视图以外的所有像素,用来提升执行效率。

5 片段着色器的主要目的是计算一个像素的最终颜色,这也是所有OpenGL高级效果产生的地方。通常,片段着色器包

3D场景的数据(比如光照、阴影、光的颜色等等),这些数据可以被用来计算最终像素的颜色。

并行管线示意图

概念还是挺多的,而且很多教程都有渲染管线图。但是涛哥觉得,对于我们开发Shader来说,一定要有并行的意识,然而大部分

管线图,都没有体现出GPU的并行特性。所以涛哥自己画了一个草图:

解释一下吧,CPU传入了3个顶点到GPU,GPU将这三个顶点,传递给三个顶点着色器。

这里要意识到,顶点着色器开始,就是并行处理了。GPU是很强大的SIMD架构(单指令流多数据流)。

如果我们自定义了一段顶点着色器代码,则三个顶点会同时运行这段代码。(后面的片段着色器代码,就是N个点同时运行)

顶点着色器进行处理,传递给图元装配。

图元装配阶段,进行了顶点扩充,变成N个点,N看作三角形面积所在的点。

之后N个点依次传给 几何着色器->光栅化->片段着色器,最后经过测试与混合后,输出到屏幕。

可以自定义编程的,有顶点着色器、几何着色器、片段着色器(有的地方也叫像素着色器),顺带提一下,还有另外三种:

曲面控制着色器、曲面评估着色器 和 计算着色器。

一般我们的关注点,都会在片段着色器上。涛哥之前写的12种特效,就只用了自定义的片段着色器。

著名的ShaderToy网站,也是只关注片段着色器。ShaderToy

着色器语言编码规范

我们可以把着色器语言,当作运行在GPU上的C语言。

Qt的ShaderEffect支持的着色器语言包括OpenGL规范中的GLSL,和DirectX规范中的HLSL,这两种着色语法上有些细微的区别,但是可以互相转换。

我们就以glsl为主。详细的语言规范,在khronos的官网, 各个版本都有: https://www.khronos.org/registry/OpenGL/specs/gl/

桌面版 OpenGL 版本众多,而嵌入式系统也有专用的OpenGL ES。

安卓手机、平板设备一般就是OpenGL ES,新的设备都支持ES 3.0,老的设备一般只支持到ES 2.0

OpenGL ES 的语言规范文档在这里: https://www.khronos.org/registry/OpenGL/specs/es/2.0/

我们就用Qt默认的版本。

着色器代码示例

示例

这里用Qt帮助文档中的示例代码,来说明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import QtQuick 2.0

Rectangle {
width: 200; height: 100
Row {
Image { id: img;
sourceSize { width: 100; height: 100 } source: "qt-logo.png" }
ShaderEffect {
width: 100; height: 100
property variant src: img
vertexShader: "
uniform highp mat4 qt_Matrix;
attribute highp vec4 qt_Vertex;
attribute highp vec2 qt_MultiTexCoord0;
varying highp vec2 coord;
void main() {
coord = qt_MultiTexCoord0;
gl_Position = qt_Matrix * qt_Vertex;
}"
fragmentShader: "
varying highp vec2 coord;
uniform sampler2D src;
uniform lowp float qt_Opacity;
void main() {
lowp vec4 tex = texture2D(src, coord);
gl_FragColor = vec4(vec3(dot(tex.rgb,
vec3(0.344, 0.5, 0.156))),
tex.a) * qt_Opacity;
}"
}
}
}

这段代码的效果是

左边是本来的绿色的Qt的logo,右边是处理过后的灰色logo。

着色器代码

ShaderEffect的vertexShader属性就是顶点着色器了,其内容是一段字符串。按照着色器规范实现的。

同样的,fragmentShader属性 即片段着色器。

我们能在着色器中看到void main函数,这个便是着色器代码的入口函数,和C语言很像。

在main之前,还有一些全局变量,我们逐条来说明一下

在顶点着色器中,有这三种不同用处的变量:uniform、attribute、varying。

这些变量的值都是从CPU传递过来的。

如果你写过原生OpenGL的代码,就会知道,其中很大一部分工作,就是在处理CPU数据传递到GPU着色器中。

而Qml的ShaderEffect简化了这些工作,只要写一个property,名字、类型和着色器中的对应上,就可以了。

顶点着色器

1
attribute highp vec4 qt_Vertex;

attribute是”属性”变量,按照前面涛哥画的管线图来说,三个顶点着色器同时运行时,每个着色器中

的attribute值都不一样。这里的qt_Vertex,可以理解为分别是三角形的三个顶点。

highp是精度限定符,这里先忽略,具体细节可以参考语言规范文档。后面的lowp、 medium也是精度限定符。

vec4就是四维向量,类似QVector4D。

qt_Vertex是变量的名字。

这条语句的作用,就是声明一个用来存储顶点的attribute变量qt_Vertex。

uniform是统一变量,三个顶点着色器同时运行时,它们取得的uniform变量值是一样的。

varying表示这个顶点着色器的输出数据,将传递给后面的渲染管线。

1
2
3
4
5
void main() 
{
coord = qt_MultiTexCoord0;
gl_Position = qt_Matrix * qt_Vertex;
}

这段main函数,将CPU传进来的纹理坐标qt_MultiTexCoord0数据,通过varying变量coord,传递给了下一个阶段,然后使用矩阵进行了坐标转换,

并将结果存储在glsl的内置变量gl_Position中。

片段着色器

片段着色器中,就没有attribute了。uniform是一样的统一变量,varying是上一个阶段传递进来的数据。

1
uniform sampler2D src;

sampler2D是二维纹理。所谓纹理嘛,可以理解成一张图片,一个Image。

src这个变量,就代表外面传进来的那个Image。 sampler2D也可以是任意可视的Item(通过ShaderEffectSource传递进来)

来看一下main函数

1
2
3
4
5
void main() 
{
lowp vec4 tex = texture2D(src, coord);
gl_FragColor = vec4(vec3(dot(tex.rgb,vec3(0.344, 0.5, 0.156))), tex.a) * qt_Opacity;
}

这里使用了纹理

1
lowp vec4 tex = texture2D(src, coord);

texture2D是一个内置函数,专业术语叫“对纹理进行采样”,什么意思呢?

假如coord的值是(0,0),那就是对src指代的这张图片,取x=0、y=0的坐标点的像素,作为返回值,存储在tex变量中。

这里注意一下纹理坐标的取值范围。假如Qml中图片的大小是100x100,其取值范围从(0, 0) -> (100, 100)

这里的传进来的纹理坐标,取值范围是(0, 0) -> (1, 1) ,GPU为了方便计算,都进行了归1化处理。将范围缩小到0 - 1

1
gl_FragColor = vec4(vec3(dot(tex.rgb, vec3(0.344, 0.5, 0.156) )), tex.a) * qt_Opacity;

dot(tex.rgb, vec3(0.344, 0.5, 0.156) ) 是对两个三维向量进行了点乘。

tex.rgb是GLSL中的取值器语法。 tex是一个四维变量,可以用tex.r tex.g tex.b tex.a分别取出其中一维,也可以任意两个组合、三个

组合取值。

rgba可以取值,xyzw也可以取值, stpq也行,但只能三种选一种,不能混用。

vec4(vec3(), tex.a) 是用三维向量再加一个变量,构造四维向量。

这条语句其实是一个RGB转灰度的公式,可以自行搜索相关的资料。

gl_FragColor 是内置变量,表示所在片段着色器的最终的输出颜色。

参考文献

[1] https://zhuanlan.zhihu.com/p/43467096

[2] https://learnopengl-cn.github.io/

玩转Qml(13)-动画特效-飞入

作者 JaredTao
2019年6月8日 12:44

简介

这次涛哥将会教大家一些Qml动画相关的知识。

源码

《玩转Qml》系列文章,配套了一个优秀的开源项目:TaoQuick

github https://github.com/jaredtao/TaoQuick

访问不了或者速度太慢,可以用国内的镜像网站gitee

https://gitee.com/jaredtao/TaoQuick

飞入效果预览

第一篇文章,就放一个简单的动画效果

实现原理

进场动画,使用了QtQuick的动画系统,以及ShaderEffect特效。

Qml中有一个模块QtGraphicalEffects,提供了部分特效,就是使用ShaderEffect实现的。

使用ShaderEffect实现特效,需要有一些OpenGL/DirectX知识,了解GPU渲染管线,同时也需要一些数学知识。

QtQuick动画系统

动画组件

Qt动画系统,在帮助文档有详细的介绍,搜索关键词”Animation”,涛哥在这里说一些重点。

涛哥用思维导图列出了Qml中所有的动画组件:

  • 右边带虚线框的部分比较常用,是做动画必须要掌握的,尤其是属性动画PropertyAnimation和数值动画NumberAinmation。
    常见的各种坐标动画、宽高动画、透明度动画、颜色动画等等,都可以用这些组件来实现。

  • 底下的States、Behavior 和 Traisitions,也是比较常用的和动画相关的组件。可在帮助文档搜索
    关键词”Qt Quick States”、”Behavior”、”Animation and Transitions”。后续的文章,涛哥会专门讲解。

  • 左边的Animator系列,属于Scene Graph渲染层面的优化,其属性Change信号只在最终值时发出,不发出中间值,使用的时候需要注意。

  • 顶上的AnimationController,属于高端玩家,用来控制整个动画的进度。

动画的使用

用例一 直接声明动画

直接声明动画,指定target和property,之后可以在槽函数/js脚本中通过id控制动画的运行。

也可以通过设定loops 和 running属性来控制动画

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Rectangle {
id: flashingblob
width: 75; height: 75
color: "blue"
opacity: 1.0

MouseArea {
anchors.fill: parent
onClicked: {
animateColor.start()
animateOpacity.start()
}
}

PropertyAnimation {id: animateColor; target: flashingblob; properties: "color"; to: "green"; duration: 100}

NumberAnimation {
id: animateOpacity
target: flashingblob
properties: "opacity"
from: 0.99
to: 1.0
loops: Animation.Infinite
easing {type: Easing.OutBack; overshoot: 500}
}
}

用例二 on语法

on语法可以使用动画组件,也可以用Behavior,直接on某个特定的属性即可。效果一样。

on动画中,如果直接指定了running属性,默认就会执行这个动画。

也可以不指定running属性,其它地方修改这个属性时,会自动按照动画来执行。

示例代码 on动画

1
2
3
4
5
6
7
8
9
Rectangle {
width: 100; height: 100; color: "green"
RotationAnimation on rotation {
loops: Animation.Infinite
from: 0
to: 360
running: true
}
}

示例代码 Behavior 动画

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import QtQuick 2.0

Rectangle {
id: rect
width: 100; height: 100
color: "red"

Behavior on width {
NumberAnimation { duration: 1000 }
}

MouseArea {
anchors.fill: parent
onClicked: rect.width = 50
}
}

用例三 Transitions或状态机

过渡动画和状态机动画,本质还是直接使用动画组件。只不过是把动画声明并存储起来,以在状态切换时使用。

这里先不细说了,后面会有系列文章<Qml特效-页面切换动画>,会专门讲解。

ShaderEffect

动画只能控制组件的属性整体的变化,做特效需要精确到像素。

Qml中提供了ShaderEffect这个组件,就能实现像素级别的操作。

大名鼎鼎的ShaderToy网站,就是使用Shader实现各种像素级别的酷炫特效。

ShaderToy

作者iq大神

ShaderToy上面的特效都是可以移植到Qml中的。

使用Shader开发,需要一定的图形学知识。其中使用GLSL需要熟悉OpenGL, 使用HLSL需要熟悉DirectX。

飞入效果源码

封装了一个平移进入的动画组件,能够支持从四个方向进场。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//ASlowEnter.qml
import QtQuick 2.12
import QtQuick.Controls 2.12
import "../.."
Item {
id: r
property int targetX: 0
property int targetY: 0
property alias animation: animation
enum Direct {
FromLeft = 0,
FromRight = 1,
FromTop = 2,
FromBottom = 3
}
property int dir: ASlowEnter.Direct.FromBottom
property int duration: 2000
//额外的距离,组件在父Item之外时,额外移动一点,避免边缘暴露在父Item的边缘
property int extDistance: 10
property var __propList: ["x", "x", "y", "y"]
property var __fromList: [
-r.parent.width - r.width - extDistance,
r.parent.width + r.width + extDistance,
-r.parent.height - r.height - extDistance,
r.parent.height + r.height + extDistance]
property var __toList: [targetX, targetX, targetY, targetY]
NumberAnimation {
id: animation
target: r
property: __propList[dir]
from: __fromList[dir]
to: __toList[dir]
duration: r.duration
loops: 1
alwaysRunToEnd: true
}
}

进场组件的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
//Enter.qml

import QtQuick 2.12
import QtQuick.Controls 2.12
import "../Animation/Enter"
Item {
anchors.fill: parent
ASlowEnter {
id: a1
width: 160
height: 108
x: (parent.width - width) / 2
targetY: parent.height / 2
dir: ASlowEnter.Direct.FromBottom
Image {
anchors.fill: parent
source: "qrc:/EffectImage/Img/baby.jpg"
}
}
ASlowEnter {
id: a2
width: 160
height: 108
x: (parent.width - width) / 2
targetY: parent.height / 2 - height
dir: ASlowEnter.Direct.FromTop
Image {
anchors.fill: parent
source: "qrc:/EffectImage/Img/baby.jpg"
}
}
ASlowEnter {
id: a3
width: 160
height: 108
targetX: parent.width / 2 - width * 1.5
y: (parent.height - height) / 2
dir: ASlowEnter.Direct.FromLeft
Image {
anchors.fill: parent
source: "qrc:/EffectImage/Img/baby.jpg"
}
}
ASlowEnter {
id: a4
width: 160
height: 108
targetX: parent.width / 2 + width / 2
y: (parent.height - height) / 2
dir: ASlowEnter.Direct.FromRight
Image {
anchors.fill: parent
source: "qrc:/EffectImage/Img/baby.jpg"
}
}
ParallelAnimation {
id: ani
ScriptAction{ script: {a1.animation.restart()} }
ScriptAction{ script: {a2.animation.restart()} }
ScriptAction{ script: {a3.animation.restart()} }
ScriptAction{ script: {a4.animation.restart()} }
}
Component.onCompleted: {
ani.restart()
}
Button {
anchors.right: parent.right
anchors.bottom: parent.bottom
text: "replay"
onClicked: {
ani.restart()
}
}
}
❌
❌