# QML语法
QML是一种声明性语言,用于描述对象如何相互关联。 QtQuick是一个基于QML的框架,用于构建应用程序的用户界面。 它将用户界面分解为更小的元素,这些元素可以组合成组件。 QtQuick描述了这些用户界面元素的外观和行为。 可以使用JavaScript代码丰富此用户界面描述,以提供简单但也更复杂的逻辑。 从这个角度来看,它遵循HTML-JavaScript模式,但QML和QtQuick从头开始设计用于描述用户界面,而不是文本文档。
QtQuick允许以其最简单的形式创建元素的层次结构。 子元素从父元素继承坐标系, x,y
坐标总是相对于父坐标的。
提示
QtQuick建立在QML之上。QML语言只知道元素、属性、信号和绑定。 QtQuick是一个建立在QML之上的框架。 使用默认属性,可以优雅地构建QtQuick元素的层次结构。
从一个简单的QML文件示例开始来解释这种不同的语法。
// RectangleExample.qml
import QtQuick
// The root element is the Rectangle
Rectangle {
// name this element root
id: root
// properties: <name>: <value>
width: 120; height: 240
// color property
color: "#4A4A4A"
// Declare a nested element (child of root)
Image {
id: triangle
// reference the parent
x: (parent.width - width)/2; y: 40
source: 'assets/triangle_red.png'
}
// Another child of root
Text {
// un-named element
// reference element by id
y: triangle.y + triangle.height + 20
// reference root element
width: root.width
color: 'white'
horizontalAlignment: Text.AlignHCenter
text: 'Triangle'
}
}
import
语句导入一个模块。 可以添加形式为<major>.<minor>
可选的版本。- 可以使用
//
进行单行注释或使用/* */
进行多行注释。 就像在C/C++和JavaScript中一样 - 每个QML文件都需要有一个根元素,如同HTML一样
- 元素按其类型声明,后面跟着
{ }
- 元素可以有属性,它们的形式是
name: value
- QML文档中的任意元素可以通过使用它们的id(一个不带引号的标识符)来访问
- 元素可以嵌套,这意味着父元素可以有子元素。 可以使用
parent
关键字访问父元素
使用 import
语句,可以按名称导入QML模块。 在Qt5中必须指定一个主版本和次版本(例如2.15
),现在在Qt6中这是可选的。 对于书籍内容,我们删除此可选版本号,因为通常会自动希望从所选Qt套件中选择可用的最新版本。
提示
通常想通过id访问特定元素或使用parent
关键字访问父元素。 因此,使用id: root
将根元素命名为“root”是一种很好的做法。 那么就不必考虑如何在QML文档中命名根元素。
提示
可以从操作系统的命令行使用Qt Quick运行时来运行示例,如下所示:
$ $QTDIR/bin/qml RectangleExample.qml
需要将$QTDIR
替换为Qt安装路径的位置。qml
可执行文件初始化Qt Quick运行时并解释提供的QML文件。
在Qt Creator中,可以打开对应的工程文件,运行文件RectangleExample.qml
。
# 属性
元素使用元素名称来声明,但通过本身属性或创建的自定义属性来定义。 属性是一个简单的键值对,例如 width: 100
, text: 'Greetings'
, color: '#FF0000'
。 属性具有明确定义的类型并且可以具有初始值。
Text {
// (1) identifier
// 标识符
id: thisLabel
// (2) set x- and y-position
// 设置x、y位置
x: 24; y: 16
// (3) bind height to 2 * width
// 绑定高是2倍的宽
height: 2 * width
// (4) custom property
// 自定义属性
property int times: 24
// (5) property alias
// 属性的别名
property alias anotherTimes: thisLabel.times
// (6) set text appended by value
// 设置被追加值的文本
text: "Greetings " + times
// (7) font is a grouped property
// 字体是一组属性
font.family: "Ubuntu"
font.pixelSize: 24
// (8) KeyNavigation is an attached property
// KeyNavigation是附属属性
KeyNavigation.tab: otherLabel
// (9) signal handler for property changes
// 属性改变的信号处理器
onHeightChanged: console.log('height:', height)
// focus is need to receive key events
// 接受关键事件需要焦点
focus: true
// change color based on focus value
// 根据焦点的变化改变颜色
color: focus ? "red" : "black"
}
来看看这些属性的不同特征:
(1)
id
是一个非常特殊的类似于属性的值,它用于引用QML文件(在QML中称为“文档”)中的元素。id
不是字符串类型,而被作为标识符和QML语法的一部分。id
在文档中必须是唯一的,并且不能被重置为不同的值,也不能被查询。 (它的行为很像C++的引用。)(2) 属性可以设置为一个具体取决于其类型的值。 如果没有为属性指定值,则将选择一个初始值。 想要获取有关属性初始值的更多信息,需要查阅特定元素的文档。
(3) 一个属性可以依赖于一个或多个其他属性。 这称为绑定(binding)。 绑定属性随着其依赖属性更改而更新。 它的工作类似于契约,在这种情况下,
height
应该始终是width
的两倍。(4) 向元素添加新属性,先使用
property
限定符,后面跟上类型、名称和可选的初始值(property <type> <name> : <value>
)。 如果未给出初始值,则选择默认初始值。
提示
还可以使用default
关键字将一个属性声明为默认属性。 如果在元素内部创建了另一个元素并且未明确绑定到的属性,则它绑定到默认属性。 例如,当添加子元素时会用到它。 如果子元素是可见元素,它们会自动添加到列表类型的默认属性children
中。
(5) 另一个声明属性的重要方式是使用
alias
关键字(property alias <name>: <reference>
)。alias
关键字允许将对象的属性或对象本身从类型内部转发到外部作用域。 稍后将使用此技术,在定义组件时将内部属性或元素id导出到根级别。 属性别名不需要类型,它使用引用的属性或对象的类型。(6)
text
属性取决于int类型的自定义属性times
。 基于int
的值会自动转换为string
类型。 表达式本身是绑定的另一个示例,每次times
属性更改时都会导致文本更新。(7) 某些属性是分组属性。 当属性更加结构化且相关属性应组合在一起时,将使用此功能。 另一种编写分组属性的方法是
font { family: "Ubuntu"; pixelSize: 24 }
。(8) 一些属性属于元素类本身。 这是针对仅在应用程序中出现一次的全局设置元素(例如键盘输入)完成的,写作是
<Element>.<property>: <value>
。(9) 对于每个属性,您都可以提供一个信号处理器。 在属性更改后调用此处处理器。 例如,这里希望在高度发生变化时收到通知,并使用内置控制台将消息记录到系统中。
警告
元素id应当仅用于在文档内部(例如当前文件)引用元素。 QML提供了一种称为“动态范围”的机制,稍后加载的文档会覆盖较早加载的文档中的元素ID。 这使得可以从先前加载的文档中引用元素ID(如果它们尚未被覆盖)。 这就像创建全局变量。 不幸的是,这在实践中经常导致非常糟糕的代码,这样的程序取决于执行顺序;更不幸的是,这无法关闭。 请谨慎使用这种机制,或者最好根本不使用它。 如果想要将元素提供给外部使用,最好使用文档根元素上的属性将其导出。
# 编写脚本
QML和JavaScript(也称为ECMAScript)是最好的朋友。 在 JavaScript 章节中,将更详细地介绍这种共生关系。 目前,只是想了解这种关系。
Text {
id: label
x: 24; y: 24
// custom counter property for space presses
property int spacePresses: 0
text: "Space pressed: " + spacePresses + " times"
// (1) handler for text changes. Need to use function to capture parameters
onTextChanged: function(text) {
console.log("text changed to:", text)
}
// need focus to receive key events
focus: true
// (2) handler with some JS
Keys.onSpacePressed: {
increment()
}
// clear the text on escape
Keys.onEscapePressed: {
label.text = ''
}
// (3) a JS function
function increment() {
spacePresses = spacePresses + 1
}
}
(1) 每次由于按下空格键而更改文本时,文本更改处理器
onTextChanged
都会打印当前文本。 当使用附带参数的信号时,需要在这里使用函数语法。 也可以使用箭头函数((text) => {}
),但作者认为function(text) {}
更具可读性。(2) 当文本元素接收到空格键时(因为用户按下了键盘上的空格键)调用一个JavaScript函数
increment()
。(3) 以
function <name>(<parameters>) { ... }
的形式定义了JavaScript函数,它增加计数器spacePressed
。 每次spacePressed
增加时,绑定的属性也将更新。
# 绑定
QML :
(绑定)和 JavaScript =
(赋值)之间的区别在于绑定是一种契约并且在绑定的生命周期内保持契约不变,而JavaScript赋值(=
)是一次性的赋值。
当在属性上设置新的绑定,或甚至为属性分配JavaScript值时,绑定的生命周期结束。 例如,将text属性设置为空字符串的键处理器会破坏我们的增量显示:
Keys.onEscapePressed: {
label.text = ''
}
按Esc键后,按空格键将不再更新显示,因为之前对text
属性的绑定 (text: “Space pressed: ” + spacePresses + ” times”) 被破坏了。
当更改属性存在冲突策略时,如在这种情况下(通过绑定更改属性增量更新的文本和JavaScript赋值清除的文本),那么不能使用绑定! 需要在两个属性更改路径上使用赋值,因为绑定将被赋值破坏(破坏合同!)。