跳转到内容
新建笔记

QML 语法与属性:对象、绑定、信号和组件

QML 是 Qt 的声明式语言:用对象树描述结构,用属性绑定描述关系,用信号处理器和 JavaScript 函数响应事件。Qt Quick 提供 Item、Rectangle、Text、Image 等界面类型;QML 语言本身还可以描述非可视对象。学习时先区分语言、导入模块和具体类型,不必依赖原笔记中互相矛盾的英文全称。

本文将原来的 QML 入门、语法解释和元素属性笔记合并。示例按 Qt 6.10 编写,使用无版本号导入;本机验证版本为 Qt 6.10.2。Qt Creator 的项目模板名称随版本变化,可以从 Qt Quick Application 类模板开始,再把示例保存为对应 .qml 文件。只有 Item 或 Rectangle 根的例子,需要由 Qt Quick 窗口或 QML 运行工具承载。

比较项QMLXML
定义与用途带对象、属性、绑定、信号和 JavaScript 表达式的声明式语言,常用于 Qt 界面可扩展标记语言,用元素和属性表示有结构的数据
常见场景桌面、移动端、车机、仪表与媒体界面等配置、数据交换、文档与其他结构化数据
语法与结构对象声明和 JavaScript 表达式共同出现;导入的模块决定可用类型标签层次与属性描述结构;含义取决于采用的词汇与应用程序
动态更新属性绑定和信号是语言机制;热重载取决于开发工具与应用支持文件本身不定义通用的响应式界面行为,需由宿主解释
性能Qt Quick 的渲染器可能使用图形硬件,也可使用软件后端;取决于场景和平台取决于解析、数据规模和上层应用;不能直接与一套界面渲染系统排名
学习需要理解对象作用域、绑定和事件,而不只是模仿 C++ 的赋值顺序基础标记规则较少,复杂词汇、XPath、XSLT 等仍需单独学习
集成可与 C++ 类型交互,也可通过 Qt 提供的接口嵌入 Widgets 应用多种语言都可解析和生成 XML;解析成功不代表业务语义正确

一个 .qml 文档通常声明一个根对象及其内部对象树,并可定义内联组件。Rectangle 是类型名,Rectangle { ... } 创建该类型的一个对象;不能把“每个可视对象”都当成一个独立的组件文件。保存为大写开头的类型文件,例如 ContentColumn.qml,可以从其他 QML 文档实例化它。

id: rocket 让同一组件作用域中的其他表达式能引用这个对象。id 不是普通可读写属性:不能在运行时重新给它赋值,也不能靠 rocket.id 读取名称;它也不是 C++ 指针声明。需要从 C++ 查找测试对象时,可另设普通字符串属性 objectName。

属性可以是类型自带的,也可以用 property int times: 24 自定义。width、height、color、opacity、visible 都是具体类型的属性;默认值应查该类型文档,不能从“类型相同”推出所有属性默认值相同。focus 是 Item 的普通属性,Keys 则提供附加事件处理。

下面保留原笔记的火箭、图片定位和空格计数场景。图片用内嵌 SVG 提供,因而没有缺失的 assets/rocket.png 外部依赖;加载 SVG 需要部署 Qt 的 SVG 图像支持。保存为 Rocket.qml:

import QtQuick
Rectangle {
id: root
width: 320
height: 220
color: "#d8d8d8"
focus: true
property int times: 24
function incrementTimes() {
root.times += 1
}
Image {
id: rocketImage
objectName: "rocketImage"
width: 64
height: 100
x: (root.width - width) / 2
y: 16
fillMode: Image.PreserveAspectFit
source: "data:image/svg+xml," + encodeURIComponent(
'<svg xmlns="http://www.w3.org/2000/svg" width="64" height="100">'
+ '<path d="M32 4 L49 42 L49 76 L15 76 L15 42 Z" fill="#b83e42"/>'
+ '<circle cx="32" cy="43" r="9" fill="#c7e8ee"/>'
+ '<path d="M22 80 L32 98 L42 80 Z" fill="#e9a343"/>'
+ '</svg>')
}
Text {
id: rocketText
objectName: "rocketText"
y: rocketImage.y + rocketImage.height + 10
width: root.width
horizontalAlignment: Text.AlignHCenter
text: "Rocket\nSpace pressed: " + root.times + " times"
font {
family: "Sans Serif"
pixelSize: 18
}
}
Keys.onSpacePressed: function(event) {
root.incrementTimes()
event.accepted = true
}
MouseArea {
anchors.fill: parent
onClicked: root.forceActiveFocus()
}
}

text 只赋值一次,并绑定 times;原稿在同一个 Text 内写两次 text: 会造成重复属性错误。空格由当前拥有活动焦点的项目接收,focus: true 配合窗口激活工作,点击区域也能重新取得焦点。// 和 /* ... */ 分别用于单行与多行注释。

