QML文档

 Qt Quick
时间:

一个QML文档就是一个符合QML文档语法的字符串,它定义了一个QML对象类型。QML文档通常从存储在本地或远程的.qml文件进行加载,也可以在代码进行手动构建。文档中定义的对象类型的实例可以在QML代码中使用Component(组件)进行创建,也可以在C++中使用QQmlComponent进行创建。另外,如果这个对象可以在其他文档的对象声明中直接使用。因为在文档中可以定义可重复使用的QML对象类型,所以在客户端可以编写出模块化的,高可读性的,易于维护的代码。

一个QML文档包含两部分: import导入语句和一个单一的根对象声明构成的对象树。需要强调的是,一个QML文档只能包含一个根对象声明,不允许出现两个平行的根对象。按照惯例,在这两部分之间需要留有一个空行进行分隔,QML文档一般使用UTF-8格式进行编码。

可以在Qt帮助中通过索引QML Documents关键字查看。

通过QML文档定义对象类型

QML的一个核心功能是,可以通过QML文档以一种轻量级的方式来方便的定义QML对象类型,从而满足不同QML应用的需求。标准的Qt Quick模块提供了多种类型(如Rectangle, Text和Image等)用于创建QML应用程序,在此之上,还可以很容易的定义自己的QML类型,并在自己的应用中进行重用。

使用QML文件定义对象类型

要创建一个对象类型,需要将一个QML文档放置到一个<TypeName>.qml命名的文本文件中。这里<TypeName>是类型的名称,必须以大写字母开头,不能包含除字母,数字和下划线以外的字符。这个文档自动被引擎识别为一个QML类型的定义。此外,引擎解析QML类型名称时需要搜索相同的目录,所以使用这种方式定义的类型时,同一个目录中的其他QML文件会被自动设置为可用。

例如,下面的文档中定义了一个Rectangle,其中包含一个MouseArea子对象,这个文档保存在以SquareButton.qml命名的文件中。

// SquareButton.qml

import QtQuick 2.9

Rectangle {
    width: 100; height: 100; color: "red"

    MouseArea {
        anchors.fill: parent
        onClicked: console.log("Button clicked!")
    }
}

由于文件名称是SquareButton.qml,因此可以被同一个目录下的其他QML文件作为SquareButton类型使用。例如,如果在相同的目录中有一个myapplication.qml文件,它可以引用SquareButton类型:

// myapplication.qml

import QtQuick 2.9

SquareButton {}

当myapplication.qml文档被引擎加载时,它会将SquareButton.qml作为一个组件进行加载,并对其进行实例化来创建一个SquareButton对象。SquareButton类型中封装了定义在SquareButton对象,也就是从定义在SquareButton.qml文件中的Rectangle对象树实例化一个对象。

注意:一些文件系统对文件名称是区分大小写的,所以建议定义QML文件名称时严格按照首字母大写而其他字母小写的格式。例如:应该设置为Box.qml,而不要设置为BoX.qml.

如果SquareButton.qml没有和myapplication.qml在同一个目录中,那么就需要在myapplication.qml中使用import语句来导入该类型,可以在文件系统中使用相对路径进行导入,也可以作为已安装的模块进行导入。

自定义类型的可访问特性

.qml 文件中的根对象定义了可用于该QML类型的一些特性。所有属于该根对象的属性,信号和方法,无论是自定义声明,还是来自QML类型,都可以在外部进行访问,并且可以被该类型的对象进行读取和修改。

例如之前的例子SquareButton.qml文件的根对象类型是Rectangle,这意味着在Rectangle类型中定义的所有属性都可以被SquareButton对象修改:

// application.qml

import QtQuick 2.9

Column {
    SquareButton { width: 50; height: 50; }
    SquareButton { x: 50; color: "blue" }
    SquareButton { radius: 10 }
}

自定义QML类型中可以被其对象访问的特性包括自定义的属性,方法和信号,例如:

// SquareButton.qml

import QtQuick 2.9

