【Qt之Quick模块】6. QML语法详解_4 集成QML和JavaScript

1. JavaScript表达式和属性绑定

QML语言允许表达式和方法使用JavaScript函数进行定义。

1.1 属性绑定

  1. 一般绑定
    当创建一个属性绑定,而且为属性设置一个表达式,那么表达式的计算结果就是属性的值。
import QtQuick

Window {
    
    
    width: 640
    height: 480
    visible: true
    title: qsTr("Hello World")

    Rectangle{
    
    
        width: 100
        height: parent.height/2
        border.color: "red"
        anchors.centerIn: parent
    }
}

如上,当Windowheight改变时,Rectangleheight也会自动更新新的值。
image.png

还可以使用别的JavaScript表达式或语句,如下:

import QtQuick

Window {
    
    
    width: 640
    height: 480
    visible: true
    title: qsTr("Hello World")


    Rectangle{
    
    
        width: 100
        height: parent.height > 480 ? parent.height/2 : parent.height/3
        border.color: "red"
        anchors.centerIn: parent
    }
}

当Window的parent.height值发生变化时,QML引擎都会重新计算这些表达式,并将更新后的值设置给QRectangle的height属性。
parent.height > 480时:
image.png
parent.height < 480时:
image.png
根据其语法可以看出,绑定是复杂的,因此不建议绑定中包含过多的代码。而且如果绑定复杂,可以考虑将绑定代码重构,放到一个函数里。
注意
如果绑定后,再次赋值,绑定会失效的。
如,设置Qrectangle的width属性为别的item的width:

Rectangle{
    
    
width:otherItem.width
}

当设置为下面代码时:

Rectangle{
    
    
Component.OnCompleted:{
    
    
	width = otherItem.width
}
}

当程序初始化完成,绑定就会失效。
2. 使用binding()绑定
属性一旦绑定一个表达式,属性就会根据表达式进行自动更新。如果再次给属性赋值一个静态值,则绑定关系失效。
如果需求是这样,那就没问题。如果是想重新建立一个新的绑定,则需要使用Qt.binding()来实现,向Qt.binding()传递一个函数来返回需要的结果。
如下:

import QtQuick

Window {
    
    
    id:root
    width: 640
    height: 480
    visible: true
    title: qsTr("Hello World")


    Rectangle{
    
    
        id:recttangle
        width: 100
        height: parent.height > 480 ? parent.height/2 : parent.height/3
        border.color: "red"
        anchors.centerIn: parent
    }

    Component.onCompleted: {
    
    
        recttangle.height = Qt.binding(function(){
    
    return root.height/4})
    }
}

image.png
当初始化完成,会创建新的绑定。
3. 在属性绑定中使用this关键字
this可以在属性绑定中用于消除歧义。
当使用JavaScript创建一个属性绑定时,QML允许使用this关键字引用该属性绑定将要分配的对象。
如下,当在Component.onCompleted中进行recttangle.height属性绑定时,后面的this则是rectangle对象,而不是Window对象。

import QtQuick

Window {
    
    
    id:root
    width: 640
    height: 480
    visible: true
    title: qsTr("Hello World")


    Rectangle{
    
    
        id:recttangle
        width: 100
        height: parent.height > 480 ? parent.height/2 : parent.height/3
        border.color: "red"
        anchors.centerIn: parent
    }

    Component.onCompleted: {
    
    
        recttangle.height = Qt.binding(function(){
    
    return this.width*2})
    }
}

打印recttangle.height,结果为200

1.2 JavaScript函数

  1. 自定义函数或方法
import QtQuick

Window {
    
    
    id:root
    width: 640
    height: 480
    visible: true
    title: qsTr("Hello World")

    Text {
    
    
        id: text
        text: qsTr("text")
        anchors.centerIn: parent
        font.pixelSize: 23
    }

    function add(a, b){
    
    
        return a+b
    }

    Component.onCompleted: {
    
    
        text.text = add(1, 2)
    }
}

