2. QML基础回顾

在进入 QML 数据绑定与业务架构的深入讨论之前,有必要把 QML 的基础语法与运行模型重新梳理一遍。很多开发者在使用 QML 时容易把它当作“另一种写界面的 XML”,结果在后续做数据绑定、状态管理和模块拆分时频频踩坑。实际上,QML 是一门声明式语言,它的元素、属性、信号与槽、组件化机制共同构成了一套完整的对象描述与交互体系,理解这套体系是构建可维护业务架构的前提。

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

QML 文件由若干元素(Element)组成,元素以类型名开头,后跟一对花括号,花括号内部是属性赋值、子元素和信号处理。最外层通常是一个可视元素或非可视的 QtObject。QML 引擎在加载文件时,会按照声明顺序创建对象,并形成一棵对象树,父子关系既决定渲染层级,也决定生命周期归属。

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: "订单总数:" + orderModel.count
        font.pixelSize: 18
    }
}

上面这段代码中,Rectangle 是根元素,Text 是它的子元素。子元素通过 parent 访问父对象,父对象也可以通过 children 访问子对象。需要特别注意的是,id 只在当前 QML 文件内有效,它不是一个全局变量,而是文件作用域内的引用标识,这一点在组件化拆分时非常关键。

二、属性、信号与槽:QML 的交互骨架

QML 的属性分为三类:内建属性(如 width、height、color)、自定义属性(通过 property 声明)以及附加属性(如 Layout.fillWidth)。属性可以绑定表达式,表达式依赖的属性变化时,绑定会自动重新求值,这正是 QML 数据绑定的基础。

信号与槽是 QML 处理事件的核心机制。元素可以声明自定义信号,也可以使用内建信号;槽函数通过 on<SignalName> 语法或 Connections 元素连接。与 Qt C++ 的 connect 不同,QML 中信号处理器的命名是固定的,例如按钮点击写作 onClicked。

Item {
    id: panel
    property int clickCount: 0
    signal submitted(string payload)

    MouseArea {
        anchors.fill: parent
        onClicked: {
            panel.clickCount++
            panel.submitted("click-" + panel.clickCount)
        }
    }

    onSubmitted: console.log("收到提交信号:", payload)
}
信号参数在处理器中可以直接按名称使用,例如 payload;如果信号有多个参数,也可以使用 function(payload) { ... } 形式显式声明。

三、组件化思想:从文件到可复用单元

QML 的组件化有两种常见方式:一是把一组元素封装成独立的 .qml 文件,文件名即组件类型名;二是使用 Component 元素在运行时动态创建对象。前者适合静态复用,后者适合列表委托、弹窗内容等动态场景。

组件化的核心价值在于边界清晰:组件对外暴露属性、信号和函数,对内隐藏实现细节。一个设计良好的组件应当满足“属性进、信号出”的原则,即外部通过属性传入数据,组件通过信号向外通知事件,而不是直接操作外部对象的 id。

// OrderCard.qml
import QtQuick 2.15

Rectangle {
    id: card
    property string orderNo: ""
    property int amount: 0
    signal selected(string no)

    width: 240; height: 72
    color: mouseArea.pressed ? "#dbeafe" : "#ffffff"
    border.color: "#cbd5e1"

    Column {
        anchors.centerIn: parent
        spacing: 4
        Text { text: "订单号:" + card.orderNo }
        Text { text: "金额:" + card.amount }
    }

    MouseArea {
        id: mouseArea
        anchors.fill: parent
        onClicked: card.selected(card.orderNo)
    }
}

使用该组件时,外部只需关心 orderNo、amount 和 selected 三个接口,无需了解内部布局。这种“接口稳定、实现自由”的特性,是业务架构中模块解耦的基础。

四、QML 在业务架构中的定位

在典型的 Qt 业务架构中,QML 负责视图层与交互层,C++ 负责业务逻辑、数据模型与持久化。两者通过属性绑定、信号槽和 QAbstractItemModel 等机制通信。QML 不应承担复杂计算或网络请求,而应专注于“把数据渲染出来、把用户操作抛出去”。

层次 典型技术 职责 QML 中的体现
视图层 QML 元素、布局 渲染界面、响应交互 Rectangle、ListView、Button
绑定层 属性绑定、信号槽 连接数据与界面 property binding、onClicked
模型层 C++ QAbstractItemModel 提供结构化数据 model、delegate
业务层 C++ 服务类、Q_INVOKABLE 业务规则、网络、存储 contextProperty、单例

下面的 SVG 图展示了 QML 基础元素在业务架构中的分层关系与数据流向,帮助建立整体认知。

QML 基础元素在业务架构中的分层与数据流 视图层(QML) Rectangle / Text / ListView / Button / MouseArea 渲染与交互 绑定层(属性绑定 + 信号槽) property binding / on<Signal> / Connections / function 数据同步 模型层(C++ QAbstractItemModel) model / delegate / roleNames / dataChanged 结构化数据 业务层(C++ 服务 / Q_INVOKABLE) 网络请求 / 数据库 / 业务规则 / 单例注册 业务逻辑 自上而下:声明与调用 自下而上:数据与信号

五、常见误区与复盘

  • 误区一:把 id 当全局变量用。id 只在当前文件有效,跨文件访问必须通过属性或信号暴露接口,否则组件无法复用。
  • 误区二:在 QML 中写复杂业务逻辑。QML 的 JavaScript 引擎不适合做大量计算或同步 IO,应下沉到 C++ 层。
  • 误区三:属性绑定与命令式赋值混用。一旦对某个绑定属性执行了命令式赋值,原有绑定会被破坏,后续数据变化不再自动同步。
  • 误区四:组件接口设计过宽。暴露过多内部属性会导致调用方与实现强耦合,应遵循“最小接口”原则。
在排查“界面不更新”问题时,首先检查该属性是否仍处于绑定状态。可以在赋值前后打印 console.log,或使用 Qt Creator 的 QML 调试器查看绑定是否被覆盖。