适合人群: 跑通过第一个 Qt Quick 应用,想系统理解 QML 语法的新手
前言
前面几篇我们已经能写出简单的 QML 界面,但对语法的理解还停留在"照着抄"的阶段。本文的目标是让你真正理解 QML 的核心机制------对象树、属性绑定、信号与处理器------这些是整个 Qt Quick 开发的地基,后续所有课程都建立在这里。
一、QML 是什么:声明式 vs 命令式
传统的命令式编程描述"怎么做":
scss
// C++ 命令式:一步步告诉程序怎么做
QLabel *label = new QLabel(this);
label->setText("Hello");
label->setGeometry(100, 100, 200, 40);
label->setAlignment(Qt::AlignCenter);
QML 的声明式描述"是什么":
less
// QML 声明式:描述这个元素的状态
Text {
x: 100; y: 100
width: 200; height: 40
text: "Hello"
horizontalAlignment: Text.AlignHCenter
}
两段代码效果相同,但 QML 版本更直接地表达了"这是一个文本,位置在这里,内容是这个",而不是一系列操作步骤。这就是声明式的本质。
二、QML 文档结构
每个 .qml 文件的基本结构如下:
arduino
// 1. 导入语句
import QtQuick
import QtQuick.Controls
// 2. 根对象(每个文件只有一个根对象)
Rectangle {
// 3. 属性赋值
width: 400
height: 300
color: "white"
// 4. 子对象
Text {
anchors.centerIn: parent
text: "Hello, QML!"
}
}
三条基本规则:
- 每个
.qml文件有且只有一个根对象 - 对象可以嵌套,形成父子关系的对象树
import语句必须写在文件最顶部
三、对象与对象树
QML 界面是一棵对象树,父对象包含子对象,子对象的坐标相对于父对象计算。
less
Rectangle { // 根对象(父)
width: 400
height: 300
color: "#f0f0f0"
Rectangle { // 子对象 1
x: 20; y: 20
width: 160; height: 120
color: "#4A90E2"
Text { // 孙对象
anchors.centerIn: parent
text: "左上角"
color: "white"
}
}
Rectangle { // 子对象 2
x: 220; y: 20
width: 160; height: 120
color: "#E24A4A"
Text {
anchors.centerIn: parent
text: "右上角"
color: "white"
}
}
}
子对象的 x: 20 是相对于父对象左上角的偏移,而不是相对于屏幕。这让布局计算变得直观:移动父对象,所有子对象跟着一起移动。
四、属性
4.1 基本属性赋值
less
Rectangle {
width: 200 // 整数
height: 100
color: "steelblue" // 颜色字符串
opacity: 0.8 // 浮点数
visible: true // 布尔值
radius: 8 // 圆角半径
}
4.2 属性的类型
QML 中常见的属性类型:
| 类型 | 示例 |
|---|---|
int |
width: 200 |
real |
opacity: 0.5 |
bool |
visible: true |
string |
text: "Hello" |
color |
color: "#FF5733" 或 color: "red" |
url |
source: "images/logo.png" |
var |
任意类型 |
4.3 自定义属性
用 property 关键字在对象上定义自己的属性:
less
Rectangle {
width: 300
height: 200
// 自定义属性
property int clickCount: 0
property string userName: "访客"
property color themeColor: "#4A90E2"
color: themeColor // 使用自定义属性
Text {
anchors.centerIn: parent
text: userName + " 点击了 " + clickCount + " 次"
}
}
自定义属性的好处:把重要的数据集中管理,而不是散落在各个子元素里。
4.4 强类型属性(推荐写法)
Qt 6 推荐使用强类型属性声明,能在编译时发现类型错误:
csharp
// 推荐:强类型声明
property int score: 0
property string playerName: ""
property bool isGameOver: false
// 不推荐:var 类型失去类型检查
property var score: 0
五、属性绑定:QML 最核心的概念
属性绑定是 QML 中最重要、也是最容易被忽视的机制。
5.1 什么是属性绑定
arduino
Rectangle {
width: 400
height: width / 2 // height 绑定到 width
}
这里 height: width / 2 不是一次性赋值,而是建立了一个持续有效的依赖关系 :每当 width 变化时,height 自动重新计算。
arduino
Rectangle {
id: container
width: 400
height: width / 2 // 绑定
// 拖动窗口改变 width 时,height 自动跟随
}
5.2 绑定 vs 赋值
这是新手最容易犯的错误:
arduino
Rectangle {
id: box
width: 200
Text {
text: "宽度:" + box.width // 绑定:自动跟随 box.width 变化
}
MouseArea {
anchors.fill: parent
onClicked: {
box.width = 300 // 普通赋值:只改变一次
// 注意:如果之前有绑定,赋值会破坏绑定!
}
}
}
关键规则: 在 JavaScript 代码块(如 onClicked)中用 = 赋值,会打断 原有的属性绑定。如果需要在事件处理中保持绑定,使用 Qt.binding():
arduino
onClicked: {
box.width = Qt.binding(function() { return parent.width / 2 })
}
5.3 绑定的实际应用
arduino
ApplicationWindow {
id: window
width: 640
height: 480
visible: true
Rectangle {
// 始终填满窗口的一半宽度
width: window.width / 2
height: window.height
color: "#E6F1FB"
Text {
anchors.centerIn: parent
// 实时显示父容器尺寸
text: parent.width + " × " + parent.height
font.pixelSize: 16
}
}
}
拖动窗口调整大小,矩形自动跟随,文字自动更新。这一切不需要写任何事件监听代码。
六、id:给对象命名
id 是对象在当前 QML 文件中的唯一标识符,用于在其他地方引用这个对象:
arduino
Rectangle {
width: 400
height: 300
Rectangle {
id: redBox // 定义 id
width: 100
height: 100
color: "red"
}
Rectangle {
// 通过 id 引用另一个对象
x: redBox.x + redBox.width + 20 // 紧跟在 redBox 右边
width: redBox.width // 与 redBox 同宽
height: redBox.height
color: "blue"
}
}
id 的命名规范:
- 以小写字母开头:
myButton、nameInput - 使用驼峰命名:
userNameLabel - 不能和 QML 关键字冲突:不要用
item、parent、root等
七、信号与信号处理器
7.1 什么是信号
信号(Signal)是 Qt 对象系统的核心通信机制。当某件事情发生时,对象发出信号;其他对象可以响应这个信号。
在 QML 中,每个信号对应一个信号处理器 ,命名规则是:on + 信号名首字母大写。
arduino
Button {
text: "点我"
onClicked: console.log("按钮被点击") // clicked 信号的处理器
onPressed: console.log("按下") // pressed 信号的处理器
onReleased: console.log("松开") // released 信号的处理器
}
7.2 常见的内置信号
arduino
// 组件加载完成
Rectangle {
Component.onCompleted: {
console.log("组件已加载,宽度:" + width)
}
}
// 属性变化信号:on + 属性名 + Changed
Rectangle {
width: 200
onWidthChanged: console.log("宽度变为:" + width)
}
// 鼠标区域信号
MouseArea {
anchors.fill: parent
onClicked: console.log("点击位置:" + mouse.x + ", " + mouse.y)
onDoubleClicked: console.log("双击")
onEntered: console.log("鼠标进入")
onExited: console.log("鼠标离开")
}
7.3 自定义信号
less
Rectangle {
id: card
width: 200
height: 120
color: "#f5f5f5"
radius: 8
// 声明自定义信号
signal cardSelected(string cardName)
property string name: "卡片 A"
MouseArea {
anchors.fill: parent
onClicked: card.cardSelected(card.name) // 发出信号
}
}
在父对象中响应这个信号:
javascript
Rectangle {
Card {
id: myCard
onCardSelected: function(name) { // 响应自定义信号
console.log("选中了:" + name)
}
}
}
八、组件:创建可复用的元素
当某段 QML 代码需要在多处使用时,把它封装成组件。
方式一:独立的 .qml 文件
新建文件 RoundButton.qml:
less
// RoundButton.qml
import QtQuick
import QtQuick.Controls
Button {
id: root
// 暴露可配置的属性
property color buttonColor: "#4A90E2"
contentItem: Text {
text: root.text
color: "white"
font.pixelSize: 14
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
color: root.buttonColor
radius: height / 2 // 完全圆角
opacity: root.pressed ? 0.8 : 1.0
}
}
在其他文件中使用(文件名即类型名):
less
import QtQuick
Rectangle {
width: 400
height: 200
RoundButton {
anchors.centerIn: parent
text: "确认"
buttonColor: "#1D9E75"
width: 120
height: 44
onClicked: console.log("确认按钮点击")
}
}
方式二:内联组件
在同一个文件内定义局部组件:
less
import QtQuick
Rectangle {
width: 400
height: 300
// 定义内联组件
component TagLabel: Rectangle {
property string labelText: ""
width: tagText.width + 16
height: 24
radius: 12
color: "#E6F1FB"
Text {
id: tagText
anchors.centerIn: parent
text: parent.labelText
color: "#185FA5"
font.pixelSize: 12
}
}
// 使用内联组件
Row {
anchors.centerIn: parent
spacing: 8
TagLabel { labelText: "Qt Quick" }
TagLabel { labelText: "QML" }
TagLabel { labelText: "跨平台" }
}
}
九、parent 关键字
在 QML 中,parent 指当前对象的父对象:
arduino
Rectangle {
width: 400
height: 300
Rectangle {
// parent 指外层 Rectangle
width: parent.width * 0.5 // 父宽度的 50%
height: parent.height * 0.5 // 父高度的 50%
anchors.centerIn: parent // 居中于父对象
color: "#4A90E2"
}
}
注意: 在信号处理器的 JavaScript 代码块中,parent 的含义可能改变。建议在需要引用特定对象时使用 id,而不是依赖 parent:
bash
Rectangle {
id: outerRect // 用 id 明确标识
Rectangle {
MouseArea {
onClicked: {
// 用 id 比用 parent.parent 更清晰可靠
outerRect.color = "red"
}
}
}
}
十、一个完整的综合示例
把本文的知识点整合成一个计数器应用:
arduino
import QtQuick
import QtQuick.Controls
ApplicationWindow {
width: 360
height: 480
visible: true
title: "计数器"
// 自定义属性:集中管理状态
property int count: 0
property int step: 1
property color activeColor: count >= 0 ? "#1D9E75" : "#E24A4A"
Column {
anchors.centerIn: parent
spacing: 24
width: 240
// 计数显示
Rectangle {
width: parent.width
height: 100
radius: 12
color: activeColor // 绑定到 activeColor
Text {
anchors.centerIn: parent
text: count // 绑定到 count
font.pixelSize: 48
font.bold: true
color: "white"
}
}
// 步长选择
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: 8
Text {
anchors.verticalCenter: parent.verticalCenter
text: "步长:"
font.pixelSize: 14
color: "#666"
}
Repeater {
model: [1, 5, 10]
delegate: Button {
required property int modelData
text: modelData
highlighted: step === modelData // 绑定高亮状态
onClicked: step = modelData
}
}
}
// 操作按钮
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: 12
Button {
text: "−"
font.pixelSize: 20
width: 72; height: 48
onClicked: count -= step
}
Button {
text: "重置"
width: 72; height: 48
onClicked: count = 0
}
Button {
text: "+"
font.pixelSize: 20
width: 72; height: 48
onClicked: count += step
}
}
// 状态文字:纯绑定,无需任何事件代码
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: count === 0 ? "归零"
: count > 0 ? "正数:" + count
: "负数:" + count
font.pixelSize: 14
color: "#888"
}
}
// 监听 count 变化
onCountChanged: {
if (Math.abs(count) > 100)
console.log("警告:计数超过 100!")
}
}
这个示例展示了:自定义属性、属性绑定(颜色跟随正负值变化)、信号处理器、Repeater 动态生成元素,以及属性变化信号 onCountChanged。
十一、常见错误与注意事项
错误一:id 重复定义
css
// 错误:同一文件中 id 必须唯一
Rectangle { id: box; color: "red" }
Rectangle { id: box; color: "blue" } // 报错!
错误二:在 JS 代码块中误用绑定语法
less
// 错误:冒号绑定语法不能用在 JS 代码块中
onClicked: {
myText.color: "red" // 语法错误!
myText.color = "red" // 正确:JS 代码块中用 =
}
错误三:循环绑定
arduino
// 错误:a 绑定 b,b 又绑定 a,产生无限循环
Rectangle {
id: a
width: b.width // a.width 依赖 b.width
}
Rectangle {
id: b
width: a.width // b.width 又依赖 a.width → 循环!
}
总结
| 概念 | 要点 |
|---|---|
| 声明式 | 描述"是什么",而不是"怎么做" |
| 对象树 | 父子嵌套,子对象坐标相对于父对象 |
| 属性 | 内置属性 + 自定义 property,推荐强类型声明 |
| 属性绑定 | a: b + c 建立持续依赖,= 赋值会打断绑定 |
id |
唯一标识符,用于跨对象引用 |
| 信号处理器 | on + 信号名,响应事件和状态变化 |
| 自定义信号 | signal 关键字声明,实现组件间通信 |
| 组件 | 独立 .qml 文件或内联 component,封装可复用逻辑 |