当初始化完成,屏幕正中间会显示3
2. 导入JavaScript文件中的函数
当有一个js文件,里面定义一个add方法,
如下:

// funAdd.js
function add(a, b) {
    
    
    return a+b
}

导入:

import QtQuick
import "./funAdd.js" as FunAdd

Window {
    
    
    id:root
    width: 640
    height: 480
    visible: true
    title: qsTr("Hello World")

    Text {
    
    
        id: text
        text: qsTr("text")
        anchors.centerIn: parent
        font.pixelSize: 23
    }

    Component.onCompleted: {
    
    
        text.text = FunAdd.add(1, 2)
	console.log(text.text) // "3"
    }
}
  1. 关联信号和JavaScript函数
    发射信号的QML对象类型会提供一个默认的信号处理器。但有时需要从一个对象发射一个信号来触发另一个对象的定义的函数,这时需要用到connect()函数。
// funAdd.js
function prin(){
    
    
    console.log("helloworld")
}

调用:

import QtQuick
import "./funAdd.js" as FunAdd

Window {
    
    
    id:root
    width: 640
    height: 480
    visible: true
    title: qsTr("Hello World")
    color: "gray"

    MouseArea {
    
    
        id: mouseArea
        anchors.fill: parent
    }

    Component.onCompleted :{
    
    
        mouseArea.clicked.connect(FunAdd.prin) // 当点击屏幕时,打印"helloworld"
    }
}

在这里插入图片描述

2. 从JavaScript动态创建QML对象

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

2.1 动态创建对象

有两种方法可以从JavaScript动态创建对象。一是可以调用Qt.createComponent()来动态地创建一个Component对象;二是使用Qt.createQmlObject()从一个QML字符串创建一个对象。如果在QML文档中定义了现有组件,并且希望动态创建该组件的实例,则第一种方式更好。当对象QML本身在运行时生成时,可以使用第二种方式。

2.1.1 Qt.createComponent()动态创建组件

要动态加载在QML文件中定义的组件,可以调用Qt对象中的Qt. createcomponent()函数。该函数将QML文件的URL作为其唯一参数,并从该URL创建一个Component对象。
一旦有了一个组件,就可以调用它的createObject()方法来创建组件的实例。这个函数可以接受一个或两个参数:

  • 第一个是新对象的父对象。父对象可以是图形对象(即Item类型)或非图形对象(即QtObject或c++ QObject类型)。只有具有图形父对象的图形对象才会呈现到Qt Quick可视化画布中。如果希望稍后设置父类,可以将null传递给此函数。
  • 第二个是可选的,它是一个属性值对的映射,用于定义对象的初始属性值。此参数指定的属性值在对象的创建完成之前应用于该对象,从而避免了在必须初始化特定属性以启用其他属性绑定时可能发生的绑定错误。此外,与在创建对象之后定义属性值和绑定相比,还有一些性能优势。
    示例:
    先创建一个Sprite.qml文件
 import QtQuick 2.0
 Rectangle {
    
     width: 80; height: 50; color: "red" }

再创建一个componentCreation.js文件

 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) {
    
    
             // 错误处理
             console.log("Error creating object");
         }
     } else if (component.status == Component.Error) {
    
    
         // 错误处理
         console.log("Error loading component:", component.errorString());
     }
 }

最后在main.qml中进行调用

 import QtQuick 2.0
 import "componentCreation.js" as MyScript

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

     Component.onCompleted: MyScript.createSpriteObjects();
 }

调用createObject前检查了组件的状态是否是Component.Ready,因为如果QML文件是从网络上加载的,它不会立即可用。
如果是本地文件加载,可以直接在createSpriteObjects函数内调用createObject方法。

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

     if (sprite == null) {
    
    
         // 错误处理
         console.log("Error creating object");
     }
 }

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

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

