第五章:快速入门
第五章:快速入门
本章介绍的QML, 是在Qt 6中使用的一种声明式用户界面语言。我们将讨论QML语法,它是各种元素构成的树,接着将会介绍最重要的一些基本元素。稍后,我们将简要介绍如何创建自定义元素(称为组件),以及如何使用属性操作来转换元素。最后,我们将看看如何在布局中安排元素,最后看看可供用户输入的元素。
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
声明引入一个模块. 模块可以带版本号,形式是 . 。- 单行注释用
//
,多行注释用/\* \*/
, 这和 C/C++ 及 JavaScript 相同。 - 每个QML必须仅有一个根元素,就象 HTML 那样。
- 元素的类型是通过在其后面跟{ }来完成的。
- 元素可以有属性,是以
名:值
(name:value
)的形式。 - 可以通过
id
(唯一值)来找到QML中的任意元素。 - 元素可以嵌套,即父元素可以有子元素。父元素可以通过
parent
关键字来访问。
import
声明会导入一个QML模块。在Qt5 中,必须指定模块的大版本和小版本号(如,2.15
),在Qt 6中,这是可选的。本书中,将不写版本号,系统将从所选择的kit中选最新的可用版本。
注意
通常,您希望通过id访问特定的元素,或者使用parent关键字访问父元素。因此,使用id: root
将根元素命名为“root”是一个很好的习惯。这样就不必考虑如何在QML文档中命名根元素。
注意
你可以像这样在你的操作系统的命令行运行Qt Quick运行时的例子:
$ $QTDIR/bin/qml RectangleExample.qml
您需要将
$QTDIR
替换为Qt安装的路径。qml可执行文件初始化Qt Quick运行时并解析所提供的qml文件。在Windows中可以很轻松地找到qml,但在Linux中,我还没找到,如果你找到了,请提醒我。
在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"
- }
一起来看下不同的属性特征:
- (1)
id
是跟属性很象的值,它是用于标识QML文件中的元素的。id
并非字符类型,它是标识符,是QML语法的一部分。它在文件中必须保持唯一且不能被重置,也不能被遍历到 (这很象C++ 中的 引用 )。 - (2) 属性可以被赋值,但要注意与其类型相匹配。如果没有被赋值,则会被赋予默认值。可以参考文档中更多关于元素初始化属性的部分。
- (3) 一个属性可以依赖于一个或多个其他属性。这叫做绑定。绑定属性在其依赖属性更改时被更新。它就像一个公式,本例中,
height
应该总是width
的两倍。 - (4) 添加自定义属性的格式是,在
property
后跟类型、名称 以及默认值(可选),格式如property <type> <name> : <value>
。定义中如果没有初始值,则会被赋予默认值。
注意
还可以使用default关键字将一个属性声明为默认属性。如果在元素内部创建了另一个元素,并且没有显式地绑定到某个属性,那么它将绑定到默认属性。例如,在添加子元素时使用This。如果子元素是可见元素,则会自动添加到list类型的默认属性children
元素中。这说起来比较抽象,后面找机会写个例子。
- (5) 另一个重要的声明属性的方法是使用
alias
(别名)关键字(property alias <name>: <reference>
)。alias
关键字允许引用一个对象的属性,或者类型之外的对象本身。我们在之后将使用这个技术:在定义组件时将内部属性或id暴露给根元素。别名属性不需要写明类型,它与所引用的属性或对象的类型相同。 - (6)
text
属性依赖于int类型的自定义属性times
,这时,int
基础类型被自动转换成了string
类型。本表达式也是一个绑定的例子,每当times
发生变化时,文本会跟着变。 - (7) 有些属性是分组属性。当属性结构化特征明显且相关属性应该组合在一起时,将使用此特性。另一种编写分组属性的方式是
font {family: "Ubuntu";pixelSize: 24}
。 - (8) 有些属性属于元素类本身。这是在应用程序中只出现一次的全局设置元素(例如键盘输入)。写法是
<元素>.<属性>:<值>
。 - (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
函数,输出当前的文本。当信号中有参数传入时,这里就要使用function语法了。也可以使用箭头函数语法((text) => {}
),但我觉得function(text) {}
这种语法可读性更好些。 - (2) 当Text元素接收到空格时(用户敲空格键),JavaScript函数
increment()
就会被调用。 - (3) 以格式
function <name>(<parameters>) { ... }
来定义JavaScript 函数,这里的函数增加了空格键点击的记录次数spacePressed
。 每当spacePressed 增加,绑定的属性也会被更新。
Binding绑定
QML中的绑定符:
与JavsScript中的赋值符=
的区别在于,绑定符是在绑定生命周期内一直有效的约定(期间随绑定的值的变化而变化),而JavaScript的赋值是一次性的。
绑定的生命周期在属性上设置新绑定时结束,或者甚至在为属性分配 JavaScript 值时结束。例如,在Esc按键处理函数中将 text 属性设置为空字符串,会破坏我们的增量显示:
- Keys.onEscapePressed: {
- label.text = ''
- }
按下 Esc 后,按下空格键将不再更新显示,因为之前text
属性的绑定(text:“Space press:”+spacePresses +“times”
)被破坏了。
当更改属性的策略存在冲突(通过绑定更改属性增量来更新文本,并通过 JavaScript 赋值清除文本),那就不能使用绑定!您需要在两个属性的更改方式上都使用赋值,因为绑定将被赋值破坏(违反绑定约定!)。
核心元素
元素可以分为可见元素和非可见元素。可见元素(如Rectangle
)有一个坐标系,一般在屏幕某块区域呈现。非可见元素(如Timer
)提供常用的功能,用于操作可视元素。
目前,我们将专注于基本的视觉元素,例如Item、Rectangle、Text、Image
和 MouseArea
。但是,通过使用 Qt Quick Controls 2 模块,可以创建由标准平台组件(如按钮、标签和滑块)构建的用户界面。
Item元素
Item
是所有可视元素的基类,所有其它可视类都是从Item
继承来的。它本身并不绘制可视内容,但它定义了所有在可视元素间都通用属性,如下(以下译文真是不知道该怎么翻了):
- Geometry -
x
和y
用于定义左上角位置,width
和height
定义元素的宽度与高度,z
定义元素堆叠的层次,以将元素从其默认顺序向前或向后调整。 - Layout handling -
anchors
锚点(left, right, top, bottom, vertical 以及 horizontal center)用于定位元素,它决定了相对于其他具有可选边距margins
的元素之间的位置。 - Key handling -
Key
和KeyNavigation
属性关联键处理,focus
属性表示是否首先启用按键处理。 - Transformation -
scale
缩放和rotate
旋转变换以及用于x,y,z变换的通用变换属性列表,以及一个变换原点transformOrigin
。 - Visual -
opacity
控制透明度,visible
用于显示/隐藏元素,clip
限定绘制的边缘,smooth
用于增强渲染的品质。 - State definition -
states
列表属性,包括支持的状态列表、当前state
状态属性和过渡列表transitions
属性,以使状态变化成为动画。
为了更好的理解这些属性,我们将在本章呈现的元素中介绍它们。再次强调 ,这些基础属性在每个可见元素中通用。
注意
Item
元素经常用于其它元素的容器,类似HTML中的div
。
矩形元素 Rectangle Element
Rectangle
继承于 Item
并添加了填充颜色属性。此外,它还支持由border.color
和border.width
定义边框。可以使用 radius
属性来创建圆角矩形。
- Rectangle {
- id: rect1
- x: 12; y: 12
- width: 76; height: 96
- color: "lightsteelblue"
- }
- Rectangle {
- id: rect2
- x: 112; y: 12
- width: 76; height: 96
- border.color: "lightsteelblue"
- border.width: 4
- radius: 8
- }
注意
有效的颜色值是来自 SVG 颜色名称(参见 这里)。您可以在 QML 中以不同的方式设置颜色,但最常见的方式是 RGB 字符串(“#FF4444”)或颜色名称(例如“white”)。
可以使用一些 JavaScript 创建随机颜色:
- color: Qt.rgba( Math.random(), Math.random(), Math.random(), 1 )
除了填充颜色和边框外,矩形还支持自定义渐变:
- Rectangle {
- id: rect1
- x: 12; y: 12
- width: 176; height: 96
- gradient: Gradient {
- GradientStop { position: 0.0; color: "lightsteelblue" }
- GradientStop { position: 1.0; color: "slategray" }
- }
- border.color: "slategray"
- }
渐变由一系列渐变色标定义。每个停靠点都有位置和颜色。该位置标记 y 轴上的位置(0 = 顶部,1 = 底部)。 GradientStop
的颜色标记了该位置的颜色。
注意
没有设置width/height
的矩形将不可见。当有几个矩形宽度(高度)属性相互依赖或某些逻辑出现问题时,通常会发生这种情况。所以小心!
注意
无法创建有角度的渐变。为此,最好使用预定义的图像。一种可行方法是仅使用渐变旋转矩形,但请注意旋转矩形的坐标系不会改变,因此会导致混乱,因为元素的几何形状与可见区域不同。从作者的角度来看,在这种情况下使用经过设计的渐变图像是更好的方法。
文本元素 Text Element
使用Text
元素来展示文本。它最显著的属性是类型为string
的text
属性。元素会根据给定内容的字体来计算其高度与宽度。字体可以使用字体属性来设置(比如font.family
,font.pixelSize
等)。修改字体颜色使用颜色属性。
- Text {
- text: "The quick brown fox"
- color: "#303030"
- font.family: "Ubuntu"
- font.pixelSize: 28
- }
文本可以使用horizontalAlignment和verticalAlignment属性对齐到两边和中间。为了进一步增强文本呈现,您可以使用
style
和styleColor
属性,它们允许您以轮廓模式、凸出模式和下沉模式呈现文本。对于较长的文本,您通常希望定义一个断点位置,如a very…long text,这可以使用
elide
属性来实现。elide
属性允许您将省略位置设置为文本的左边、右边或中间。如果你不希望省略模式的“…”出现,但仍然希望看到全文,你也可以使用
wrapMode
属性包装文本(只有当显式设置宽度时才有效):
- Text {
- width: 40; height: 120
- text: 'A very long text'
- // '...' shall appear in the middle
- elide: Text.ElideMiddle
- // red sunken text styling
- style: Text.Sunken
- styleColor: '#FF4444'
- // align text to the top
- verticalAlignment: Text.AlignTop
- // only sensible when no elide mode
- // wrapMode: Text.WordWrap
- }
Text
元素只显示给定的文本,它占用的剩余空间是透明的。这意味着它不渲染任何背景装饰,所以如果需要,可以由您指定合适的背景图片。
注意
请注意,Text
项的初始宽度依赖于所设置的字体和文本字符串。没有设置宽度及文本的Text元素将不可见,因为初始宽度将为0。
注意
通常,当你想要布局文本元素时,你需要区分文本元素边界框内的文本对齐和元素边界框本身的对齐。在前一种情况下,需要使用horizontalAlignment
和verticalAlignment
属性,而在后一种情况下,需要操作元素坐标或使用锚点。
图象元素
Image
元素可以呈现各种格式的图片(PNG,JPG,GIF,WEBP
)。更多受支持的图片格式,可以参考Qt文档。
除了提供图像URL的source
属性外,它还包含一个fillMode
来控制调整大小的行为。
- Image {
- x: 12; y: 12
- // width: 72
- // height: 72
- source: "assets/triangle_red.png"
- }
- Image {
- x: 12+64+12; y: 12
- // width: 72
- height: 72/2
- source: "assets/triangle_red.png"
- fillMode: Image.PreserveAspectCrop
- clip: true
- }
注意
URL可以是本地形式( “./images/home.png” ) ,也可以是远程网页形式(e.g. “http://example.org/home.png ”)
注意
使用PreserveAspectCrop
的Image
元素还应该启用剪辑,以避免图像数据被渲染到Image
边界之外。默认情况下,剪辑被禁用(clip: false
)。需要启用剪辑 (clip: true
) 以将绘画约束到元素边界内。这可以用于任何视觉元素,但应谨慎使用。
注意
使用 C++,您可以使用QQuickImageProvider
创建自己的图像提供程序。这允许您动态创建图像并使用线程加载图像。
鼠标区域元素 MouseArea Element
与元素交互通常需要用到MouseArea
。它是一种可以捕捉鼠标事件的,矩形的不可见元素。鼠标区域元素通常与可视元素一起使用,当用户与可视元素互动时,它可以完成某些命令。
- Rectangle {
- id: rect1
- x: 12; y: 12
- width: 76; height: 96
- color: "lightsteelblue"
- MouseArea {
- id: area
- width: parent.width
- height: parent.height
- onClicked: rect2.visible = !rect2.visible
- }
- }
- Rectangle {
- id: rect2
- x: 112; y: 12
- width: 76; height: 96
- border.color: "lightsteelblue"
- border.width: 4
- radius: 8
- }
注意
这是Qt Quick中很重要的一点:输入响应与界面展示分离。这允许将交互区域设置的比与可视元素区域大。
提示
更多的复杂交互,参考Qt Quick输入处理。它们试图用于代替MouseArea
和Flickable
等元素,并提供更好的控制和灵活性。这个想法是在每个处理程序实例中处理一个交互方面,而不是像以前那样,将来自一个特定来源的所有事件集中在一个元素中处理。
组件
组件是可重用的元素。QML有多种创建组件的方式,本节只涉及最简单的方式:文件式组件。文件式组件的创建方式是,将QML的元素写在文件中,并将文件命名为元素名(如Button.qml
)。可以象其它Qt Quick 组件一样使用文件式组件。本例,在代码中大概就象Button{...}
。
本例中,将会创建一个包括文本组件和鼠标区域的矩形。这就象一个简单按钮,能验证组件的一些用法而已,没有涉及更复杂的东西。
- Rectangle { // our inlined button ui
- id: button
- x: 12; y: 12
- width: 116; height: 26
- color: "lightsteelblue"
- border.color: "slategrey"
- Text {
- anchors.centerIn: parent
- text: "Start"
- }
- MouseArea {
- anchors.fill: parent
- onClicked: {
- status.text = "Button clicked!"
- }
- }
- }
- Text { // text changes when button was clicked
- id: status
- x: 12; y: 76
- width: 116; height: 26
- text: "waiting ..."
- horizontalAlignment: Text.AlignHCenter
- }
形成的UI如下图所。第一张是初始状态样式,第二张是点击鼠标后的样式。
接下来,我们将按钮界面抽象成可重用的组件。本例中,要为按钮设计可能的API。想象下别人如何使用你设计的按钮,有助于帮你设计按钮。我是这么做的:
- // minimal API for a button
- Button {
- text: "Click Me"
- onClicked: { /* do something */ }
- }
我想设置文本使用text
属性,并实现自定义的单击处理程序。此外,我希望按钮有一个可以覆盖的合理的初始大小(例如,宽度:240
)。
我们创建Button.qml
文件并将按钮UI代码写入文件。而且,我们需要将用户要用到的属性(在树状结构的)最上层暴露出来。
- // Button.qml
- import QtQuick
- Rectangle {
- id: root
- // export button properties
- property alias text: label.text
- signal clicked
- width: 116; height: 26
- color: "lightsteelblue"
- border.color: "slategrey"
- Text {
- id: label
- anchors.centerIn: parent
- text: "Start"
- }
- MouseArea {
- anchors.fill: parent
- onClicked: {
- root.clicked()
- }
- }
- }
我们已经将文本属性和点击信号提取到顶层。通常将最顶层的元素命名为root
,这样方便引用。这里使用了QML的alias
特性,这是一种将QML中的元素属性导出到顶层的方式,如此便可以在元素外部使用(元素属性)了。只有根级/顶层的属性才能被文件外的其它组件引用,这一点很重要。
在文件中简单声明便要使用Button
元素了。所以早先的例子可以再简洁一些:
- Button { // our Button component
- id: button
- x: 12; y: 12
- text: "Start"
- onClicked: {
- status.text = "Button clicked!"
- }
- }
- Text { // text changes when button was clicked
- id: status
- x: 12; y: 76
- width: 116; height: 26
- text: "waiting ..."
- horizontalAlignment: Text.AlignHCenter
- }
现在就可以象这样Button{...}
在你的代码中使用很多按钮了。实际的按钮组件可能会更复杂,比如,有更好地点击反馈或更漂亮的外观。
**注意**
如果你愿意,你可以更进一步使用Item
作为根元素。这可以防止用户更改我们设计的按钮的颜色,并为我们提供对导出 API 的更多控制。目标应该是导出最小的 API。实际上,这意味着我们需要将根元素Rectangle
替换为一个Item
,并将该矩形元素放在根元素内。
- Item {
- id: root
- width: 116; height: 26
- property alias text: label.text
- signal clicked
- Rectangle {
- anchors.fill parent
- color: "lightsteelblue"
- border.color: "slategrey"
- }
- ...
- }
使用这个技术,很容易构建整套可用组件。
简单变换
变换就是操作修改了元素的坐标位置。QML 元素通常可以被移动、旋转、缩放,有简单的和相对复杂的方式。
我们就从比较简单的变换讲起。
一个简单的变换是通过改变x,y
的坐标来完成的。旋转通过rotation
属性实现,其取值是角度形式(0...360)。缩放通过scale
实现,当取值小于1时缩小,大于1时放大。旋转和缩放不会改变元素的坐标值(x,y
)和宽高比(width/height
)。只是绘画指令发生了改变。
在展示示例之前,我想介绍一个小帮手:ClickableImage
元素。ClickableImage
只是一个带有鼠标区域的图像。这带来了一个有用的经验法则——如果你已经复制了一段代码三次,将它提取到一个组件中。
- // ClickableImage.qml
- // Simple image which can be clicked
- import QtQuick
- Image {
- id: root
- signal clicked
- MouseArea {
- anchors.fill: parent
- onClicked: root.clicked()
- }
- }
我们使用可点击的图像来呈现三个对象(框、圆、三角形)。单击时,每个对象都会执行简单的转换。单击背景将重置场景。
- // transformation.qml
- import QtQuick
- Item {
- // set width based on given background
- width: bg.width
- height: bg.height
- Image { // nice background image
- id: bg
- source: "assets/background.png"
- }
- MouseArea {
- id: backgroundClicker
- // needs to be before the images as order matters
- // otherwise this mousearea would be before the other elements
- // and consume the mouse events
- anchors.fill: parent
- onClicked: {
- // reset our little scene
- circle.x = 84
- box.rotation = 0
- triangle.rotation = 0
- triangle.scale = 1.0
- }
- }
- ClickableImage {
- id: circle
- x: 84; y: 68
- source: "assets/circle_blue.png"
- antialiasing: true
- onClicked: {
- // increase the x-position on click
- x += 20
- }
- }
- ClickableImage {
- id: box
- x: 164; y: 68
- source: "assets/box_green.png"
- antialiasing: true
- onClicked: {
- // increase the rotation on click
- rotation += 15
- }
- }
- ClickableImage {
- id: triangle
- x: 248; y: 68
- source: "assets/triangle_red.png"
- antialiasing: true
- onClicked: {
- // several transformations
- rotation += 15
- scale += 0.05
- }
- }
-
- // ...
圆圈会在每次单击时增加 x 位置,矩形框会在每次单击时旋转一定角度。每次单击时,三角形都会做旋转和放大的组合变换。对于缩放和旋转操作,我们设置
antialiasing: true
以启用抗锯齿,因性能原因该属性默认是关闭的(与裁剪属性 clip 相同)。在自己的项目中,当看到图形边缘有锯齿时,应该打开该属性。
**注意 **
为了在缩放图像时获得更好的视觉质量,建议缩小而不是放大图片。使用较大的比例放大图像将导致缩放伪影(图像模糊)。缩放图像时,您应该考虑使用smooth: true
以启用更高质量的过滤器,但会以性能为代价。
顶层的 MouseArea
覆盖了整个背景并重置ClickableImage元素中的MouseArea
。
**注意 **
在QML代码中出现越早的元素,其z轴的层级越低。如果点击圆形图片足够多次,它会跑到矩形下面,这是因为box
在代码中出现的晚于circle
。z轴的顺序也可以由元素的z
属性来设置。
同样的规则也适用于鼠标区域。后定义的鼠标区域会覆盖早先定义的鼠标区域,也因此抢占了鼠标事件。
位置元素
在QML中有一些元素是用于定位其它元素的,被称为定位器。Qt Quick提供了如下定位器:Row
, Column
, Grid
and Flow
。可以在下图的各元素的不同位置关系进行验证。
注意
在详细讲解前,先引入一些辅助元素:红、蓝、绿、浅色和深色的小方块。每个组件都包含一个48×48象素的矩形色块。以下列出RedSquare
源码以备引用:
- // RedSquare.qml
- import QtQuick
- Rectangle {
- width: 48
- height: 48
- color: "#ea7025"
- border.color: Qt.lighter(color)
- }
请注意Qt.lighter(color)
这种用法,它根据填充颜色设置了更浅的边框颜色。为了提高代码的紧凑性和可读性,我们将在后面的示例代码中直接引用这些辅助色块。谨记,每个矩形是48×48象素的初始大小。
Column
元素将子元素自上而下排成一列,spacing
属性用来定义元素间的间距。
- // column.qml
- import QtQuick
- DarkSquare {
- id: root
- width: 120
- height: 240
- Column {
- id: row
- anchors.centerIn: parent
- spacing: 8
- RedSquare { }
- GreenSquare { width: 96 }
- BlueSquare { }
- }
- }
Row
元素将其子项彼此相邻放置,从左到右或从右到左,具体取决于 layoutDirection
属性。同样,apacing
间距用于分隔子项。
- // row.qml
- import QtQuick
- BrightSquare {
- id: root
- width: 400; height: 120
- Row {
- id: row
- anchors.centerIn: parent
- spacing: 20
- BlueSquare { }
- GreenSquare { }
- RedSquare { }
- }
- }
Grid
元素将其子元素排列在一个网格中。通过设置rows
和columns
属性,可以限制行数或列数。如果不设置其中任何一个,另一个是根据子项的数量计算的。例如,将行设置为 3 并添加 6 个子项将导致 2 列。属性 flow
和 layoutDirection
用于控制项目添加到网格的顺序,而spacing
控制分隔子项目的空间量。
- // grid.qml
- import QtQuick
- BrightSquare {
- id: root
- width: 160
- height: 160
- Grid {
- id: grid
- rows: 2
- columns: 2
- anchors.centerIn: parent
- spacing: 8
- RedSquare { }
- RedSquare { }
- RedSquare { }
- RedSquare { }
- }
- }
最后一个定位器是 Flow
。它将其子项添加到流中。使用 flow
和 layoutDirection
控制流的方向。它可以横向运行,也可以从上到下运行。它也可以从左到右或相反方向运行。当项目添加到流中时,它们会根据需要进行包装以形成新的行或列。为了使流生效,它必须具有宽度或高度。这可以直接设置,也可以通过锚布局设置。
- // flow.qml
- import QtQuick
- BrightSquare {
- id: root
- width: 160
- height: 160
- Flow {
- anchors.fill: parent
- anchors.margins: 20
- spacing: 20
- RedSquare { }
- BlueSquare { }
- GreenSquare { }
- }
- }
Repeater
元素经常与定位器一起使用。它像 for 循环一样工作并迭代模型。在最简单的用法,模型可以只提供循环次数。
- // repeater.qml
- import QtQuick
- DarkSquare {
- id: root
- width: 252
- height: 252
- property variant colorArray: ["#00bde3", "#67c111", "#ea7025"]
- Grid{
- anchors.fill: parent
- anchors.margins: 8
- spacing: 4
- Repeater {
- model: 16
- Rectangle {
- width: 56; height: 56
- property int colorIndex: Math.floor(Math.random()*3)
- color: root.colorArray[colorIndex]
- border.color: Qt.lighter(color)
- Text {
- anchors.centerIn: parent
- color: "#f0f0f0"
- text: "Cell " + index
- }
- }
- }
- }
- }
在这个复制器(Repeater)示例中,我们使用了一些新的技能。我们定义了自己的 colorArray 属性,它是一个颜色数组。复制器创建一系列矩形(16 个,由模型定义)。对于每个循环,它都会创建由复制器的子级定义的矩形。在矩形中,我们使用 JS 数学函数选择颜色:Math.floor(Math.random()\*3)
。这给了我们一个 0..2 范围内的随机数,我们用它来从颜色数组中选择颜色。如前所述,JavaScript 是 Qt Quick 的核心部分,因此我们可以使用js标准函数。
复制器将index
属性注入复制器。它包含当前循环索引 (0,1,..15)。我们可以使用它来根据索引选出相应颜色,或者在我们的例子中使用 Text
元素来显示当前索引值。
**注意 **
更大的模型、更复杂的处理、更灵活的视图等更高级用法,在模型视图章节中进行了介绍。当要呈现少量静态数据时,比较适合使用复制器。
布局元素
QML 提供了一种使用锚点布局项目的灵活方式。锚定的概念是 Item
的基础,适用于所有 QML 可视元素。锚点的作用就像契约一样,比坐标变换更强大。锚是相对性的表达;需要参照一个相关的元素来锚定。
一个元素有 6 条主要的锚线(top, bottom, left, right, horizontalCenter, verticalCenter
)。此外,Text
元素中的文本还有基线锚点。每条锚线都有一个偏移量,对于top, bottom, left, right
,它们被称为边距,对于horizontalCenter, verticalCenter, baseline
,它们被称为偏移量。
- (1)填充父元素的元素
- GreenSquare {
- BlueSquare {
- width: 12
- anchors.fill: parent
- anchors.margins: 8
- text: '(1)'
- }
- }
- (2)停靠在父元素左侧的元素
- GreenSquare {
- BlueSquare {
- width: 48
- y: 8
- anchors.left: parent.left
- anchors.leftMargin: 8
- text: '(2)'
- }
- }
- (3)元素的左侧锚定在父元素右侧
- GreenSquare {
- BlueSquare {
- width: 48
- anchors.left: parent.right
- text: '(3)'
- }
- }
- (4)元素居中。
Blue1
水平居中于父级。Blue2
也是水平居中的,但在Blue1
上,它的顶部与Blue1
的底线对齐。
- GreenSquare {
- BlueSquare {
- id: blue1
- width: 48; height: 24
- y: 8
- anchors.horizontalCenter: parent.horizontalCenter
- }
- BlueSquare {
- id: blue2
- width: 72; height: 24
- anchors.top: blue1.bottom
- anchors.topMargin: 4
- anchors.horizontalCenter: blue1.horizontalCenter
- text: '(4)'
- }
- }
- (5)在父元素中居中
- GreenSquare {
- BlueSquare {
- width: 48
- anchors.centerIn: parent
- text: '(5)'
- }
- }
- (6)利用相对于水平中线和垂直中线的偏移量来锚定元素
- GreenSquare {
- BlueSquare {
- width: 48
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.horizontalCenterOffset: -12
- anchors.verticalCenter: parent.verticalCenter
- text: '(6)'
- }
- }
彩蛋
这些小方块被赋予了神奇的拖拽能力。试着拖动示例中的方块,你会发现(1)是不能被拖动的,因为它锚定了各个方向(但你可以拖动(1)的父元素,因为它根本没有锚定)。(2)可以竖向拖动,因为它只做了左侧锚定,(3)也是同理。(4)只能竖向拖动,因为两个方块都是水平居中。(5)相对于父元素居中,因此不能拖动,(7)也是同理。拖动元素意味着改变其x,y
坐标。而锚定比设置x,y
坐标有更强的控制力,拖动受限于锚定线。稍后讨论动画时,可以看到其效果。
输入元素
前面已经使用过鼠标区域MouseArea
作为鼠标输入元素了。接下来,我们将聚焦于键盘输入。以文本编辑元素入手:TextInput
和 TextEdit
。
TextInput
TextInput
允许用户输入一行文本。该元素支持输入输入内容验证,如validator
,inputMask
,echoMode
。
- // textinput.qml
- import QtQuick
- Rectangle {
- width: 200
- height: 80
- color: "linen"
- TextInput {
- id: input1
- x: 8; y: 8
- width: 96; height: 20
- focus: true
- text: "Text Input 1"
- }
- TextInput {
- id: input2
- x: 8; y: 36
- width: 96; height: 20
- text: "Text Input 2"
- }
- }
用户可以点击
InputText
以改变焦点。可以通过KeyNavigation
绑定属性来实现焦点的切换。
- // textinput2.qml
- import QtQuick
- Rectangle {
- width: 200
- height: 80
- color: "linen"
- TextInput {
- id: input1
- x: 8; y: 8
- width: 96; height: 20
- focus: true
- text: "Text Input 1"
- KeyNavigation.tab: input2
- }
- TextInput {
- id: input2
- x: 8; y: 36
- width: 96; height: 20
- text: "Text Input 2"
- KeyNavigation.tab: input1
- }
- }
KeyNavigation
绑定属性支持预设一个按键,当按下这个按键时,切换焦点到绑定的元素上。
文本组件除了闪动的光标和用户输入的文本内容外,没有其它可视内容。为了让用户识别到文本输入元素,需要为其增加点辍,比如,一个简单的矩形。当将TextInput
嵌入其它元素时,要确保将可能要访问的重要属性导出。
将以下代码写到可重用的自定义组件,并命名为TLineEditV1
- // TLineEditV1.qml
- import QtQuick
- Rectangle {
- width: 96; height: input.height + 8
- color: "lightsteelblue"
- border.color: "gray"
- property alias text: input.text
- property alias input: input
- TextInput {
- id: input
- anchors.fill: parent
- anchors.margins: 4
- focus: true
- }
- }
重要
如果想全部导出TextInput
,可以写成property alias input : input
。第一个input
是属性名,第二个input
是元素id。
然后用TLineEditV1
组件重写关于KeyNavigation
的例子。
- Rectangle {
- ...
- TLineEditV1 {
- id: input1
- ...
- }
- TLineEditV1 {
- id: input2
- ...
- }
- }
按下Tab键试试,你会看到焦点并未转移到
input2
。focus: true
并未生效。问题在于,当焦点转移到input2
元素时,TLineEditV1
内的顶层元素(Rectangle
)接收到了焦点,而未将焦点转发到TextInput
。QML提供了FocusScope
来避免焦点争抢。
FocusScope
焦点范围规定,当焦点范围接收到焦点时,它将会把焦点转发至其内部具有focus: true
的最末级子元素。我们将创建第二个版本的TLineEdit,名为TLineEditV2,使用focus scope作为根元素。
- // TLineEditV2.qml
- import QtQuick
- FocusScope {
- width: 96; height: input.height + 8
- Rectangle {
- anchors.fill: parent
- color: "lightsteelblue"
- border.color: "gray"
- }
- property alias text: input.text
- property alias input: input
- TextInput {
- id: input
- anchors.fill: parent
- anchors.margins: 4
- focus: true
- }
- }
示例代码现在长这样:
- Rectangle {
- ...
- TLineEditV2 {
- id: input1
- ...
- }
- TLineEditV2 {
- id: input2
- ...
- }
- }
这回按下Tab键,就能在两个组件间正常切换焦点了,且焦点落在组件内的正确的子元素上。
TextEdit
TextEdit
与TextInput
非常象,但可以录入多行文本。它没有文本约束属性,因为这取决于查询文本的绘制尺寸(paintedHeight, paintedWidth
)。我们同样先创建一个自定义组件TTextEdit
以提供文本录入区域,并使用focus scope实现更好的焦点转发。
- // TTextEdit.qml
- import QtQuick
- FocusScope {
- width: 96; height: 96
- Rectangle {
- anchors.fill: parent
- color: "lightsteelblue"
- border.color: "gray"
- }
- property alias text: input.text
- property alias input: input
- TextEdit {
- id: input
- anchors.fill: parent
- anchors.margins: 4
- focus: true
- }
- }
可以象组件TLineEdit
那样使用它。
- // textedit.qml
- import QtQuick
- Rectangle {
- width: 136
- height: 120
- color: "linen"
- TTextEdit {
- id: input
- x: 8; y: 8
- width: 120; height: 104
- focus: true
- text: "Text Edit"
- }
- }
按键元素
Keys
绑定属性允许在按下相应按键时,执行相应代码。比如,移动和缩放方块的操作,可以按上 下 左 右 键来移动它,按 加 减 键来缩放元素。
- // keys.qml
- import QtQuick
- DarkSquare {
- width: 400; height: 200
- GreenSquare {
- id: square
- x: 8; y: 8
- }
- focus: true
- Keys.onLeftPressed: square.x -= 8
- Keys.onRightPressed: square.x += 8
- Keys.onUpPressed: square.y -= 8
- Keys.onDownPressed: square.y += 8
- Keys.onPressed: {
- switch(event.key) {
- case Qt.Key_Plus:
- square.scale += 0.2
- break;
- case Qt.Key_Minus:
- square.scale -= 0.2
- break;
- }
- }
- }
技能进阶
QML的性能
QML和Javascript 是解释型语言,这意味着它们不需要在执行前就编译好。相应的,他们在一个执行引擎内运行。虽然解释操作很耗费资源,但有一些技术可以改善其性能。
QML引擎使用JIT(just-in-time)编译机制来改善性能。它也缓存中间结果来避免重复编译。对开发者来说,这很有效。这唯一的缺点就是在源文件里会生成以qmlc
和jsc
后辍的文件。
如果你想避免因初始化而带来的程序启动迟缓,也可以预编译QML和Javascript。这需要将代码放到Qt源码文件中,具体参考Qt文档中的Compiling QML Ahead of Time章节。