这是《玩转QtQuick》系列文章的第二篇,主要是介绍Qt Quick的默认渲染器。
本文会涉及到一些图形学的基本概念,例如:材质、纹理、光栅化、图元等,建议参考相关资料,本文不做进一步的解释。
因为Qt官方文档写的比较全面,所以本文主要是对官方文档的翻译,同时会补充一些个人理解。
翻译主要参考Qt5.15的文档,适当做了一些调整,尽量信达雅,尽量说人话。
下面翻译开始
本文介绍默认渲染器在内部的工作方式,以方便开发者们以最佳的方式
使用它(编写代码),包括性能和功能。
通常无需了解渲染器的内部结构,就能够获得良好的性能。
但是,在与场景图集成或弄清楚为什么无法从图形芯片中挤出最大效率时,这可能会有所帮助。
(即使在每个帧都是唯一的并且所有内容都是从头开始上传的情况下,默认渲染器也将表现良好)
Qml场景中的Item
将填充QSGNode
实例树。一旦实例树创建好之后,此树将完整描述如何渲染特定的帧。
它不会包含对任何Item
的反向引用,并且在大多数平台上将通过单独的线程进行处理和渲染。
渲染器是“场景图”的自包含部分,它遍历QSGNode
树,并使用QSGGeometryNode
中定义
的几何形状和QSGMaterial
中定义的着色器状态来更新图形状态并生成DrawCall。
如果有需要,可以使用内部的“场景图”后端API完全替换渲染器。
对于希望利用非标准硬件功能的平台供应商来说,这最为有趣。
对于大多数用例,默认渲染器就足够了。
默认渲染器着重于优化渲染的两种主要策略:批量处理调用和在GPU上保留几何图元。
传统的2D API (例如QPainter,Cairo或者Context2D)被设计为每帧处理大量单独的DrawCall,而当DrawCall的次数
非常少且状态更改保持在一定水平时,OpenGL和其它硬件加速的API表现最佳。
考虑以下用例:
绘制此列表的最简单方法是逐行进行。
首先,绘制背景。背景是特定颜色的矩形。在OpenGL术语中,这意味着使用一个着色器程序进行纯色填充,设置填充颜色,
设置包含x和y偏移量的转换矩阵,然后使用例如glDrawArrays
绘制组成矩形的两个三角形。
接下来绘制图标。用OpenGL术语来说,这意味着使用一个着色器程序来绘制纹理,激活要使用的纹理,设置转换矩阵,启用alpha混合,
然后使用例如glDrawArrays
绘制组成图标边界矩形的两个三角形。
行 之间的文本和分隔线遵循类似的模式。
对于列表中的每一行都重复此过程,因此对于更长的列表,OpenGL状态变更和DrawCall所带来的
开销完全超过了使用硬件加速API所能提供的好处。
当每个图元都很大时,此开销可以忽略不计,但是在典型的UI环境中,有许多小项加起来会产生相当大的开销。
默认的“场景图”渲染器也在这些限制内运行,并且会尝试将单个图元合并到批次中,同时保留完全相同的视觉效果。
结果是更少的OpenGL状态变更和最少的DrawCall调用,从而实现了最佳性能。
渲染器将不透明图元和需要透明度的图元进行了分类。
通过使用OpenGL的Z缓冲为每一个图元赋予唯一的z值,渲染器可以自由地对不透明图元进行重新排序,而无需考虑
它们在屏幕上的位置以及与它们重叠的其它元素。通过查看每个图元的材质状态,渲染器将创建不透明的批次渲染。
在QtQuick的主要Item中,属于不透明图元的包括不透明颜色的Rectangle
和完全不透明的Image
,主要是JPEG
和BMP
格式。
使用不透明图元的另一个好处是,不透明图元不需要启用GL_BLEND
,这个操作可能会非常耗性能,尤其是在移动端和嵌入式GPU上。
不透明图元在启用glDepthMask
和GL_DEPTH_TEST
的情况下以从前到后的方式渲染。在内部进行early-z
校验的GPU上,这意味着
片元着色器不需要针对被遮盖的像素或像素块运行。
请注意,渲染器仍需要考虑这些节点,并且顶点着色器仍将为这些图元的每个顶点运行,因此,如果应用程序知道某些东西被完全遮盖,则
最好的办法是设置Item::visible
或Item::opacity
隐藏它。
Item::z
用来控制Item相对于其同级元素的“堆叠顺序”,它与渲染器和OpenGL的z缓冲没有直接关系。
一旦绘制了不透明的图元,渲染器将禁用glDepthMask
,启用GL_BLEND
并以从后到前的方式渲染所有alpha混合图元。
alpha混合图元的批次渲染在渲染器内需要更多的工作,因为重叠的元素需要以正确的顺序进行渲染,以使alpha看起来正确。
仅仅依靠Z缓冲是不够的。渲染器在所有alpha混合图元上进行传递,除了其材质状态外,还将查询其边框,以确定哪些元素
可以批次渲染,哪些元素不能批次。
上图左边的情况,可以在一次DrawCall中渲染蓝色背景,而在另一次DrawCall中渲染两个文本元素,因为这些文本仅与其
同一层的背景重叠。
右边的情况,Item 4
的背景覆盖了Item 3
的背景,因此每一个背景和文本需要在不同的DrawCall中渲染。
在Z方向上,alpha节点与不透明节点交错,且在可用时触发early-z
。同样的,将Item::visible
设置为false会快很多。
“场景图”支持伪3D和适当的3D图元。
例如,可以用ShaderEffect
来实现“页面卷曲”效果,或者可以使用QSGGeometry
和自定义材质来实现凹凸贴图。实现这
些功能时,开发者需要意识到默认渲染器已经使用了深度缓冲区。
渲染器修改了QSGMaterialShader::vertexShader()
返回的顶点着色器,并在应用了模型视图和投影矩阵之后压缩了
顶点的z值,然后在z上添加了一个小平移以将其放置在正确的z位置。
压缩时会假定z值在0到1的范围内。
激活的纹理在OpenGL中是个唯一的状态,这意味着使用不同纹理的多个图元无法批次渲染。因此,Qt Quick“场景图”允许
将多个QSGTexture
实例分配为较大纹理的较小子区域,也就是“纹理集”。
纹理集的最大好处是多个QSGTexture
实例引用同一个OpenGL纹理实例。这样还可以批量处理带纹理的DrawCall,例如
Qml中的Image
,BorderImage
,ShaderEffect
等,以及C++中的QSGSimpleTextureNode
和自定义
的QSGGeometryNode
都使用了纹理。
尺寸过大的纹理不会进入纹理集。
纹理集使用带参数QQuickWindow::TextureCanUseAtlas
的函数调用QQuickWindow::createTextureFromImage()
创建。
纹理集没有范围从0到1的坐标。使用QSGTexture::normalizedTextureSubRect()
获取纹理坐标。
“场景图”使用试探法来确定纹理集应该多大以及输入大小的阈值。如果需要不同的值,可以通过设置环境变量
QSG_ATLAS_WIDTH=[width]
, QSG_ATLAS_HEIGHT=[height]
和QSG_ATLAS_SIZE_LIMIT=[size]
来覆盖试探法。
对于平台供应商而言,更改这些数值通常是有趣的。
除了将兼容的图元合并到一个批次,默认渲染器还尝试将每帧需要发送到GPU的数据量减到最少。
默认渲染器会标记在一起的子树,并尝试将它们放入单独的批次中。
识别批次后,即可使用顶点缓冲对象
将其合并,上传并存储在GPU内存中。
每个QtQuick中的Item
会往场景树中插入一个QSGTransformNode
来管理其x、y坐标、缩放比例。子Item
会
附加在此变换节点之下。默认渲染器会跟踪帧之间变换节点的状态,并将查看子树以确定:变换节点作为一个
批次渲染的根节点是否良好。在帧之间变化且具有相当复杂的子树的变换节点可以成为批次渲染的根节点。
批次渲染根节点的子树中QSGGeometryNodes
相对于CPU上的根节点已经预先转换过了,然后讲它们上传
并保留在GPU上。当变换发生时,渲染器仅需要更新根节点的矩阵,而无需更新每个单独的节点,从而使列表
和网格滚动非常快。对于连续的帧,只要不添加或删除节点,就可以快速地、不增加消耗地渲染。当新内容
进入子树时,将对其进行重建,但这仍然相当较快。
在Grid
或List
中滚动时,会有节点添加或者删除,但也总会有一些帧是不变的。
将变换节点 标记为 批次根节点的另一个好处是,它允许渲染器保留树中未更改的部分。
例如:UI由一个List
和一行按钮 组成。滚动List
并添加或删除Delegate
时,UI的其余
部分(一行按钮)保持不变,可以使用存储在GPU上的几何图元进行绘制。
可以使用环境变量QSG_RENDERER_BATCH_NODE_THRESHOLD=[count]
和QSG_RENDERER_BATCH_VERTEX_THRESHOLD=[count]
来
覆盖要成为批次根节点的转换节点和顶点阈值。覆盖这些标志对平台供应商最有用。
在批次渲染根节点之下,会为每个唯一的材质状态集和几何图元类型创建一个批次节点。
将Item::clip
设置为true时,将创建一个QSGClipNode
,其几何形状为矩形。
默认渲染器将通过在OpenGL中使用scissoring
来应用此裁剪操作。如果将Item
旋转了非90°角,则使用OpenGL的模版缓冲区。QtQuick的Item
仅支持通过Qml启用
矩形的裁剪,“场景图”API和渲染器则支持任何形状的裁剪。
将裁剪应用于子树时,该子树需要使用唯一的OpenGL状态进行渲染。这意味着当Item::clip
为true时,该Item
的批次渲染仅限于其子Item
。当有许多子级(例如ListView
或GridView
)或
复杂的子级(例如TextArea
)时,这是好事。
应该避免在较小的Item
上使用裁剪,因为它会阻止批次渲染。这包括Button
上面的Label
,TextField
和Table
中的Delegate
。
每个批次渲染都会使用顶点缓冲区对象(VBO)将其数据存储在GPU上。该顶点缓冲区保留在帧之间,并在“场景图”所
表示的部分发生更改是更新。
默认情况下,渲染器将使用GL_STATIC_DRAW
将数据上传到VBO。
通过环境变量QSG_RENDERER_BUFFER_STRATEGY=[strategy]
可以选择其它上传策略,有效的策略还包括stream
和dynamic
。
更改此值对平台供应商最有用。
“场景图”支持两种类型的抗锯齿。
默认情况下,诸如Rectangle
和Image
之类的图元,将通过沿图元的边缘添加顶点的方式,使
边缘淡化到透明,以实现抗锯齿。我们称此方法为顶点抗锯齿。
如果用户通过QQuickWindow::setFormat()
将QSurfaceFormat
设置为大于0的值,请求OpenGL多重采样,“场景图”将首选
基于多重采样的抗锯齿(MSAA)。
这两种技术将影响渲染器的内部实现方式,并且具有不同的限制。
通过设置环境变量QSG_ANTIALIASING_METHOD
为msaa
或者vertex
也可以覆盖使用的抗锯齿方法。
即使两个图元的边在数学上相同,顶点抗锯齿也会在相邻图元的边缘之间产生接缝。多重采样抗锯齿则不会如此。
可以使用Item::antialiasing
属性启用和禁用单个Item
的顶点抗锯齿。在硬件支持的前提下,无论是正常渲染的图元,还是捕获到
帧缓冲区对象中的图元(例如使用ShaderEffectSource
),顶点抗锯齿都可以正常运行并产生更高质量的抗锯齿功能。
使用顶点抗锯齿的不利之处在于,每个启用了抗锯齿的图元都必须进行混合。在批次渲染方面,这意味着渲染器需要做更多的工作来确定
图元是否可以进行批次渲染。如果和场景中其它元素重叠,也可能导致更少的批次渲染,从而影响性能。
在低端硬件上,混合操作也可能会非常耗性能。对于覆盖屏幕大部分区域的图像或者圆角矩形,这些图元内部所需要的混合操作数量
可能会导致严重的性能损耗,因为必须混合整个图元。
多采样抗锯齿是一项硬件功能,其中硬件会计算图元中每个像素的覆盖值。一部分硬件可以以非常低的成本进行多次采样,而另一些
硬件需要更多的内存和GPU周期来渲染一帧,
使用多采样可以对许多图元进行抗锯齿(例如圆角矩形和图片),并且在“场景图”中仍然是不透明的。这意味着在创建渲染批次时,
渲染器的工作会更加轻松,并且可以依赖early-z
来避免过渡渲染。
使用多重采样抗锯齿时,渲染到帧缓冲区对象中的内容需要额外的扩展以支持帧缓冲区的多重采样。通常是GL_EXT_framebuffer_multisample
和GL_EXT_framebuffer_blit
。大多数台式机芯片都具有这些扩展,但是在嵌入式芯片中却很少见。
如果硬件中不提供帧缓冲区多采样,则不会进行多采样抗锯齿,包括ShaderEffectSource
。
如文章开头所说,不需要了解渲染器的详细信息就能够获得良好的性能。默认渲染器在设计时就针对常见用例进行了优化,
并且在几乎任何情况下都将表现良好。
有效的批次渲染可带来良好的性能,并尽可能少地上传几何图形。通过设置环境变量QSG_RENDERER_DEBUG=render
,渲染
器将输出相应的统计信息,包括:批次渲染进行的程度,使用的批次数量,保留的批次及不透明和透明的批次数量等。
追求最佳性能时,应仅在真正需要时上传数据,批次数量应该少于10个,且至少3-4个不透明批次。
默认渲染器不执行任何CPU端的视口裁剪或遮挡检测。如果某些内容不可见,则不应该显示,使用Item::visible
将其设置
为false。不添加这样的逻辑的主要原因是,它增加了额外的成本,这也将损害那些表现良好的应用程序。
确保纹理集被使用。除非图像特别大,否则Image
和BorderImage
将使用它。C++代码中想要创建纹理集,需在调用
QQuickWindow::createTexture()
时传递QQuickWindow::TextureCanUseAtlas
参数。通过设置环境变量
QSG_ATLAS_OVERLAY
,所有纹理集将被着色,以便在应用程序中轻松识别它们。
尽可能使用不透明图元。不透明图元在渲染器中处理速度更快,在GPU上绘制速度更快。例如,即使每个像素都是不透明的,PNG
文件也会经常具有alpha通道。JPG文件始终是不透明的。当图像提供给QQuickImageProvider
或者使用
QQuickWindow::createTextureFromImage()
创建图像时,请尽可能使用QImage::Format_RGB32
格式。
如前文所示,重叠的复合Item
无法批次渲染。
裁剪会中断批次渲染。切勿在表格内的单元格,delegate
或者类似的元素中使用裁剪。使用省略
代替文本裁剪。
创建一个返回裁剪后图像的QQuickImageProvider
,代替图像裁剪。
批次渲染仅适用于16位索引。所有QtQuick内置的Item
都使用了16位索引,但是自定义几何图元也可以自由使用32位索引。
一些材质的标志会阻止批次渲染,其中最受限制的一个是QSGMaterial::RequiresFullMatrix
,它阻止了所有批次渲染。
具有单色背景的应用程序应使用QQuickWindow::setColor()
而不是顶级带颜色的Rectangle
。
QQuickWindow::setColor()
将在glClear()
的调用中使用,这是比较快的。
生成Mipmap
的Image
不会放在纹理集中,也不会进行批次渲染。
存在一个OpenGL驱动程序相关的Bug
:帧缓冲对象(FBO)回读时发生的一些错误会损坏渲染的字形。如果在环境变量
中设置QML_USE_GLYPHCACHE_WORKAROUND
,则Qt会在RAM
中保留该字形的其它副本。这意味着当渲染以前
未渲染的字形时,性能会稍低,因为Qt通过CPU访问额外的副本。这也意味着字形缓存将使用两倍的内存。渲染质量不受影响。
如果应用程序性能不佳,需要确认瓶颈是否在渲染。此时可以使用探查器Profiler
! 设置环境变量QSG_RENDER_TIMING=1
将输出许多有用的时序参数,这些参数可以用来查明问题所在。
为了可视化“场景图”默认渲染器的各个方面,可以将QSG_VISUALIZE
环境变量设置为下面每个部分中详细介绍的值其中之一。
下面这段Qml代码提供了一些变量输出的示例:
1 | import QtQuick 2.2 |
左侧的ListView
,我们将其clip
属性设置为true。右侧的ListView
,我们将每个delegate
的clip属性设置为true。
以此来说明裁剪对批次渲染的影响。
这是正常的运行结果
可视化元素不考虑裁剪,并且渲染顺序是任意的。
设置环境变量QSG_VISUALIZE
为batches
可以在渲染器中可视化查看批次。
合并过的批次以纯色渲染,未合并的批次以对角线图案渲染。独立的颜色越少意味着批次分配的越好。
如果未合并的批次中包含许多单独的节点,则是比较糟糕的。
QSG_VISUALIZE=batches
设置环境变量QSG_VISUALIZE
为clip
,渲染器会在“场景图”中渲染红色区域来指示裁剪区域。
默认情况下Item
不裁剪,因此不会显示裁剪区域。
QSG_VISUALIZE=clip
设置环境变量QSG_VISUALIZE
为changes
,可以在“场景图”中看到变更。“场景图”中的变更以随机颜色的
闪烁叠加显示。图元上的变更以纯色显示,而批次渲染根节点的变更以特定的pattern
显示。
设置环境变量QSG_VISUALIZE
为overdraw
,可以在“场景图”中看到过渡绘制。可视化的3D视图中,
所有过渡绘制的Item
会高亮显示。此模式也可以用来检测视口之外的几何图元。不透明的Item
以绿色显示,
而半透明的Item
以红色显示。视口的边框为蓝色显示。不透明的内容使“场景图”更易于处理,渲染速度更快。
请注意,上面的代码中顶层矩形框Rectangle
是多余的,因为窗口也是白色的,在这种情况下渲染矩形框会造成资源浪费。
将其更改为Item
会略微提高性能。
QSG_VISUALIZE=overdraw
从Qt5.14开始,默认适配层增加了一个选项,可以使用 QtGui模块提供的图形抽象层Qt Rendering Hardware Interface (RHI)
进行渲染。启用后,将不进行OpenGL调用,而是使用抽象层提供的API来渲染“场景图”,然后将其转换为OpenGL,
Vulkan,Metal或者Direct3D调用。
通过一次编写着色器代码,编译为SPIR-V,然后转换为适用于各种图形API的语法,实现着色器的统一处理。
要启用此功能,代替直接OpenGL调用,可以通过下面的变量:
环境变量 | 有效值 | 描述 |
---|---|---|
QSG_RHI | 1 | 启用通过RHI的渲染。除非被QSG_RHI_BACKEND 覆盖,否则将根据平台选择目标图像API。默认值为window平台使用Direct3D 11, MacOS平台使用metal,其它平台使用OpenGL |
QSG_RHI_BACKEND | vulkan,metal,opengl,d2d11 | 请求使用指定的图形API |
QSG_INFO | 1 | 与基于OpenGL的渲染路径一样,设置此选项将在初始化Qt Quick“场景图”时启用打印信息。 这对于故障排除非常有用。 |
QSG_RHI_DEBUG_LAYER | 1 | 在适用的情况下(Vulkan,Direct3D),启用图形API实现的调试或验证层(如果有)。 |
QSG_RHI_PREFER_SOFTWARE_RENDERER | 1 | 请求使用软光栅化的适配器或物理设备。仅在API支持枚举适配器(Direct3D或Vulkan)时适用,否则会被忽略 |
希望始终使用单个指定的图形API运行应用程序,也可以通过C++代码来设置。
例如,在构造任何QQuickWindow
之前,在main函数的早期进行以下调用将强制使用vulkan
1 | QQuickWindow::setSceneGraphBackend(QSGRendererInterface::VulkanRhi); |
可以查看QSGRendererInterface::GraphicsApi
文档。以Rhi结尾的枚举值等价于设置QSG_RHI
和QSG_RHI_BACKEND
。
除非被QSG_RHI_PREFER_SOFTWARE_RENDERER
或特定后端的变量(例如QT_D3D_ADAPTER_INDEX
或者 QT_VK_PHYSICAL_DEVICE_INDEX
)覆盖,
否则所有QRhi后端都会选择系统默认的GPU适配器或物理设备。目前没有进一步的适配器相关配置项。
]]>这是《玩转QtQuick》系列文章的第一篇,主要是介绍Qt Quick Scene Graph “场景图”的关键特性、主要架构及实现原理等等。
(不是QWidget 框架中那个 QGraphicsView哦,是Qt Quick的Scene Graph,不一样)
Scene Graph 是QtQuick/Qml所依赖的渲染框架。
本文会涉及到一些图形学的基本概念,例如:材质、纹理、光栅化、图元等,建议参考相关资料,本文不做进一步的解释。
因为Qt官方文档写的比较全面,所以本文主要是对官方文档的翻译,同时会补充一些个人理解。
翻译主要参考Qt5.15的文档,适当做了一些调整,尽量信达雅,尽量说人话。
下面翻译开始
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 “场景图”的内置默认行为。如果使用替代的方案时,并非所有概念都适用。
“场景图” 由许多预定义的节点类型组成,每种类型都有专门的用途。
尽管我们将其称为“场景图”,但更精确的定义是“节点树”。
该树根据Qml场景中的QQuickItem
类型构建,然后在内部对该场景进行渲染,最终呈现该场景。
“节点” 本身不包含任何 绘制 或者 paint() 虚函数。
“节点树”主要由内建的预定义类型组成,用户也可以添加具有自定义内容的完整子树,包括表示3D模型的子树。
一般是指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“着色器程序” |
QSGMaterialType | 与QSGMaterial 结合用作唯一类型标记 |
QSGFlatColorMaterial | “场景图”中渲染纯色图元的便捷方法 |
QSGOpaqueTextureMaterial | “场景图”中渲染不透明纹理图元的便捷方法 |
QSGTextureMaterial | “场景图”中渲染纹理图元的便捷方法 |
QSGVertexColorMaterial | “场景图”中渲染 逐顶点彩色图元的便捷方法 |
更多详细的信息,可以参考Qt文档: Scene Graph - Simple Material
“场景图”API是一套 偏底层的接口,专注于性能而不是易用性。
从头开始编写自定义的几何图形和材质,即使是最基本的几何图形和材质,也需要大量的代码。
因此,“场景图”API包含了一些节点类,以使最常用自定义节点的开发更便捷。
节点名称 | 描述 |
---|---|
QSGSimpleRectNode | QSGGeometryNode 的子类,定义了矩形图元和纯色材质 |
QSGSimpleTextureNode | QSGGeometryNode 的子类,定义了矩形图元和纹理材质 |
“场景图”的渲染发生在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的步骤也是相同的。
Qml场景中发生变化,触发调用QQuickItem::update()
, 这可能是动画或者用户操作的结果。
一个 事件
会被post
到渲染线程来启动新的一帧。
渲染线程准备渲染新的一帧,GUI线程会启动阻塞。
当渲染线程准备新的一帧时,GUI线程调用QQuickItem::updatePolish()
对场景中节点进行最终的“润色”,再渲染它们。
GUI 线程阻塞。
QQuickWindow::beforeSynchronizing()
信号发出。应用程序可以对此信号进行直连(Qt::DirectConnection
),
以进行QQuickItem::updatePaintNode()
之前所需的任何准备工作。
将Qml状态同步到“场景图”中。自上一帧以来,所有已更改的节点上调用QQuickItem::updatePaintNode()
函数完成同步。
这是Qml与“场景图”中的节点唯一的交互时机。
GUI线程不再阻塞。
渲染“场景图”:
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()
信号发出。
渲染线程正在渲染时,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时,驱动渲染循环的责任将转移到应用程序中。
在这种情况下,不使用内置的渲染循环。
取而代之的是,由应用程序在适当的时候调用 polish
synchronize
rendering
等渲染步骤,实现类似于上述
行为的线程渲染循环或非线程渲染循环。
“场景图”提供了两种方法,来集成应用程序提供的图形命令:
直接发出OpenGL、Vulkan、Metal等命令,以及在“场景图”中创建纹理化节点。
通过连接到QQuickWindow::beforeRendering
和 QQuickWindow::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线程中运行。
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.general
和qt.rhi
,或者
设置QSG_INFO=1
的情况下启动应用程序。然后这将在初始化期间将一些基本信息打印到调试输出。
除了公共API外,“场景图”还具有适配层,该适配层用以实现特定硬件的适配。这是一个未公开的、内部的、私有实现的插件,
可以让硬件适配团队充分利用其硬件。这包括:
自定义纹理; 特别是QQuickWindow::createTextureFromImage
的实现以及Image和BorderImage类型使用的纹理的内部表示。
自定义渲染器;适配层使插件可以决定如何遍历和渲染“场景图”,从而有可能针对特定硬件优化渲染
算法或 使用可提高性能的扩展。
许多默认Qml类型的自定义“场景图”实现,包括其文本和字体渲染。
自定义动画驱动程序;允许动画系统连接到低级“垂直同步”的显示设备,以获得平滑的渲染。
自定义渲染循环;可以更好地控制Qml如果处理多个窗口。
本文分享特定问题的解法,用不到的可以忽略。
使用QQuickWidget的时候,遇到过这个问题:界面的TextInput 或者TextEdit, 鼠标点击聚焦后,切换为光标输入状态,此时切换系统中文输入法,会发现无法输入。
(系统任务栏的输入法状态是正确的,界面上输入字符,直接显示英文,无法显示输入法的候选框)
需要把界面切到其它软件,再切换回来,之后就能够输入了。
可以参考Qt官方bug报告:
https://bugreports.qt.io/browse/QTBUG-61475
这个Bug是2018年报告的,我们当时做项目,也被这个Bug坑到了。
当时我给出了一个弱化版本的解法,原理是在第一次聚焦的时候,清理掉QQuickWidget的焦点。
1 | QuickWidget::QuickWidget(QWidget *parent) |
此方法勉强能用,一些细节上体验不太好。
当时找不到更好的方法,就这样用着了。
2020年Qt官方终于派出了资深的专家,在Qt5.15.2中,彻底解决了这个问题。
(看到有不少博客、论坛,还在流传我提供的旧版本,于心不忍)
于是我从新版本里面,提炼出来了代码,给使用旧版本的同学解决此问题。
1 | QuickWidget::QuickWidget(QWidget *parent) |
很多现代化的软件,都会有向导功能,相信大家并不陌生。
“用户向导”的作用,可以帮助新用户快速了解产品,也可以用来提醒用户该如何操作。
这次涛哥就分享一个Qml制作“用户向导”的方案。
看一下最终效果
在整个软件界面上,覆盖一层遮罩,只保留一部分镂空区域,并用箭头指向镂空区域,
以此,将用户的眼球聚焦到镂空区域,并用适当的文字,说明镂空区域的作用。
这个遮罩层是不能操作到软件界面的,点击任意区域,会转到下一步的向导,直到最后向导退出。
《玩转Qml》系列文章,配套了一个优秀的开源项目:TaoQuick
github https://github.com/jaredtao/TaoQuick
访问不了或者速度太慢,可以用国内的镜像网站gitee
https://gitee.com/jaredtao/TaoQuick
向导的实现方案有很多,比如贴图之类的。按不同的需求,方案也不尽相同。
这里先列一个涛哥的需求,再讨论方案才有意义。
窗口大小不固定
焦点区域可以是Qml界面中的任意可视组件
向导功能要独立实现,且对已有代码改动最小
遮罩的制作,在Qml中就是几个半透明的Rectangle拼在一起,附带一个顶层MouseArea,
将所有鼠标事件都过滤掉,这些都比较简单。
焦点区域才是本文的核心。
先来说一下Qml中的对象作用域吧。
如果你阅读过《Qml Book》或者有一定的Qml经验,就知道Qml中的对象,都是通过id相互引用的。
像html / Android,都有类似findElementById这样的接口,获取任意的组件。
在Qml,省去了find这一步,可以直接使用id进行访问。
不过呢,Qml这个id的可访问性,不是任何场景都能直接用的,有诸多情况下,不能直接使用, 例如:
平级的两个组件,它们的子组件不能互相访问到(同一个文件作用域中的除外)
各种View的delegate中的组件id,view外部不能直接访问(这是因为delegate有多个实例的情况下,id不能唯一标识)
Loader/Component动态加载后的子组件,不能被直接访问(动态加载,都不知道它什么时候创建,当然不能用它)
诸如此类,都不能直接访问。
扩展说明:
顶层组件的id,可以被所有子组件访问,因此可以用来存储全局对象;
Qml单例组件,或者C++上下文对象,也可以被所有子组件访问。
这两种方式也可以用来实现向导功能,有兴趣的读者可自己探索。
Qml中的所有可视化组件,大都是继承于Item,Item的继承关系是 Item ==> QQuickItem ==> QObject
因此呢,Qml中的所有对象,都在一个QObject树上,具有父子关系。
我们可以从任意节点,向上找到根节点,也可以通过根节点的findChild的方式,找到任意一个带objectName的节点。
涛哥的向导方案,便是基于此实现。
在已有的Qml代码中,对于想当作焦点区域的组件,最小改动是增加一个objectName
例如涛哥想把已有的标题栏按钮,作为焦点区域:
改动前:
1 | ... |
改动后:
1 | ... |
像前面的组件,只要有了objectName,就可以从任意位置获取到它了
这需要一点C++代码扩展
1 | QQuickItem *getObject(QObject * pRootObject, const QString &targetObjName) |
通过findChild就可以拿到目标对象了, rootObject是根节点,可以从任意节点向上找到,
也可以通过QQmlContex直接获取到。
找到了对象,就可以计算它的坐标,用于向导了。
为了方便计算,同时避免处理繁杂的父子关系,可以直接把坐标映射到顶层窗口。向导的坐标也以顶层窗口为准即可。
这里进一步封装了C++代码,直接计算好坐标
1 | QRect getItemGeometryToScene(const QString &targetObjName) const |
完整的代码,可以在TaoQuick项目中看到, 封装了一个QuickTool类,
所在路径是TaoQuick/3rdparty/TaoCommon/QuickTool
接下来就是封装一个向导页面了,遮罩围绕在焦点四周,同时带上箭头和文字说明即可
1 | //CusWizardPage.qml |
向导一般不止一页,而是很多页,这里就封装了一个向导组件。
使用model-view的方式,数据源由外部设置,组件只管按照model去实例化对应的向导页即可
1 | //CusWizard.qml |
最后来看一下,TaoQuick项目的首页,提供的向导数据model
1 | ListModel { |
最近遇到一些需求,要在Qt/Qml中开发树结构,并能够导入、导出json格式。
于是我写了一个简易的Demo,并做了一些性能测试。
在这里将源码、实现原理、以及性能测试都记录、分享出来,算是抛砖引玉吧,希望有更多人来讨论、交流。
起初的代码在单独的仓库
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的声明如下:
1 | template <typename T> |
其中数据成员使用 QList m_nodeList 存储, 大部分成员函数是对此数据的操作。
Json格式的model声明如下:
1 | const static QString cDepthKey = QStringLiteral("TModel_depth"); |
TaoJsonTreeModel继承于TaoListModel,并提供大量Q_INVOKABLE函数,以供Qml调用。
TreeView的模拟实现如下:
1 | Item { |
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 | ... |
初始化函数initTestCase中,组织了一个QList,然后使用QtConcurrent::map并发调用genJson函数,生成数据json文件。
node和depth每次扩大10倍。
经过测试,嵌套层数在100以上时,Qt可能会崩溃。要么是QJsonDocument无法解析,要么是Qml挂掉。所以不使用100以上的嵌套级别。
QTest十分好用,简单易上手,参考帮助文件即可
例如测试加载的代码如下:
1 | void LoadTest::prepareData() |
一秒内最多可以加载的数据量在十万级别,包括
10000 x 10耗时在 386毫秒,1000 x 100 耗时在671毫秒。
]]>这次讨论Qt与Web混合开发相关技术。
这类技术存在适用场景,例如:Qt项目使用Web大量现成的组件/方案做功能扩展,
Qt项目中性能无关/频繁更新迭代的页面用html单独实现,Qt项目提供Web形式的SDK给
用户做二次开发等等,或者是Web开发人员齐全而Qt/C++人手不足,此类非技术问题,
都可以使用Qt + Web混合开发。
(不适用的请忽略本文)
上次的文章《Qt与Web混合开发》,讨论了Qt与Web混合开发相关技术。
这次通过一个web控制小车的案例,继续讨论相关技术。
本文会先介绍Qt与Web嵌套使用,再介绍Qt与Web分开使用,之后着重讨论分开使用
的一些实现细节,特别是WebChannel通信、WebChannel在Web/typescript中的使用。
这里以Qt官方的例子MiniBrowser来说明吧。
打开方式如下:
运行效果如下:
这个例子是在Qml中嵌套了WebView。
涛哥做了一个简单的半透明测试。
增加了两个半透明的小方块,蓝色的在WebView上面,红色的在WebView下面。
运行效果也是正确的:
代码是这样的:
红色框中是我增加的代码。
为什么要做半透明测试呢?根据以往的经验,不同渲染方式的两种窗口/组件嵌套在一起,总会出现透明失效之类的问题,例如 qml与Widget嵌套。
涛哥翻了一下Qt源码,了解到渲染的实现方式,Windows平台大致如下:
chromium在单独的进程处理html渲染,并将渲染结果存储在共享内存中;主窗口在需要重绘的时候,从共享内存中获取内容并渲染。
这里的WebView内部封装好了WebEngine,其本身也是一个Item,就和普通的Qml一样,属性绑定、js function都可以正常使用,暂时不深入讨论了。
Qt与Web分离,就是字面意思,Web在单独的浏览器或者App中运行,不和Qt堆在一起。两者通过socket进行通信。
这里用我自己做的例子来说明吧。
先看看效果:
左边是Qt实现的一个简易小车,可以前进和转向。右边是Html5实现的控制端,控制左边的小车。
源码在github上: https://github.com/jaredtao/QtWeb
小车来自Qt的D-Bus Remote Controller 例子
原版的例子,实现了通过QDBus 跨进程 控制小车。
(吐槽:这是一个古老的例子,使用了GraphicsView 和QDBus)
(知识拓展1: DBus是unix系统特有的一种进程间通信机制,使用有些复杂。Qt对DBus机制进行了封装/简化,即QDBus模块,
通过xml文件的配置后,把DBus的使用转换成了信号-槽的形式。类似于现在的Qt Remote Objects)
(知识拓展2: Windows本身不支持DBus,网上有socket模拟DBus的方案。参考: https://www.freedesktop.org/wiki/Software/dbus/)
我做了一些修改,主要如下:
这里贴一些关键代码
Car的头文件:
其中要说明的是:
speed和angle属性具备 读、写、change信号。
还有加速、减速、左转、右转四个公开的槽函数。
WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。
WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
Qt为我们封装好了WebSocket,即QWebSocket和QWebSocketServer,简单易用。
如果你了解socket编程,就看作TCP好了;如果不了解,请先去补充一下知识吧。
按涛哥的理解,WebChannel是在socket上建立的一种通信协议,这个协议的作用是把QObject暴露给远端的HTML。
大致使用流程:
Qt程序中,要暴露的QObject全部注册到WebChannel。
Qt程序中,启动一个WebSocketServer,等待Html的连接。
Html加载好qwebchannel.js文件, 然后去连接WebSocket。
连接建立以后,Qt程序中,由WebChannel接手这个WebSocket,按协议将QObject的各种“元数据”传输给远端Html。
Html端,qwebchannel.js处理WebSocket收到的各种“元数据”,用js的Object 动态创建出对应的QObject。
到这里两边算是做好了准备,可以互相调用了。
Qt端QObject数据变化只要发出信号,就会由WebChannel自动通知Web端;
Web端可以主动调用QObject的public的 invok函数、槽函数,以及读、写属性。
在使用WebChannel的时候,Qt端建立了WebSocketServer,之后要把server的路径(例如:ws://127.0.0.1:12345)告诉Html。
一般就是在打开Html的时候带上Query参数,例如: F:\QtWeb\index.html?webChannelBaseUrl=ws://127.0.0.1:12345
Qml中有 Qt.openUrlExternally, C++ 中有 QDesktopServices::openUrl,本质一样, 都可以打开一个本地的html网页。
其在Windows平台的底层实现是Win32 API。这里有个Win32 API的缺陷,传Query参数会被丢掉。
涛哥找到了替代的方案:
.net framework / .net core有个启动进程的函数: System.Diagnostics.Process::Start, 可以调用浏览器并传query参数
1 | //C# 启动chrome |
Qt中直接写C#当然不太好,不过呢,Win7/Win10 系统都带有Powershell,而powershell依赖于.net framework, 我们可以调用powershell来间接使用.net framework。
所以有了下面的代码:
1 | ... |
结果完美运行。
Web端就按照Web常规流程开发。
Web部分的源码也在前文提到的github仓库,子路径是QtWeb\WebChannelCar\Web
如下是Web部分的目录结构:
脚本用typescript,包管理用npm,打包用webpack,编辑器用vs code, 都中规中矩。
内容比较简单,暂时不需要前端框架,手(复)写(制)的html和css。
html部分比较简单
1 | //index.html |
样式和布局全靠css,这里就不贴了。
脚本部分需要细说了。
src文件夹为全部脚本,目录结构如下:
从main开始, 加点注释:
1 | //main.ts |
可以看到我们从WebChannelCore.SDK 中获取了一个car对象,之后就当作QObject来用了,包括调用它的函数、连接change信号、访问属性等。
这一切都得益于WebSocket/WebChannel.
接下来看一下WebChannelCore的实现
1 | //WebChannelCore.ts |
这部分代码不复杂,主要是连接WebSocket,连接好之后创建一个QWebChannel。
观察仔细的同学会发现,src文件夹下面,没有叫‘qwebchannel.ts’的文件,而是‘qwebchannel.js’,和一个‘qwebchannel.d.ts’
这涉及到另一个话题:
‘qwebchannel.js’是Qt官方提供的,在js中用足够了。
而我们这里是用TypeScript,按照TypeScript的规则,直接引入js是不行的,需要一个声明文件 xxx.d.ts
所以我们增加了一个qwebchannel.d.ts文件。
(熟悉C/C++的同学,可以把d.ts看作typescript的头文件)
内容如下:
1 | //qwebchannel.d.ts |
只是导出了一个函数。
这个函数的实现在‘qwebchannel.js’中:
1 | //qwebchannel.js |
这个代码比较长,就不全部贴出来了。主要实现了两个类,QWebChannel和QObject。
QWebChannel就是用来接管websocket的,而QObject是用js Object模拟的 Qt的 QObject。
这一块不细说了,感兴趣的同学可以自己去研究源码。
Qt默认的qwebchannel.js在实际使用过程中,有些不好的地方,就是函数的返回值不是直接返回,而是要在回调函数中获取。
比如car.getAngle要这样用:
1 | let angle = 0; |
我们的实际项目中,有大量带返回值的api,这样的用法每次都嵌套一个回调函数,很不友好,容易造成回调地狱。
我们同事的解决方案是,在typescript中把这些api再用Promise封装一层,外面用await调用。
例如这样封装一层:
1 | function getAngle () { |
使用和前面的代码一样:
1 | //异步更新 angle |
这种解决方案规避了回调地狱,但是工作量增加了。
涛哥思考良久,稍微改造一下qwebchannel.js,自动把Promise加进去,也不需要再额外封装了。
改动如下:
我们在Qt 程序中写了QObject,然后暴露给了ts。
在ts这边,一般也需要提供一个声明文件,明确有哪些api可用。
例如我们的car声明:
1 | //CarObject.ts |
这里涛哥写了一个小工具,能够解析Qt中的QObject,并生成对应的ts文件。
当然还是实验阶段,有兴趣的也可以关注一下
]]>这次讨论Qt与Web混合开发相关技术。
这类技术存在适用场景,例如:Qt项目使用Web大量现成的组件/方案做功能扩展,
Qt项目中性能无关/频繁更新迭代的页面用html单独实现,Qt项目提供Web形式的SDK给
用户做二次开发等等,或者是Web开发人员齐全而Qt/C++人手不足,此类非技术问题,
都可以使用Qt + Web混合开发。
(不适用的请忽略本文)
这篇文章,会先整体介绍一下Qt的各种Web方案,再提供简单的Demo,并做一些简要的说明。
之后的一篇文章,会通过一个Web控制Qt端小车的案例,来做进一步讨论。
Qt提供的Web方案主要包括 WebEngine/WebView、Quick WebGL Stream、QtWebAssembly三种。
可以参考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技术,在5.13中正式发布。本质是把Qt程序编译成浏览器支持的二进制文件,由浏览器加载运行。
一方面可以将现有的Qt程序编译成Web,另一方面可以用Qt/C++来弥补Web程序的性能短板。
Qt WebAssembly在使用细节上还有一些坑的地方,需要踩一踩。后续我再写文章吧。
Qt提供了WebEngine模块以支持Web功能。
Qt WebEngine基于google的开源浏览器chromium实现,类似的项目还有cef、miniblink等等。
QtWebEngine可以看作是一个完整的chromium浏览器。
(WebView是同类的方案,稍微有些区别。后文再说。)
浏览器技术十分的庞大,这里先不深入展开,先来关注一下Qt WebEngine对chromium的跟进情况。
数据来源于Qt wiki,Qt每个版本的change log
Qt版本 | chromium后端 | chromium安全更新 |
---|---|---|
5.9.0 | 56 | |
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.0 | 69 | |
5.12.1 | 71.0.3578.94 | |
5.12.2 | 72.0.3626.121 | |
5.12.3 | 73.0.3683.75 | |
5.12.4 | 74.0.3729.157 | |
5.12.5 | 76.0.3809.87 | |
5.12.6 | 77.0.3865.120 | |
5.12.7 | 79.0.3945.130 | |
5.14.0 | 77 | |
5.14.1 | 79.0.3945.117 |
可以看到Qt在WebEngine模块,一直持续跟进Chromium的更新。
当前(2020/3/4)最新的chromium版本是80。
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混合在一起)
(以Qt5.12为参考)
首先一条是:不支持静态编译 (因为依赖的chromium、chromium本身的依赖库 不能静态编译)
接下来再看看各平台的要求和限制:
编译器要 Visual Studio 2017 version 15.8 以上
系统环境要 Windows 10 SDK
默认只支持X64版本,如果要x86版本,要自己编译qt源码。
MacOS 10.12以上
XCode 8.3.3以上
MacOS 10.12以上 SDK
不支持32-bit
不兼容 Mac App Store (chromium使用了私有api,App Sandbox和chromium Sandbox优先级问题)
编译器要 clang, 或者 gcc 5以上
需要pkg-config来探测依赖库,dbus-1和 fontconfig是必须的。
如果配置了xcb,还要额外配置相关库。
Qt还提供了一个WebView组件,可以用来将Web内容嵌入到Qml程序中。(这个没有提供Widget的接口)
WebView组件的实现,使用了平台原生api,在移动端意义重大,特别是在ios平台,使用
原生的web view,这样就能兼容App Store了。
在Windows/MacOS/Linux平台,则是渲染部分还是使用了WebEngine。
WebView的使用可以参考官方例子Minibrowser
这里示例一个最简单的demo, 使用WebEngine Widget模块提供的QWebEngineView:
1 | //Demo.pro |
注意pro文件中包含的Qt模块
1 | //main.cpp |
上面代码以打开知乎首页为例,运行结果如下
涛哥尝试了在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模块。
再做一个纯Qml的Demo
pro中增加webengine模块即可
1 | //WebQml.pro |
注意初始化。
1 | //main.cpp |
qml导入模块,填入url
1 | //main.qml |
运行结果和上一个Demo一样
这回可以去掉Widget模块
同时也去掉不必要的翻译文件。包大小160M左右,和前面的差别不大。
]]>Qt for android 环境搭建,以Windows平台 Qt5.12为基准。
需要先安装Android版的Qt。 一般在Qt的安装包中都有集成,安装的时候勾上就行
Android开发必备的工具链包括:
下载链接分别点击QtCreator中的按钮就能跳转到
如果不能访问,找镜像网站。
这里要注意版本,和Qt5.12适配的才能用。Qt官方也没有说,都是自己试出来的。
JDK: OpenJDK没有试过,Oracle 的JDK, LTS版本有 1.8 和1.11,但是目前的Android SDK都是基于1.8开发的,1.11还用不了。
SDK: SDK下最新的就好。
NDK: Qt5.12能用的NDK是R19C,当前最新的是R20,里面工具链修改了,Qt还没有跟上,得后续版本(Qt5.12.5)
都装好就行了,不要去修改环境变量。直接在QtCreator里面选好路径即可。
到SDK 管理页面,安装一堆必要的工具和镜像。注意Android SDK Build Tool , 能用的是28.0.3,最新的29用不了。
创建一个Qt项目,使用Andorid kit。
之后在项目管理页面中,点一下 “Create Template”按钮,生成一堆android相关配置文件。
之后去项目路径下,找到build.gradle文件
把buildToolsVersion改成”28.0.3”
最后编译运行就可以了。模拟器或真机连接,就不说了。
]]>在之前两篇文章《github-Actions自动化编译》《github-Actions自动化发行》中,
介绍了github-Actions的一些用法,其中有部分配置,已经有了缓存相关的步骤。
这里专门开一篇文章,来记录github-Actions的缓存优化相关的知识。
github-Actions提供了缓存模板cache
官方文档也有说明 缓存文档
缓存大致原理就是把目标路径打包存储下来,并记录一个唯一key。
下次启动时,根据key去查找。找到了就再按路径解压开。
注意缓存有大小限制。对于免费用户,单个包不能超过500MB,整个仓库的缓存不能超过2G。
一般我们在任务步骤中增加一个cache
1 | steps: |
那么在这个地方,缓存执行的操作是restore。
在steps的末尾,会自动增加一个PostCache,执行的操作是record。
Qt项目每次运行Actions时,都是先通过install-qt-action模板,安装Qt,之后再获取代码,编译运行。
安装Qt这个步骤,可快可慢,涛哥在windows平台测试下来,平均要1分30秒左右。
加上cache后,平均只有25秒。
先看一个Qt项目的编译配置
1 | name: Windows |
缓存步骤,一般尽量写steps最前面。
1 | # 步骤 |
install-qt-action有默认的Qt安装路径${RUNNER_WORKSPACE},不过这个环境变量不一定能取到。
涛哥实际测试下来,以当前路径的上一级作为Qt路径即可。
缓存只是把文件还原了,环境变量并没有还原,我们还需要手动还原环境变量。
install-qt-action这个模板增加了一个环境变量Qt5_Dir,值为Qt的安装路径,并把对应的bin添加到了Path。
我们要做的,就是在缓存恢复成功后,重新设置这两个变量,并去掉install-qt的步骤。
1 | - name: setupQt |
steps.WindowsCacheQt.outputs.cache-hit == ‘true’
是缓存模板的输出值,可以作为后续步骤的条件判断。
写个伪配置,简单示例一下缓存流程
steps:
实际配置
1 | name: Windows |
在上一篇文章《github-Actions自动化编译》中,介绍了github-Actions的基本用法,
本文来介绍github-Actions的自动化发布。
先来回顾一下,上一篇文章中的Qt项目的编译流程
安装Qt环境
这一步用第三方Action模板:install-qt-action
获取项目代码
这一步用Actions官方核心模板:actions/checkout
执行qmake、make
这一步用自定义脚本,也可以换成cmake、qbs、gn、ninja等构建工具
执行test
这一步可以引入单元测试、自动化UI测试等。暂无完善的方案,以后再说。
发布
见下文。
Qt程序在编译完成后,发布的大致流程是:
1、 查找依赖库
2、制作压缩包或者安装包
3、上传压缩包或者安装包到网站、网盘。
Qt官方提供的查找依赖库的命令行工具,包括:Windows平台的Windeployqt、MacOS平台的Macosdeployqt。
在这两个平台,只使用Qt库的情况下,这两个工具足够了。
做压缩包比较简单。(我们常说的‘绿色软件’,就是一个压缩包)
一般安装7z、rar之类的压缩工具,用一条命令行就行了。
涛哥这里再说一下,github-Actions给所有平台都提供了PowerShell,而PowerShell内置了压缩命令Compress-Archive。
使用也很简单,只要路径和名字,例如:
1 | Compress-Archive -Path .\MyFolder 'MyRelease.zip' |
做安装包,Qt官方有功能很全面的安装包制作工具:QtInstallFrameWork, 稍微翻看一下文档或者例子即可。本文先不展开了。
github 本身提供了’Release’功能,每个仓库都有一个’Release’页面
可以将打包好的压缩包或者安装包,直接上传上去, 供他人下载。
github-Actions还提供了 创建’Release’、上传’Release’的模板
这两个模板的用法也很简单,在yml文件中直接use就行了,不赘述了。
前面介绍了一些简单的理论,接下来通过实例,教大家github-Actions的使用。
以HelloActions-Qt项目为例,做一些定制。
需求如下:
1、每次提交代码,同时在Windows、MacOS、Ubuntu、Android、IOS五个平台编译
2、每次提交tag,在windows和MacOS平台制作软件包,并发布到同一个github-‘Release’
需求1已经实现了,着重讨论一下需求2:
‘每次提交tag’限定了发布的时机。
涛哥尝试了一番,最终得到答案。
回顾一下, Windows平台的编译配置:
1 | name: Windows |
steps中的每一个步骤,可以有触发条件。我们可以在这里指定,只有github的事件为tag时才执行:
1 | steps: |
这里给出一个实际的打包步骤:
1 | # tag 打包 |
做一些说明:
其中的VCINSTALLDIR环境变量,是给windeployqt用的。有了这个环境变量,windeployqt会去msvc的安装路径提取‘运行时安装程序’。
打包完以后,将包名设置为环境变量,后续的步骤就可以通过环境变量拿到包名字了。
普通的设置环境变量,在步骤执行完成后就失效了,
这里使用github-Actions的‘记录命令’set-env ,具体可以参考文档github-Actions记录命令
文档说不要用双引号,应该都是针对linux的,我试出来的PowerShell用法如下:
1 | $name = ${env:archiveName} |
先取环境变量到一个局部变量,再在‘记录命令’中引用局部变量。
如果只有一个平台、一种配置,直接用那两个模板就能解决问题。
这是官方给的例子upload-release-asset:
1 | steps: |
在多平台 或者 多配置的情况下,同一个tag, 只有第一个执行create-release的任务可以成功,后续任务
再次执行create-release时,该tag下已经有了同名的‘Release’,所以会create失败。
这个问题折磨了涛哥好一阵子。找不到现成的解决方案,涛哥就自己实现了一种:
先用github的REST API去判断该tag下有没有‘Release’:
没有则执行create-release,并提取upload_url;
有则提取upload_url。
最后执行upload-release-asset
调用REST API,涛哥依旧使用了方便的PowerShell,
实际的配置如下:
1 | # tag 查询github-Release |
1 | name: Windows |
1 | name: MacOS |
代码在github HelloActions-Qt
另外在涛哥的Qml控件库TaoQuick,也使用了这一套配置
]]>几个月前写过两篇持续集成的教程,当时使用的是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 | git clone https://github.com/jaredtao/HelloActions-Qt |
拷贝文件夹’.github’到你的代码仓库根目录
在你的仓库中commit并添加.github文件夹中的文件
push你的仓库到github
push完就可以了,到你的github相应仓库页面-Actions子页面查看状态吧。
没错,复制、粘贴,就这么简单。
.github/workflows文件夹中包括写好的5个模板:
你也可以根据你的需要,只选择你需要的。
授人以鱼,不如授人以渔
这里再来介绍一些基本的原理。
可以参考 github Actions官方文档
中文文档目前翻译不全面,建议优先看英文的。
github-Actions 主要提供了windows server 2019、macos 10.15、ubuntu 18.04三个平台的docker环境,
并预装了大量开发者常用的软件,比如Java SDK、Android SDK、VisualStudio、python、golang、nodejs等,
可以在文档github Actions默认环境及预装软件 中看到详细的信息。
github-Actions和大部分docker环境一样,使用yaml/yml格式的配置文件。
同时github-Actions还提供了一些便利的功能、函数,可以参考
更多细节请大家参考文档,这里就不赘述了。
每个github仓库,都有一个Actions页面,在这里可以创建、管理Actions
一般使用nodejs、python、golang等环境的项目,github提供了现成的Actions模板,可以
直接在Actions创建页面或者Marketplace(github的应用商店)进行搜索、引用。
有闲暇的开发者,也可以开发自己的Actions并提交到github商店,甚至可以赚点零花钱哦。
(Actions开发使用TypeScript)
简单总结一下Qt项目的编译流程
安装Qt环境
这一步用下文的Action模板:install-qt-action
获取项目代码
这一步用Actions官方核心模板:actions/checkout@v1
执行qmake、make
这一步用自定义脚本,可以换成qbs、cmake、gn、ninja等构建工具
执行test
这一步可以引入单元测试、自动化UI测试等。以后再说。
执行deployment
等我下一篇文章
Qt项目暂时没有公开、完整的Actions模板,不过有一个安装Qt的Actions,解决了在不同平台安装不同版本Qt的问题。
github的Actions有一个非常强大的功能,就是引用外部模板。
比如要引入这个install-qt-Actions模板,只要在配置文件中添加两行即可:
1 | ... |
Qt的安装路径、版本、目标平台、目标架构都有默认配置,当然你也可以手动配置
1 | ... |
这个Actions模板的实现,是按照Actions的工作原理(TypeScript),调用另一个python仓库aqtinstall,
把配置参数传递过去,由该库完成Qt的安装。
aqtinstall由一位日本的程序员使用python开发,直接访问Qt官方的发布仓库
http://download.qt.io/online/qtsdkrepository/ , 下载指定平台的各模块压缩包,并解压到指定目录。
直接绕过了我们平常使用的Qt安装器。
aqtinstall没有实现‘只安装指定模块’,默认全安装。希望后续能做支持,毕竟Qt全安装太大了。
涛哥还发现一个开源的action,并没有进商店,功能是适配所有平台的Qt环境变量
https://github.com/Skycoder42/action-setup-qt
可以在该作者的’Json序列化库’中,看到实际应用
https://github.com/Skycoder42/QtJsonSerializer
目前是固定在Qt5.13.2版本,包含winrt、wasm等所有平台。
接下来,说一下涛哥提供的模板,对各平台的配置。
以方便那些,需要对模板做修改的同学。
涛哥在这个配置文件中,写了一些注释。
1 | # windows.yml |
大部分配置都是显而易见的,这里对一些特殊情况做一些说明吧。
windows平台优先推荐用msvc编译,不过有些情况不得不用mingw。
github-Actions提供的Windows Server 2019环境,预装Mingw为8.1.0,版本太高了。
Qt5.9需要的mingw版本是5.3,而5.12则需要7.3,涛哥试过简单的HelloWorld程序,都会报链接失败。
所以需要使用MinGW的同学,需要自己安装了。
github-Actions在Windows平台默认的shell是PowerShell,其它平台是bash。
使用msvc命令行编译项目时,一般要先调用’vcvarsxxx.bat’脚本来设置环境变量。
Powershell虽然强大,却不太方便直接调用这个bat。要么安装Powershell扩展Pcsx,要么
用一些取巧的方式:
github-Actions当然也可以直接指定使用cmd。
1 | ... |
Ubuntu 平台看配置吧。
1 | # ubuntu.yml |
MacOS平台和Ubuntu差别不大
1 | # macos.yml |
Android使用ubuntu编译,Windows那个ndk似乎没装,未尝试。
如果只使用Qt5.12.6,默认的配置可以直接用,编译前设置环境变量 ANDROID_SDK_ROOT
和ANDROID_NDK_ROOT就可以了。
Qt5.9.8要指定低版本的NDK、SDK才行,这里涛哥没有进一步尝试。
1 | # android.yml |
ios只能使用MacOS编译。
qmake的时候要指定平台、release模式等。
1 | #ios.yml |
这次讨论发布Qt应用程序的知识点。
有很多人向涛哥询问,Qt程序发布的相关问题,网络上虽然可以搜到一大堆教程,但是可靠的比较少。
所以这次尽我所能,全面、详细地整理一些Qt程序发布的知识点,希望能帮助到更多人。
对老手来说,很多坑都踩过了,无非就是把正确的dll放在正确的路径。
对新手来说,细节上能多说几句,都将是莫大的帮助,少走弯路,节省几个小时、甚至几天都是有可能的。
如果有疏漏、错误,也欢迎大家补充、指正。
Qt官网下载地址在这: http://download.qt.io/official_releases
离线安装包 或者 在线安装包 都行。
关于Qt版本的选择,涛哥建议:
体验新特性,就用最新版本;项目开发,用长期支持版(LTS)的最后一个修正版本,稳定、bug最少。
可以在Qt官方wiki上查看相关信息 https://wiki.qt.io/Main
目前为止(2019/9/2),最新版为5.13.0,LTS版本有5.9 和 5.12, 而5.9最后一个修正版本是5.9.8, 5.12则是到5.12.4
例如上图是5.9.8的离线安装包,提供了windows、mac以及linux三种系统的可执行程序。
其中windows的安装程序”qt-opensource-windoiws-x86-5.9.8.exe”, 大小有2.4G,里面
包含了msvc_x86、msvc_x64、mingw、Android等多个版本的Qt工具链。在下载完成,安装
过程中可以分别勾选。其它版本也是类似的。
如何安装Qt,就不细说了,搞不定的去参考入门级教程吧…
这里假设大家都装好了Qt,先来了解一下Qt的安装路径都有哪些东西。
涛哥用的是Windows 10系统,安装的Qt版本是5.12.4,以此为例来说明,其它系统和版本以实际为准。
涛哥安装在了D:\Qt\Online 路径下, 如图:
其中“vcredist”文件夹包含了msvc2015 和 msvc2017的运行时库安装程序(后面会说怎么用,不是msvc编译器不需要)
“Tools”文件夹,包括QtCreator、OpenSSL库(可选)以及两种版本MinGW(可选)。
(图中还有Qt3DStudio,可忽略)
“5.12.4”文件夹,是Qt的核心路径, 里面包含多个版本的Qt工具链、头文件、动态链接库等
这里涛哥安装了msvc2017、msvc2017_64、mingw73_64以及android_x86.
注意msvc2017是x86架构的Qt库,msvc2017_64则是x64架构的。
如果有msvc2013、msvc2015也同理。
接下来看一下重点,Qt的核心路径, 以msvc2017_64文件夹为例
bin文件夹包含了Qt提供的各种工具exe程序,以及动态链接库的dll
其中工具包括qmake.exe 和 windeployqt.exe,windeployqt.exe是我们今天主要讨论的工具。
动态链接库全部是两份dll,比如Qt5Cored.dll和Qt5Core.dll,文件名末尾带’d’表示debug版本的,另一个不带’d’的是release版本。
debug版本和release版本的主要区别:debug没有开编译器优化、携带了调试信息,release开了编译器优化O2,去掉了多余的信息
(图中还有pdb文件,是涛哥单独安装的,用来调试Qt源码,可以忽略)
和bin同级的,还有plugins文件夹,包含一些Qt用到的插件
比如imageformats文件夹中提供了jepg、gif、webp等图片格式的功能支持的插件,platforms文件夹则提供了平台插件,特别是
qwindows.dll这一个,在windows平台是必不可少的。
和bin同级的,另外一个文件夹是’qml’文件夹,包含Qml的各种功能模块。
和bin同级的其它文件夹,resources是WebEngine模块专用的,translations提供了
Qt内置的翻译文件,剩下的和发布无关,就不多说了。
这里新建一个简单的Hello World程序,名字就叫”HelloDeploy”。
同时为了说明问题,涛哥添加一些常用的模块。
在pro文件中,QT += 那一行该写的都写上:
在main.cpp中包含一下各个模块的头文件,再分别创建一个对象实例,调用一些简单的函数:
这样一个多模块依赖的程序就写好了。
这里要特别注意,编译器的选择, 以及编译用的是debug模式还是release模式。
涛哥这里是msvc2017_x64版本
一般发布用release模式。
编译完成后,默认在build-xxxx-release/release/文件夹中会生成我们的exe程序。
我们将这个exe复制出来,新建一个release文件夹,放进去
这时候可以尝试双击运行它,会提示缺少dll
发布程序,其实就是把exe程序依赖的dll和相关资源都放在一起,保证双击运行即可。
我们前面提过的windeployqt.exe,是Qt提供的命令行工具,能帮助我们自动把需要的dll或资源复制过来。
可以从开始菜单找到Qt提供的命令行
注意选对版本。这种命令行在启动时已经设置好了QT的环境变量,可以直接输入windeployqt.exe
也可以用普通的命令行,使用windeployqt.exe时带上绝对路径即可。
涛哥一般用普通的命令行,因为绝对路径不易出错。
这里说一个windows启动命令行的小技巧:在release文件夹中,按住键盘shift键,然后按鼠标右键,弹出的右键菜单,
会比普通的右键菜单多一个“在此处打开命令窗口”,点击就能在release文件夹打开命令行窗口。
如果没有这个功能,就得手动输入cd指令,进入release路径。
这里通过绝对路径来使用windeployqt:
d:\qt\Online\5.12.4\msvc2017_64\bin\windeployqt.exe HelloDeploy.exe
HelloDeploy这个程序还用到了Qml,用到Qml的程序,要给windeployqt加上qmldir参数,写上你的Qml文件所在文件夹
(没用到qml的程序,不要加这一步)
d:\qt\Online\5.12.4\msvc2017_64\bin\windeployqt.exe HelloDeploy.exe –qmldir .\qml
写好windeployqt命令后按回车执行
正确执行后,release文件夹下,多了很多dll,以及一些文件夹。
这时候我们双击运行HelloDeploy.exe, 就可以正常启动了。
将整个文件夹压缩或拷贝到其它没有Qt环境的电脑上,也是可以启动的。
只要dll齐备了,制作安装包也不是问题。(后续有时间,我再写安装包制作的教程)
如果是VS编译的程序,需要将QT路径下对应的vcredist_xxx.exe带上。
如果其它电脑上有vs运行时则可以直接运行,如果没有,就需要运行一下vs运行时安装包。
或者将运行时库里面的dll复制出来即可。
一般在VS的安装路径,都有展开的dll,可以直接拷贝。
例如,涛哥电脑上的vs2017路径如下:
按实际的路径找到这几个dll,全部拷贝即可。注意x86和x64,别拿错了。
一般使用windeployqt,大部分库都能自动拷贝,不需要手动处理,
只有极少数情况下,windeployqt跑完,会缺失一些库,还要手动处理一下。
遇到这种情况,用依赖检查工具Dependencies即可快速定位问题。
Dependencies下载链接: https://github.com/lucasg/Dependencies
Dependencies 下载好,点击”DependenciesGui.exe”就可以打开界面。注意是名字带Gui的那个,不带gui的“Dependencies.exe”是命令行程序。
下面列举一些常见的错误信息
最容易出现这种错误的情况是,程序是64位编译出来的,而同级目录下的dll是32位的,
或者同级目录下没有dll,但是环境变量中指向了32位的dll。(所以涛哥没有设置环境变量)
32位和64位倒过来也是。
如果dll版本是匹配的,还有可能出现的情况是缺少第三方库。
这里说一个检查依赖的方法:
将HelloDeploy.exe重命名为HelloDeploy.dll,然后用Dependencies打开,就可以查看少哪些库
如上图,红色问号的表示缺少的库。
找齐了依赖的库,再把程序的扩展名改回exe即可。
这种情况,是QT路径下的 plugins/platforms/qwindows.dll文件没有复制过来。
注意这个dll文件直接复制到exe同级是不起作用的,要放在exe程序同级的platforms文件夹里,或者同级
的plugins/platforms文件夹里
这种情况,一般是OpenGL相关的库没有复制过来,补上就好了
我们看到,exe同级目录下,windeployqt将一堆的文件夹放在了那里,有些混乱。
涛哥观察并验证了一下,其实可以做个简单的整理。
Qt开头的文件夹都是qml的模块,剩下的文件夹除了translations都是Qt的插件,
所以新建两个文件夹qml和plugins, 分别把qml模块和插件归入其中。
这样的结构,和QT安装路径下的结构是相似的。
这也正是Qt支持的插件加载路径、qml模块加载路径。
同级的dll则是windows系统默认的动态库加载规则,不方便修改
可以参考msdn:
https://docs.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-search-order
如果你熟悉Qt的各个模块,可以进行一些裁剪。以下都是些个人经验。
不熟悉请慎重!
不熟悉请慎重!
不熟悉请慎重!
(当然静态编译也是一种裁剪的途径)
首先可以把单元测试的dll去掉
Qt5Test.dll
Qt5QuickTest.dll
如果没用到windows扩展,Qt5WinExtras.dll也可以去掉
其次,如果你不需要内置的翻译文件,translations文件夹也可以删掉
再来看一下plugins:
其中platforms是必不可少的,剩下的HelloDeploy都没用到,可以去掉。
常见程序会用的包括:
imageformats 图片格式支持
iconengines 小图标功能
sqldrivers 数据库驱动,这个保留用到的数据库足够了
其他的看情况删减。
最后看一下Qml文件夹,如果程序完全没用qml,直接删掉就好了。
按windeployqt给HelloDeploy提供的这些,逐个文件夹来说:
如果Qml中使用了Quick.Dialog(不是labs.Dialog),它本身还是依赖的labs中的东西,一般是folderlistmodel和settings,
这时候还是不要动labs了,就按照windeployqt给的放着。
Qt/WebSockets Qml的Websocket功能,用了就放着,没用可以删掉。
QtGraphicalEffects Qml的一些ShaderEffect特效,用了就放着,没用到可以删掉
QtMultimedia Qml的多媒体模块,用了就放着,没用到可以删掉
QtQml/Models.2 数据Model, 经常用。
QtQuick 这里面大部分都是Qml中常用的,QtQuick/Extras可以按情况删掉
QtQuick.2 常用的
QtTest 单元测试,删掉吧
QtWinExtras Windows扩展,没用到可以去掉
之前的文章《认清信号槽的本质》、《窥探信号槽的实现细节》讨论了一些原理,
这次我们来讨论一些信号-槽的使用细节。
要使用信号-槽功能,先决条件是继承QObject类,并在类声明中增加Q_OBJECT宏。
之后在”signals:” 字段之后声明一些函数,这些函数就是信号。
在”public slots:” 之后声明的函数,就是槽函数。
例如下面的代码:
1 | //Tom.h |
1 | //Jerry.h |
使用比较简单,先说一下使用。
信号 就是普通的类成员函数,信号只要声明(declare),不需要实现(implement),实现由moc(元对象编译器)自动生成。
信号的触发,可以用emit,也可以直接调用函数。
例如:
1 | //实例化一个tom对象 |
Qt源码的qobejctdefs.h头文件中,可以看到emit宏其实是空的。
1 | //qobejctdefs.h |
槽函数和普通的成员函数一样。。。
信号-槽特殊的地方,是moc(元对象编译器)为其生成了一份”元信息”,可以通过QMetaObject::invokeMethod的方式调用
例如:
1 | //实例化一个tom对象 |
一般在知道如何声明qobject的场景,没必要多此一举用invoke。
在一些需要”运行期反射”的情况下(头文件都没有,只知道有这么个对象,和函数的名字),invoke十分有用。
invokeMethod还可以带参数、可以获取返回值,这不是本文的重点,这里就不展开了,详细的可以参考Qt帮助文档和元对象系统。
信号可以带参数,参数的类型,必须是元对象系统能够识别的类型, 即元类型。(元对象系统后面再细说)
Qt已经将大部分常用的基础类型,都注册进了元对象系统,可以在QMetaType类中看到。
通常写的继承于QObject的子类,本身已经附带了元信息,可以直接在信号-槽中使用。
不是继承于QObject的结构体、类等自定义类型,可以通过Q_DECLARE_METATYPE宏 和 qRegisterMetaType函数进行注册,之后就可以在信号-槽中使用。
例如:
1 | struct MyStruct |
或者带命名空间的:
1 | namespace MyNamespace |
这里说明一下细节,Q_DECLARE_METATYPE宏声明过后,只是生成了元信息,可以被QVariant识别,还不能
用于队列方式的信号、槽,需要用qRegisterMetaType进行注册。而qRegisterMetaType要求”全定义”,也就是
提供类的”复制构造函数”和”赋值操作符”。
前面那种简单类型,C++编译器默认提供浅拷贝的”复制构造函数”和”赋值操作符”实现,可以直接用。
1 | struct MyStruct |
而复杂一些的类,就要提供”全定义”。
(顺带一提,信号的参数可以是任意注册过的对象,而C++11的lambda、std::bind也是对象,只要注册过,也是可以通过信号参数发送出去的。)
信号与槽,通过connect函数进行连接,之后就可以用信号去触发槽函数了。
连接的一般格式是Connectin = connect(obj1, signal1, obj2, slot1, connectType);
connect函数重载实现了多种不同的参数写法,以Qt5.12为例,大致分为三类:
元方法式、函数指针式、functor式
元方法式是最常用的写法,函数声明如下:
1 | //connect(1) 字符串式信号槽 |
Qt应用程序中用到最多的是connect(1)的写法,例如:
1 | Tom tom; |
其中SIGNAL、SLOT两个宏, 作用是将函数转换成字符串。
connect(1)的实现是靠字符串去查找元方法,以实现连接。
connect(2) 则是把信号槽的字符串换成了元方法QMetaMethod, 一般不会直接用这种写法。
connect(3)是对connect(1)的重载,非静态成员函数,本身有this指针,所以省略了receiver参数。
函数指针式写法,声明如下:
1 | //connect(4) 连接信号到qobject的成员函数 |
connect(4)用的也比较多,用法如下:
1 | Tom tom; |
信号-槽换成了C++的 取成员函数指针 的形式。
connect(4)本身的实现,比connect(1)快一些,因为省去了字符串查找的过程。
而连接建立后,从信号触发到槽函数的执行,两种写法是没有区别的。
在一些需要”运行期反射”的情况下(头文件都没有,只知道有这么个对象,和函数的名字),只能用connect(1)。
connect(5)可以连接信号到任意非成员函数指针上。除了槽函数,普通的函数也可以连接。这种连接不支持设置连接类型,可以看作是单纯的函数调用。
connect(6)是对connect(5)的重载,增加了一个context对象代替reveicer对象的作用。这种连接是可以设置连接类型的。
信号-槽函数有重载的情况下,写函数指针式connect会报错,就需要类型转换。
比如:QLocalSocket有一个成员函数error,也有一个信号error,直接写connect会报错的。
Qt为我们提供了QOverload这个模板类,以解决这个问题。
1 | //连接重载过的函数,使用QOverload做leixing 转换 |
编译器支持C++14,还可以用qOverload模板函数
1 | //连接重载过的函数,使用QOverload做leixing 转换 |
还有像QNetworkReply::error、QProcess::finished等等,都有重载,用的时候要转换处理一下。
问: 什么是functor?functor有什么用?
答: 在C++11之前, Qt通过自己的实现来推导函数指针及其参数,即QtPrivate::FunctionPointer, 用来处理信号-槽的连接。
C++11带来了lambda, 以及std::bind和std::function, std::function本身可以存储lambda、std::bind以及FunctionPointer。
这时候Qt已有的connect(4)、connect(5)、connect(6)是可以支持FunctionPointer的,而新出现的lambda以及std::bind是不支持的,
QtPrivate::FunctionPointer推导不出这些类型。所以Qt把这些不支持的新类型(主要是lambda和std::bind)称为functor(文档和源码都这么命名),
并增加了connect(7)和connect(8)以支持functor。
functor式写法,声明如下:
1 | //connect(7) 连接信号到任意functor |
connect(7)可以连接信号到任意lambda、std::bind上。
connect(8)是对(7)的重载,增加了一个context对象代替reveicer对象的作用。这种连接是可以设置连接类型的。
connectType为连接类型,默认为AutoConnection,即Qt自动处理,大部分情况下也不用管。个别情况,需要手动指定。
可选的连接类型有
自动 AutoConnection
直连 DirectConnection
队列 QueuedConnection
唯一连接 UniqueConnection
自动处理的逻辑是,如果发送信号的线程和receiver在同一个线程,就是DirectConnection(直接函数调用),不是同一个线程,则转换为QueuedConnection。
1 |
|
下面举例一些需要手动指定连接类型的场景:
例1-跨多个线程:
A线程中写connect,让B线程中的信号连到C线程的槽中,希望C的槽在C中执行。
这种情况要明确指定QueuedConnection,不写的话按照Auto处理,C中的槽会在A中执行。
例2-跨线程DirectConnection
(这种用法在Qml的渲染引擎SceneGraph中比较常见)。
A线程为内部代码,不能修改,一些特定的节点会有信号发出。
B线程为用户代码,有一些功能函数,希望在A线程中去执行。
这种情况,将A的信号连接到B的函数,连接方式指定为DirectConnection,就可以把B的函数插入到A线程发信号的地方了。
效果类似于子类重载父类的函数。
connect的返回值为QMetaObject::Connection,代表一个连接。大部分情况下,不用管返回值。
Connection可以用来验证链接是否有效,可以用来断开连接。
一般用disconnect函数就可以断开连接;而signal-functor的这种形式的连接,没有object的存在,只能用Connection断开。
]]>这次讨论Qt信号-槽的实现细节。
上次的文章《认清信号槽的本质》中介绍过,信号-槽是一种对象之间的通信机制,是
Qt在标准C++之外,使用元对象编译器(MOC)实现的语法糖。
这次通过一个简单的案例,学习一些信号-槽的实现细节。
还是拿上次的设定来说明:Tom有个技能叫”喵”,就是发出猫叫,而正在偷吃东西的Jerry,听见猫叫声就会逃跑。
我们用信号-槽的方式写出来。
1 | //Tom.h |
1 | //Jerry.h |
以上面的代码为例,要使用信号-槽功能,先决条件是继承QObject类,并在类声明中增加Q_OBJECT宏。
之后在”signals:” 字段之后声明一些函数,这些函数就是信号。
在”public slots:” 之后声明的函数,就是槽函数。
接下来看看我们的main函数:
1 | //main.cpp |
信号-槽都准备好了,接下来创建两个对象实例,并使用QObject::connect将信号和槽连接起来。
最后使用emit发送信号,就会自动触发槽函数了。
运行结果:
信号和槽的本质都是函数。
我们知道C++中的函数要有声明(declare),也要有实现(implement),
而信号只要声明,不需要写实现。这是因为moc会为我们自动生成。
另外触发信号时,不写emit关键字,直接调用信号函数,也是没有问题的。
我们来看一下Q_OBJECT宏,展开如下:
(不同的Qt版本有些差异,涛哥这里用的是5.12.4,以此为例)
1 | public: \ |
我们看到,关键的地方,是声明了一个只读的静态成员变量staticMetaObject,以及3个public的成员函数
1 | static const QMetaObject staticMetaObject; |
还有一个private的静态成员函数qt_static_metacall
1 | static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **) |
那么声明的这些成员变量/函数,在哪里实现?答案是moc生成的cpp文件。
如上图所示目录结构,项目编译完成后,在build文件夹中,自动生成了moc_Jerry.cpp 和 moc_Tom.cpp两个文件
其中moc_Tom.cpp内容如下:
1 | /**************************************************************************** |
可以大致看出,生成的cpp文件中,就是变量staticMetaObject以及 那几个函数的实现。
staticMetaObject是一个结构体,用来存储Tom这个类的信号、槽等元信息,并把
qt_static_metacall静态函数作为函数指针存储起来。
因为是静态成员,所以实例化多少个Tom对象,它们的元信息都是一样的。
qt_static_metacall函数提供了两种“元调用的实现”:
如果是InvokeMetaMethod类型的调用,则直接 把参数中的QObject对象,
转换成Tom类然后调用其miao函数
如果是IndexOfMethod类型的调用,即获取元函数的索引号,则计算miao函数的偏移并返回。
而moc_Tom.cpp末尾的
1 | // SIGNAL 0 |
就是信号函数的实现。
miao信号的实现,直接调用了QMetaObject::activate函数。其中0代表miao这个函数的索引号。
QMetaObject::activate函数的实现,在Qt源码的QObject.cpp文件中,略微复杂一些,
且不同版本的Qt,实现差异都比较大,这里总结一下大致的实现:
先找出与当前信号连接的所有对象-槽函数,再逐个处理:
这里处理的方式,分为三种:
1 | if((c->connectionType == Qt::AutoConnection && !receiverInSameThread) |
receiverInSameThread表示当前线程id和接收信号的对象的所在线程id是否相等。
如果信号-槽连接方式为QueuedConnection,不论是否在同一个线程,按队列处理。
如果信号-槽连接方式为Auto,且不在同一个线程,也按队列处理。
如果信号-槽连接方式为阻塞队列BlockingQueuedConnection,按阻塞处理。
(注意同一个线程就不要按阻塞队列调用了。因为同一个线程,同时只能做一件事,
本身就是阻塞的,直接调用就好了,如果走阻塞队列,则多了加锁的过程。如果槽中又发了
同样的信号,就会出现死锁:加锁之后还未解锁,又来申请加锁。)
队列处理,就是把槽函数的调用,转化成了QMetaCallEvent事件,通过QCoreApplication::postEvent
放进了事件循环, 等到下一次事件分发,相应的线程才会去调用槽函数。
关于事件循环,可以参考之前的文章《Qt实用技能3-理解事件循环》
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/
]]>这次讨论Qt信号-槽相关的知识点。
信号-槽是Qt框架中最核心的机制,也是每个Qt开发者必须掌握的技能。
网络上有很多介绍信号-槽的文章,也可以参考。
涛哥的专栏是《Qt进阶之路》,如果连信号-槽的文章都没有,将是没有灵魂的。
所以这次涛哥就由浅到深地说一说信号-槽。
如果一上来就讲一大堆概念和定义,读者很容易读睡着。所以涛哥从一个故事/场景开始说起。
涛哥小时候喜欢看动画片《猫和老鼠》, 里面有汤姆猫(Tom)和杰瑞鼠(Jerry)斗智斗勇的故事。。。
现在做个简单的设定:Tom有个技能叫”喵”,就是发出猫叫,而正在偷吃东西的Jerry,听见猫叫声就会逃跑。
我们尝试用C++面向对象的思想,描述这个设定。
先是定义Tom和Jerry两种对象
1 | //Tom的定义 |
接下来模拟场景
1 | int main(int argc, char *argv[]) |
这个场景看起来很简单,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 | class Connections |
那么这个映射关系存储在哪里呢? 显然是一个Tom和Jerry共有的”上下文环境”中。
我们用一个全局变量来表示,这样就可以简单地模拟了:
1 | //全局共享的Connections。 |
看一下运行结果:
RunAway没有被直接调用,而是被自动触发。
分析:这里是以”mouse”这个字符串作为连接tom和jerry的关键。这只是一种简单、粗糙的示例实现。
在GOF四人帮的书籍《设计模式》中,有一种观察者模式,可以比较优雅地实现同样的功能。
(顺便说一下,GOF总结的设计模式一共有23种,涛哥曾经用C++11实现了全套的,github地址是:https://github.com/jaredtao/DesignPattern)
初级的观察者模式,涛哥就不重复了。这里涛哥用C++11搭配一点模板技巧,实现一个更加通用的观察者模式。
也可以叫发布-订阅模式。
1 | //Subject.hpp |
1 | //main.cpp |
任意类只要继承Subject模板类,提供观察者参数,就拥有了发布-订阅功能。
信号-槽 是Qt自定义的一种通信机制,它不同于标准C/C++ 语言。
信号-槽的使用方法,是在普通的函数声明之前,加上signal、slot标记,然后通过connect函数把信号与槽 连接起来。
后续只要调用 信号函数,就可以触发连接好的信号或槽函数。
连接的时候,前面的是发送者,后面的是接收者。信号与信号也可以连接,这种情况把接收者信号看做槽即可。
信号-槽要分成两种来看待,一种是同一个线程内的信号-槽,另一种是跨线程的信号-槽。
同一个线程内的信号-槽,就相当于函数调用,和前面的观察者模式相似,只不过信号-槽稍微有些性能损耗(这个后面细说)。
跨线程的信号-槽,在信号触发时,发送者线程将槽函数的调用转化成了一次“调用事件”,放入事件循环中。
接收者线程执行到下一次事件处理时,处理“调用事件”,调用相应的函数。
(关于事件循环,可以参考专栏上一篇文章《Qt实用技能3-理解事件循环》)
信号-槽的实现,借助一个工具:元对象编译器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的本质,其实是一个反射器。标准C++没有反射功能(将来会有),所以Qt用moc实现了反射功能。
什么叫反射呢? 简单来说,就是运行过程中,获取对象的构造函数、成员函数、成员变量。
举个例子来说明,有下面这样一个类声明:
1 | class Tom { |
类的使用者,看不到类的声明,头文件都拿不到,不能直接调用类的构造函数、成员函数。
从配置文件/网络拿到了一段字符串“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
这次讨论事件循环相关的知识点。
从Hello World说起吧
1 |
|
这是一段大家都很熟悉的命令行程序,运行起来会在终端输出”Hello World”,之后程序就退出了。
我们稍微加点需求: 程序能够一直运行,每次用户输入一些信息并按下回车时,打印出用户的输入。直到输入的内容为“quit”时才退出。
按照这个需求,代码实现如下:
1 |
|
我们使用了一个while循环。在这个循环体内,不停地处理用户的输入。当输入的内容为”quit”时,循环终止条件被设置为true,循环将终止。
在上面这个例子中,“用户输入并按下回车”这件事情,我们可以称作一个“事件”或者“用户输入事件”,不停的去处理“事件”的这段代码,
我们可以称作“事件循环”, 也可以叫做”消息循环”,是一回事。
一般对于带UI窗口的程序来说,“事件”是由操作系统或程序框架在不同的时刻发出的。
当用户按下鼠标、敲下键盘,或者是窗口需要重新绘制的时候,计时器触发的时候,都会发出一个相应的事件。
我们把“事件循环”的代码 提炼/抽象 如下:
1 | function loop() { |
在事件循环中, 不停地去获取下一个事件,然后做出处理。直到quit事件发生,循环结束。
有“取事件”的过程,那么自然有“存储事件”的地方,要么是操作系统存储,要么是软件框架存储。
存储事件的地方,我们称作 “事件队列” Event Queue
处理事件,我们也称作 “事件分发” Event Dispatch
先来看一个Windows系统的事件循环示例(win32 API):
1 | MSG msg = { 0 }; |
思路和前面介绍的一致
有些linux系统使用X11窗口系统,看看其窗口事件循环
1 | Atom wmDeleteMessage = XInternAtom(mDisplay, "WM_DELETE_WINDOW", False); |
思路也是和前面一致的
在Cocoa Application中, 有一种获取事件的机制,叫做runloop(一个NSRunLoop对象,它允许进程接收窗口服务的各种事件)
一般的Cocoa Application运行流程是,从runloop的事件队列中获取一个事件(NSEvent)
派发事件(NSEvent)到合适的对象(Object)
事件被处理完成后,再取下一个事件(NSEvent),直到应用退出.
思路也是和前面一致的。
Qt作为一个跨平台的UI框架,其事件循环实现原理, 就是把不同平台的事件循环进行了封装,并提供统一的抽象接口。
和Qt做了类似工作的,还有glfw、SDL等等很多开源库。
QEventLoop即Qt中的事件循环类,主要接口如下:
1 | int exec(QEventLoop::ProcessEventsFlags flags = AllEvents) |
其中exec是启动事件循环,调用exec以后,调用exec的函数就会被“阻塞”,直到EventLoop里面的while循环结束。
这里画个简单的示意图:
exit是退出事件循环(将EventLoop中的退出标识设为true)
processEvents是及时处理队列中的事件(这个很有用,后面还会讲)。
这里有个问题,exec阻塞了当前函数,还怎么退出EventLoop呢?
答案是:在派发事件后,某个事件处理的函数中,达到事件退出条件时,调用exit函数,将EventLoop中的退出标识设为true。
这样的程序运行流程,我们叫做 “事件驱动”式的程序。
一般的Qt程序,main函数中都有一个QCoreApplication/QGuiApplication/QApplication,并在末尾调用 exec。
1 | int main(int argc, char *argv[]) |
Application类中,除去启动参数、版本等相关东西后,关键就是维护了一个QEventLoop,Application的exec就是QEventLoop的exec。
不过Application中的这个EventLoop,我们称作“主事件循环”Main EventLoop。
所有的事件分发、事件处理都从这里开始。
Application还提供了sendEvent和poseEvent两个函数,分别用来发送事件。
sendEvent发出的事件会立即被处理,也就是“同步”执行。
postEvent发送的事件会被加入事件队列,在下一轮事件循环时才处理,也就是“异步”执行。
还有一个特殊的sendPostedEvents,是将已经加入队列中的准备异步执行的事件立即同步执行。
以QWidget为例来说明。
QWidget是Widget框架中,大部分UI组件的基类。QWidget类拥有一些名字为xxxEvent的虚函数,比如:
1 | virtual void keyPressEvent(QKeyEvent *event) |
keyPressEvent就表示按键按下时的处理,keyReleaseEvent表示按键松开时的处理。
主事件循环中(注册过QWidget类之后),事件分发会在按键按下时调用QWidget的keyPressEvent函数,按键松开时调用QWidget的keyReleaseEvent函数。
有了上面的事件处理机制,我们就可以在自己的QWidget子类中,通过重载keyPressEvent、keyReleaseEvent等等事件处理函数,做一些自定义的事件处理。
每一个事件处理函数,都是带有参数的,这个参数是QEvent的子类,携带了各种事件的参数。比如
按键事件 void keyPressEvent(QKeyEvent *event) 中的QKeyEvent, 就包括了按下的按键值key、 count等等。
Qt还提供了事件过滤机制,在事件分发之前先过滤一部分事件。
用法如下:
1 | class KeyPressEater : public QObject |
自定义一个QObject子类,重载eventFilter函数。之后在要过滤的QObject对象上,调用installEventFilter函数以安装过滤器上去。
过滤器函数的返回值为bool,true表示这个事件被过滤掉了,不用再往下分发了。false表示没有过滤。
我们的UI界面,要持续不断地刷新(对于QWidget就是触发paintEvent事件),以保证显示流畅、能及时响应用户输入。
一般要有一个良好的帧率,比如每秒刷新60帧, 即经常说的FPS 60, 换算一下 1000 ms/ 60 ≈ 16 ms,也就是每隔16毫秒刷新一次。
而我们有时候又需要做一些复杂的计算,这些计算的耗时远远超过了16毫秒。
在没有计算完成之前,函数不会退出(相当于阻塞),事件循环得不到及时处理,就会发生UI卡住的现象。
这种场景下,就可以使用Qt为我们提供的接口,立即处理一次事件循环,来保证UI的流畅
(后续再讨论多线程)
1 | //耗时操作 |
经常会有这种场景: “触发 ”了某项操作,必须等该操作完成后才能进行“ 下一步 ”
比如:软件的登录界面,向服务器发起登录请求后,必须等收到服务器返回的登录数据,才知道登录结果并决定下一步如何执行。
这种场景,如果设计成异步调用,直接用Qt的信号/槽即可,如果要设计成同步调用,就可以使用本地QEventLoop
这里写段伪代码示例一下:
1 | bool login(const QString &userName, const QString &passwdHash, const QString &slat) |
这次涛哥将会教大家移植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网站
用一些Shader代码和简单的纹理,就可以输出各种酷炫的图形效果和音频效果。
如果你还不知道,赶紧去看看吧https://www.shadertoy.com
顺便提一下,该网站的作者是IQ大神,这里有他的博客:
http://www.iquilezles.org/www/articles/raymarchingdf/raymarchingdf.htm
本文主要讨论图形效果,音频效果以后再实现。
Qml中实现ShaderToy,最快的途径就是ShaderEffect了。
上一篇文章《Qml特效-着色器效果ShaderEffect》已经介绍过ShaderEffect了, 本文重点是移植ShaderToy。
在涛哥写这篇文章之前,已经有两位前辈做过相关的研究。
陈锦明: https://zhuanlan.zhihu.com/p/38942460
qyvlik: https://zhuanlan.zhihu.com/p/44417680
涛哥参考了他们的实现,做了一些改进、完善。
在此感谢两位前辈。
下面正文开始
OpenGL的可编程渲染管线中,着色器代码是可以动态编译、加载到GPU运行的。
而OpenGL又包括了桌面版(OpenGL Desktop)、嵌入式版(OpenGL ES)以及网页版(WebGL)
ShaderToy网站是以WebGL 2.0为基础,提供内置函数、变量,并约定了一些输入变量,由用户按照约定编写着色器代码。
只要不是太老的OpenGL版本,内置函数、变量基本都是通用的。
ShaderToy网站约定的变量如下:
1 | vec3 iResolution image/buffer The viewport resolution (z is pixel aspect ratio, usually 1.0) |
Qml中的相应实现
1 | ShaderEffect { |
其中时间、日期通过Timer刷新,鼠标位置用MouseArea刷新。
同时涛哥导出了hoverEnabled、running属性和restart函数,以方便Qml中控制Shader的运行。
1 | ShaderEffect { |
GLSL Versions
OpenGL Version | GLSL Version |
---|---|
2.0 | 110 |
2.1 | 120 |
3.0 | 130 |
3.1 | 140 |
3.2 | 150 |
3.3 | 330 |
4.0 | 400 |
4.1 | 410 |
4.2 | 420 |
4.3 | 430 |
GLSL ES Versions (Android, iOS, WebGL)
OpenGL ES Version | GLSL ES Version |
---|---|
2.0 | 100 |
3.0 | 300 |
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 |
|
versionString 这里,主要测试了Desktop和 android设备,Desktop只要显卡不太搓,都能运行的。
Android ES3的也是全部支持,ES2的部分不能运行,比如iq大神的蜗牛Shader,使用了textureLod等一系列内置函数,就不能在ES2上面跑。
本来是不需要写顶点着色器的。如果我们想把ShaderToy做成一个任意坐标开始的Item来用,就需要适配一下坐标。
涛哥写的顶点着色器如下,仅在默认着色器的基础上,传递qt_Vertex给下一阶段的vertex
1 |
|
片段着色器这里处理一下,适配出一个符合shaderToy的mainImage作为入口函数
1 | readonly property string startCode: " |
稍微说明一下,qyvlik大佬的Shader使用gl_FragCoord作为片段坐标传进去了,这种用法的ShaderToy坐标将会占据整个Qml的窗口,
而实际ShaderToy坐标不是整个窗口的时候,超出去的地方就会被切掉,显示出来的只有一小部分。
涛哥研究了一番后,顶点着色器把vertex传过来,vertex.x就是x坐标,vertex.y坐标从上到下是0 - height,而gl_FragCoord 从下到上是0 - height,
所以要翻一下。
最后,看一下代码的全貌吧
1 | //TaoShaderToy.qml |
这次涛哥将会教大家一些ShaderEffect(参考QmlBook,译作:着色器效果)的相关知识。
前面的文章,给大家展示了进场动画,以及页面切换动画,大部分都使用了ShaderEffect,所以这次专门来说一下ShaderEffect。
《玩转Qml》系列文章,配套了一个优秀的开源项目:TaoQuick
github https://github.com/jaredtao/TaoQuick
访问不了或者速度太慢,可以用国内的镜像网站gitee
https://gitee.com/jaredtao/TaoQuick
动画只能控制组件的属性整体的变化,做特效需要精确到像素。
Qml中提供了ShaderEffect这个组件,就能实现像素级别的操作。
ShaderEffect允许我们在Qml的渲染引擎SceneGraph上,利用强大的GPU进行渲染。
使用ShaderEffect,需要有一些图形学知识,了解GPU渲染管线,了解图形API如OpenGL、DirectX等,同时也需要一些数学知识。
图形学的知识体系还是非常庞大的,要系统的学习,需要看很多书籍。入门级的比如“红宝书”《OpenGL编程指南》、“蓝宝书”《OpenGL超级宝典》……
一篇文章是说不完的,涛哥水平也有限。所以本文从实用的角度出发,按照涛哥自己的理解,提炼一些必要的知识点,省略一些无关的细节,
让各位Qt开发者能了解GPU原理,能看懂、甚至于自己写一些简单的着色器代码,就大功告成了。说的不对的地方,也欢迎大佬来指点。
先来了解一下,显示器是如何显示出各种色彩的。
假如我们把显示器的画面放大100倍,就会看到很多整齐排列的像素点。
继续放大,就会发现每个像素点,由三种发光的元件组成,这三种元件分别发出红、绿、蓝三种颜色的光。三种颜色的光组合在一起,
就是人眼看到的颜色。这就是著名的RGB颜色模型。
如果把这三种光的亮度分为255个等级,就能组合出16777216种不同颜色的光。
GPU的任务,就是通过计算,给出每一个像素的红、绿、蓝 (简称r g b)三种颜色的数值,让显示器去”发出相应的光”。
(这样说可能不太严谨、不太专业,只是方便大家理解。另一方面,本文的目的,
是让大家学习如何写特效,不是去造显卡/造显示器。所以请专业人士见谅!)
注:参考[1]
我们以画一个填充色的三角形为例,来说明
下图是一个简易的渲染管线,引用自 LearnOpenGL
画一个三角形,要经历顶点着色器、图元装配、几何着色器、片段着色器、光栅化等阶段。
其中蓝色部分是可以自定义的,自定义是指,按照图形API规范,写一段GPU能编译、运行的代码。
(这种代码就是着色器代码。可以自定义的这种渲染管线,就是可编程渲染管线,与之相对的是古老的固定渲染管线。)
这里各个阶段,分别引用一下,LearnOpenGL中的介绍(看不懂可以先跳过,看我画的图):
1 | 1 管线的第一个部分是顶点着色器(Vertex Shader),它把一个单独的顶点作为输入。顶点着色器主要的目的是 |
概念还是挺多的,而且很多教程都有渲染管线图。但是涛哥觉得,对于我们开发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 | import QtQuick 2.0 |
这段代码的效果是
左边是本来的绿色的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 | void main() |
这段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 | void main() |
这里使用了纹理
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 是内置变量,表示所在片段着色器的最终的输出颜色。
这是《Qml特效-进场动画》系列文章的第二篇,涛哥将会教大家一些Qml进场动画相关的知识。
《玩转Qml》系列文章,配套了一个优秀的开源项目:TaoQuick
github https://github.com/jaredtao/TaoQuick
访问不了或者速度太慢,可以用国内的镜像网站gitee
https://gitee.com/jaredtao/TaoQuick
梯度效果,支持从四个方向梯度出现
通过数值动画,控制百分比属性percent从0 到100变化
1 | //AGrad.qml |
在Shader中,使用glsl片段着色器实现像素的控制:
1 | in vec2 qt_TexCoord0; |
效果比较简单,以从左向右为例(dir == 0), 说明一下:
先是把percent 归一化处理 (float p = percent / 100.0),
纹理坐标qt_TexCoord0.x的取值范围为 0 - 1,按照Qml的坐标系统,左边为0,右边为1。
之后纹理坐标与p进行比较,坐标小于p则显示(透明度为1),大于p则不显示(透明度为0). (也可以直接用discard丢弃片段)
step是glsl内置函数,step(p, qt_TexCoord0.x) 就是x小于p返回0,大于等于p返回1。 结果正好与上面分析的相反,用1 减去即可: alpha = 1.0 - step(p, qt_TexCoord0.x);
最终输出颜色即可:
fragColor = vec4(color.rgb, alpha);
]]>这次涛哥将会教大家一些Qml动画相关的知识。
《玩转Qml》系列文章,配套了一个优秀的开源项目:TaoQuick
github https://github.com/jaredtao/TaoQuick
访问不了或者速度太慢,可以用国内的镜像网站gitee
https://gitee.com/jaredtao/TaoQuick
第一篇文章,就放一个简单的动画效果
进场动画,使用了QtQuick的动画系统,以及ShaderEffect特效。
Qml中有一个模块QtGraphicalEffects,提供了部分特效,就是使用ShaderEffect实现的。
使用ShaderEffect实现特效,需要有一些OpenGL/DirectX知识,了解GPU渲染管线,同时也需要一些数学知识。
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 | Rectangle { |
on语法可以使用动画组件,也可以用Behavior,直接on某个特定的属性即可。效果一样。
on动画中,如果直接指定了running属性,默认就会执行这个动画。
也可以不指定running属性,其它地方修改这个属性时,会自动按照动画来执行。
示例代码 on动画
1 | Rectangle { |
示例代码 Behavior 动画
1 | import QtQuick 2.0 |
过渡动画和状态机动画,本质还是直接使用动画组件。只不过是把动画声明并存储起来,以在状态切换时使用。
这里先不细说了,后面会有系列文章<Qml特效-页面切换动画>,会专门讲解。
动画只能控制组件的属性整体的变化,做特效需要精确到像素。
Qml中提供了ShaderEffect这个组件,就能实现像素级别的操作。
大名鼎鼎的ShaderToy网站,就是使用Shader实现各种像素级别的酷炫特效。
ShaderToy上面的特效都是可以移植到Qml中的。
使用Shader开发,需要一定的图形学知识。其中使用GLSL需要熟悉OpenGL, 使用HLSL需要熟悉DirectX。
封装了一个平移进入的动画组件,能够支持从四个方向进场。
1 | //ASlowEnter.qml |
进场组件的使用
1 | //Enter.qml |