这里拼接字符串时,JavaScript 把整数转成文本。QML 也提供部分字符串到类型值的转换,例如 color: "red";这不表示任何字符串都能赋给任何类型。像 property int count: "four" 这样的声明无效,不能把转换失败当成默认得到零。

绑定、赋值、组属性与对象属性

跳转到“绑定、赋值、组属性与对象属性”

height: width * 2 表达持续关系,依赖的 width 变化后重新求值。height = width * 2 是执行到该语句时的一次赋值,并会移除这个属性原有的绑定。需要从 JavaScript 恢复绑定时,赋予 Qt.binding(function() { ... }) 的结果。

font.pixelSize 与 font { pixelSize: ... } 是组属性的两种写法。gradient: Gradient { ... } 则把一个 Gradient 对象赋给 Rectangle.gradient,内部的 GradientStop 描述颜色位置;不能因为其他类型也叫“图形元素”就假定它们都有同样的 gradient 属性。

保存为 Bindings.qml,依次调用 setFixedHeight()、修改 baseWidth、再调用 restoreHeightBinding(),可以观察绑定被替换与恢复的差别:

import QtQuick
Item {
id: root
width: 400
height: 320
property int baseWidth: 100
property alias rectWidth: panel.width
property alias rectHeight: panel.height
property int heightChanges: 0
function setFixedHeight() {
panel.height = 75
}
function restoreHeightBinding() {
panel.height = Qt.binding(function() { return panel.width * 2 })
}
Rectangle {
id: panel
width: root.baseWidth
height: width * 2
opacity: 0.8
visible: true
gradient: Gradient {
GradientStop { position: 0.0; color: "yellow" }
GradientStop { position: 1.0; color: "green" }
}
onHeightChanged: root.heightChanges += 1
}
Text {
anchors.right: parent.right
anchors.bottom: parent.bottom
text: "Rectangle: " + root.rectWidth + " × " + root.rectHeight
font.pixelSize: 18
}
}

property alias 给已有属性建立另一个访问入口。这里通过 root.rectHeight 写入,操作的就是 panel.height;它不是保存副本,也不是两个独立属性之间自动建立的双向绑定。别名不能写任意 JavaScript 计算表达式,别名能引用的对象还受声明作用域限制。

默认属性是组件的内容入口

跳转到“默认属性是组件的内容入口”

default 指定调用方省略属性名时,对象声明要写入的属性。它和“某个属性没写值时的默认值”是两件事。Item 默认接收对象的属性是 data;其中的可视对象也会成为可视子项,非可视对象则不因此变成画面元素。

要把调用方声明的子项送进一个 Column,应把容器和调用处分成两个文件。ContentColumn.qml:

import QtQuick
Item {
id: root
default property alias content: column.children
property alias gap: column.spacing
implicitWidth: column.implicitWidth
implicitHeight: column.implicitHeight
Column {
id: column
objectName: "contentColumn"
spacing: 10
}
}

同目录的 ContentDemo.qml:

import QtQuick
Item {
width: 400
height: 300
ContentColumn {
id: content
anchors.centerIn: parent
gap: 10
Rectangle { width: 100; height: 50; color: "red" }
Rectangle { width: 100; height: 50; color: "blue" }
}
}

这里调用方的两个矩形会进入 column.children,由 Column 排列。原稿把别名、内部 Column 和两个矩形全写在同一个组件定义里,却声称两个矩形会自动进入该列:在 Qt 6.10.2 实测,三个可视对象仍直接位于根项目下,列内没有子项。不能把组件内部声明和调用方提供的内容混为一谈。children 仅接收可视 Item;若要接收包含 Timer 等非可视对象的内容,应按用途选择 data 等对象列表接口。

信号、处理器与附加对象

跳转到“信号、处理器与附加对象”

signal submitted(string message) 声明信号,submitted("ready") 发出信号,onSubmitted: function(message) { ... } 是处理器。普通 QML 自定义属性通常带有对应的变更通知;heightChanged 是信号,onHeightChanged 是处理器,二者都不等于“height 是特殊信号属性”。

附加属性和处理器由另一类型提供,例如 ListView.isCurrentItem、Keys.onPressed、Component.onCompleted。每个附加对象对应特定接收对象。嵌套在委托内部的子项要读委托状态,应显式使用 delegateItem.ListView.isCurrentItem;裸写 ListView.isCurrentItem 访问的是该子项自己的附加对象。完整可运行例子见附加属性与生命周期。

语义依据:Qt 6.10 QML 对象属性、属性绑定、QML 文档结构。原笔记保留的中文学习资源为 QML Book 中文版,其中示例版本需要与当前 Qt 环境核对。