Rectangle {
    id: root

    property bool pressed: mouseArea.pressed

    signal buttonClicked(real xPos, real yPos)

    function randomizeColor() {
        root.color = Qt.rgba(Math.random(), Math.random(), Math.random(), 1)
    }

    width: 100; height: 100
    color: "red"

    MouseArea {
        id: mouseArea
        anchors.fill: parent
        onClicked: root.buttonClicked(mouse.x, mouse.y)
    }
}

所有的SquareButton对象都可以使用这里定义的pressed属性,buttonClicked信号和randomizeColor()方法。

// application.qml

import QtQuick 2.9

SquareButton {
    id: squareButton

    onButtonClicked: {
        console.log("Clicked", xPos, yPos)
        randomizeColor()
    }

    Text { text: squareButton.pressed ? "Down" : "Up" }
}

注意:在SquareButton.qml中定义的任何一个id值都不能在SquareButton对象中进行访问。因为id值只能在组件作用域中进行访问。另外,SquareButton对象也无法通过mouseArea来引用MouseArea子对象。如果想使用MouseArea等子对象中的内容,则需要像这里定义的SquareButton对象的id值不是squareButton,而是root,它也不会与SquareButton.qml中定义的根对象的id值发生冲突,因为它们定义在不同的作用域。

QML组件

组件是可重用的,封装好的QML类型,并提供了定义好的接口。组件一般使用一个.qml文件类型。前面讲到的使用QML文档定义对象类型,其实就是创建了一个组件。这种使用独立QML文件创建组件的方法,这里不再讨论。除了使用单独的QML文件。还可以使用Component类型在一个QML文档中定义一个组件。这种方式是很有用的,如在QML文件中重用一个小型组件或定义一个逻辑上属于该文件中其他QML组件的组件。

import QtQuick 2.9

Item {
    width: 100; height: 100

    Component {
        id: redSquare

        Rectangle {
            color: "red"; width: 10; height: 10
        }
    }

    Loader { sourceComponent: redSquare }
    Loader { sourceComponent: redSquare; x: 20 }
}

注意: 一般Rectangle 会自己渲染并进行显示,但是这里却不会。因为它定义在一个Component内部。组件内部封装的QML类型相当于定义在独立的QML文件中,需要时才进行加载(如这里由两个Loader对象进行加载)。Component不是继承自Item,所以不能对其进行布局或锚定其他对象。

定义Component与定义QML文档类似。QML文档类似。QML文档包含一个唯一的根对象来定义组件的行为和属性。类似的,Component定义也包含一个唯一的根对象(如这里的Rectangle),并且不能在根对象之外定义任何数据,只能使用id进行引用(如在Loader中使用redSquare).

Component类型一般用于为视图提供图形组件。例如,ListView::deletegate属性需要一个Component指定它的每一个列表项需要怎样显示.

另外,还可以使用Qt.createComponent()来动态创建Component.Component的创建上下文(context)对应于Component声明处的上下文.当一个组件被ListView或Loader这样的对象实例化时,这个上下文就是父对象的上下文.例如,下面代码中compl在MyItem.qml的根对象上下文中被创建,在这个组件中实例化的任何对象都可以访问这个上下文中的id和属性,如internalSettings.color.当compl在其他上下文中用作ListView的委托时,依然可以访问它创建上下文的属性.

// MyItem.qml

import QtQuick 2.9

Item {
    property Component mycomponent: compl

    QtObject {
        id: internalSettings
        property color color: "green"

    }

    Component {
        id: compl
        Rectangle {
            color: internalSettings.color;

            width: 400; height: 50
        }
    }
}

下面是mycomponent.qml

import QtQuick 2.9

ListView {
    width: 400; height: 400; model: 5
    delegate: myItem.mycomponent

    MyItem {
        id: myItem
    }
}

作用域和命名解析

QML属性绑定,内联函数和导入的JavaScript文件都运行在一个JavaScript作用域中.作用域主要控制两点: 一是表达式可以访问那些变量;二是当两个或多个名字冲突时,哪个变量优先.由于JavaScript的内建作用域机制非常简单,QML对其进行了加强,使其可以更加自然地适应QML语言地扩展.

    1. JavaScript作用域

