QML 最佳实践写出高质量、可维护、高性能的代码(十二)

适合人群: 已能独立写 QML 应用,想提升代码质量和性能的开发者

前言

会写 QML 和写好 QML 之间,有一段不小的距离。本文覆盖 Qt 官方推荐的 QML 最佳实践,涉及类型安全、属性绑定、JavaScript 使用边界、组件封装、可维护性和性能优化六大主题,每条都配有"反例 vs 正例"的对比代码。


一、使用强类型属性声明

问题:var 类型丢失所有静态检查

csharp 复制代码
// 不推荐:var 类型
property var name        // 是字符串?整数?对象?
property var count       // 无法做类型检查
property var config      // 工具无法推断类型

var 属性:

  • 无法被 qmllint 静态分析
  • 无法被 Qt Quick Compiler 编译优化
  • 赋值类型错误时,报错指向声明处而非赋值处,难以定位

解决:始终使用具体类型

vbnet 复制代码
// 推荐:强类型声明
property string  userName: ""
property int     itemCount: 0
property real    progress: 0.0
property bool    isLoading: false
property color   accentColor: "#4A90E2"
property url     avatarSource: ""
property date    createdAt
property var     rawData       // 只有真正需要动态类型时才用 var

强类型的好处:

markdown 复制代码
强类型属性
    ├── qmllint 可静态分析       → 编码阶段发现错误
    ├── Qt Quick Compiler 可编译  → 绑定表达式运行更快
    ├── 错误信息指向赋值处        → 调试更容易
    └── 代码即文档               → 阅读者一眼知道期望类型

二、避免非限定访问(Unqualified Access)

问题:直接访问父级属性,不带 id 前缀

arduino 复制代码
// 不推荐:非限定访问
Item {
    property int fontSize: 16

    Item {
        Text {
            font.pixelSize: fontSize    // 非限定访问!
            // qmllint 警告:[unqualified]
            // Qt Quick Compiler 无法编译此绑定
        }
    }
}

非限定访问的问题:

  • 运行时动态查找,性能差
  • 工具链(qmllint、编译器)无法静态确认访问是否合法
  • 当嵌套层级复杂时,fontSize 到底来自哪里?------代码难以阅读

解决:始终通过 id 限定访问

arduino 复制代码
// 推荐:限定访问
Item {
    id: root
    property int fontSize: 16

    Item {
        Text {
            font.pixelSize: root.fontSize    // 限定访问,清晰明确
        }
    }
}

在 Delegate 中用 required property 替代非限定访问:

vbnet 复制代码
// 不推荐:Delegate 直接访问 model 角色(非限定)
ListView {
    delegate: Text {
        text: name       // 非限定访问 model 角色
        color: isActive ? "green" : "gray"
    }
}

// 推荐:required property 显式声明
ListView {
    delegate: Text {
        required property string name
        required property bool   isActive

        text: name
        color: isActive ? "green" : "gray"
    }
}

三、理解并正确使用属性绑定

3.1 声明式绑定 vs 命令式赋值

arduino 复制代码
// 不推荐:在 Component.onCompleted 中命令式设置初始值
Rectangle {
    id: box
    color: "blue"

    Component.onCompleted: {
        box.width = parent.width / 2    // 命令式赋值
        box.height = parent.height / 2  // 这会破坏任何后续绑定
    }
}

// 推荐:声明式绑定,始终保持响应式
Rectangle {
    id: box
    width: parent.width / 2     // 声明式绑定:parent 宽度变化时自动更新
    height: parent.height / 2
    color: "blue"
}

3.2 在 JS 代码块中赋值会打断绑定

arduino 复制代码
Rectangle {
    id: box
    width: parent.width    // 绑定

    MouseArea {
        anchors.fill: parent
        onClicked: {
            box.width = 200    // 赋值后,上面的绑定被永久打断!
                               // 之后 parent.width 变化,box.width 不再跟随
        }
    }
}

如果必须在事件中重新建立绑定,使用 Qt.binding()

