一、快速入门
本章提供了 QML (Qt6中使用的声明性用户界面语言)的概述。我们将讨论 QML 语法,它是一个元素树,然后概述最重要的基本元素。稍后我们将简要介绍如何创建我们自己的元素,称为组件,以及如何使用属性操作转换元素。最后,我们将研究如何在布局中将元素排列在一起,并最终研究用户可以提供输入的元素。
二、 QML 语法
QML 是一种声明性语言,用于描述对象之间的相互关系。Qt Quick 是一个构建在 QML 之上的框架,用于构建应用程序的用户界面。它将用户界面分解为更小的元素,这些元素可以组合成组件。Qt Quick 描述了这些用户界面元素的外观和行为。这个用户界面描述可以通过 JavaScript 代码来丰富,以提供简单但更复杂的逻辑。从这个角度来看,它遵循 HTML-JavaScript 模式,但是 QML 和 QtQuick 是从头开始设计的,用于描述用户界面,而不是文本文档。
在最简单的形式中,Qt Quick 允许创建元素的层次结构。子元素继承父元素的坐标系。x,y 坐标总是相对于父坐标。
Qt Quick 基于 QML 构建。QML 语言只知道元素、属性、信号和绑定。Qt Quick 是基于 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
语句导入一个模块, 使用.(例如2.15) 的形式添加可选的版本.- 注释可以使用
//
进行单行注释 或者使用/* */
进行多行注释, 就像 C/C++ 和 JavaScript那样。 - 每个 QML 文件都需要有一个确切的根元素,比如 HTML
- 元素由其类型声明,后跟{ }
- 元素可以具有属性,它们的形式是name: value
- 文档中的任意元素可以通过使用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: 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"
}
让我们来看看属性的不同特性:
- id 是一个非常特殊的类属性值,用于引用 QML 文件中的元素(在 QML 中称为“ document”)。Id不是字符串类型,而是一个标识符和 QML 语法的一部分。Id 在文档中必须是唯一的,不能被重置为不同的值,也不能被查询。(它的行为很像C + + 世界中的引用。)
- 属性可以根据其类型设置为值。如果没有为属性赋值,则将选择一个初始值。您需要查阅特定元素的文档,以获得有关属性初始值的更多信息。
- 一个属性可以依赖于一个或多个其他属性。这就是所谓的绑定。绑定属性在其依赖属性更改时更新。它的工作原理类似于一个契约,在这种情况下,高度应该始终是宽度的两倍。
- 使用属性限定符,后跟类型、名称和可选的初始值(property < type > < name > : < value >)向元素添加新属性。如果没有给出初始值,则选择默认初始值。
还可以使用 default 关键字将一个属性声明为默认属性。如果在元素内创建了另一个元素,但没有显式绑定到属性,则该元素将绑定到默认属性。例如,在添加子元素时使用 This。如果子元素是可见元素,则会自动添加到列表类型的默认属性子元素中。
- 声明属性的另一个重要方法是使用
alias
关键字(property alias < name > : < reference >
)。Alias 关键字允许我们将对象的属性或对象本身从类型内部转发到外部作用域。稍后在定义组件以将内部属性或元素 id 导出到根级别时,我们将使用此技术。属性别名不需要类型,它使用引用的属性或对象的类型。 - text 属性取决于 int 类型的自定义属性次数。基于 int 的值自动转换为字符串类型。表达式本身是绑定的另一个示例,每次 times 属性更改时都会更新文本。
- 有些属性是分组属性。当一个属性结构化程度更高并且相关的属性应该组合在一起时,就会使用这个特性。编写分组属性的另一种方法是
font{ family: "Ubuntu"; pixelSize: 24 }
。 - 一些属性属于元素类本身。对于应用程序中只出现一次的全局设置元素(例如键盘输入) ,可以这样做。写法是
<Element>.<property>: <value>
.。 - 对于每个属性,您可以提供一个信号处理程序。此处理程序在属性更改后调用。例如,在这里,我们希望在高度变化时得到通知,并使用内置控制台将消息记录到系统中。
警告
元素 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
}
}
- 文本更改处理程序
onTextChanged
在每次由于按下空格键而更改文本时打印当前文本。当我们使用信号注入的参数时,我们需要在这里使用函数语法。也可以使用箭头函数((text) = > {})
,但我们觉得函数(text){}
更具可读性。 - 当文本元素接收到空格键(因为用户在键盘上按了空格键)时,我们调用一个 JavaScript 函数 increment ()。
(3) Definition of a JavaScript function in the form of function () { … }, which increments our counter spacePresses. Every time spacePresses is incremented, bound properties will also be updated.
(3)定义一个 JavaScript 函数,其格式为 function <name>(<parameters>) { ... }
,这会增加计数器的 spacePresses。每次 spacePress 递增时,绑定属性也将更新。
Binding 绑定
QML: (binding)和 JavaScript = (assignment)之间的区别在于绑定是一个契约,并且在绑定的生命周期内保持为真,而 JavaScript 赋值(=)是一次性的值赋值。
绑定的生命周期在对属性设置新绑定时结束,甚至在为属性分配 JavaScript 值时结束。例如,键处理程序将文本属性设置为空字符串会破坏我们的增量显示:
Keys.onEscapePressed: {
label.text = ''
}
按转义键后,按空格键将不再更新显示,因为之前的文本属性绑定(text: “Space pressed: ” + spacePresses + ” times”)
已被销毁。
如果您有冲突的策略来更改属性,比如在本例中(通过绑定更改属性增量来更新文本,通过 JavaScript 赋值来清除文本) ,那么您就不能使用绑定!您需要在两个属性更改路径上使用赋值,因为该赋值将破坏绑定(违反约定!).