QML语法与类型系统
# 第 3 章 QML 语法与类型系统
本章定位:从"看得懂 QML"到"写得对 QML"。上一章你知道了 QML 引擎在底层把声明式文本编译为 C++ 对象树,本章回答:这些文本该怎么写,类型又是如何映射到 C++ 的。读懂本章,你才能在 IDE 的"红线报错"出现时准确判断"语法问题"还是"类型不匹配",而不是靠猜。
# 目录介绍
# 3.1 案例引入
# 3.1.1 Import 崩溃
某工程师从 Qt 5.12 迁移到 Qt 6.5,把代码原样搬过来,编译通过,启动直接白屏 + 控制台一行致命错误:
import QtQuick 2.12
import QtQuick.Controls 2.5
ApplicationWindow {
visible: true
width: 400; height: 300
Button {
text: "Click"
onClicked: console.log("hello")
}
}
启动日志:
QQmlApplicationEngine failed to load component
qrc:/main.qml:1:1: module "QtQuick" version 2.12 is not installed
疑惑:
- Qt 5 的 QML 语法不是和 Qt 6 差不多吗?
- 为什么
import QtQuick 2.12在 Qt 6 上找不到? import里的版本号到底控制了什么东西,为什么不同 Qt 版本不兼容?
# 3.1.2 版本解码
Qt 6 里 QtQuick 的主版本号与 Qt 版本绑定——Qt 6.x 只能 import QtQuick 6.x,不存在 QtQuick 2.x:
// Qt 5:版本号独立于 Qt 版本
import QtQuick 2.15 // Qt 5.15 提供 QtQuick 2.15
// Qt 6:主版本号与 Qt 版本对齐
import QtQuick 6.5 // Qt 6.5 提供 QtQuick 6.5
import QtQuick.Controls 6.5
但事情没这么简单:
为什么你写 import QtQuick 2.12 会有"2.12 is not installed"这个报错?
原因:QQmlEngine 在启动时根据 Qt 版本注册了所有可用模块。
Qt 6.x 的 qmlRegisterType 注册的是 QtQuick 的"6.x"版本系列,
根本没有"2.x"系列——所以 engine 遍历 importPaths 找不到匹配的
qmldir 文件,直接报 fatal error。
核心:
import不是"随便写个数字就能工作"——它是 QQmlEngine 查找模块的精确路由指令,版本号必须与 Qt 版本匹配。
# 3.1.3 三大问题
| 问题 | 在哪节回答 |
|---|---|
| QML 文件的结构规则是怎样的?import、根元素、嵌套各有什么约束? | §3.2 |
QML 里的 int、string、var 底层对应哪个 C++ 类型?写错类型编译器会拦吗? | §3.3 |
id 为什么能跨文件引用?property 和 JS 变量有什么本质区别? | §3.4 + §3.5 |
读完本章你会亲手写出一个包含属性/信号/方法的完整组件,并理解每一行背后对应的 C++ 类型映射。
# 3.2 文件结构
# 3.2.1 导入语句
import 是 QML 文件的第一条有效语句(注释之前可以写),作用是告诉 QQmlEngine:
import QtQuick // ← ① 模块导入:加载内置类型
import QtQuick.Controls //
import "components" // ← ② 目录导入:加载本地 QML 文件
import "utils.js" as Utils // ← ③ JS 导入:加载 JavaScript 模块
三种 import 的详细对比:
| import 类型 | 语法 | 查找路径 | 加载时机 | 常见用途 |
|---|---|---|---|---|
| 模块导入 | import QtQuick 6.5 | Qt 安装目录 + 自定义 importPaths | 启动时一次性 | 加载 Rectangle、ListView 等内置 QML 类型 |
| 目录导入 | import "mycomponents" | 相对于当前文件路径 | 启动时一次性 | 加载项目自写的 QML 组件 |
| JS 导入 | import "utils.js" as U | 相对于当前文件路径 / qrc | 首次引用时 | 加载计算逻辑、工具函数 |
模块导入的查找流程(QQmlEngine 内部):
1. engine.importPathList() 拿到所有搜索路径
└── 包括:Qt 安装目录(/usr/lib/qt6/qml)
QML2_IMPORT_PATH 环境变量
engine.addImportPath() 手动加的自定义路径
2. 对每个路径,依次查找:
路径/QtQuick.6/qmldir → 找到!读取元信息
路径/QtQuick.6.5/qmldir → 找到!匹配次要版本号
3. 如果 qmldir 存在,读取文件内容:
module QtQuick
typeinfo plugins.qmltypes
plugin qtquick2plugin
4. engine 根据 plugin 名字加载对应的 C++ 动态库
→ libqtquick2plugin.so 提供所有 QQuickItem 子类
版本号的匹配规则:
import QtQuick 6.5 // 要求 ≥6.5,<7.0
import QtQuick 6 // 等价于 import QtQuick 6.0
import QtQuick // Qt 6 默认用最高可用版本
目录导入的典型用法——组织项目自有组件:
project/
├── main.qml ← import "components"
└── components/
├── MyButton.qml
├── qmldir ← 可选:目录元信息
└── styles/
└── Theme.qml
// main.qml
import "components"
import "components/styles" as Theme
ApplicationWindow {
MyButton { } // ← 直接引用 components/MyButton.qml
}
qmldir 文件可以约定"哪些文件对外暴露":
# components/qmldir
MyButton 1.0 MyButton.qml
CustomSlider 1.0 CustomSlider.qml
InternalHelper 1.0 InternalHelper.qml # 没写这句 = 外部不可 import
# 3.2.2 元素嵌套
每个 QML 文件必须且仅能有一个根元素:
// ✅ 正确的单根元素
Rectangle {
width: 100; height: 100
color: "red"
}
// ❌ 错误:两个根元素
Rectangle { width: 50 }
Rectangle { width: 50 }
// → engine: "Expected a single root object"
// ✅ 多个根元素用 Item / Column / Row 包一层
Item {
Rectangle { width: 50; height: 50 }
Rectangle { width: 50; height: 50; anchors.left: parent.right }
}
对象嵌套的两种写法:
// 写法 A:嵌套到父元素的 {} 里(推荐——视觉层级清晰)
Rectangle {
width: 200; height: 200
Text {
anchors.centerIn: parent
text: "Hello"
}
}
// 写法 B:用属性赋值——适用于单子元素(如 delegate、contentItem)
Button {
contentItem: Text { text: "OK" }
}
对象树 = 父子关系协议:
Item {
id: root
Rectangle {
id: child
// 这里的 "parent" 就是 root
width: parent.width / 2
}
}
子元素四大自带的只读属性,不用声明就能用:
| 属性 | 类型 | 含义 |
|---|---|---|
parent | Item | 父元素引用 |
children | list<Item> | 视觉子元素列表 |
data | list<QtObject> | 所有子元素(含非视觉) |
resources | list<QtObject> | 资源子元素列表 |
# 3.2.3 注释编码
// 单行注释
/*
多行注释
不能嵌套
*/
/*
✅ QML 文件必须 UTF-8 编码(Qt 6 默认)
✅ 中文可以直接写,不需要 Unicode 转义
✅ qmlcachegen 预编译时对非 ASCII 字符无额外开销
*/
# 3.3 类型系统
# 3.3.1 基本类型
QML 的基本类型是值类型(value type)——赋值时复制,不是引用:
| QML 类型 | 字面量示例 | 对应 C++ 类型 | 说明 |
|---|---|---|---|
int | 42 | int | 32 位有符号整数 |
bool | true / false | bool | 必须全小写 |
real | 3.14 | double | 双精度浮点 |
double | 3.14 | double | Qt 5.12+ 引入,同 real |
string | "hello" | QString | UTF-16 编码 |
url | "qrc:/img.png" | QUrl | 自动解析相对路径 |
color | "red" 或 "#ff0000" | QColor | 支持命名颜色 / hex / rgba |
date | "2025-06-24" | QDate | ISO 8601 |
point | Qt.point(10, 20) | QPoint | 无字面量,用 Qt 函数 |
size | Qt.size(100, 50) | QSize | 同上 |
rect | Qt.rect(0,0,100,50) | QRect | 同上 |
font | { family:"Arial"; pixelSize:14 } | QFont | 对象字面量赋值 |
real 数组 | [0.1, 0.5] | QVector4D | Qt 6 起用 vector2d/3d/4d |
类型注解——声明时指定类型:
Item {
property int count: 0 // ✅ 明确 int 类型
property string name: "QML" // ✅ 明确 string 类型
property var data: {} // ✅ var 接收任意类型(尽量避免)
// ❌ 不推荐:无类型声明(实际等价于 var)
property count: 0 // 编译期无类型检查
}
var 的代价——var 会绕过编译期类型校验,赋值出错在运行时才暴露:
property var maybeNumber: 42
property var maybeColor: "red"
Component.onCompleted: {
maybeNumber = "hello" // ✅ 编译通过!运行时才出问题
let w = maybeNumber.width // ❌ undefined(QML engine 不报错,静默 bug)
}
铁律:能明确类型的声明永远不要用
var;仅在需要存放"类型不确定的 QML 对象引用"时才用它。
# 3.3.2 对象类型
QML 对象类型是引用类型——赋值时传引用,不是复制:
Rectangle {
id: rect1
width: 100; height: 100; color: "blue"
}
Rectangle {
id: rect2
color: "red"
// rect1 是对象引用——改 rect1 会影响 rect2 看到的内容
property Item target: rect1
}
对象类型的两大来源:
| 来源 | 例子 | 底层 |
|---|---|---|
| 内置 QML 类型(C++ 注册) | Rectangle, ListView, Timer | qmlRegisterType<QQuickRectangle>() |
| 自定义 QML 文件 | MyButton { } | 文件名即类型名(首字母必须大写) |
自定义类型——文件名约定:
// MyButton.qml
import QtQuick
Item {
property string text: ""
signal clicked()
// ...
}
// main.qml — 引用
import "."
MyButton { // ← 文件名即类型名
text: "Submit"
onCliked: console.log("clicked")
}
规则:
- 文件名首字母必须大写(否则 QQmlEngine 不会识别为类型)
- 文件名不能与内置类型重名(
Rectangle.qml会造成歧义) - 同一目录不可有两个同名组件
# 3.3.3 脚本类型
在 QML 的 JS 表达式里,你可以用 JS 的内置类型,但它们与 QML 类型之间存在隐式转换:
| JS 类型 | QML 等价 | 转换规则 |
|---|---|---|
number | real / int | 自动转换 |
string | string | 自动转换 |
boolean | bool | true ← 非空字符串 / 非零数;false ← "" / 0 / undefined |
undefined | undefined | QML 属性未赋值时 = undefined(视为 false) |
null | null | 用于清空对象引用 |
Object | var | 惰性映射(V4 引擎内部处理) |
Array | list / var | 数组可以直接赋给 var,但不能赋给 list<T> |
Date | date | new Date() → QML date 类型,自动转换 |
隐式转换的陷阱:
Item {
property int counter: 0
Component.onCompleted: {
counter = "123" // ✅ 自动转换 → 123
counter = "abc" // ⚠️ 转换失败 → 0(静默!不抛异常)
counter = undefined // ⚠️ 变成 0
console.log(counter) // 0(原本的值丢失了)
}
}
铁律:QML 属性如果声明了具体类型(
int/string/bool),赋值失败不会报运行时错误——它只是静默地给一个默认值。所以int收到undefined会变成0,bool收到"hello"会变成false。
# 3.3.4 C++ 映射
这是"QML 类型 → C++ 类"的完整映射表。理解这张表,你写 C++ 扩展 QML 时才不会选错类型:
| QML 自定义类型关键字 | C++ 注册宏 / 方法 | 生成的 QML 类型名 | 能力 |
|---|---|---|---|
| C++ 类直接注册 | qmlRegisterType<MyClass>("MyModule", 1, 0, "MyClass") | MyClass | 完整对象:属性/信号/方法 |
QML_ELEMENT 宏 | 写在类声明里,构建系统自动注册 | 类名 | Qt 6 推荐方式 |
QML_NAMED_ELEMENT(Name) | 给 C++ 类指定 QML 中的别名 | Name | C++ 类名可以与 QML 名不同 |
QML_SINGLETON | 全局单例,qmlRegisterSingletonType | 类名 | 不需要实例化 |
QML_UNCREATABLE(reason) | 禁止在 QML 中 new | 类名 | 只作为父类型(如 AbstractButton) |
QML_ANONYMOUS | 只作为值类型,不可直接引用 | — | 如 QJSValue |
Qt 6 推荐写法(不再需要 qmlRegisterType 手动注册):
// MyCounter.h
#include <QObject>
#include <QtQml>
class MyCounter : public QObject {
Q_OBJECT
QML_ELEMENT // ← 自动注册为 QML 类型
Q_PROPERTY(int count READ count WRITE setCount NOTIFY countChanged)
public:
int count() const { return m_count; }
void setCount(int value) {
if (m_count != value) { m_count = value; emit countChanged(); }
}
Q_INVOKABLE void reset() { setCount(0); }
signals:
void countChanged();
private:
int m_count = 0;
};
// main.qml
import MyModule 1.0
MyCounter {
id: counter
onCountChanged: console.log("count =", counter.count)
Component.onCompleted: counter.reset()
}
QML 内置类型的 C++ 真实身份(来自第 2 章 §2.2.2):
QML 写 Rectangle → 底层是 QQuickRectangle
QML 写 Text → 底层是 QQuickText
QML 写 ListView → 底层是 QQuickListView
QML 写 MouseArea → 底层是 QQuickMouseArea
QML 写 Image → 底层是 QQuickImage
QML 写 Timer → 底层是 QTimer(QObject 子类,在 QtCore 中)
# 3.3.5 综合案例
下面这个案例把类型系统的三个层面(基本类型、对象类型、JS 类型)用到同一个组件里,演示它们如何协作:
import QtQuick
import QtQuick.Controls
ApplicationWindow {
width: 400; height: 300; visible: true
// ───── 基本类型声明 ─────
property int clickCount: 0 // int ← C++ int
property real currentProgress: 0.0 // real ← C++ double
property string statusText: "待开始" // string ← C++ QString
property color accentColor: "#2196F3" // color ← C++ QColor
// ───── 对象类型 ─────
Rectangle {
id: card
anchors.centerIn: parent
width: 300; height: 200
radius: 12; color: "white"
border { width: 1; color: "#e0e0e0" } // 对象字面量
Column {
anchors.centerIn: parent
spacing: 12
Text {
id: statusLabel
text: statusText // ← 读取基本类型属性
font.pixelSize: 18
color: accentColor
}
ProgressBar {
id: bar
width: 200
value: currentProgress // ← 绑定 real 属性
}
Button {
id: actionBtn
text: "点击 (" + clickCount + ")"
onClicked: {
// ───── JS 类型 ─────
clickCount += 1 // int ← JS number
currentProgress = Math.min(1.0, clickCount / 10.0) // real ← JS number
statusText = clickCount >= 10 ? "完成!" : "进行中…" // string ← JS string
if (clickCount >= 10) {
accentColor = "#4CAF50" // 10 次变绿色
}
}
}
}
}
}
运行效果:按钮每点一次,进度条前进 10%,点击 10 次后文字变"完成!"、进度条满格、颜色变绿。
案例知识融合:本案例把 QML 类型系统的三个层串起来——①基本类型 int/real/string/color 背后都是 C++ 值类型,赋值时隐式转换;②Rectangle/Button/ProgressBar 是 C++ 注册的 QML 对象类型,在对象树中有 parent/children 关系;③onClicked 里的 Math.min 是 V4 JS 引擎执行,返回值 JS number 赋值给 QML 属性时经过 V4 → QQmlEngine 的桥接转换。
思考题:
property var statusText: "待开始"和property string statusText: "待开始"在运行时有什么本质差异?statusText.length在两种声明下都能拿到字符串长度吗?- 如果把
property int clickCount: 0改成property var clickCount: 0,clickCount += 1的执行路径会多出什么开销? accentColor = "#4CAF50"这句赋值——是走的 QML 属性绑定通道,还是 JS 直接写属性?它与accentColor: pressed ? "red" : "blue"这种声明式绑定有什么区别?
# 3.4 对象特性
# 3.4.1 引用系统
id 是 QML 最重要的对象特性之一——它让你在当前文件内的任何位置引用一个对象:
Item {
width: 400; height: 300
Rectangle {
id: header // ← 定义一个 id
width: parent.width; height: 50; color: "steelblue"
}
Text {
anchors.centerIn: header // ← 引用 header(QML engine 保证运行时解析)
text: "Title"
}
Component.onCompleted: {
console.log(header.color) // ← JS 中也能用 id 引用
}
}
id 的五个核心规则:
| 规则 | 说明 |
|---|---|
| 全局唯一(本文件内) | 同一个 QML 文件里不能有两个相同 id |
| 首字母必须小写 | id: Header ❌,id: header ✅ |
| 不可变 | 运行时不能 header = somethingElse |
| 仅在当前文件可见 | 子组件内部的 id 对外部不可见(封装) |
| 本质是 C++ 指针 | id = QObject* 被 QQmlEngine 存到上下文中 |
为什么 id 不能跨 C++/QML 边界?
// main.cpp:这是在 QQmlEngine 外部,没有 QML 上下文
QObject* obj = engine.rootObjects().first();
auto* header = obj->findChild<QObject*>("header"); // ❌ id 不进入 QObject::objectName
auto* header = obj->findChild<QObject*>("myHeader"); // ✅ 必须用 objectName
id 只存在 QQmlEngine 的符号表里——它是一个 QML 上下文的别名,不会写到 QObject::objectName()。跨边界(C++ 想访问 QML 的对象)只能用 objectName。
# 3.4.2 属性系统
QML 属性的完整语法:
[default] [required] [readonly] property <type> <name>[: <value>]
属性的四种声明方式:
Item {
// 方式 1:带默认值
property int counter: 0
// 方式 2:readonly(外部只读,内部可写)
readonly property string deviceId: "DEV-" + Math.random().toString(36).substr(2, 9)
// 方式 3:required(调用方必须赋值,Qt 5.15+)
required property string title
required property int maxItems
// 方式 4:default(标记一个属性为"默认属性"——赋值时不写属性名)
default property list<Item> content
// 方式 5:property alias(别名——给内部子元素的属性暴露一个外部名字)
property alias text: label.text // 外部改 text = 内部改 label.text
Text { id: label }
}
属性别名 alias 的双向绑定机制:
// MyButton.qml
Item {
property alias text: innerText.text // 暴露内部的 Text.text
Rectangle {
id: bg; width: 100; height: 40; color: "steelblue"
Text {
id: innerText
anchors.centerIn: parent
text: "Button"
}
}
}
// main.qml
MyButton {
text: "Submit" // ← 最终设置到 innerText.text
Component.onCompleted: console.log(text) // ← 读到的也是 innerText.text
}
别名不是"新属性"——它只是给内部属性一个外部名字,读/写/绑定全透明转发。
属性变更信号——每个非 readonly 属性自动生成一个 on<Property>Changed 信号:
Item {
property int counter: 0
onCounterChanged: {
console.log("counter 变了:", counter) // ← 自动生成,无需声明 signal
}
Component.onCompleted: counter = 42 // → 触发 onCounterChanged
}
这就是为什么 QML 里写
onWidthChanged、onColorChanged不用先写signal——QQmlEngine 为每个属性自动生成变更信号。
# 3.4.3 信号处理
信号的两种定义方式:
Item {
// 方式 1:无参数信号
signal clicked()
// 方式 2:带参数信号
signal valueChanged(int oldValue, int newValue)
// 方式 3:带类型名(跨文件通信时推荐)
signal requestSubmitted(string url, var payload)
MouseArea {
anchors.fill: parent
onClicked: parent.clicked() // ← 转发到父组件
}
}
信号处理器的两种写法:
// 写法 A:on<SignalName> 处理器(最常用)
Button {
onClicked: console.log("clicked")
}
// 写法 B:Connections 对象(动态连接 / 跨文件连接)
Connections {
target: someObject
function onDataReady(data) {
console.log("收到数据:", data)
}
}
// 写法 C:connect() JS 方法(Qt 6.5+ / Qt 5.15+)
Component.onCompleted: {
someObject.valueChanged.connect((oldV, newV) => {
console.log(oldV, "→", newV)
})
}
| 写法 | 何时用 | 优点 |
|---|---|---|
on<Signal> | 组件定义时已知信号源 | 声明式、语法最简 |
Connections | 信号源在外部 / 动态指定 | 可以在运行时切换 target |
.connect() | 需要 lambda / 临时绑定 | 最灵活、支持一次性绑定 |
# 3.4.4 方法
QML 方法本质是在 QML 对象上注册的 JS 函数:
Item {
id: root
// 方式 1:对象方法(可以访问 root 的属性和子元素)
function calculateDistance(a, b) {
return Math.sqrt((b.x - a.x) ** 2 + (b.y - a.y) ** 2)
}
// 方式 2:箭头函数(无自己的 this)
property var add: (a, b) => a + b
Component.onCompleted: {
let d = root.calculateDistance(
{x: 0, y: 0}, {x: 3, y: 4}
)
console.log(d) // 5
}
}
方法 vs 属性:
| 维度 | property | function |
|---|---|---|
| 能否绑定 | ✅ x: parent.x + 10 | ❌ 不能直接绑定 |
能否有 onChanged | ✅ 自动生成 | ❌ 无 |
| 调用开销 | 读属性 = 一次 Q_PROPERTY 取值(快) | 调用 = V4 函数调用(有 JS 开销) |
| 适合存什么 | 状态 | 行为 |
Q_INVOKABLE——C++ 方法暴露给 QML:
class MyObject : public QObject {
Q_OBJECT
public:
Q_INVOKABLE QString formatDate(const QDate& d) {
return d.toString("yyyy-MM-dd");
}
Q_INVOKABLE static int version() { return 2; }
};
MyObject {
Component.onCompleted: {
console.log(formatDate(new Date())) // ← 可以调 C++ 方法
console.log(version()) // ← static 方法也可以
}
}
# 3.4.5 用户卡片
把 id、属性、信号、方法放到一个组件里,完成一个"用户卡片":
// UserCard.qml
import QtQuick
import QtQuick.Controls
Item {
id: root
// ───── 属性 ─────
required property string userName
required property string avatarUrl
property int followerCount: 0
readonly property bool isVerified: followerCount >= 1000
// ───── 别名 ─────
property alias avatarSource: avatar.source
// ───── 信号 ─────
signal followClicked(string userId)
signal unfollowClicked(string userId)
// ───── 属性变更响应 ─────
onFollowerCountChanged: {
if (isVerified) console.log(userName + " 已认证")
}
// ───── 方法 ─────
function toggleFollow() {
following = !following
}
property bool following: false
implicitWidth: 280; implicitHeight: 80
Rectangle {
anchors.fill: parent
radius: 12; color: "#ffffff"
border { width: 1; color: "#e0e0e0" }
Row {
anchors.centerIn: parent
spacing: 16
Image {
id: avatar
width: 48; height: 48
source: avatarUrl
fillMode: Image.PreserveAspectCrop
}
Column {
spacing: 4
Text { text: userName; font.bold: true; font.pixelSize: 16 }
Text { text: followerCount + " 关注者"; color: "#888"; font.pixelSize: 12 }
}
Button {
text: following ? "已关注" : "关注"
highlighted: following
onClicked: {
toggleFollow()
following ? followClicked(userName) : unfollowClicked(userName)
}
}
}
}
}
// main.qml
import QtQuick
import QtQuick.Controls
ApplicationWindow {
width: 360; height: 200; visible: true
UserCard {
anchors.centerIn: parent
userName: "杨充"
avatarUrl: "qrc:/avatar.png"
followerCount: 1523
onFollowClicked: (uid) => console.log("关注了:", uid)
}
}
案例知识融合:UserCard 用了本章学到的所有语法——①property 声明了 string/int/bool 三种基本类型;②signal 带参数传给外部调用方;③function 封装内部逻辑;④property alias 把子元素 Image.source 暴露给外部;⑤onFollowerCountChanged 是属性变更信号自动生成的处理器;⑥外部通过 onFollowClicked 接收子组件信号——这就是 QML 组件化通信的全套语法。
# 3.5 脚本集成
# 3.5.1 表达式函数
QML 里的 JS 无处不在——绑定表达式、信号处理器、独立函数:
Rectangle {
// ① 绑定表达式内的 JS
width: Math.min(parent.width, 600) // 标准 JS 函数
color: pressed ? "red" : "blue" // 三元运算符
opacity: enabled ? 1.0 : 0.4
// ② 信号处理器内的 JS
MouseArea {
onClicked: {
let items = [1, 2, 3]
items.forEach(i => console.log(i)) // forEach、箭头函数
let doubled = items.map(x => x * 2) // map
console.log(doubled)
}
}
// ③ 独立函数
function fibonacci(n) {
if (n <= 1) return n
return fibonacci(n - 1) + fibonacci(n - 2)
}
}
JS 表达式的求值时机:
// 表达式绑定 → 依赖变化时重新求值
Item {
width: parent.width * 0.8
// parent.width 变化 → 表达式重新求值 → 设置 Item.width
}
// 信号处理器 → 信号触发时执行一次
Button {
onClicked: console.log("clicked")
}
// 独立函数 → 被调用时才执行
function formatPrice(price) { return "¥" + price.toFixed(2) }
# 3.5.2 模块作用域
JS 文件导入——把纯逻辑抽到独立.mjs文件:
// utils.mjs
.import QtQuick.LocalStorage 2.0 as Sql
function formatPrice(amount) {
return "¥" + Number(amount).toFixed(2)
}
function daysBetween(a, b) {
const ms = Math.abs(new Date(b) - new Date(a))
return Math.floor(ms / (1000 * 60 * 60 * 24))
}
// 通过 pragma 引入 Qt 功能
.pragma library // ← 标记为库文件(全局共享一个实例)
// main.qml
import QtQuick
import "utils.mjs" as Utils
Item {
Component.onCompleted: {
console.log(Utils.formatPrice(99.9)) // "¥99.90"
console.log(Utils.daysBetween("2025-01-01", "2025-06-24")) // 174
}
}
.pragma library vs 普通 JS:
| 维度 | 普通 .mjs | .pragma library |
|---|---|---|
| 实例化 | 每次 import 创建新的实例 | 全局共享同一个实例 |
| 适合存什么 | 逻辑函数 | 逻辑函数 + 全局状态 |
| 内存 | 多份(每个 import 处一份) | 一份 |
| ⚠️ 陷阱 | 无 | 状态被共享,一处修改处处影响 |
作用域规则:
Item {
property int globalX: 10 // ① 属性 = QML 作用域
Rectangle {
property int localY: 20 // ② 嵌套作用域(内部可访问外层属性)
Component.onCompleted: {
let z = 30 // ③ JS 作用域(仅在这个 block 内可见)
console.log(globalX, localY, z) // ✅ 三层都能访问
}
Component.onCompleted: {
console.log(z) // ❌ z 在另一个 block,不可见
}
}
Component.onCompleted: {
console.log(localY) // ❌ 外层不能访问内层属性
}
}
# 3.5.3 V4 交互
当 QML 里写 width: parent.width * 0.8 这句时,底下到底发生了什么:
1. QQmlEngine 解析到绑定表达式 "parent.width * 0.8"
2. 识别到这是 JS 表达式,交给 V4 引擎
3. V4 把这个表达式编译为字节码:
LOAD_VAR "parent"
GET_PROPERTY "width"
PUSH_CONST 0.8
MUL
4. parent.width 变化 → QQmlEngine 通知绑定重新求值
→ V4 再次执行字节码 → 返回新值 → setProperty("width", newValue)
V4 的 JIT 对 QML 绑定的影响:
- 热点绑定表达式(如 ListView delegate 里频繁变化的属性)会被 V4 JIT 编译为机器码
- JIT 后的执行速度可接近 C++ 原生代码
- 绑定表达式越简单,JIT 优化空间越大
// ✅ JIT 友好——纯算术
width: parent.width - 40
// ⚠️ JIT 不友好——每次求值都要字符串拼接 + 查询
color: "rgba(" + r + "," + g + "," + b + "," + a + ")"
// → 改用 QML 内置的 Qt.rgba()
color: Qt.rgba(r, g, b, a) // C++ 实现,绕过 V4
铁律:能用 QML 内置函数的不要用 JS 手写等价逻辑——前者走 C++ 路径,后者走 V4 路径,性能差可到 10x。
# 3.6 语法陷阱
| # | 陷阱 | 正确做法 |
|---|---|---|
| 1 | import QtQuick 2.15 在 Qt 6 上 | import QtQuick 6.x |
| 2 | 文件名首字母小写(mybutton.qml) | 首字母大写(MyButton.qml),否则不可作类型引用 |
| 3 | property 不写类型 | 始终写类型:property int count: 0 |
| 4 | id 首字母大写 | id 必须小写开头(id: Header → 编译报错) |
| 5 | 两个根元素 | 用 Item { } 包一层 |
| 6 | property alias 指向不存在的属性 | 编译期不报错、运行时无声失败——指向的内部元素必须存在 |
| 7 | 信号参数类型与处理器不匹配 | signal changed(int v) → onChanged: console.log(v * 2) ✅;onChanged: console.log(v.text) ❌ |
| 8 | JS 里用 this | QML 信号处理器里 this = 信号源对象,不是 JS 的 this;建议完全不用 this,用 id 引用 |
| 9 | 在绑定里调用有副作用的函数 | 绑定可以在一帧内被求值多次——调 API/写数据库的代码必须在 onClicked 等信号处理器里写,不能塞进绑定 |
| 10 | property var 滥用 | property var x: 0 → property int x: 0 |
# 3.7 新手陷阱
| # | 陷阱 | 说明 | 修复 |
|---|---|---|---|
| 1 | id 不是 objectName | findChild("btn") 找不到 id: btn 的对象 | C++ 侧用 objectName: "btn";QML 侧只用 id |
| 2 | property alias 被误解为指针 | property alias text: label.text — 改 text 确实会更新 label | 但别名不能绑定到"还不存在的元素"——label 必须在别名声明之前定义 |
| 3 | JS 函数里 this 不可靠 | function fn() { console.log(this.width) } | 用 id 引用具体对象:root.width |
| 4 | required property 不写值就忽略 | MyCard {} 没有给 title 赋值 | Qt 5.15+ 启动报错提示缺失;Qt 5.12 静默忽略 → 致命 |
| 5 | 把 property 当成 JS let | property int x = 0 然后在 onClicked 里写 let x = 5 | QML 属性和 JS 变量是两个作用域——let x = 5 覆盖了同名属性,外部引用仍然拿到旧值 0 |
# 3.8 训练题
训练题 1:诊断 QML 语法错误
要求:下面的 QML 文件有 4 处语法错误。列出并修复。
import QtQuick 2.12
Item {
width: 200; height: 200
Rectangle {
Id: box // ← 错 1
property alias title: label // ← 错 2
Text { id: label; text: "hello" }
}
property count: 0 // ← 错 3
Component.onCompleted: {
console.log(box) // ← 错 4(在 Qt 6 中)
}
}
答案
Id: box→id: box(id 首字母必须小写)property alias title: label→property alias title: label.text(alias 必须指向具体属性,不能指向整个对象)property count: 0→property int count: 0(缺少类型声明)import QtQuick 2.12→import QtQuick 6.x(Qt 6 不提供 2.x 版本)
训练题 2:写一个类型安全的倒计时组件
要求:用本章学的属性/信号/方法语法,写一个倒计时组件:
- 属性:
int totalSeconds(外部设) - 信号:
finished()(倒数为零时发出) - 方法:
start()(开始倒时) - 内部用
Timer每秒驱动,导出只读属性int remainingSeconds
// Countdown.qml — 参考答案
import QtQuick
Item {
id: root
required property int totalSeconds
readonly property int remainingSeconds: totalSeconds - elapsed
property int elapsed: 0
signal finished()
function start() {
elapsed = 0
timer.start()
}
Timer {
id: timer
interval: 1000; repeat: true
onTriggered: {
elapsed++
if (elapsed >= totalSeconds) {
timer.stop()
root.finished()
}
}
}
}
# 3.9 思考题
propertyvs JSlet的底层差异:QMLproperty int x: 0对应QObject::setProperty("x", 0),JSlet x = 0存在 V4 栈帧里。它们在"绑定能力""信号通知""C++ 可见性"上有什么本质差异?什么场景下必须用 QML 属性?property alias的实现原理:property alias text: label.text没有创建新的内存空间。如果label被 Loader 销毁后又重建,别名还有效吗?如果不效,你该怎么设计这个组件?required property为什么 Qt 5.12 没有?这个语法在 QML 编译流程的哪一步被检查?为什么早期 Qt 版本选择了"静默忽略"而不是"编译报错"?这反映了 QML 引擎什么样的演进方向?
# 3.10 速查表
| 概念 | 一句话 |
|---|---|
| import | 告诉 QQmlEngine 去哪里加载类型(模块 / 目录 / JS) |
| 根元素 | 每个 QML 文件有且仅有一个根对象 |
| id | 本文件内唯一的对象引用,首字母小写,只读 |
| property | QML 属性,底层 = QObject::property,自动带 onChanged 信号 |
| property alias | 给内部子元素属性一个外部名字——无新内存,全透明转发 |
| required property | Qt 5.15+ 的强制赋值属性,调用方不赋值启动报错 |
| readonly property | 外部只读,内部可变 |
| default property | 默认属性——赋值不写属性名(如 Item { Rectangle {} }) |
| signal | 信号声明,处理器 = on<SignalName> |
| function | QML 对象方法,本质是 V4 JS 函数 |
| Q_INVOKABLE | C++ 成员函数标记为"可在 QML 中调用" |
| QML_ELEMENT | Qt 6 自动注册 C++ 类为 QML 类型 |
| var | 动态类型——绕过编译期检查,慎用 |
| int / real / string / bool / color | QML 基本类型,背后是 C++ 值类型 |
| Rectangle / ListView / Timer | QML 对象类型,背后是 C++ QObject 子类 |
.pragma library | JS 库文件标记——全局共享单例 |
| V4 | Qt 内嵌 JS 引擎,绑定表达式先 V4 求值再写 QML 属性 |
核心哲学:
QML 语法不是"另一种 HTML"——
它的每一行声明背后,
都有一个 C++ QObject 在对象树中等待。
理解类型映射 = 理解 QML 与 C++ 的对话协议。