编程进阶网 编程进阶网
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机的原理
  • 操作系统原理
  • 网络协议原理
  • 数据库的原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 技能之旅
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Kotlin精通
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机的原理
  • 操作系统原理
  • 网络协议原理
  • 数据库的原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 技能之旅
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Kotlin精通
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • Android提升进阶

  • iOS开发和进阶

  • Web开发和进阶

  • Linux应用开发

    • Linux应用开发
    • QML基础入门

      • QML基础入门
      • 嵌入式GUI技术全景
      • QML引擎与渲染原理
      • QML语法与类型系统
        • 3.1 案例引入
          • 3.1.1 Import 崩溃
          • 3.1.2 版本解码
          • 3.1.3 三大问题
        • 3.2 文件结构
          • 3.2.1 导入语句
          • 3.2.2 元素嵌套
          • 3.2.3 注释编码
        • 3.3 类型系统
          • 3.3.1 基本类型
          • 3.3.2 对象类型
          • 3.3.3 脚本类型
          • 3.3.4 C++ 映射
          • 3.3.5 综合案例
        • 3.4 对象特性
          • 3.4.1 引用系统
          • 3.4.2 属性系统
          • 3.4.3 信号处理
          • 3.4.4 方法
          • 3.4.5 用户卡片
        • 3.5 脚本集成
          • 3.5.1 表达式函数
          • 3.5.2 模块作用域
          • 3.5.3 V4 交互
        • 3.6 语法陷阱
        • 3.7 新手陷阱
        • 3.8 训练题
        • 3.9 思考题
        • 3.10 速查表
      • 属性绑定与响应式原理
      • 可视元素与布局原理
      • 事件处理与传播机制
      • 模型视图架构原理
      • 动画与状态机原理
      • Canvas与自定义渲染
      • QML与C++集成原理
      • 自定义SceneGraph节点
      • 交叉编译与部署
      • 嵌入式渲染后端
      • 性能优化与真机调试
    • QT核心库实践

    • Linux系统编程

    • 综合项目实战

  • IoT智能硬件开发

  • Apps
  • Linux应用开发
  • QML基础入门
杨充
2025-06-24
目录

QML语法与类型系统

# 第 3 章 QML 语法与类型系统

本章定位:从"看得懂 QML"到"写得对 QML"。上一章你知道了 QML 引擎在底层把声明式文本编译为 C++ 对象树,本章回答:这些文本该怎么写,类型又是如何映射到 C++ 的。读懂本章,你才能在 IDE 的"红线报错"出现时准确判断"语法问题"还是"类型不匹配",而不是靠猜。

# 目录介绍

  • 3.1 案例引入
    • 3.1.1 Import 崩溃
    • 3.1.2 版本解码
    • 3.1.3 三大问题
  • 3.2 文件结构
    • 3.2.1 导入语句
    • 3.2.2 元素嵌套
    • 3.2.3 注释编码
  • 3.3 类型系统
    • 3.3.1 基本类型
    • 3.3.2 对象类型
    • 3.3.3 脚本类型
    • 3.3.4 C++ 映射
    • 3.3.5 综合案例
  • 3.4 对象特性
    • 3.4.1 引用系统
    • 3.4.2 属性系统
    • 3.4.3 信号处理
    • 3.4.4 方法
    • 3.4.5 用户卡片
  • 3.5 脚本集成
    • 3.5.1 表达式函数
    • 3.5.2 模块作用域
    • 3.5.3 V4 交互
  • 3.6 语法陷阱
  • 3.7 新手陷阱
  • 3.8 训练题
  • 3.9 思考题
  • 3.10 速查表

# 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 的桥接转换。

思考题:

  1. property var statusText: "待开始" 和 property string statusText: "待开始" 在运行时有什么本质差异?statusText.length 在两种声明下都能拿到字符串长度吗?
  2. 如果把 property int clickCount: 0 改成 property var clickCount: 0,clickCount += 1 的执行路径会多出什么开销?
  3. 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 中)
    }
}
答案
  1. Id: box → id: box(id 首字母必须小写)
  2. property alias title: label → property alias title: label.text(alias 必须指向具体属性,不能指向整个对象)
  3. property count: 0 → property int count: 0(缺少类型声明)
  4. 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 思考题

  1. property vs JS let 的底层差异:QML property int x: 0 对应 QObject::setProperty("x", 0),JS let x = 0 存在 V4 栈帧里。它们在"绑定能力""信号通知""C++ 可见性"上有什么本质差异?什么场景下必须用 QML 属性?

  2. property alias 的实现原理:property alias text: label.text 没有创建新的内存空间。如果 label 被 Loader 销毁后又重建,别名还有效吗?如果不效,你该怎么设计这个组件?

  3. 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++ 的对话协议。

下一篇:04. QML 布局与锚定系统 (opens new window)

上次更新: 2026/07/12, 19:39:51
QML引擎与渲染原理
属性绑定与响应式原理

← QML引擎与渲染原理 属性绑定与响应式原理→

最近更新
01
audit
07-27
02
C++入门教程全章思考题汇编
07-24
03
12.技术团队建设能力
07-21
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 鄂ICP备2024073355号-1 | 鄂ICP备2024073355号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式