arduino 复制代码
onClicked: {
    box.width = Qt.binding(function() { return parent.width })
}

3.3 避免绑定循环

less 复制代码
// 错误:绑定循环,会产生运行时警告
Item {
    property int a: b + 1    // a 依赖 b
    property int b: a + 1    // b 依赖 a → 循环!
}

// 正确:其中一个属性改为普通赋值或由外部驱动
Item {
    property int a: 0
    property int b: a + 1    // 单向依赖,安全
}

3.4 保持绑定表达式简单

csharp 复制代码
// 不推荐:绑定中包含复杂逻辑
Text {
    text: {
        var result = ""
        for (var i = 0; i < model.count; i++) {
            result += model.get(i).name + ", "
        }
        return result.slice(0, -2)
    }
}

// 推荐:复杂逻辑提取到函数,绑定只调用函数
Item {
    function buildNameList() {
        var names = []
        for (var i = 0; i < model.count; i++) {
            names.push(model.get(i).name)
        }
        return names.join(", ")
    }

    Text {
        text: buildNameList()    // 绑定表达式简洁
    }
}

四、JavaScript 的使用边界

QML 中的 JavaScript 是把双刃剑,用好了事半功倍,滥用了则带来维护噩梦。

4.1 适合用 JavaScript 的场景

javascript 复制代码
// ✅ 简单的条件表达式(三元运算符)
color: isActive ? "#4A90E2" : "#CCCCCC"

// ✅ 简单计算
width: parent.width * 0.8

// ✅ 事件处理(onClicked 等)
onClicked: {
    model.remove(index)
    showToast("已删除")
}

// ✅ 辅助函数(封装复杂逻辑,供绑定调用)
function formatDate(dateStr) {
    var d = new Date(dateStr)
    return d.getFullYear() + "-" + (d.getMonth()+1) + "-" + d.getDate()
}

4.2 不适合用 JavaScript 的场景

css 复制代码
// ❌ 在绑定中做大量数据处理(每次绑定求值都会执行)
ListView {
    model: {
        var filtered = []
        for (var i = 0; i < sourceModel.count; i++) {
            if (sourceModel.get(i).price > 100)
                filtered.push(sourceModel.get(i))
        }
        return filtered    // 每次 sourceModel 变化都重新过滤,性能差
    }
}

// ✅ 用 C++ 代理模型或专门的过滤函数,不放在绑定里
arduino 复制代码
// ❌ 用 JS 模拟属性绑定(既不响应式,也不可读)
Component.onCompleted: {
    labelText.text = "Hello " + userName    // 只执行一次,userName 变化后不更新
}

// ✅ 直接用绑定
Text {
    id: labelText
    text: "Hello " + userName    // 声明式,自动响应
}

4.3 复杂逻辑放到 C++ 或独立 .js 文件

javascript 复制代码
// utils.js --- 独立的工具函数库
.pragma library    // 共享模式,只加载一次

function formatCurrency(amount, symbol) {
    return symbol + amount.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",")
}

function timeAgo(dateStr) {
    var diff = (Date.now() - new Date(dateStr)) / 1000
    if (diff < 60)    return "刚刚"
    if (diff < 3600)  return Math.floor(diff / 60) + " 分钟前"
    if (diff < 86400) return Math.floor(diff / 3600) + " 小时前"
    return Math.floor(diff / 86400) + " 天前"
}
scss 复制代码
import "utils.js" as Utils

Text { text: Utils.formatCurrency(price, "¥") }
Text { text: Utils.timeAgo(createdAt) }

五、属性遮蔽(Property Shadowing)陷阱

问题:子组件定义了与父组件同名的属性

arduino 复制代码
// 危险:属性遮蔽
Rectangle {
    property color color: "blue"    // 遮蔽了 Rectangle 自带的 color 属性!
    // 此时 color 既指自定义属性,又指 Rectangle.color
    // 绑定行为变得不可预测
}
vbnet 复制代码
// 危险:在 Delegate 中声明与 model 角色同名的属性
ListView {
    delegate: Rectangle {
        property string name: "默认"    // 遮蔽了 model 的 name 角色!
        Text { text: name }             // 显示的是 "默认",而不是 model 数据
    }
}

