QML 入门对象、属性、绑定与信号(六)

13 阅读8分钟

适合人群: 跑通过第一个 Qt Quick 应用,想系统理解 QML 语法的新手

前言

前面几篇我们已经能写出简单的 QML 界面,但对语法的理解还停留在"照着抄"的阶段。本文的目标是让你真正理解 QML 的核心机制——对象树、属性绑定、信号与处理器——这些是整个 Qt Quick 开发的地基,后续所有课程都建立在这里。

一、QML 是什么:声明式 vs 命令式

传统的命令式编程描述"怎么做":

// C++ 命令式:一步步告诉程序怎么做
QLabel *label = new QLabel(this);
label->setText("Hello");
label->setGeometry(100, 100, 200, 40);
label->setAlignment(Qt::AlignCenter);

QML 的声明式描述"是什么":

// QML 声明式:描述这个元素的状态
Text {
    x: 100; y: 100
    width: 200; height: 40
    text: "Hello"
    horizontalAlignment: Text.AlignHCenter
}

两段代码效果相同,但 QML 版本更直接地表达了"这是一个文本,位置在这里,内容是这个",而不是一系列操作步骤。这就是声明式的本质。


二、QML 文档结构

每个 .qml 文件的基本结构如下:

// 1. 导入语句
import QtQuick
import QtQuick.Controls

// 2. 根对象(每个文件只有一个根对象)
Rectangle {

    // 3. 属性赋值
    width: 400
    height: 300
    color: "white"

    // 4. 子对象
    Text {
        anchors.centerIn: parent
        text: "Hello, QML!"
    }
}

三条基本规则:

  1. 每个 .qml 文件有且只有一个根对象
  2. 对象可以嵌套,形成父子关系的对象树
  3. import 语句必须写在文件最顶部

三、对象与对象树

QML 界面是一棵对象树,父对象包含子对象,子对象的坐标相对于父对象计算。

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 基本属性赋值

Rectangle {
    width: 200          // 整数
    height: 100
    color: "steelblue"  // 颜色字符串
    opacity: 0.8        // 浮点数
    visible: true       // 布尔值
    radius: 8           // 圆角半径
}

4.2 属性的类型

QML 中常见的属性类型:

类型示例
intwidth: 200
realopacity: 0.5
boolvisible: true
stringtext: "Hello"
colorcolor: "#FF5733"color: "red"
urlsource: "images/logo.png"
var任意类型

4.3 自定义属性

property 关键字在对象上定义自己的属性:

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 推荐使用强类型属性声明,能在编译时发现类型错误:

// 推荐:强类型声明
property int score: 0
property string playerName: ""
property bool isGameOver: false

// 不推荐:var 类型失去类型检查
property var score: 0

五、属性绑定:QML 最核心的概念

属性绑定是 QML 中最重要、也是最容易被忽视的机制。

5.1 什么是属性绑定

Rectangle {
    width: 400
    height: width / 2    // height 绑定到 width
}

这里 height: width / 2 不是一次性赋值,而是建立了一个持续有效的依赖关系:每当 width 变化时,height 自动重新计算。

Rectangle {
    id: container
    width: 400
    height: width / 2    // 绑定

    // 拖动窗口改变 width 时,height 自动跟随
}

5.2 绑定 vs 赋值

这是新手最容易犯的错误:

Rectangle {
    id: box
    width: 200

    Text {
        text: "宽度:" + box.width    // 绑定:自动跟随 box.width 变化
    }

    MouseArea {
        anchors.fill: parent
        onClicked: {
            box.width = 300           // 普通赋值:只改变一次
            // 注意:如果之前有绑定,赋值会破坏绑定!
        }
    }
}

关键规则: 在 JavaScript 代码块(如 onClicked)中用 = 赋值,会打断原有的属性绑定。如果需要在事件处理中保持绑定,使用 Qt.binding()

onClicked: {
    box.width = Qt.binding(function() { return parent.width / 2 })
}

5.3 绑定的实际应用

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 文件中的唯一标识符,用于在其他地方引用这个对象:

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 的命名规范:

  • 以小写字母开头:myButtonnameInput
  • 使用驼峰命名:userNameLabel
  • 不能和 QML 关键字冲突:不要用 itemparentroot

七、信号与信号处理器

7.1 什么是信号

信号(Signal)是 Qt 对象系统的核心通信机制。当某件事情发生时,对象发出信号;其他对象可以响应这个信号。

在 QML 中,每个信号对应一个信号处理器,命名规则是:on + 信号名首字母大写。

Button {
    text: "点我"
    onClicked: console.log("按钮被点击")    // clicked 信号的处理器
    onPressed: console.log("按下")          // pressed 信号的处理器
    onReleased: console.log("松开")         // released 信号的处理器
}

7.2 常见的内置信号

// 组件加载完成
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 自定义信号

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)    // 发出信号
    }
}

在父对象中响应这个信号:

Rectangle {
    Card {
        id: myCard
        onCardSelected: function(name) {          // 响应自定义信号
            console.log("选中了:" + name)
        }
    }
}

八、组件:创建可复用的元素

当某段 QML 代码需要在多处使用时,把它封装成组件

方式一:独立的 .qml 文件

新建文件 RoundButton.qml

// 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
    }
}

在其他文件中使用(文件名即类型名):

import QtQuick

Rectangle {
    width: 400
    height: 200

    RoundButton {
        anchors.centerIn: parent
        text: "确认"
        buttonColor: "#1D9E75"
        width: 120
        height: 44
        onClicked: console.log("确认按钮点击")
    }
}

方式二:内联组件

在同一个文件内定义局部组件:

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 指当前对象的父对象:

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

Rectangle {
    id: outerRect    // 用 id 明确标识

    Rectangle {
        MouseArea {
            onClicked: {
                // 用 id 比用 parent.parent 更清晰可靠
                outerRect.color = "red"
            }
        }
    }
}

十、一个完整的综合示例

把本文的知识点整合成一个计数器应用:

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 重复定义

// 错误:同一文件中 id 必须唯一
Rectangle { id: box; color: "red" }
Rectangle { id: box; color: "blue" }   // 报错!

错误二:在 JS 代码块中误用绑定语法

// 错误:冒号绑定语法不能用在 JS 代码块中
onClicked: {
    myText.color: "red"    // 语法错误!
    myText.color = "red"   // 正确:JS 代码块中用 =
}

错误三:循环绑定

// 错误: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,封装可复用逻辑