2. QML基础回顾

在进入组件化界面架构设计之前,有必要把 QML 的核心机制重新梳理一遍。组件化并不是另起炉灶,而是把 QML 已有的语法、元素、绑定、信号与 JavaScript 能力组织成可复用、可组合、可维护的结构。理解这些基础概念之间的协作关系,才能判断哪些内容应该封装为组件、哪些应该通过属性暴露、哪些应该用信号解耦。

一、QML 语法与元素类型:声明式对象树

QML 是一种声明式语言,文件通常以 .qml 结尾。每个 QML 文件定义一个根元素,根元素内部可以嵌套子元素,形成一棵对象树。对象树不仅是视觉层级,也是生命周期与属性继承的载体。例如一个矩形内部放置文本和鼠标区域,父元素的尺寸变化可以影响子元素的布局,子元素也可以访问父级作用域中的 id。

import QtQuick 2.15
import QtQuick.Controls 2.15

Rectangle {
    id: root
    width: 320
    height: 200
    color: "#f5f7fa"

    Text {
        id: title
        anchors.centerIn: parent
        text: "QML 基础回顾"
        font.pixelSize: 20
    }

    MouseArea {
        anchors.fill: parent
        onClicked: root.color = "#dbeafe"
    }
}

QML 元素大致分为几类:可视化元素(如 Rectangle、Text、Image)、交互元素(如 MouseArea、Button)、布局元素(如 Row、Column、Grid、AnchorLayout)、模型视图元素(如 ListView、Repeater)以及非可视化元素(如 Timer、Connections、Binding)。组件化设计时,通常会把可视化与交互组合成自定义元素,把非可视化逻辑抽离为独立模块。

二、属性绑定:自动依赖追踪

属性绑定是 QML 最具表现力的特性之一。使用冒号 : 赋值时,如果右侧表达式引用了其他属性,QML 会自动建立依赖关系。当被依赖属性变化时,绑定表达式会重新求值。这种机制让界面状态与数据状态保持同步,而不需要手动编写更新代码。

Rectangle {
    id: card
    width: 200
    height: width * 0.6

    Text {
        text: "宽度:" + card.width
        anchors.centerIn: parent
    }
}

绑定不是一次性赋值,而是一个持续存在的表达式。若后续用命令式赋值覆盖了绑定,例如 card.width = 300,原来的绑定关系会被破坏。组件化设计中,应尽量通过属性暴露和绑定传递来维持数据流,而不是在外部随意改写内部属性。

建议:对外暴露的属性尽量使用 property alias 或只读属性,避免外部直接修改内部实现细节,从而保护绑定关系。

三、信号与槽:事件驱动与解耦

QML 中的信号用于表达“发生了什么事”,槽则是响应信号的函数或表达式。元素自带信号,例如 MouseArea 的 clicked、Timer 的 triggered;也可以自定义信号,例如 signal submitted(string value)。信号与槽让子组件可以把内部事件向上传递,而不需要知道外部谁在监听。

Item {
    id: editor
    signal submitted(string content)

    function submit() {
        submitted("hello qml")
    }

    Component.onCompleted: submit()
}

Connections {
    target: editor
    function onSubmit(content) {
        console.log("收到内容:", content)
    }
}

在组件化架构中,信号是组件对外的“事件接口”。子组件只负责发出信号,父组件或控制器负责决定后续行为。这样组件之间不直接依赖,替换或复用组件时成本更低。

四、JavaScript 集成:逻辑表达与边界

QML 内嵌 JavaScript 引擎,可以在属性绑定、信号处理函数和自定义函数中编写 JavaScript。它适合做轻量计算、条件判断、字符串处理和局部状态转换。但不应把复杂业务逻辑全部塞进 QML,否则会降低可测试性与可维护性。

function formatSize(bytes) {
    if (bytes < 1024) return bytes + " B"
    if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB"
    return (bytes / 1024 / 1024).toFixed(1) + " MB"
}

常见做法是:QML 负责视图与交互,JavaScript 负责视图逻辑,C++ 或后端负责业务与数据。组件化时,可以把通用 JavaScript 函数放入独立的 .js 文件,通过 import 引入,避免在多个组件中重复实现。

五、核心概念关系图

下面这张图把 QML 基础概念放在同一张关系图中:语法与元素构成对象树,属性绑定维持状态同步,信号与槽负责事件传递,JavaScript 提供逻辑表达,它们共同支撑组件化设计。

QML 基础概念关系图 语法 · 元素 · 绑定 · 信号与槽 · JavaScript QML 对象树 根元素 + 子元素 id / 作用域 / 生命周期 语法与元素类型 Rectangle / Text / Image MouseArea / Timer / ListView 属性绑定 依赖追踪 · 自动更新 绑定 vs 命令式赋值 信号与槽 clicked / triggered 自定义 signal + Connections JavaScript 集成 函数 · 表达式 · 逻辑 独立 .js 文件复用 组件化 = 对象树封装 + 属性绑定暴露 + 信号解耦 + JavaScript 逻辑分层

六、常见误区与复盘清单

误区 表现 改进方式
破坏属性绑定 外部直接给内部属性赋值,导致绑定失效 通过属性别名或只读属性暴露,内部维护绑定
信号滥用 所有交互都向上传递,父组件承担过多逻辑 区分局部状态与全局事件,局部状态留在组件内
JavaScript 过重 QML 中堆积大量业务计算与网络请求 把业务逻辑下沉到 C++ 或后端,QML 只做视图逻辑
元素层级过深 一个文件内嵌套十几层,难以定位与复用 按职责拆分为自定义组件,控制单文件复杂度
id 命名随意 id 冲突或语义不清,影响作用域与可读性 使用语义化 id,避免跨组件依赖内部 id
复盘时重点检查:属性绑定是否被命令式赋值覆盖;信号是否只传递必要信息;JavaScript 是否承担了本应属于业务层的职责;组件边界是否清晰。