QML的作用域扩展并没有干扰JavaScript本身的作用域.JavaScript开发人员可以在编写函数,属性绑定或者在QML中导入JavaScript文件时使用现有的知识.

QtQObject {
    property int a: 3
    property int b: 9

    function addConstant(b) {
        var a = 13;
        return b + a;
    }
}

QML遵循JavaScript一般的作用域规则,甚至在应用绑定时也是这样

// 将会为a属性绑定一个值12

QtObject {
    property int a
    a: { var a = 12; a; }
}

每一个在QML中的JavaScript表达式,函数或者文件都有它们自己唯一的变量对象,在它们任意一个里面声明的局部变量都不会和在另外一个里面声明的局部变量冲突.

  • 2 类型名称和导入的JavaScript文件

QML文档使用导入语句来定义类型名称和JavaScript文件,使其在文档中可见.除了在QML声明时使用,在访问附加属性和枚举值时,JavaScript代码也会使用类型名称.QML的import语句会影响到每一个属性绑定,QML文件中的JavaScript函数以及那些嵌套的内联组件.下面的代码片段中显示了一个简单的QML文件,其中访问了一些枚举值,而且调用了一个导入的JavaScript函数:

import QtQuick 2.9
import "code.js" as Code

ListView {
    snapMode: ListView.SnapToItem

    delegate: Component {
        Text {
            elide: Text.ElideMiddle
            text: "A really long string that will require eliding."
            color: Code.defaultColor()
        }
    }
}
  • 3 绑定的作用域对象

属性绑定是QML中最常见的JavaScript应用.属性绑定关联了一个JavaScript表达式的结果和对象的一个属性,该属性所归属的对象被称为绑定的作用域对象.

// Item 对象就是一个绑定的作用域对象
Item {
    anchors.left: parent.left
}

绑定可以无条件地访问作用域对象的属性.在前面的例子中,绑定可以直接访问Item的parent属性,不需要任何形式的对象前缀.QML为JavaScript引入了一个更加结构化,面向对象的方式,因此不再需要使用JavaScript的this属性.

当从绑定表达式中访问附加属性时要非常小心,因为它们会与作用域对象交互.从概念上讲,附加属性在所有对象上都存在,即使它们只对这些对象的子集有影响.因此,非限定的附加属性的读取总会解析到作用域对象的附加属性的值,这并不总是开发人员的意图.例如: PathView元素会向它的委托附加一个插值属性,这个插值依赖于在路径中具体的位置.因为PathView只会向委托的根对象附加这些属性,任何子对象要访问这些属性都要明确限定根对象.

PathView {
    delegate: Component {
        delegate: Component {
            Rectangle {
                id: root
                Image {
                    scale: root.PathView.scale
                }
            }
        }
    }
}

如果Image对象忽略了root前缀,那么它就会在无意中访问它自己上未设置的PathView.scale附加属性.

  • 4 组件作用域

QML文档的每一个组件都定义了一个逻辑作用域.每一个文档都至少有一个根组件,但是也可以拥有其他的内联子组件.组件的作用域是组件内的对象id和组件的根对象的属性的联合:

Item {
    property string title

    Text {
        id: title
        text: "<b>" + title + "</b>"
        font.pixelSize: 22
        anchors.top: parent.top
    }

    Text {
        text: title.text
        font.pixelSize: 18
        anchors.bottom: parent.bottom
    }
}

这里的组件先在上面显示了一个富文本标题字符串,然后再下面显示了相同文本的一个副本.第一个Text类型在构造显示的文本时直接访问了组件的title属性,根类型的属性可以直接被访问使得在整个组件中都可以分配数据.第二个Text类型使用了一个id来直接访问第一个Text的文本.由于id由开发者明确指定,所以它们总是优先于其他属性名称.

  • 5 组件实例的层次

在QML中,组件实例将它们的作用域关联在一起,形成了一个带有层次结构的作用域。组件实例可以直接访问它们祖先的作用域。

例如,使用内联子组件时,它的组件作用域隐式地设置为其外围组件作用域地孩子:

Item {
    property color defaultColor: "blue"

    ListView {
        delegate: Component {
            Rectangle {
                color: defaultColor
            }
        }
    }
}

