从JavaScript动态创建QML对象

从JavaScript实例化和管理QML对象

QML 支持从 JavaScript 中动态创建对象。这对于延迟对象的实例化直到必要时非常有用,从而改善应用程序的启动时间。它还允许根据用户输入或其他事件动态创建视觉对象并将其添加到场景中。

动态创建对象

有两种方法可以从JavaScript动态创建对象。你可以调用createComponent()来动态创建一个Component对象,或者使用createQmlObject()从QML字符串创建对象。如果你在QML文档中定义了一个现有的组件,并且想要动态创建该组件的实例,那么创建组件是更好的选择。否则,当对象QML本身在运行时生成时,从QML字符串创建对象是有用的。

动态创建组件

要动态加载在QML文件中定义的组件,请在Qt对象中调用createComponent()函数。此函数将QML文件的URL作为其唯一参数,并从此URL创建一个Component对象。

一旦你有了一个Component,你可以调用它的createObject()方法来创建组件的一个实例。这个函数可以接受一个或两个参数:

  • 第一个是新对象的父对象。父对象可以是图形对象(即Item类型)或非图形对象(即QtObject或C++ QObject类型)。只有具有图形父对象的图形对象才会被渲染到Qt Quick可视化画布上。如果您希望稍后设置父对象,可以安全地将null传递给此函数。

  • 第二个是可选的,是一个属性-值对的映射,定义了对象的初始属性值。通过此参数指定的属性值在对象创建完成之前应用于对象,避免了如果必须初始化特定属性以启用其他属性绑定可能发生的绑定错误。此外,与在对象创建后定义属性值和绑定相比,还有一些小的性能优势。

这是一个例子。首先是Sprite.qml,它定义了一个简单的QML组件:

import QtQuick

Rectangle { width: 80; height: 50; color: "red" }

我们的主应用程序文件,main.qml,导入了一个componentCreation.js JavaScript 文件,该文件将创建Sprite对象:

import QtQuick
import "componentCreation.js" as MyScript

Rectangle {
    id: appWindow
    width: 300; height: 300

    Component.onCompleted: MyScript.createSpriteObjects();
}

这里是 componentCreation.js。请注意,在调用 createObject() 之前,它会检查组件 status 是否为 Component.Ready,以防 QML 文件通过网络加载,因此不会立即准备好。

var component;
var sprite;

function createSpriteObjects() {
component = Qt.createComponent("Sprite.qml");
if (component.status == Component.Ready)
    finishCreation();
else
    component.statusChanged.connect(finishCreation);
}

function finishCreation() {
    if (component.status == Component.Ready) {
        sprite = component.createObject(appWindow, {x: 100, y: 100});
        if (sprite == null) {
            // Error Handling
            console.log("Error creating object");
        }
    } else if (component.status == Component.Error) {
        // Error Handling
        console.log("Error loading component:", component.errorString());
    }
}

如果您确定要加载的QML文件是本地文件,您可以省略finishCreation()函数并立即调用createObject()

function createSpriteObjects() {
component = Qt.createComponent("Sprite.qml");
sprite = component.createObject(appWindow, {x: 100, y: 100});

if (sprite == null) {
    // Error Handling
    console.log("Error creating object");
}
}

请注意,在这两种情况下,createObject() 被调用时,appWindow 作为父参数传递,因为动态创建的对象是一个可视的(Qt Quick)对象。创建的对象将成为 main.qmlappWindow 对象的子对象,并出现在场景中。

当使用相对路径的文件时,路径应相对于执行createComponent()的文件。

要将信号连接到(或从)动态创建的对象,请使用信号connect()方法。有关更多信息,请参见将信号连接到方法和信号

也可以通过incubateObject()函数非阻塞地实例化组件。

从QML字符串创建对象

警告

从QML字符串创建对象非常慢,因为每次执行时引擎都必须编译传递的QML字符串。此外,在编程构造QML代码时,很容易产生无效的QML。最好将QML组件保持为单独的文件,并通过添加属性和方法来定制它们的行为,而不是通过字符串操作生成新的组件。