要将信号连接到(或从)动态创建的对象接收信号,可以使用signal connect()方法。
也可以通过incubateObject()函数实例化组件,该函数是非阻塞的。

2.1.2 Qt.createQmlObject()从QML字符串创建对象

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

 const newObject = Qt.createQmlObject(`
     import QtQuick 2.0

     Rectangle {
    
    
         color: "red"
         width: 20
         height: 20
     }
     `,
     parentItem,
     "myDynamicSnippet"
 );
  • 第一个参数是要创建的QML字符串。就像在新文件中一样,您需要导入希望使用的任何类型。
  • 第二个参数是新对象的父对象,应用于组件的父参数语义同样适用于createQmlObject()。
  • 第三个参数是要与新对象关联的文件路径;这用于错误报告。

如果QML字符串使用相对路径导入文件,则该路径应该相对于定义父对象(方法的第二个参数)的文件.
重要:在构建静态QML应用程序时,将扫描QML文件以检测导入依赖项。这样,所有必要的插件和资源都可以在编译时解析。但是,只考虑显式导入语句(在QML文件顶部找到的语句),而不考虑包含在字符串字面量中的导入语句。因此,为了支持静态构建,需要确保使用Qt.createQmlObject()的QML文件在文件顶部显式地包含所有必要的导入,而不是在字符串文字中。

2.2 维护动态创建的对象

在管理动态创建的对象时,必须确保创建上下文比创建的对象更长寿。否则,如果首先销毁创建上下文,则动态对象中的绑定和信号处理程序将不再工作。
实际的创建上下文取决于对象是如何创建的:

  • 如果使用了Qt.createComponent(),那么创建上下文就是调用该方法的QQmlContext
  • 如果调用Qt.createQmlObject(),则创建上下文是传递给该方法的父对象的上下文
  • 如果定义了一个Component{}对象,并且在该对象上调用了createObject()或cucubateobject(),那么创建上下文就是定义组件的上下文

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

2.3 动态删除对象

在许多用户界面中,将可视对象的不透明度设置为0或将可视对象移出屏幕而不是删除它就足够了。但是,如果有很多动态创建的对象,那么如果删除未使用的对象,可能会获得有价值的性能优势。
注意,永远不要手动删除由方便的QML对象工厂(如Loader和Repeater)动态创建的对象。此外,应该避免删除不是自己动态创建的对象。
可以使用destroy()方法删除项。此方法有一个可选参数(默认为0),该参数指定对象被销毁前的大约毫秒延迟。
如下例子:application.qml创建了5个SelfDestroyingRect.qml组件。每个实例都运行一个NumberAnimation,当动画结束时,调用它的根对象destroy()来销毁自己:

// application.qml
 import QtQuick 2.0

 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 2.0

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

     NumberAnimation on opacity {
    
    
         to: 0
         duration: 1000

         onRunningChanged: {
    
    
             if (!running) {
    
    
                 console.log("Destroying...")
                 rect.destroy();
             }
         }
     }
 }

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

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

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

 Item {
    
    
     SelfDestroyingRect {
    
    
         // ...
     }
 }

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