组件实例层次允许委托组件地实例访问Item类型地defaultColor属性。当然,如果委托组件也有一个名为defaultColor的属性,那么将会优先访问它。

组件实例作用域层次可以扩展到非内联的组件。

//TitleText.qml

import QtQuick 2.9

Text {
    property int size
    text: "<b>" + title + "<b>"
    font.pixelSize: size
}

尽管在TitleText的属性中没有title,但是,我们可以在TitlePage中这样使用:

//TitlePage.qml

import QtQuick 2.9
Item {
    property string title

    TitleText {
        size: 22
        anchors.top: parent.top
    }

    TitleText {
        size: 18
        anchors.bottom: parent.bottom
    }
}

下面在mytext.qml中使用TitlePage:

import QtQuick 2.9

TitlePage { title: "hello" }

TitlePage组件创建了两个TitleText实例。即使TitleText类型在一个独立的文件中,当它在TitlePage中使用时依然可以访问到title属性。因此可以说,QML是一个动态作用域语言,QML的实际作用域依赖于QML文档使用的位置。

尽管动态作用域非常强大,但是必须谨慎使用,以避免QML代码的行为变得难以预料。如同上面的例子,它巧妙地利用了这种动态作用域,但是却不可避免地将两个组件精密耦合。在这种情况下,TitleText虽然是一个独立的组件,但这种设计严重影响到它的重用。

下面使用属性接口优化代码:

// TitleText.qml

import QtQuick 2.9

Text {
    property string title
    property int size

    text: "<b>" + title + "</b>"
    font.pixelSize: size
}
// TitlePage.qml

Item {
    id: root
    property string title

    TitleText {
        title: root.title
        size: 22
        anchors.top: parent.top
    }

    TitleText {
        title: root.title
        size: 18
        anchors.bottom: parent.bottom
    }
}
  • 6 重写属性

QML允许定义在一个对象声明中的属性名称被另外一个对象(其扩展了第一个对象)声明中的属性进行重写。

//Displayable.qml

import QtQuick 2.9

Item {
    property string title
    property string detail

    Text {
        text: "<b>" + title + "</b><br>" + detail
    }

    function getTitle() { return title }
    function setTitle(newTitle) { title = newTitle }
}
// Person.qml

import QtQuick 2.9

Displayable {
    property string title
    property string firstName
    property string lastName

    function fullName()  { return title + " " + firstName + " " + lastName }
}
// myproperty.qml

import QtQuick 2.9

Displayable {
    title: "hello"

    Person { id: person; title: "Qt" }

    Component.onCompleted: console.log(person.fullName() + getTitle())
}

这里,名称title同时用在了Displayable和Person组件的根对象中,而且Person组件的根对象是Displayable类型的。一个重写属性会根据其被引用的作用域进行解析。在Person组件的作用域中。或者在指定了Person组件实例的外部作用域中,title会解析为Person.qml内部的属性,fullName函数也会引用在Person中声明的title属性。而在Displayble组件中,title会解析为Displayable.qml中声明的属性,getTitle(),setTitle()函数以及Text对象中text属性的绑定都会引用声明在Displayable组件中的title属性。尽管共享了相同的名称,两个属性是完全分离的,一个属性的onChanged信号处理器不会由于另外一个属性的更改而触发,一个alias别名也只会引用其中一个属性,而不会同时进行引用。

  • 7 JavaScript 全局对象

除了JavaScript全局对象的所有属性以外,QML还添加了一些自定义扩展,以便更容易地完成UI或者QML指定的任务。QML不允许类型,id和属性名称与全局对象的属性同名,以免不必要的冲突。例如,开发人员可以确信Math,min(10,9)总是可以像所期望的那样工作。

资源加载和网络透明性

QML支持通过使用URL,而不是简单的文件名称,对所有引用实现网络透明性。这意味者在使用资源的地方都可以指定URL;QML既可以处理远程资源也可以处理本地资源。

例如加载一个远程的图片资源:

Image {
    source: "https://www.graycatya.com/static/images/portrait.jpg";
}

整个QML中都支持网络透明性,例如FontLoader中的source属性就是一个URL。甚至QML类型本身也可以放在网络上。例如,要加载一个要加载一个

