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 运行工具承载。
QML 与 XML 比较的边界
跳转到“QML 与 XML 比较的边界”| 比较项 | QML | XML |
|---|---|---|
| 定义与用途 | 带对象、属性、绑定、信号和 JavaScript 表达式的声明式语言,常用于 Qt 界面 | 可扩展标记语言,用元素和属性表示有结构的数据 |
| 常见场景 | 桌面、移动端、车机、仪表与媒体界面等 | 配置、数据交换、文档与其他结构化数据 |
| 语法与结构 | 对象声明和 JavaScript 表达式共同出现;导入的模块决定可用类型 | 标签层次与属性描述结构;含义取决于采用的词汇与应用程序 |
| 动态更新 | 属性绑定和信号是语言机制;热重载取决于开发工具与应用支持 | 文件本身不定义通用的响应式界面行为,需由宿主解释 |
| 性能 | Qt Quick 的渲染器可能使用图形硬件,也可使用软件后端;取决于场景和平台 | 取决于解析、数据规模和上层应用;不能直接与一套界面渲染系统排名 |
| 学习 | 需要理解对象作用域、绑定和事件,而不只是模仿 C++ 的赋值顺序 | 基础标记规则较少,复杂词汇、XPath、XSLT 等仍需单独学习 |
| 集成 | 可与 C++ 类型交互,也可通过 Qt 提供的接口嵌入 Widgets 应用 | 多种语言都可解析和生成 XML;解析成功不代表业务语义正确 |
一个 .qml 文档通常声明一个根对象及其内部对象树,并可定义内联组件。Rectangle 是类型名,Rectangle { ... } 创建该类型的一个对象;不能把“每个可视对象”都当成一个独立的组件文件。保存为大写开头的类型文件,例如 ContentColumn.qml,可以从其他 QML 文档实例化它。
对象、id 与属性值
跳转到“对象、id 与属性值”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 访问的是该子项自己的附加对象。完整可运行例子见附加属性与生命周期。
- Connections 与信号连接:独立监听、切换目标与信号参数。
- 锚点与位置约束:父项、兄弟项和布局边界。
- Qt Quick 元素与模块:按模块查找类型与迁移旧接口。
- QML 中的 JavaScript:语言标准、作用域和宿主差异。
语义依据:Qt 6.10 QML 对象属性、属性绑定、QML 文档结构。原笔记保留的中文学习资源为 QML Book 中文版,其中示例版本需要与当前 Qt 环境核对。