13.动态 QML(Dynamic QML)
本章的作者:e8johan
** 注意: **
最新的构建时间:2016/03/21
这章的源代码能够在assetts folder找到。
到目前为止,我们已经将 QML 作为构建静态场景并在它们之间进行导航的工具。根据各种状态和逻辑规则,构建实时和动态的用户界面。通过以更加动态的方式处理 QML 和 JavaScript,灵活性和可能性进一步扩大。组件可以在运行时加载和实例化,元素可以被销毁。动态创建的用户界面可以保存到磁盘,然后恢复。
13.1 动态加载组件
动态加载 QML 不同部分的最简单方法是使用 Loader 元素。它作为正在加载的项目的占位符。要加载的项目通过 source 属性或 sourceComponent 属性进行控制。前者从给定的 URL 加载项目,而后者从一个实例化的组件加载项目。
由于加载器作为加载项目的占位符,其大小取决于项目的大小,反之亦然。如果 Loader 元素被设置大小(通过设置宽度和高度或通过锚定),那么通过加载器加载的项目将被赋予加载器的大小。如果装载程序没有设置大小,则根据要加载的项目的大小调整大小。
下面描述的示例演示如何使用 Loader 元素将两个单独的用户界面部件加载到同一空间。想法是具有数字或模拟的快速拨号,如下图所示。拨盘周围的代码不受任何加载的项目的影响。
应用程序的第一步是声明一个 Loader 元素。请注意,source 属性没有被设置。这是因为源取决于用户界面的状态。
Loader {
id: dialLoader
anchors.fill: parent
}
在 dialLoader 的父级的 state 属性中,一组 PropertyChanges 元素根据状态驱动加载不同的 QML 文件。源代码恰好在这个例子中是一个相对的文件路径,但它也可以是一个完整的 URL,通过 Web 获取该项目。
states: [
State {
name: "analog"
PropertyChanges { target: analogButton; color: "green"; }
PropertyChanges { target: dialLoader; source: "Analog.qml"; }
},
State {
name: "digital"
PropertyChanges { target: digitalButton; color: "green"; }
PropertyChanges { target: dialLoader; source: "Digital.qml"; }
}
]
为了使加载项目动起来,其 speed 属性必须绑定到根元素的 speed 属性。这不能作为直接绑定完成,因为项目不总是被加载,并随着时间的推移而改变。必须使用 Binding 元素。每次 Loader 触发 onLoaded 信号时,绑定的 target 属性都会更改。
Loader {
id: dialLoader
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: analogButton.top
onLoaded: {
binder.target = dialLoader.item;
}
}
Binding {
id: binder
property: "speed"
value: speed
}
onLoaded 信号允许加载 QML 在加载项目时起作用。以类似的方式,加载的 QML 可以依赖于 Component.onCompleted 信号。这是所有组件都可用的信号,无论它们如何加载。例如,整个应用程序的根组件可以让我们在整个用户界面加载时自动启动。
13.1.1 间接连接
动态创建 QML 元素时,无法使用用于静态设置的 onSignalName 方法连接到信号。而是必须使用 Connections 元素。它可以连接到 target 元素的任何数量的信号。
设置了 Connections 元素的 target 属性后,可以照常连接信号,也就是使用onSignalName 方法。然而,通过改变目标属性,可以在不同的时间监视不同的元素。
在上面的示例中,向用户呈现由两个可点击区域组成的用户界面。当任一区域被点击时,它将使用动画闪烁。左侧区域显示在下面的代码段中。在 MouseArea 中,会触发 leftClickedAnimation,导致该区域闪烁。
Rectangle {
id: leftRectangle
width: 290
height: 200
color: "green"
MouseArea {
id: leftMouseArea
anchors.fill: parent
onClicked: leftClickedAnimation.start();
}
Text {
anchors.centerIn: parent
font.pixelSize: 30
color: "white"
text: "Click me!"
}
}
除了两个可点击的区域之外,还使用了一个 Connections 元素。当活动,即元素的 target 被点击时,这将触发第三个动画。
Connections {
id: connections
onClicked: activeClickedAnimation.start();
}
要确定作为 target 的 MouseArea,定义了两个状态。请注意,我们无法使用 PropertyChanges 元素设置 target 属性,因为它已包含 target 属性。而是使用 StateChangeScript。
states: [
State {
name: "left"
StateChangeScript {
script: connections.target = leftMouseArea
}
},
State {
name: "right"
StateChangeScript {
script: connections.target = rightMouseArea
}
}
]
当尝试这个例子时,值得注意的是,当使用多个信号处理程序时,所有这些都被调用。然而,这些的执行顺序是未定义的。
创建 Connections 元素而不设置 target 属性时,该属性默认为 parent。这意味着它必须设置为 null,以避免捕获来自 parent 的信号,直到 target 设置为止。这种行为使得可以基于 Connections 元素创建自定义信号处理程序组件。这样,与信号反应的代码可以被封装并重新使用。
在下面的示例中,Flasher 组件可以放在任何 MouseArea 中。点击后,会触发动画,导致父项闪烁。在同一个 MouseArea 中,也可以执行正在触发的实际任务。 这将标准化的用户反馈(即闪烁)与实际动作分开。
import QtQuick 2.5
Connections {
onClicked: {
// Automatically targets the parent
}
}
要使用 Flasher,只需在每个 MouseArea 中实例化一个 Flasher,并且它们都可以正常工作。
import QtQuick 2.5
Item {
// A background flasher that flashes the background of any parent MouseArea
}
当使用 Connections 元素监视多种类型的 target 元素的信号时,我们有时会发现自己处于可用信号在目标之间变化的情况。这导致连接元件输出运行时错误信号。为了避免这种情况,ignoreUnknownSignal 属性可以设置为 true。这将忽略所有这些错误。
** 注意: **
抑制错误消息通常是一个坏主意。
13.1.2 间接绑定
正如不可能直接连接到动态创建的元素的信号,也不可能绑定动态创建的元素的属性,而不使用桥元素。要绑定任何元素的属性,包括动态创建的元素,可以使用 Binding 元素。
Binding 元素允许我们指定 target 元素,要绑定的 property 和绑定它的 value。通过使用 Binding 元素,例如可以绑定动态加载元素的属性。这在本章的介绍性例子中有所展示,如下所示。
Loader {
id: dialLoader
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: analogButton.top
onLoaded: {
binder.target = dialLoader.item;
}
}
Binding {
id: binder
property: "speed"
value: speed
}
由于 Binding 的 target 元素不总是设置,并且可能并不总是具有给定的属性,所以可以使用 Binding 元素的 when 属性来限制绑定活动的生效时间。例如,它可以限制在用户界面中的特定模式下运行。
13.2 创建和销毁对象
Loader 元素可以动态地填充用户界面的一部分。但是,该接口的整体结构仍然是静态的。通过 JavaScript,可以进一步完成动态实例化 QML 元素。
在我们深入了解动态创建元素的细节之前,我们需要了解其工作流程。从文件甚至通过 Internet 加载一个 QML 元素时,会创建一个组件。该组件封装解析好的 QML 代码,并可用于创建项目。这意味着从它加载一条 QML 代码和实例化项目是两个阶段的过程。首先将 QML 代码解析成一个组件。然后组件用于实例化实际项目对象。
除了从存储在文件或服务器上的 QML 代码创建元素之外,还可以直接从包含 QML 代码的文本字符串创建 QML 对象。一旦实例化,动态创建的项目就以类似的方式进行处理。
13.2.1 动态加载和实例化项目
当加载一个 QML 时,它首先被解释为一个组件。这包括加载依赖关系并验证代码。正在加载的 QML 的位置可以是本地文件、Qt 资源、或甚至由 URL 指定的远端网络位置。这意味着加载时间可以是即时的,例如位于 RAM 中的 Qt 资源,而不需要任何非加载的依赖关系,也可能意味着需要加载多个依赖关系的位于较慢服务器上的一段代码。
正在创建的组件的状态可以通过其 status 属性来跟踪。可用的值是Component.Null、Component.Loading、Component.Ready 和 Component.Error。通常的流程就是 Null 到 Loading 到 Ready 。在任何阶段,status 可能会改变为 Error。在这种情况下,组件不能用于创建新的对象实例。 Component.errorString() 函数可用于检索用户可读的错误描述。
通过慢速连接加载组件时,可以使用 progress 属性。它的范围从 0.0 开始,意味着没有加载,到 1.0 表示已经加载。当组件的 status 更改为 “Ready” 时,组件可用于实例化对象。下面的代码演示了如何实现这一点,同时考虑到组件变得准备好或不能直接创建的事件,以及组件可能会稍后才能准备好的情况。
var component;
function createImageObject() {
component = Qt.createComponent("dynamic-image.qml");
if (component.status === Component.Ready || component.status === Component.Error) {
finishCreation();
} else {
component.statusChanged.connect(finishCreation);
}
}
function finishCreation() {
if (component.status === Component.Ready) {
var image = component.createObject(root, {"x": 100, "y": 100});
if (image === null) {
console.log("Error creating image");
}
} else if (component.status === Component.Error) {
console.log("Error loading component:", component.errorString());
}
}
上面的代码可以封装在单独的 JavaScript 源文件中,而后可以从主 QML 文件中进行引用。
import QtQuick 2.5
import "create-component.js" as ImageCreator
Item {
id: root
width: 1024
height: 600
Component.onCompleted: ImageCreator.createImageObject();
}
组件的 createObject 函数用于创建对象实例,如上所示。这不仅适用于动态加载的组件,还适用于 QML 代码内联的 Component 元素。所得到的对象可以像任何其他对象一样在 QML 场景中使用。唯一的区别是它没有 id。
createObject 函数有两个参数。第一个是 Item 类型的 parent。第二个是格式为 {"name": value, "name": value} 的属性和值列表。这在下面的示例中被证明。请注意,properties 参数是可选的。
var image = component.createObject(root, {"x": 100, "y": 100});
** 注意:**
动态创建的组件实例与内嵌 Component 元素没有任何区别。在线 Component 元素还提供动态实例化对象的功能。
13.2.2 从文本动态实例化项目
有时,能够从 QML 的文本字符串实例化一个对象是方便的。如果没有别的,那么比将代码放在一个单独的源文件中更快。为此,可以使用 Qt.createQmlObject 函数。
该函数有三个参数:qml、parent 和 filepath。qml 参数包含要实例化的 QML 代码字符串。parent 参数为新创建的对象提供一个父对象。当从对象的创建报告任何错误时,使用 filepath 参数。从函数返回的结果是一个新对象或 null。
** 警告: **
createQmlObject 函数始终立即返回。要使功能成功,必须加载调用的所有依赖项。这意味着如果传递给函数的代码是指未加载的组件,否则调用将失败并返回null。为了更好地处理这个问题,必须使用 createComponent / createObject 方法。
使用 Qt.createQmlObject 函数创建的对象类似于任何其他动态创建的对象。这意味着它与每个其他 QML 对象相同,除了没有 id。在下面的示例中,当创建 root 元素时,新的 Rectangle 元素将从内联 QML 代码实例化。
import QtQuick 2.5
Item {
id: root
width: 1024
height: 600
function createItem() {
Qt.createQmlObject("import QtQuick 2.5; Rectangle { x: 100; y: 100; width: 100; height: 100; color: \"blue\" }", root, "dynamicItem");
}
Component.onCompleted: root.createItem();
}
13.2.3 管理动态创建的元素
动态创建的对象可以被视为 QML 场景中的任何其他对象。但是,有一些陷阱需要处理。最重要的是创作环境的概念。
动态创建对象的创建上下文是正在创建的上下文。这不一定与父对象存在的上下文相同。当创建上下文被销毁时,关于对象的绑定也是如此。这意味着重要的是在代码中的一个地方实现动态对象的创建,该对象将在对象的整个生命周期中被实例化。
动态创建的对象也可以被动态地销毁。当这样做时,有一个经验法则:永远不要试图销毁你没有创建的对象。这还包括我们创建的元素,但不使用动态机制,如Component.createObject 或 createQmlObject。
通过调用其 destroy 函数来销毁对象。该函数采用可选参数,该参数是一个整数,指定对象在销毁之前应存在多少毫秒。这对于例如让对象完成最后的转换是有用的。
item = Qt.createQmlObject(...);
...
item.destroy();
** 注意: **
可以从内部的对象中销毁,例如可以创建自毁的弹出窗口。
13.3 跟踪动态对象
使用动态对象,通常需要跟踪创建的对象。另一个常见的功能是能够存储和恢复动态对象的状态。这两个任务都可以使用我们动态填充的 ListModel 轻松处理。
在下面的示例中,用户可以创建和移动两种类型的元素,火箭和 ufos。为了能够操纵动态创建的元素的整个场景,我们使用模型来跟踪项目。
该模型,一个 ListModel,在创建项目时填充。对象引用在实例化时使用的源 URL 旁边跟踪。跟踪对象并不是绝对必需的,后来会派上用场。
import QtQuick 2.5
import "create-object.js" as CreateObject
Item {
id: root
ListModel {
id: objectsModel
}
function addUfo() {
CreateObject.create("ufo.qml", root, itemAdded);
}
function addRocket() {
CreateObject.create("rocket.qml", root, itemAdded);
}
function itemAdded(obj, source) {
objectsModel.append({"obj": obj, "source": source})
}
从上面的示例可以看出,create-object.js 是前面介绍的 JavaScript 的更一般化形式。 create 方法使用三个参数:源 URL,根元素和完成后调用的回调。使用两个参数调用回调:对新创建的对象的引用和所使用的源 URL。
这意味着每次调用 addUfo 或 addRocket 函数时,将在创建新对象时调用 itemAdded 函数。后者将对象引用和源 URL 附加到 objectsModel 模型中。
objectsModel 可以在很多方面使用。在该示例中,clearItems 函数依赖于它。这个功能演示了两件事情。首先,如何迭代模型并执行任务,即调用每个项目的 destroy 功能以将其删除。其次,它强调了模型不被更新,因为对象被销毁。而不是删除连接到该对象的模型项目,该模型项目的 obj 属性设置为 null。要解决这个问题,代码显式地必须在对象被删除时清除模型项。
function clearItems() {
while(objectsModel.count > 0) {
objectsModel.get(0).obj.destroy();
objectsModel.remove(0);
}
}
拥有代表所有动态创建的项目的模型,很容易创建一个序列化项目的功能。在示例代码中,序列化信息由沿着 x 和 y 属性的每个对象的源 URL 组成。这些是用户可以更改的属性。该信息用于构建 XML 文档字符串。
function serialize() {
var res = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<scene>\n";
for(var ii=0; ii < objectsModel.count; ++ii) {
var i = objectsModel.get(ii);
res += " <item>\n <source>" + i.source + "</source>\n <x>" + i.obj.x + "</x>\n <y>" + i.obj.y + "</y>\n </item>\n"
}
res += "</scene>";
return res;
}
XML 文档字符串可以通过设置模型的 xml 属性与 XmlListModel 一起使用。在下面的代码中,模型沿着反序列化(deserialize)功能显示。反序列化(deserialize)函数通过将 dsIndex 设置为引用模型的第一项,然后调用该项目的创建来启动反序列化。回调,dsItemAdded 然后设置新创建的对象的 x 和 y 属性。 然后它更新索引并创建 nexts 对象(如果有)。
XmlListModel {
id: xmlModel
query: "/scene/item"
XmlRole { name: "source"; query: "source/string()" }
XmlRole { name: "x"; query: "x/string()" }
XmlRole { name: "y"; query: "y/string()" }
}
function deserialize() {
dsIndex = 0;
CreateObject.create(xmlModel.get(dsIndex).source, root, dsItemAdded);
}
function dsItemAdded(obj, source) {
itemAdded(obj, source);
obj.x = xmlModel.get(dsIndex).x;
obj.y = xmlModel.get(dsIndex).y;
dsIndex ++;
if (dsIndex < xmlModel.count)
CreateObject.create(xmlModel.get(dsIndex).source, root, dsItemAdded);
}
property int dsIndex
该示例演示了如何使用模型来跟踪创建的项目,以及如何轻松地将此类信息序列化和反序列化。这可以用于存储动态填充的场景,例如一组小部件。在该示例中,使用模型来跟踪每个项目。
一个备用解决方案将是使用场景根的 children 属性来跟踪项目。然而,这需要自己的项目来知道用于重新创建它们的源 URL。它还要求场景仅由动态创建的项目组成,以避免尝试序列化,然后将任何静态分配的对象反序列化。
13.4 本章总结
在本章中,我们已经看到了动态创建 QML 元素。 这使我们可以自由创建 QML 场景,为用户配置和基于插件的体系结构打开了大门。
动态加载 QML 元素的最简单方法是使用 Loader 元素。这充当被加载的内容的占位符。
对于更动态的方法,Qt.createQmlObject 函数可用于实例化一个 QML 字符串。然而,这种方法有局限性。完整的解决方案是使用 Qt.createComponent 函数动态创建 Component。然后通过调用 Component 的 createObject 函数创建对象。
由于绑定和信号连接依赖于对象 id 的存在或对对象实例化的访问,必须为动态创建的对象使用备用方法。要创建绑定,使用 Binding 元素。Connections 元素使得可以连接到动态创建的对象的信号。
使用动态创建的项目的一个挑战是跟踪它们。这可以使用 ListModel 完成。通过使用跟踪动态创建的项目的模型,可以实现序列化和反序列化的功能,使得可以存储和恢复动态创建的场景。
本章完。