http://example.com/mystuff/Hello.qml

文件,其中Hello.html中引用了World类型。这时引擎就会加载

http://example.com/mystuff/qmldir

路径并解析其中的类型,如果在qmldir文件中包含了World.qml,引擎便会加载

http://example.com/mystuff/World.qml

如果在Hello.qml中引用了其他资源,则会以相同的方式从网络上进行加载。

    1. 相对URL和绝对URL

当一个对象包含一个URL类型的属性时,给该属性分配一个字符串,实际上会为该属性分配一个绝对URL。例如,在

http://exatnple.com/mystuff/test.qml

文件中包含下面的代码:

Image {
    source: "images/logo.png";
}

这里Image的source属性会被指定为

http://example.com/mystuff/images/logo.png

但是如果这个QML正在开发中,比如说C:\User\Fred\Documents\Mystuff\test.qml,则source属性分配的值为C:\User\Fred\Documents\MyStuff\images\logo.png.如果分配给一个URL的字符串已经是绝对URL,那么引擎解析时会直接进行分配。

    1. QRC 资源

在Qt中内建的一个URL方案是”qrc”方案,就是可以通过Qt资源系统将一些资源编译到可执行文件中。

QQuickView *view = new QQucikView;
view->setUrl(QUrl("qrc:/dial.qml"));

这里可以使用相对的URL来指定文件,关于Qt资源系统,可以在帮助中查看The Qt Resource System 文档。

    1. 限制

只有当包含as时import语句才是网络透明的。具体来说:

  • import “dir” 只适用于本地文件系统;
  • import libraryUri 只适用于本地文件系统;
  • import “dir” as D是网络透明的;
  • import libraryUrl as U 是网络透明的。

QML的国际化

与Qt C++类似,QML同样支持国际化,其国际化的操作步骤与C++中是一样的。下面针对QML编程中涉及的一些内容进行讲解。可以在帮助中通过索引Internationalization and Localization with Qt Quick 关键字查看本节内容。

    1. 对所有需要在界面上显示的字符串使用qsTr()

QML中可以使用qsTr(),qsTranslate(),qsTrld(),QT_TR_NOOP(),QT_TRANSLATE_NOOP()和QT_TRID_NOOP()等函数将字符串标记为可翻译的。标记字符串最普通的方式是使用qsTr()函数。

Text {
    id: tex1;
    text: qsTr("Back");
}

这样会在翻译文件中将”Back”标记为关键项。运行时,翻译系统会查找关键字”Back”,然后获取与当前系统语言环境对应的翻译值,结果返回给text属性,用户界面将根据当前语言环境显示”Back”合适的翻译。

    1. 为翻译添加上下文

用户界面上的字符串一般较短,所以需要给翻译人员一些提示来帮助其了解该字符串的上下文。源代码中要被翻译的字符串之前可以利用描述性文本添加一些上下文信息,这些额外的描述性文本包含到.ts翻译文件中。下面的代码片段中,在”//:”一行中的文本是给翻译的主要注释信息,”//~”一行中的文本是可选的额外信息。文本中的第一个单词作为.ts文件中XML元素的附加标识符,所以要确保该单词不是句子的一部分。例如,在.ts文件中会将注释”Context Not related to that”转换为”<extra-Context>Not related to that”.

Text {
    id: txt1;
    // This user interface string is only used here
    //: The Back of the object, not the front
    //~ Context Not related to back - stepping
    text: qsTr("Back");
}
    1. 为相同的文本消除歧义

翻译系统会整合用户界面文本字符串为一些独立的项目,从而避免多次翻译相同文本的情况,然而,有时候相同文本却包含不同的意思。例如,在英语中”back”即意味着向后退一步,也意味着一个对象与前相反的那一面。所以翻译时告诉翻译系统这里应该使用那种翻译。

通过 qsTr()函数的第二个参数添加一些文本,可以消除相同文本的歧义。

Text {
    id: txt1;
    // This user interface string is only used here
    //: The Back of the object, not the front
    //~ Context Not related to back - stepping
    text: qsTr("Back", "not front");
}
    1. 使用%x来为字符串插入参数

