概述
QML 是一种声明性语言,用于描述对象如何相互关联。
Qt Quick 是一个基于 QML 构建的框架,用于构建应用程序的用户界面。它将用户界面分解为更小的元素,这些元素可以组合成组件。Qt Quick 描述了这些用户界面元素的外观和行为。可以使用 Java Script 代码丰富此用户界面描述,以提供简单但也更复杂的逻辑。从这个角度来看,它遵循 HTML-Java Script 模式,但 QML 和 Qt Quick 设计的初衷是用于描述用户界面,而不是文本文档。
在最简单的形式下,Qt Quick允许您创建元素的层次结构。子元素从父元素继承坐标系统。x,y
坐标总是相对于父坐标的。
:::success
Qt Quick构建于QML之上。
QML语言只知道元素、属性、信号和绑定。
Qt Quick是一个基于QML的框架。使用默认属性,可以以优雅的方式构造Qt Quick元素的层次结构。
:::
让我们从一个简单的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++ 和 Java Script 中一样。- 每个 QML 文件都需要有一个根元素,比如 HTML。
- 元素按其类型声明,后跟
{ }
。- 元素可以有属性,它们的形式是
name: value
。- QML 文档中的任意元素可以通过使用它们的 id(一个不带引号的标识符)来访问。
- 元素可以嵌套,这意味着父元素可以有子元素。可以使用
parent
关键字访问父元素。
使用 import 语句,您可以按名称导入 QML 模块。
在 Qt5 中,您必须指定主要和次要版本(例如 2.15),现在在 Qt6 中这是可选的。对于书籍内容,我们删除此可选版本号,因为您通常会自动希望从所选 Qt 套件中选择可用的最新版本。
:::success
通常,您希望通过 id 访问特定元素或使用 parent 关键字访问父元素。因此,使用 id:root 将根元素命名为“root”是一种很好的做法。
那么您就不必考虑如何在 QML 文档中命名根元素。
:::
:::success
你可以从你的操作系统的命令行使用Qt Quick运行时运行这个例子,像这样:$ $QTDIR/bin/qml RectangleExample.qml
您需要将 $QTDIR
替换为 Qt 安装路径的位置。
qml 可执行文件初始化 Qt Quick 运行时并解释提供的 QML 文件。
在Qt Creator中,可以打开对应的工程文件,运行文件Rectangle Example.qml
。
:::
属性
元素通过使用它们的元素名称来声明,但通过使用它们的属性或通过创建自定义属性来定义。
属性是一个简单的键值对,例如:
width: 100, text: 'Greetings', color: '#FF0000'
属性具有明确定义的类型并且可以具有初始值。
Text {
// (1) identifier
id: thisLabel
// (2) set x- and y-position
x: 24; y: 16
// (3) bind height to 2 * width
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.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) 一个属性可以依赖于一个或多个其他属性。这称为绑定。绑定属性在其依赖属性更改时更新。它像合同一样,在这种情况下,
height
应始终是width
的两倍。 (4)使用属性限定符后跟类型、名称和可选的初始值来向元素添加新属性,形式如:
(property <type> <name> : <value>)
,如果没有给出初始值,则选择默认初始值。 :::success 您还可以使用default
关键字将一个属性声明为默认属性。
如果在元素内部创建了另一个元素,并且没有显式地绑定到属性,那么它将绑定到默认属性。
例如,在添加子元素时使用This
。
如果子元素是可见元素,则会自动添加到类型的list
默认子属性元素中。 :::(5) 声明属性的另一个重要方法是使用别名关键字
alias
形式例如:(property alias <name>: <reference>)
,alias
关键字允许我们将对象的属性或对象本身从类型内部转发到外部作用域。稍后我们将在定义组件时使用此技术将内部属性或元素id
导出到根级别。属性别名不需要类型,它使用引用的属性或对象的类型.- (6)
text
属性依赖于int类型的自定义属性times
。int类型的值会自动转换为字符串类型。表达式本身是绑定的另一个示例,每次times
属性更改时都会更新文本内容。 - (7)某些属性是分组属性。当属性更加结构化且相关属性应组合在一起时,将使用此功能。另一种编写分组属性的方法是
font { family: "Ubuntu";pixelSize:24 }
。 - (8)一些属性属于元素类本身。这是针对仅在应用程序中出现一次的全局设置元素(例如键盘输入)完成的。写法是
<Element>.<property>: <value>
。 (9) 对于每个属性,您都可以提供一个信号处理程序。在属性更改后调用此处理程序。例如,这里我们希望在高度发生变化时收到通知,并使用内置控制台将消息记录到系统中。 :::warning 元素id只能用于引用文档中的元素(例如当前文件)。
QML提供了一种称为“动态作用域”的机制,稍后加载的文档会覆盖先前加载的文档中的元素id
。
这样就可以从以前加载的文档中引用元素id
(如果它们还没有被覆盖的话)。这就像创建全局变量。
不幸的是,这在实践中经常会导致非常糟糕的代码,程序依赖于执行顺序。而且这个不能关闭。
因此,请谨慎使用,
或者,更好的做法是完全不使用这种机制。
最好使用文档root
元素上的属性将想要提供的元素导出到外部世界。 :::脚本
QML 和 Java Script(也称为 ECMAScript)是最好的朋友。在 Java Script 一章中,我们将更详细地介绍这种共生关系。目前,我们只是想让您了解这种关系。
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)
Text Changed
上的文本更改处理程序会在每次按下空格键时,更改文本时打印当前文本。当我们使用信号注入的参数时,我们需要在这里使用函数语法。也可以使用箭头函数 ((text) => {}
),但我们觉得function(text) {}
更具可读性。- (2) 当文本元素接收到空格键时(因为用户按下了键盘上的空格键)我们调用一个Java Script 函数
increment()
。 - (3) 以
function () { ... }
的形式定义一个Java Script 函数,它增加了我们的计数器spacePresses
。每次spacePresses
增加时,绑定属性也将更新。绑定
QML :(binding
)和 Java Script =(assignment
)之间的区别在于绑定是一种契约,并且在绑定的整个生命周期内保持真实,而 Java Script 赋值 (=) 是一次性值赋值。当在属性上设置了新的绑定,或者为属性分配了Java Script值时,绑定的生命周期结束。例如,一个键处理程序将text
属性设置为空字符串会破坏增量显示:
按Keys.onEscapePressed: {
label.text = ''
}
escape
键后,按空格键将不再更新显示,因为先前的文本属性绑定(text:"space pressed:"space press"times")
已被破坏。
当你的策略改变一个属性有冲突时,就像在这种情况下(通过绑定改变属性增量而更新文本和通过Java Script赋值清除的文本),你不能使用绑定,您需要在两个属性更改路径上使用赋值,因为绑定将被赋值破坏(违约!).