在Qt5之前,GUI开发使用的是现在的QtWidgets,已经被大家所熟悉,ui描述界面布局组合C++细节实现,实现效果很好,界面开发的速度还算中肯。在不具备开发自定义UI又需跨平台(C++源码)的组织和个人面前,几乎是不二选择。但在移动开发面前不灵了,因为QtWidgets本质是使用平台绘图工具来绘绘制界面,而每个界面的绘制都需要各自有自己的绘制状态,这个消耗对于手机这类移动平台来说是不可忽视的
Qt Quick 一大特色在于其改变了界面渲染方式,自Qt Quick 2起统一使用OpenGL ES 2.0 或者 OpenGL 2.0 来渲染界面。这样做的好处是,所有要渲染的界面元素均在最后统一提供给OpenGL,极大减少状态切换时间和渲染时间(相比于之前使用QPainter依次为每个界面元素渲染,不断地重复渲染状态)。举例来讲,就像写文件,把要写的内容存在缓存后统一写入文件,一定比每次写入都重复打开关闭文件要快。
既然说到场景,总得有个管理类,它就是QQuickWindow。包括QtQuick中常用的QQuickView也继承于QQuickWindow。换句话说,场景不是单独使用的,必须配合QQuickWindow。如果用户(使用C++)定义的Item想把内容添加到场景中,那么就调用QQuickItem::updatePaintNode()。
再继续深入说下场景的好处。场景是QML文件中Item对象的图形表达,一个包含要显示的所有信息又完全独立于Item的结构。也就是说,一旦Item对象把图形参数传递给场景,之后的渲染再和Item无关了,Item对象就是用来传图形参数的。除此之外,在很多平台上,渲染线程和主纯种(GUI线程)是不同的两个线程,两个可以并行着来,这点上又提升了效率。
场景由预定义好的类型的对象组成,其本质是由继承于QQuickItem的类型的节点组成的节点树。这个节点树类似于图形引擎中的场景树,不过这个场景树目前主要用于处理二维内容(不排队Qt公司有自己做图形引擎的想法)。场景树将图形内容提供给OpenGL来统一渲染,自己不做任何渲染工作,即使是在vitual paint()函数内。
只要用户使用QML的Item及其子类来构建界面,QQuickWindow就获得场景树,会维护这个场景树,并最终回收资源。场景树对于只使用QML和Javascript的开发人员来说是完全透明的。
注意:Scene Graph的场景树每次渲染时都会做裁剪,没有改变的场景是不更新的。也就是说如果用户点了一个按钮告诉后台完成一个计算,计算完成后界面元素按说应该变化的,但因为Scene Graph查看当前界面元素没有变化,它就不会更新这块的结点。举个例子:
MyItem { // 自定义类,继承于QQuickItem
id: item
Rectangle{
MouseArea {
onClicked: {
item.calc() // 自定义计算过程,更新要显示的内容。但计算后界面内容不会改变,因为Scene Graph检查这块时没有发现变化!!!
}
}
}
}
改为如下就可以自动更新:
MyItem { // 自定义类,继承于QQuickItem
id: item
Item {id: invisibleItem}
function forceUpdate(){
invisileItem.visible = !invisileItem.visible
}
Rectangle{
MouseArea {
onClicked: {
item.calc()
forceUpdate() //Scene Graph检查场景有变化,于是更新场景。仅管实际界面上没有变化
}
}
}
}
最常用的场景结点是QSGGeometryNode,它是用来为图形定义几何形体和光学材质的。其中QSGGeometryNode使用QSGGeometry来表达几何形体(点、线、多边形及3D图形)。材质定义了像素被如何填充。
目前可用节点包括:
类名 | 描述 |
---|---|
QSGClipNode | 表达裁剪信息的节点 |
QSGGeometryNode | 表达几何信息的节点 |
QSGNode | 场景中所有节点的基类 |
QSGOpacityNode | 表达透明信息 |
QSGTransformNode | 表达场景中矩阵操作信息 |
注意:一般来讲,OpenGL的渲染进程不同于当前(GUI)线程,其添加节点的是通过QQuickItem::updatePaintNode()
。所以(C++)自定义节点是通过继承QQuickItem,重写updatePaintNode()
并设置 QQuickItem::ItemHasContents
来添加到场景的。而最好的定义节点内容办法是在QQuickItem::updatePaintNode()
中使用以QSG
开头的类。
更多详情请见Scene Graph - Custom Geometry。
场景节点会在渲染前通过QSGNode::preprocess()
被统一调用。开发人员需要继承QSGNode
(或其子节点),重写QSGNode::preprocess()
,并设置QSGNode::UsePreprocess
为渲染做准备。
节点所有权是通过显示在构造时指定或通过设置QSGNode::OwnedByParent
来指定。一般来讲,将节点将给场景树管理是比较好,因为开发人员不必关心资源回收问题。
材质确定了几何图形在屏幕上的点如何被着色。它封装了一段OpenGL着色代码(Shader Program),提供了足够的灵活性让开发人员决定如何着色。仅管目前大多数Item
对象使用最简单的着色代码。
如何开发人员想修改材质,在QML代码中使用ShaderEffect就可以实现一些特定效果。更高级地需使用C++代码。
目前可用的材质类如下。
类名 | 描述 |
---|---|
QSGFlatColorMaterial | 单色材质类 |
QSGMaterial | 保存着色程序状态的类 |
QSGMaterialShader | OpenGL着色代码类 |
QSGMaterialType | 和QSGMaterial配合使用时的唯一标识符 |
QSGOpaqueTextureMaterial | 不透明纹理材质类 |
QSGSimpleMaterial | 和QSGSimpleMateralShader配合使用时的着色状态保存类的模板类 |
QSGSimpleMaterialShader | 场景着色程序的基类 |
QSGTextureMaterial | 纹理材质类 |
QSGVertexColorMaterial | 顶点材质类 |
详情请见Scene Graph - Simple Material
场景中所有节点的设计初衷是更好的性能,而不是易用。而为了实现好的着色效果,需要非常多的代码。因此,两个类被设计来简化工作:
类名 | 描述 |
---|---|
QSGSimpleRectNode | 继承于QSGGeometryNode,定义了一个被赋予单色材质的矩形图形的节点类 |
QSGSimpleTextureNode | 继承于QSGGeometryNode,定义了一个被赋予纹理材质的矩形图形的节点类 |
场景渲染是在QQuickWindow内部完成的,外部无法访问。但在渲染管线上有几个点可以供开发人员插入自定义节点代码或直接访问OpenGL。
渲染如何具体工作请参照Qt Quick Scene Graph Renderer。
场景渲染有三种方式:basic, windows 和 threaded。其中basic 和 windows 是单线程,而threaded是指定线程内渲染。Qt会根据情况自动选择使用哪种方式。当性能不满足,或者出于测试考虑时,可以强制启动QSG_RENDER_LOOP
。想知道具体是哪种方式,需要在启动应用时添加参数将QSG_INFO
设置为1.
注意: windows 和 threaded方式非常依赖于OpenGL将交换间隔设定为1.一些显卡驱动允许用户覆盖或关闭这个值,并忽略Qt的修改请求。但如果没有这个设置,会导致交换间隔太短,CPU满负荷运转。如果知道系统不能自动调整vsync-based
,请手动设置QSG_RENDER_LOOP=basic
来启动basic渲染方式。
threaded渲染方式使用独立线程来渲染,由于使用多线程,性能得到显著提升,且界面反馈更加流畅。其简易示意图如下:
这两种方式使用单线程渲染,但写代码时要按照threaded渲染方式来写,方便日后移植。其简易示意图如下:
QQuickRenderControl
自定义渲染方式如果不想使用系统渲染,开发人员可以使用QQuickRenderControl
来完全控制渲染过程。这时,美化、同步和渲染都由应用来控制。
场景提供两种方式来整合OpenGL内容:直接使用OpenGL代码或者直接在场景中添加纹理节点。
注意:不管理采用哪种方式,都要保存使用同一个OpenGL环境,否则将产生未知错误。
注意:渲染代码要做到线程安全,因为渲染线程很可能不在主(GUI)线程中。
通过槽连接QQuickWindow::beforeRendering()
或QQuickWindow::afterRendering()
,让OpenGL在场景渲染前或后来执行OpenGL代码。其结果就是OpenGL内容在场景内容下(被覆盖)或者在其上。这种方式的优点是不需额外使用帧缓存或者内存,缺点是OpenGL渲染时机固定。这种方式不能手动update元素内容,只能通过在这个元素区域内有其他元素发生变化(比如在QML中调用这个元素区域内一个不可见元素的visible属性变一变)才会更新,这是其一大缺点。可参考Scene Graph - OpenGL Under QML
这种方式需借助于QQuickFramebufferObject
类,场景会将此节点添加到其中。只要给当前元素设置QQuickItem::setFlag(ItemHasContents)
,Scene Graph就会调用updatePainedNode更新这个元素内容。这种方式与Scene Graph的结合性最好。具体可参考Scene Graph - Rendering FBOs,thread方式可参考Scene Graph - Rendering FBOs in a thread。
场景提供了一些日志功能方便使用。
类名 | 描述 |
---|---|
qt.scenegraph.time.texture | 记录加载纹理时间 |
qt.scenegraph.time.compilation | 记录着色器编译时间 |
qt.scenegraph.time.renderer | 记录渲染时间 |
qt.scenegraph.time.renderloop | 记录渲染循环时间 |
qt.scenegraph.time.glyph | 记录准备glyphs图的时间 |
qt.scenegraph.info | 记录所以信息 |
qt.scenegraph.renderloop | 记录沉浸循环状态信息 |
参考:Scene Graph