如果QML在运行时才定义,你可以使用createQmlObject()函数从QML字符串创建QML对象,如下例所示:

const newObject = Qt.createQmlObject(`
    import QtQuick

    Rectangle {
        color: "red"
        width: 20
        height: 20
    }
    `,
    parentItem,
    "myDynamicSnippet"
);

第一个参数是要创建的QML字符串。就像在一个新文件中一样,你需要导入你希望使用的任何类型。第二个参数是新对象的父对象,适用于组件的父参数语义同样适用于createQmlObject()。第三个参数是与新对象关联的文件路径;这用于错误报告。

如果QML导入文件使用相对路径,路径应相对于定义父对象(方法的第二个参数)的文件。

重要提示: 在构建静态QML应用程序时,会扫描QML文件以检测导入依赖项。这样,所有必要的插件和资源在编译时都会被解析。然而,只有显式的导入语句(位于QML文件顶部的那些)会被考虑,而不会考虑包含在字符串字面量中的导入语句。因此,为了支持静态构建,你需要确保使用createQmlObject()的QML文件在文件顶部显式包含所有必要的导入,同时在字符串字面量中也包含这些导入。

维护动态创建的对象

在管理动态创建的对象时,必须确保创建上下文比创建的对象更持久。否则,如果创建上下文首先被销毁,动态对象中的绑定和信号处理程序将不再起作用。

实际的创建上下文取决于对象的创建方式:

  • 如果使用了 createComponent(),创建上下文是调用此方法的 QQmlContext

  • 如果调用了 createQmlObject(),创建上下文是传递给此方法的父对象的上下文

  • 如果定义了Component{}对象并且在该对象上调用了createObject()incubateObject(),则创建上下文是定义Component的上下文

另外,请注意,虽然动态创建的对象可以像其他对象一样使用,但它们在QML中没有id。

动态删除对象

在许多用户界面中,将视觉对象的不透明度设置为0或将视觉对象移出屏幕而不是删除它就足够了。然而,如果你有很多动态创建的对象,如果删除未使用的对象,你可能会获得值得的性能提升。

请注意,您不应手动删除由方便的QML对象工厂(如Loader和Repeater)动态创建的对象。此外,您应避免删除您自己未动态创建的对象。

可以使用destroy()方法删除项目。此方法有一个可选参数(默认为0),用于指定对象在被销毁之前的近似延迟时间(以毫秒为单位)。

这里有一个例子。application.qml 创建了五个 SelfDestroyingRect.qml 组件的实例。每个实例运行一个 NumberAnimation,当动画结束时,调用其根对象上的 destroy() 来销毁自己:

application.qml

import QtQuick

Item {
    id: container
    width: 500; height: 100

    Component.onCompleted: {
        var component = Qt.createComponent("SelfDestroyingRect.qml");
        for (var i=0; i<5; i++) {
            var object = component.createObject(container);
            object.x = (object.width + 10) * i;
        }
    }
}

SelfDestroyingRect.qml

import QtQuick

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

    NumberAnimation on opacity {
        to: 0
        duration: 1000

        onRunningChanged: {
            if (!running) {
                console.log("正在销毁...")
                rect.destroy();
            }
        }
    }
}

或者,application.qml 可以通过调用 object.destroy() 来销毁创建的对象。

请注意,在对象内部调用destroy()是安全的。对象不会在调用destroy()的瞬间被销毁,而是在该脚本块结束和下一帧之间的某个时间被清理(除非你指定了非零延迟)。

还要注意,如果像这样静态创建了一个SelfDestroyingRect实例:

这将导致错误,因为对象只有在动态创建时才能被动态销毁。

使用createQmlObject()创建的对象同样可以使用destroy()销毁:

const newObject = Qt.createQmlObject(`
    import QtQuick

    Rectangle {
        color: "red"
        width: 20
        height: 20
    }
    `,
    parentItem,
    "myDynamicSnippet"
);
newObject.destroy(1000);