适合人群: 已能独立写 QML 应用,想提升代码质量和性能的开发者
前言
会写 QML 和写好 QML 之间,有一段不小的距离。本文覆盖 Qt 官方推荐的 QML 最佳实践,涉及类型安全、属性绑定、JavaScript 使用边界、组件封装、可维护性和性能优化六大主题,每条都配有"反例 vs 正例"的对比代码。
一、使用强类型属性声明
问题:var 类型丢失所有静态检查
// 不推荐:var 类型
property var name // 是字符串?整数?对象?
property var count // 无法做类型检查
property var config // 工具无法推断类型
var 属性:
- 无法被
qmllint静态分析 - 无法被 Qt Quick Compiler 编译优化
- 赋值类型错误时,报错指向声明处而非赋值处,难以定位
解决:始终使用具体类型
// 推荐:强类型声明
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
强类型的好处:
强类型属性
├── qmllint 可静态分析 → 编码阶段发现错误
├── Qt Quick Compiler 可编译 → 绑定表达式运行更快
├── 错误信息指向赋值处 → 调试更容易
└── 代码即文档 → 阅读者一眼知道期望类型
二、避免非限定访问(Unqualified Access)
问题:直接访问父级属性,不带 id 前缀
// 不推荐:非限定访问
Item {
property int fontSize: 16
Item {
Text {
font.pixelSize: fontSize // 非限定访问!
// qmllint 警告:[unqualified]
// Qt Quick Compiler 无法编译此绑定
}
}
}
非限定访问的问题:
- 运行时动态查找,性能差
- 工具链(qmllint、编译器)无法静态确认访问是否合法
- 当嵌套层级复杂时,
fontSize到底来自哪里?——代码难以阅读
解决:始终通过 id 限定访问
// 推荐:限定访问
Item {
id: root
property int fontSize: 16
Item {
Text {
font.pixelSize: root.fontSize // 限定访问,清晰明确
}
}
}
在 Delegate 中用 required property 替代非限定访问:
// 不推荐: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 命令式赋值
// 不推荐:在 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 代码块中赋值会打断绑定
Rectangle {
id: box
width: parent.width // 绑定
MouseArea {
anchors.fill: parent
onClicked: {
box.width = 200 // 赋值后,上面的绑定被永久打断!
// 之后 parent.width 变化,box.width 不再跟随
}
}
}
如果必须在事件中重新建立绑定,使用 Qt.binding():
onClicked: {
box.width = Qt.binding(function() { return parent.width })
}
3.3 避免绑定循环
// 错误:绑定循环,会产生运行时警告
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 保持绑定表达式简单
// 不推荐:绑定中包含复杂逻辑
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 的场景
// ✅ 简单的条件表达式(三元运算符)
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 的场景
// ❌ 在绑定中做大量数据处理(每次绑定求值都会执行)
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++ 代理模型或专门的过滤函数,不放在绑定里
// ❌ 用 JS 模拟属性绑定(既不响应式,也不可读)
Component.onCompleted: {
labelText.text = "Hello " + userName // 只执行一次,userName 变化后不更新
}
// ✅ 直接用绑定
Text {
id: labelText
text: "Hello " + userName // 声明式,自动响应
}
4.3 复杂逻辑放到 C++ 或独立 .js 文件
// 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) + " 天前"
}
import "utils.js" as Utils
Text { text: Utils.formatCurrency(price, "¥") }
Text { text: Utils.timeAgo(createdAt) }
五、属性遮蔽(Property Shadowing)陷阱
问题:子组件定义了与父组件同名的属性
// 危险:属性遮蔽
Rectangle {
property color color: "blue" // 遮蔽了 Rectangle 自带的 color 属性!
// 此时 color 既指自定义属性,又指 Rectangle.color
// 绑定行为变得不可预测
}
// 危险:在 Delegate 中声明与 model 角色同名的属性
ListView {
delegate: Rectangle {
property string name: "默认" // 遮蔽了 model 的 name 角色!
Text { text: name } // 显示的是 "默认",而不是 model 数据
}
}
解决:使用不会冲突的命名,或改用 required property
// 推荐:使用不冲突的命名
Rectangle {
property color backgroundColor: "blue" // 不与内置属性冲突
color: backgroundColor
}
// 推荐:Delegate 用 required property 而不是声明同名属性
ListView {
delegate: Rectangle {
required property string name // 明确声明来自 model
Text { text: name }
}
}
六、组件封装原则
6.1 单一职责:一个组件做一件事
// 不推荐:一个组件承担太多职责
// UserCard.qml — 包含数据获取、显示、编辑、删除...
// 推荐:拆分为职责单一的小组件
// UserAvatar.qml — 只负责头像显示
// UserInfo.qml — 只负责用户信息文本
// UserCard.qml — 组合 Avatar + Info,加入卡片样式
// UserActions.qml — 只负责操作按钮区域
6.2 明确暴露的接口:property + signal
// 好的组件接口设计
// 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
// 不推荐:组件直接引用外部 id(强耦合,组件无法复用)
// MyButton.qml
Button {
onClicked: mainWindow.showDialog() // 直接访问外部 id!
}
// 推荐:通过信号解耦
// MyButton.qml
Button {
signal buttonClicked()
onClicked: buttonClicked() // 发出信号,由外部决定做什么
}
// main.qml
MyButton {
onButtonClicked: mainWindow.showDialog() // 外部连接信号
}
七、代码组织:QML 文件内部的书写顺序
Qt 官方推荐的 QML 文件内部属性书写顺序:
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 延迟加载非关键内容
ApplicationWindow {
// 主内容立即加载
MainContent { anchors.fill: parent }
// 设置页面、帮助面板等用 Loader 延迟加载
Loader {
id: settingsLoader
active: false // 默认不加载
sourceComponent: SettingsPanel {}
}
Button {
text: "设置"
onClicked: settingsLoader.active = true // 第一次点击时才加载
}
}
8.2 避免在 Delegate 中使用 Layouts 和 Anchors
// 不推荐: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 终端运行:
# 检查单个文件
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() 包裹用户可见字符串的习惯:
// 不推荐:硬编码字符串(之后国际化要改遍全部文件)
Button { text: "确认" }
Label { text: "请输入用户名" }
// 推荐:从一开始就用 qsTr()
Button { text: qsTr("确认") }
Label { text: qsTr("请输入用户名") }
用 lupdate 提取所有 qsTr() 字符串到 .ts 翻译文件:
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() 国际化 | 从第一行起包裹所有用户可见字符串 |