使用Qt.createQmlObject()创建的对象也可以使用destroy()销毁。

 const newObject = Qt.createQmlObject(`
     import QtQuick 2.0

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

3. 在QML中定义JavaScript资源

QML应用程序的程序逻辑可以用JavaScript定义。JavaScript代码可以在QML文档中内联定义,也可以分离到JavaScript文件中(在QML中称为JavaScript资源)。

QML中支持两种不同类型的JavaScript资源:代码隐藏实现文件和共享(库)文件。这两种JavaScript资源都可以被其他JavaScript资源导入,或者包含在QML模块中。

3.1 代码隐藏实现资源

导入到QML文档中的大多数JavaScript文件都有状态的。在这些情况下,文档中定义的QML对象类型的每个实例都需要JavaScript对象和状态的单独副本才能正确运行。

导入JavaScript文件时的默认行为是为每个QML组件实例提供唯一的、隔离的副本。如果该JavaScript文件没有使用.import语句导入任何资源或模块,则其代码将在与QML组件实例相同的作用域中运行,因此可以访问和操作在该QML组件中声明的对象和属性。否则,它将有自己独特的作用域,如果需要的话,QML组件的对象和属性应该作为参数传递给JavaScript文件的函数。

代码隐藏实现资源的示例如下:

 // my_button_impl.js
 var clickCount = 0;   // this state is separate for each instance of MyButton
 function onClicked(button) {
    
    
     clickCount += 1;
     if ((clickCount % 5) == 0) {
    
    
         button.color = Qt.rgba(1,0,0,1);
     } else {
    
    
         button.color = Qt.rgba(0,1,0,1);
     }
 }
 // MyButton.qml
 import QtQuick 2.0
 import "my_button_impl.js" as Logic // A new instance of this JavaScript resource
                                     // is loaded for each instance of Button.qml.

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

     MouseArea {
    
    
         id: mousearea
         anchors.fill: parent
         onClicked: Logic.onClicked(rect)
     }
 }

一般来说,应该在QML文件中内联定义简单的逻辑,但是为了可维护性和可读性,应该将更复杂的逻辑分离到代码隐藏实现资源中。

3.2 共享JavaScript资源库

默认情况下,从QML导入的JavaScript文件与QML组件共享它们的上下文。这意味着JavaScript文件可以访问相同的QML对象并可以修改它们。因此,每次导入必须具有这些文件的唯一副本。

前一节介绍了JavaScript文件的有状态导入。然而,有些JavaScript文件是无状态的,更像是可重用的库,因为它们提供了一组不需要从它们被导入的地方获取任何东西的辅助函数。如果使用特殊的pragma标记这些库,可以节省大量内存并加快QML组件的实例化速度,如下面的示例所示。

 // factorial.js
 .pragma library

 var factorialCount = 0;

 function factorial(a) {
    
    
     a = parseInt(a);

     // factorial recursion
     if (a > 0)
         return a * factorial(a - 1);

     // shared state
     factorialCount += 1;

     // recursion base-case.
     return 1;
 }

 function factorialCallCount() {
    
    
     return factorialCount;
 }

pragma声明必须出现在除注释以外的任何JavaScript代码之前。

注意,多个QML文档可以导入“factorial.js”并调用它提供的factorial和factorialCallCount函数。JavaScript导入的状态在导入它的QML文档之间共享,因此在从未调用阶乘函数的QML文档中调用factoralcallcount函数时,其返回值可能是非零的。

例如:

 // Calculator.qml
 import QtQuick 2.0
 import "factorial.js" as FactorialCalculator // This JavaScript resource is only
                                              // ever loaded once by the engine,
                                              // even if multiple instances of
                                              // Calculator.qml are created.

 Text {
    
    
     width: 500
     height: 100
     property int input: 17
     text: "The factorial of " + input + " is: " + FactorialCalculator.factorial(input)
 }

因为它们是共享的,所以.pragma库文件不能直接访问QML组件实例对象或属性,尽管QML值可以作为函数参数传递。

4. 在QML中导入JavaScript资源

JavaScript资源可以通过QML文档和其他JavaScript资源导入。JavaScript资源可以通过相对url或绝对url导入。在相对URL的情况下,相对于包含导入的QML文档或JavaScript资源的位置解析位置。如果脚本文件不可访问,则会出现错误。如果需要从网络资源中获取JavaScript,则该组件的状态被设置为“Loading”,直到脚本下载完成。

JavaScript资源也可以导入QML模块和其他JavaScript资源。JavaScript资源中的导入语句的语法与QML文档中的导入语句略有不同,下面将详细说明这一点。

4.1 在QML文档中导入JavaScript资源

QML文档可以用以下语法导入JavaScript资源:

 import "ResourceURL" as Qualifier

如:

 import "jsfile.js" as Logic

导入的JavaScript资源总是使用“as”关键字进行限定。JavaScript资源的限定符必须以大写字母开头,并且必须是唯一的,因此限定符和JavaScript文件之间始终存在一对一的映射。(这也意味着限定符的命名不能与内置JavaScript对象(如Date和Math)相同。)

通过“Qualifier.functionName(params)”语法,导入的JavaScript文件中定义的函数可用于导入的QML文档中定义的对象。JavaScript资源中的函数可以接受参数,这些参数的类型可以是任何受支持的QML基本类型或对象类型,也可以是普通的JavaScript类型。当从QML调用这些函数时,普通的数据类型转换规则将应用于参数和返回值。

4.2 在JavaScript资源中进行导入

在QtQuick 2.0中,添加了支持,允许JavaScript资源使用标准QML导入语法的一种变体导入其他JavaScript资源和QML类型名称空间(其中应用前面描述的所有规则和限定条件)。
由于JavaScript资源能够在QtQuick 2.0中以这种方式导入另一个脚本或QML模块,因此定义了一些额外的语义:

  • 带有导入的脚本不会从导入它的QML文档继承导入(因此访问组件)。(例如,无法访问Component.errorString)
  • 没有导入的脚本将从导入它的QML文档继承导入(因此访问组件)。(例如,可以访问Component.errorString)
  • 共享脚本(即,定义为.pragma库)不继承任何QML文档的导入,即使它不导入其他脚本或模块

第一种语义在概念上是正确的,因为一个特定的脚本可以被任意数量的QML文件导入。保留第二个语义是为了向后兼容。第三种语义与当前共享脚本的语义保持不变,但在这里针对新的可能情况(脚本导入其他脚本或模块)进行了澄清。
从另一个JavaScript资源中导入JavaScript资源:

 import * as MathFunctions from "factorial.mjs";

或者

 .import "filename.js" as Qualifier

前者是用于导入ECMAScript模块的标准ECMAScript语法,并且只能在由mjs文件扩展名表示的ECMAScript模块中工作。后者是由QML引擎提供的对JavaScript的扩展,也可以与非模块一起工作。作为一个被ECMAScript标准取代的扩展,不鼓励使用它。

当以这种方式导入JavaScript文件时,它是用限定符导入的。然后可以从导入脚本中通过限定符(即qualifier.functionname (params))访问该文件中的函数。

有时,希望在导入上下文中提供可用的函数,而不需要对它们进行限定。在这种情况下,ECMAScript模块和JavaScript import语句应该不带as限定符。

例如,下面的QML代码在脚本中调用script.mjs中的showcalculation()。而在script.mjs中调用了factorial.mjs中的factorial()函数。

 import QtQuick 2.0
 import "script.mjs" as MyScript

 Item {
    
    
     width: 100; height: 100

     MouseArea {
    
    
         anchors.fill: parent
         onClicked: {
    
    
             MyScript.showCalculations(10)
             console.log("Call factorial() from QML:",
                 MyScript.factorial(10))
         }
     }
 }
 // script.mjs
 import {
    
     factorial } from "factorial.mjs"
 export {
    
     factorial }

 export function showCalculations(value) {
    
    
     console.log(
         "Call factorial() from script.js:",
         factorial(value));
 }
 // factorial.mjs
 export function factorial(a) {
    
    
     a = parseInt(a);
     if (a <= 0)
         return 1;
     else
         return a * factorial(a - 1);
 }

5. JavaScript宿主环境

QML提供了一个专门用于编写QML应用程序的JavaScript主机环境。该环境不同于浏览器提供的主机环境或服务器端JavaScript环境(如Node.js)。例如,QML不提供在浏览器环境中常见的窗口对象或DOM API。

猜你喜欢

转载自blog.csdn.net/MrHHHHHH/article/details/135310862