不同的语言会将单词以不同的顺序排放,所以通过串联一些单词和数据来构建句子不是理想的方式。通过使用%向句子中插入参数可以解决这一问题。例如,下面的代码片段在句子里面包含了%1和%2两个数字参数,会使用.arg()函数来插入这些参数。

Text {
    text.qsTr("File %1 of %2").arg(counter).arg(total);
}

这里%1 指定了第一个参数,%2指定了第二个参数。

    1. 本地化数字使用%Lx

如果指定一个参数时包含了%L修饰符,该数字便是根据当前区域设置的本地化数字。例如:

Text {
    text: qsTr("%L1").arg(total)
}

如果total是数字”4321.56”(四千三百二十一点五六),在英语区域设置中,输出是”4,321.56”;而在德语区域设置中,输出是”4.321,56”.

    1. 日期,时间和货币国际化

QML中并没有特殊的字符串修饰符来格式化日期和时间,需要自己查询当前的语言环境(地理区域),并使用Date的方法来格式化字符串。Qt.locale()会返回一个Locale对象,其中包含了关于语言环境的所有信息,可以通过解析这些值来为当前语言环境设置合适的翻译。

在下面的代码片段中使用Date()获得了当前的日期,并为当前语言环境转换成了相应的字符串,然后使用%1参数将日期字符串插入到了翻译中:

Text {
    text: qsTr("Date %1").arg(Date().toLocaleString(Qt.locale()))
}

要确保货币数字的本地化,可以使用Number类型,这个类型与Date类型拥有相似的函数,可以用来将数字转换成本地货币字符串。

    1. 使用QT_TR_NOOP()来翻译数据中的文本字符串

如果用户改变了系统语言,但是没有重启,根据系统不同,在数组,列表模型或其他数据结构中的字符串可能不会自动刷新。当文本在用户界面显示时,如果要强制刷新它们,则需要使用QT_TR_NOOP()宏来声明字符串。当要填充用于显示的对象时(例如,为ListModel设置项),则需要显示地为每一个文本设置翻译。

ListModel {
    id: myListModel;
    ListElement {
        //: Capital city of Finland
        name: QT_TR_NOOP("Helsinki");
    }
}

...

Text {
    text: qsTr(myListModel.get(0).name);    // 获取第一个元素地name属性地翻译文本。
}
  • 8 . 使用语言环境来扩展本地化功能

如果要在不同的地理区域使用不同的图形和声音,则可以使用Qt.locale()获取当前的语言环境,然后为该语言选择合适的图形和声音。

Component.onCompleted: {
    switch (Qt.locale().name.substring(0,2))
    {
        case "en": // 显示英文
            languageIcon = "en";
            break;
        case "fi": // 显示芬兰语
            languageIcon = "fi";
            break;
        default: // 显示默认
            languageIcon = "default";
    }
}
  • 9 . 本地化应用程序

QML程序和C++程序使用了相同的底层本地化系统(lupdate, lrelease和.ts文件),可以在相同的程序中同时包含C++和QML用户界面字符串。系统会创建一个组合的翻译文件,QML和C++都可以访问其中的字符串。
国际化时,lupdate工具会从程序中提取用户界面字符串,该工具会读取程序的.pro文件来确定哪些源文件包含需要翻译的文本,这意味着源文件必须在.pro文件中罗列到SOURCES或HEADERS项中,否则,该文件就不会被发现。但是,SOURCES变量只适用于C++源文件,如果将QML或者JavaScript源文件罗列在这里,编译器会将它们作为C++文件处理。作为一种变通的方法,可以使用lupdate_only{…}条件语句,这样lupdate工具就可以发现.qml文件,但是C++编译器会忽略它们。例如,下面的.pro代码片段中指定了两个.qml文件:

lupdate_only {
    SOURCES = main.qml \
    MainPage.qml
}

还可以使用通配符来匹配.qml源文件,不过搜索不是递归的,所以需要指定每个目录,例如:

lupdate_only {
    SOURCES = *.qml \
    *.js \
    content/ *.qml \
    content/ *.js
}

0 评论