解决:使用不会冲突的命名,或改用 required property

vbnet 复制代码
// 推荐:使用不冲突的命名
Rectangle {
    property color backgroundColor: "blue"    // 不与内置属性冲突
    color: backgroundColor
}

// 推荐:Delegate 用 required property 而不是声明同名属性
ListView {
    delegate: Rectangle {
        required property string name    // 明确声明来自 model
        Text { text: name }
    }
}

六、组件封装原则

6.1 单一职责:一个组件做一件事

arduino 复制代码
// 不推荐:一个组件承担太多职责
// UserCard.qml --- 包含数据获取、显示、编辑、删除...

// 推荐:拆分为职责单一的小组件
// UserAvatar.qml  --- 只负责头像显示
// UserInfo.qml    --- 只负责用户信息文本
// UserCard.qml    --- 组合 Avatar + Info,加入卡片样式
// UserActions.qml --- 只负责操作按钮区域

6.2 明确暴露的接口:property + signal

csharp 复制代码
// 好的组件接口设计
// SearchBar.qml
Rectangle {
    id: root

    // 对外暴露的属性(接口)
    property string placeholder: "搜索..."
    property alias  searchText: field.text     // alias 透传内部属性
    property int    maxLength: 100

    // 对外发出的信号(接口)
    signal searchSubmitted(string query)
    signal cleared()

    // 内部实现细节(不对外暴露)
    TextField {
        id: field
        placeholderText: root.placeholder
        maximumLength: root.maxLength
        onAccepted: root.searchSubmitted(text)
    }

    Button {
        text: "清除"
        onClicked: {
            field.clear()
            root.cleared()
        }
    }
}

6.3 不要在组件内部直接访问外部 id

csharp 复制代码
// 不推荐:组件直接引用外部 id(强耦合,组件无法复用)
// MyButton.qml
Button {
    onClicked: mainWindow.showDialog()    // 直接访问外部 id!
}

// 推荐:通过信号解耦
// MyButton.qml
Button {
    signal buttonClicked()
    onClicked: buttonClicked()           // 发出信号,由外部决定做什么
}

// main.qml
MyButton {
    onButtonClicked: mainWindow.showDialog()    // 外部连接信号
}

七、代码组织:QML 文件内部的书写顺序

Qt 官方推荐的 QML 文件内部属性书写顺序:

arduino 复制代码
Rectangle {
    // 1. id(第一行,方便快速定位)
    id: root

    // 2. 属性声明(property / required property / readonly property)
    property string title: ""
    required property int index
    readonly property int maxCount: 10

    // 3. 信号声明
    signal itemSelected(int idx)

    // 4. JavaScript 函数
    function doSomething() { }

    // 5. 对象属性赋值(x, y, width, height, color...)
    x: 0; y: 0
    width: 200; height: 100
    color: "#f5f5f5"

    // 6. 子对象
    Text {
        anchors.centerIn: parent
        text: root.title
    }

    // 7. 状态和过渡
    states: [ State { name: "active" } ]
    transitions: [ Transition { } ]
}

八、性能最佳实践

8.1 使用 Loader 延迟加载非关键内容

arduino 复制代码
ApplicationWindow {
    // 主内容立即加载
    MainContent { anchors.fill: parent }

    // 设置页面、帮助面板等用 Loader 延迟加载
    Loader {
        id: settingsLoader
        active: false    // 默认不加载
        sourceComponent: SettingsPanel {}
    }

    Button {
        text: "设置"
        onClicked: settingsLoader.active = true    // 第一次点击时才加载
    }
}

8.2 避免在 Delegate 中使用 Layouts 和 Anchors

css 复制代码
// 不推荐:Delegate 中使用 ColumnLayout(创建和销毁开销大)
delegate: ColumnLayout {
    Text { text: name }
    Text { text: description }
}

// 推荐:Delegate 中用简单的 x/y/width/height 定位
delegate: Item {
    width: ListView.view.width; height: 60
    Text {
        x: 16; y: 8
        text: name
        font.pixelSize: 15; font.bold: true
    }
    Text {
        x: 16; y: 32
        text: description
        font.pixelSize: 13; color: "#888"
    }
}

8.3 使用 qmllint 进行静态检查

在 Qt Creator 终端运行:

bash 复制代码
# 检查单个文件
qmllint Main.qml

# 检查整个项目(编译警告级别)
qmllint --compiler warning *.qml

qmllint 能发现:

  • 非限定访问 [unqualified]
  • 未声明的属性
  • 废弃的 API 用法
  • 信号处理器参数未命名

8.4 使用 QML Profiler 定位性能瓶颈

在 Qt Creator 中:Analyze → QML Profiler

复制代码
QML Profiler 时间线视图:
┌─────────────────────────────────────────────────────┐
│ Animations    ████░░░████░░░████░░░                 │
│ Compiling     █░░░░░░░░░░░░░░░░░░░░░░░             │
│ Creating      ██░░░░░░░░░░░░░░░░░░░░░░             │
│ Binding       ░░░██░░░██░░░██░░░                   │
│ Handling Sig  ░░░░░█░░░░░█░░░░░█░░░                │
│ JavaScript    ░░░░░░█████░░░░░░████                │
│                                                     │
│  ← 帧时间不应超过 16ms(60fps)→                   │
└─────────────────────────────────────────────────────┘

重点关注:JavaScript 函数执行时间是否超过 16ms,Binding 是否被频繁触发。


九、可维护性:做好国际化准备

从第一行代码起就养成用 qsTr() 包裹用户可见字符串的习惯:

css 复制代码
// 不推荐:硬编码字符串(之后国际化要改遍全部文件)
Button { text: "确认" }
Label  { text: "请输入用户名" }

// 推荐:从一开始就用 qsTr()
Button { text: qsTr("确认") }
Label  { text: qsTr("请输入用户名") }

lupdate 提取所有 qsTr() 字符串到 .ts 翻译文件:

bash 复制代码
lupdate MyProject.pro -ts translations/app_zh_CN.ts

总结

最佳实践 核心要点
强类型属性 int/string/bool 而不是 var
限定访问 通过 id.property 访问,避免裸用父级属性名
required property Delegate 中声明 model 角色的推荐方式
声明式绑定 能用 : 绑定就不用 = 赋值
简单绑定表达式 复杂逻辑提取为函数,不放在绑定中
避免属性遮蔽 不要用与父级或内置属性同名的属性名
单一职责组件 每个 .qml 文件只做一件事
Loader 延迟加载 非关键 UI 按需加载,减少启动时间
Delegate 简化定位 x/y 代替 Layouts,减少对象创建开销
qmllint 静态检查 每次提交前运行,发现潜在问题
qsTr() 国际化 从第一行起包裹所有用户可见字符串
相关推荐
weixin_3823952320 小时前
为小工厂量身打造:本地部署的物料管理系统带缺料计算
前端·制造
__zRainy__20 小时前
解决pnpm v10+不自动构建
前端·pnpm·工程化
猫猫不是喵喵.21 小时前
Vue3 Props 属性
前端·javascript·vue.js
醉城夜风~1 天前
CSS元素显示模式(display)
前端·css
AI大模型-小华1 天前
Codex 三方充值快速入门指南
java·前端·数据库·chatgpt·ai编程·codex·chatgpt pro
做前端的娜娜子1 天前
同一链接实现 PC Web 与移动 H5 自适应
前端·掘金·金石计划
小帅不太帅1 天前
架构没变、规模没变,DeepSeek V4 Flash 正式版凭什么暴涨 47 分?
前端·aigc·deepseek
jarvisuni1 天前
DeepSeekFlash前端依旧拉垮,而且变慢了很多!
前端·javascript·算法
卷福同学1 天前
AI编程出海第二步:验证关键词能否做站
前端·人工智能·后端