编程进阶网 编程进阶网
首页
  • 在线工具
  • 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语法与类型系统
      • 属性绑定与响应式原理
      • 可视元素与布局原理
      • 事件处理与传播机制
      • 模型视图架构原理
      • 动画与状态机原理
      • Canvas与自定义渲染
      • QML与C++集成原理
        • 10.1 案例引入
          • 10.1.1 幽灵 Bug
          • 10.1.2 根因分析
          • 10.1.3 三大问题
        • 10.2 属性暴露
          • 10.2.1 完整声明
          • 10.2.2 通知机制
          • 10.2.3 MEMBER 读写
          • 10.2.4 传感器暴露
          • 10.2.5 MOC 探秘
        • 10.3 方法暴露
          • 10.3.1 容器暴露
        • 10.4 跨语言通信
          • 10.4.1 C++ 到 QML
          • 10.4.2 QML 到 C++
          • 10.4.3 connect 语法
        • 10.5 上下文注入
          • 10.5.1 时机作用域
          • 10.5.2 上下文层级
        • 10.6 类型注册
          • 10.6.1 手动注册
          • 10.6.2 宏注册体系
          • 10.6.3 单例注册
        • 10.7 C++ 操作对象
        • 10.8 线程与生命周期
        • 10.9 CAN 全栈实战
        • 10.10 新手陷阱
        • 10.11 训练题
        • 10.12 思考题
        • 10.13 速查表
      • 自定义SceneGraph节点
      • 交叉编译与部署
      • 嵌入式渲染后端
      • 性能优化与真机调试
    • QT核心库实践

    • Linux系统编程

    • 综合项目实战

  • IoT智能硬件开发

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

QML与C++集成原理

# 第 10 章 QML 与 C++ 集成原理

本章定位:打通 C++ 和 QML 的最后一道墙。前面 9 章教你在 QML 内部写 UI——但传感器数据来自 C++ 驱动、CAN 总线报文字段是 struct、摄像头解码在硬件线程。QML 的声明式界面需要与 C++ 的命令式逻辑无缝对接。本章从 Q_PROPERTY 的属性暴露、Q_INVOKABLE 的方法调用、setContextProperty 的上下文注入,到 qmlRegisterType 的类型注册、MOC 元对象系统的编译原理——每一条通信路径的底层机制和线程安全约束。

# 目录介绍

  • 10.1 案例引入
    • 10.1.1 幽灵 Bug
    • 10.1.2 根因分析
    • 10.1.3 三大问题
  • 10.2 属性暴露
    • 10.2.1 完整声明
    • 10.2.2 通知机制
    • 10.2.3 MEMBER 读写
    • 10.2.4 传感器暴露
    • 10.2.5 MOC 探秘
  • 10.3 方法暴露
    • 10.3.1 容器暴露
  • 10.4 跨语言通信
    • 10.4.1 C++ 到 QML
    • 10.4.2 QML 到 C++
    • 10.4.3 connect 语法
  • 10.5 上下文注入
    • 10.5.1 时机作用域
    • 10.5.2 上下文层级
  • 10.6 类型注册
    • 10.6.1 手动注册
    • 10.6.2 宏注册体系
    • 10.6.3 单例注册
  • 10.7 C++ 操作对象
  • 10.8 线程与生命周期
  • 10.9 CAN 全栈实战
  • 10.10 新手陷阱
  • 10.11 训练题
  • 10.12 思考题
  • 10.13 速查表

# 10.1 案例引入

# 10.1.1 幽灵 Bug

某车机工程师用 setContextProperty 注入 C++ 的后端对象 Backend。白天运行正常,晚上启动偶尔白屏:

// main.cpp
int main(int argc, char* argv[]) {
    QGuiApplication app(argc, argv);
    QQmlApplicationEngine engine;
    engine.load(QUrl("qrc:/main.qml"));                         // ① 先 load
    engine.rootContext()->setContextProperty("backend",         // ② 后注入
                                             new Backend());
    return app.exec();
}
// main.qml
ApplicationWindow {
    Component.onCompleted: {
        console.log(backend.speed)    // ← 偶尔 ReferenceError: backend is not defined
    }
}

诡异之处:同一份代码,有时正常、有时崩溃——取决于 engine.load() 时 QQmlEngine 内部的 AST 解析与 Component.onCompleted 的执行顺序竞争。

# 10.1.2 根因分析

engine.load("main.qml") 执行时:
  Step 1: 词法/语法分析 → AST
  Step 2: 对象实例化 → 创建 Window、Rectangle...
  Step 3: 绑定求值 + Component.onCompleted 执行
          ↓ 这时 QML 引擎查找 "backend" 符号
          ↓ 查当前 QQmlContext → 没有!  ← 还没注入!
          ↓ 查父级 QQmlContext   → 也没有!
          ↓ 引擎不抛异常——只是静默跳过,onCompleted 继续执行
          → backend 是 undefined,后续读取 backend.speed = crash

修复:

QQmlApplicationEngine engine;
engine.rootContext()->setContextProperty("backend", new Backend());  // ← 在 load 之前
engine.load(QUrl("qrc:/main.qml"));

更健壮的方案——不在 QML 侧依赖全局上下文变量,改为 C++ 注册类型:

// 注册为 QML 类型——Qt 6 推荐方式
qmlRegisterType<Backend>("com.myapp", 1, 0, "Backend");
import com.myapp 1.0
ApplicationWindow {
    Backend { id: backend }    // ← QML 自己创建,永远不存在时序问题
    Component.onCompleted: console.log(backend.speed)
}

# 10.1.3 三大问题

问题 在哪节回答
Q_PROPERTY 底层如何与 QML 的绑定机制协作?NOTIFY 信号是必须的吗? §10.2
C++ signal → QML handler 和 QML signal → C++ slot 两条链路各有什么陷阱? §10.4
传感器的后台线程如何安全地把数据投递给 QML 界面? §10.8

# 10.2 属性暴露

# 10.2.1 完整声明

class SpeedSensor : public QObject {
    Q_OBJECT
    Q_PROPERTY(double speed    READ speed    WRITE setSpeed    NOTIFY speedChanged)
    Q_PROPERTY(bool   connected READ connected                                   CONSTANT)
    Q_PROPERTY(Status status    READ status                                      MEMBER m_status)

public:
    enum Status { Idle, Active, Fault };
    Q_ENUM(Status)

    double speed() const { return m_speed; }
    void setSpeed(double v) {
        if (qFuzzyCompare(v, m_speed)) return;
        m_speed = v;
        emit speedChanged(v);               // ← 触发 QML 的 onSpeedChanged
    }

    bool connected() const { return true; }

signals:
    void speedChanged(double newSpeed);

private:
    double m_speed = 0;
    Status m_status = Idle;                  // Qt 6: MEMBER 直接读写
};
SpeedSensor {
    id: sensor
    // 绑定——依赖 NOTIFY speedChanged
    Text { text: sensor.speed.toFixed(1) + " km/h" }
    // 信号处理器
    onSpeedChanged: (v) => console.log("速度变为:", v)
}

Q_PROPERTY 的完整语法:

Q_PROPERTY(type name
    READ getter                     // 必须——读取
    [WRITE setter]                  // 可选——赋值(缺则只读)
    [NOTIFY signal]                 // 可选——变更信号(QML 绑定依赖它)
    [RESET resetter]                // 可选——重置为默认值
    [MEMBER memberVariable]         // Qt 6:直接读写成员变量
    [CONSTANT]                      // 常量——值永不改变
    [FINAL]                         // 不可被子类覆写
    [REQUIRED]                      // Qt 6:QML 必须赋值
)

# 10.2.2 通知机制

机制 语法 QML 绑定支持 适用场景 性能
NOTIFY signal NOTIFY speedChanged ✅ 完整 值会变化 emit 开销(很小)
CONSTANT CONSTANT ✅ 但静态 值永不改变 零开销
MEMBER MEMBER m_value ❌ 无自动通知 需要手动 emit 零开销

NOTIFY 信号与 QML 绑定的协作:

QML 写: Text { text: sensor.speed }
  ↓ QQmlEngine 首次求值 sensor.speed → 10.5
  ↓ 依赖收集: QML 的 Text.text 绑定注册到 speedChanged 信号上
  ↓ C++ 侧: setSpeed(50.2)
    ↓ emit speedChanged(50.2)
      ↓ QQmlNotifier 收到信号
        ↓ 通知所有依赖项: Text.text 标记 dirty
          ↓ 惰性求值: Text.text = "50.2 km/h"

没有 NOTIFY 的后果:

// ❌ 没有 NOTIFY——QML 绑定不会自动更新
Q_PROPERTY(double speed READ speed WRITE setSpeed)  // 缺少 NOTIFY
Text { text: sensor.speed }  // 初始求值 OK——但是 setSpeed 后 text 不再变

# 10.2.3 MEMBER 读写

Qt 6 引入 MEMBER——编译器自动生成 getter/setter/signal:

class Config : public QObject {
    Q_OBJECT
    Q_PROPERTY(QString theme MEMBER m_theme NOTIFY themeChanged)
    Q_PROPERTY(int fontSize MEMBER m_fontSize NOTIFY fontSizeChanged FINAL)

signals:
    void themeChanged();
    void fontSizeChanged();

private:
    QString m_theme = "dark";
    int m_fontSize = 14;
};
// 编译器生成:
//   QString theme() const { return m_theme; }
//   void setTheme(const QString& v) { if(v!=m_theme){m_theme=v; emit themeChanged();} }
//   int fontSize() const { return m_fontSize; }
//   void setFontSize(int v) { if(v!=m_fontSize){m_fontSize=v; emit fontSizeChanged();} }

# 10.2.4 传感器暴露

// SpeedSensor.h
class SpeedSensor : public QObject {
    Q_OBJECT
    Q_PROPERTY(double speed READ speed WRITE setSpeed NOTIFY speedChanged)
    Q_PROPERTY(bool warning READ warning NOTIFY warningChanged)
    Q_PROPERTY(QString unit READ unit CONSTANT)

public:
    double speed() const { return m_speed; }
    void setSpeed(double v) {
        if (qFuzzyCompare(v, m_speed)) return;
        m_speed = v;
        emit speedChanged(v);
        checkWarning();
    }

    bool warning() const { return m_warning; }
    QString unit() const { return "km/h"; }

signals:
    void speedChanged(double newSpeed);
    void warningChanged(bool warning);

private:
    void checkWarning() {
        bool w = m_speed > 120;
        if (w != m_warning) { m_warning = w; emit warningChanged(w); }
    }

    double m_speed = 0;
    bool m_warning = false;
};
SpeedSensor {
    id: sensor
    onSpeedChanged: (v) => speedText.text = v.toFixed(0)
    onWarningChanged: (w) => bg.color = w ? "red" : "green"
}

思考题:

  1. unit 用 CONSTANT——如果以后需求变成"用户可切换英里/公里",应该怎么改?
  2. checkWarning() 被设计为 setSpeed 的副作用。如果在 100ms 内连续收到 50 条速度更新,QML 的 warningChanged 会被 emit 50 次吗?如何优化?

# 10.2.5 MOC 探秘

你是否好奇:QML 写 sensor.speed 时,C++ 侧是怎么"找到" speed() 这个 getter 的?答案在 MOC——Meta-Object Compiler。

MOC 在构建时做了什么?

SpeedSensor.h
  ↓ CMake/qmake 检测 Q_OBJECT 宏
  ↓ qt6_generate_moc(SpeedSensor.h → moc_SpeedSensor.cpp)
  ↓
moc_SpeedSensor.cpp 包含:
  ├── staticMetaObject(静态元对象表)
  │   ├── 类名:"SpeedSensor"
  │   ├── 父类:"QObject"
  │   ├── 属性列表:
  │   │   [0] { name:"speed",    type:6(double), getter:&getter_0, setter:&setter_0, notifier:speedChanged }
  │   │   [1] { name:"warning",  type:1(bool),   getter:&getter_1,          notifier:warningChanged }
  │   │   [2] { name:"unit",     type:10(QString),getter:&getter_2, flags:CONSTANT }
  │   ├── 方法列表:
  │   │   [0] { name:"speed",     returnType:6, params:[] }   ← getter
  │   │   [1] { name:"setSpeed",  returnType:0, params:[6] } ← setter
  │   ├── 信号列表:
  │   │   [0] { name:"speedChanged",   params:[6] }
  │   │   [1] { name:"warningChanged", params:[1] }
  │   └── 枚举列表:
  │       [0] { name:"Status", keys:[Idle=0, Active=1, Fault=2] }
  └── qt_static_metacall()  ← 根据索引 dispatch 到真实的 C++ 方法

运行时:QML 如何通过 sensor.speed 调用 C++ getter?

QML: sensor.speed
  ↓ QQmlEngine 解析为属性访问
  ↓ 查 QQmlPropertyCache(缓存避免重复查表)
  ↓ → qmlContext->metaObject->indexOfProperty("speed")  → 得到属性索引 = 0
  ↓ → metaObj->property(0).read(sensor)                  ← 调用 QMetaProperty::read()
  ↓    → metaObj->qt_static_metacall(QMetaObject::ReadProperty, 0, ...)
  ↓       → return this->speed()                          ← 真正的 C++ getter
  ↓ → V4 JS 引擎把 double → JS Number
  ↓ → Text.text 绑定得到值 → 渲染

为什么 MOC 比 Java 反射快?

维度 Qt MOC Java Reflection
查表方式 常量索引 → 数组 → 函数指针 HashMap<String,Method> → hashCode → equals
调用方式 编译期函数指针 Method.invoke() → 运行时 JNI 拆箱
缓存级别 QQmlPropertyCache 进程内缓存在 L1 JVM 反射缓存(SoftReference)
类型安全 C++ 模板 + 编译期检查 运行时 ClassCastException

探索性问题——如果不写 NOTIFY,MOC 生成的表里会少什么?

拿掉 NOTIFY speedChanged 后,moc_SpeedSensor.cpp 的属性表里 speed 条目的 notifier 字段变为空指针:

原来: { name:"speed", ..., notifier:&signal_0 }
现:     { name:"speed", ..., notifier:nullptr }

QQmlEngine 在首次求值 sensor.speed 时,调用 QMetaProperty::hasNotifySignal() → 返回 false → 不会创建 QQmlBinding(因为无法订阅变更通知)→ 仅做一次静态求值,之后永不再更新。

这就是"QML 绑定不会更新的根本原因"——不是"信号忘了发",而是"元对象表里根本没有信号索引"。


# 10.3 方法暴露

class DataProcessor : public QObject {
    Q_OBJECT
public:
    Q_INVOKABLE double calculateAverage(const QVector<double>& data) {
        if (data.isEmpty()) return 0;
        return std::accumulate(data.begin(), data.end(), 0.0) / data.size();
    }

    Q_INVOKABLE QString format(const QString& tmpl, double value) {
        return tmpl.arg(value, 0, 'f', 1);
    }

    Q_INVOKABLE static int version() { return 2; }   // ← static 也可以
};
DataProcessor { id: proc }
Component.onCompleted: {
    var avg = proc.calculateAverage([1.2, 3.4, 5.6])
    console.log(proc.format("平均: %1", avg))  // "平均: 3.4"
    console.log(proc.version())                 // 2
}

Q_INVOKABLE vs Q_PROPERTY 选择:

场景 推荐
需要 QML 绑定自动更新 Q_PROPERTY + NOTIFY
一次性计算(不存状态) Q_INVOKABLE
有参数的计算 Q_INVOKABLE
需要 QML 属性绑定语法 x: obj.prop Q_PROPERTY

# 10.3.1 容器暴露

除了单个属性和方法,QML 经常需要访问 C++ 持有的对象列表——比如传感器的历史记录、CAN 报文队列。Qt 提供 QQmlListProperty 作为 QML 可见的容器属性:

class SensorHistory : public QObject {
    Q_OBJECT
    Q_PROPERTY(QQmlListProperty<QObject> items READ items)

    // ← QML 中 Repeater { model: history.items }
    //                                   ↑ 自动绑定到 QQmlListProperty

public:
    QQmlListProperty<QObject> items() {
        return QQmlListProperty<QObject>(
            this,                       // owner——生命周期锚点
            &m_records,                 // 实际数据存储(QList<QObject*>)
            &countImpl,                 // int count(QQmlListProperty*) → 返回列表长度
            &atImpl,                    // QObject* at(QQmlListProperty*, int) → 按索引取
            &clearImpl);                // void clear(QQmlListProperty*) → 清空
    }

    void addRecord(double speed, double temp) {
        auto* rec = new SensorRecord(speed, temp, this);  // QObject parent = this
        m_records.append(rec);
        emit itemsChanged();   // ← 手动通知 QML 列表变更
    }

signals:
    void itemsChanged();

private:
    QList<QObject*> m_records;

    static int countImpl(QQmlListProperty<QObject>* list) {
        return static_cast<SensorHistory*>(list->object)->m_records.size();
    }
    static QObject* atImpl(QQmlListProperty<QObject>* list, int idx) {
        return static_cast<SensorHistory*>(list->object)->m_records.at(idx);
    }
    static void clearImpl(QQmlListProperty<QObject>* list) {
        // 不做真正 clear——数据生命周期由 C++ 管理
    }
};
SensorHistory {
    id: history

    // QQmlListProperty 可直接作为 Repeater 的 model
    Repeater {
        model: history.items
        delegate: Text { text: speed + " km/h, " + temp + "°C" }
    }
}

QQmlListProperty vs QAbstractListModel 选择

场景 推荐 原因
列表长度固定、Item 有 Q_PROPERTY QQmlListProperty less boilerplate
列表长度动态变化、需要增量通知 QAbstractListModel beginInsertRows/endInsertRows 支持
列表中每项为复杂 QML 组件 QAbstractListModel + roleNames delegate 通过 role 读取数据
列表需要排序/过滤/代理Model QAbstractItemModel 子类 QSortFilterProxyModel
纯粹的 QObject 子对象挂载 QQmlListProperty Qt 默认 children 就是

# 10.4 跨语言通信

# 10.4.1 C++ 到 QML

class CanBusReader : public QObject {
    Q_OBJECT
public slots:
    void onFrameReceived(const QCanBusFrame& frame) {
        emit messageReceived(frame.frameId(), frame.payload().toHex());
    }

signals:
    void messageReceived(int canId, QString data);    // ← QML 侧 onMessageReceived
};
CanBusReader {
    id: reader
    onMessageReceived: (canId, data) => {
        canView.model.append({ "id": canId, "data": data })
    }
}

信号参数类型映射:

C++ 信号参数类型 QML 信号处理器参数类型 自动转换
int / double / bool int / real / bool ✅
QString string ✅
QVariantMap var (JS Object) ✅
QVector<int> JS Array ✅
QDateTime date ✅
自定义 struct — ❌ 需拆为基本类型或传 QVariantMap

# 10.4.2 QML 到 C++

Item {
    id: qmlRoot
    signal userAction(string action, var payload)
    // QML 侧 emit
    Component.onCompleted: userAction("start", { speed: 80 })
}
QObject* root = engine.rootObjects().first();
QObject::connect(root, SIGNAL(userAction(QString, QVariant)),
                 &controller, SLOT(onUserAction(QString, QVariant)));

两种 connect 语法对比:

// Qt 5 传统——字符串,编译期不检查
connect(sender, SIGNAL(valueChanged(int)), receiver, SLOT(onValue(int)));

// Qt 5.15+ / Qt 6 新语法——函数指针,编译期类型检查 ✅
connect(sender, &Sender::valueChanged, receiver, &Receiver::onValue);

# 10.4.3 connect 语法

auto* sensor = root->findChild<QObject*>("speedSensor");
connect(sensor, SIGNAL(speedChanged(double)),    // 跨语言必须用 SIGNAL 宏
        controller, [](double v) {
            qDebug() << "QML 来的速度:" << v;
        });

# 10.5 上下文注入

# 10.5.1 时机作用域

QQmlApplicationEngine engine;

// ✅ 方式 1:注入到 root context——全局可见
engine.rootContext()->setContextProperty("config", new AppConfig());

// ✅ 方式 2:创建子 context——限定可见范围
QQmlContext* subContext = new QQmlContext(engine.rootContext());
subContext->setContextProperty("dashboardData", new DashboardModel());
QObject* dashboard = component.create(subContext);   // dashboard.qml 内可见

engine.load(QUrl("qrc:/main.qml"));  // ← 必须在注入之后

setContextProperty 的生命周期:

行为 结果
QML 引用 config 对象 QQmlEngine 增加引用计数——C++ 侧不能 delete
QML 侧有 Config { id: c } 且 C++ 又注入同名 QML 侧的 id 覆盖 C++ 侧的 context property
换一个新的 config 对象 setContextProperty("config", newObj) → 旧对象引用计数 -1

# 10.5.2 上下文层级

Engine Root Context ← setContextProperty("config", ...)
    ├── MainWindow Context
    │   └── Page Context ← setContextProperty("pageData", ...)
    └── Dialog Context  ← setContextProperty("dialogData", ...)

QML 符号解析:从当前 Context 向上查找,直到 Root Context
  Page.qml 里引用 "config" → 当前 Context 没有 → 向上一级 → 找到
  Page.qml 里引用 "dialogData" → 当前 Context 没有 → 向上一级 → 也没有 → undefined

# 10.6 类型注册

# 10.6.1 手动注册

// 在 main() 里注册
qmlRegisterType<SpeedSensor>("com.myapp.car", 1, 0, "SpeedSensor");
//    C++ 类         QML 模块名       版本  QML 类型名
import com.myapp.car 1.0
SpeedSensor { id: sensor }    // ← QML 自己 new——不依赖 setContextProperty

# 10.6.2 宏注册体系

宏 效果 用途
QML_ELEMENT 自动注册为 QML 类型 普通类型
QML_NAMED_ELEMENT(Name) 指定 QML 名(不同于 C++ 类名) C++ 叫 SpeedSensorImpl,QML 叫 SpeedSensor
QML_SINGLETON 全局单例 应用配置
QML_UNCREATABLE(reason) 禁止 QML new 抽象基类
QML_ANONYMOUS 不注册类型名 仅作为基类
class SpeedSensor : public QObject {
    Q_OBJECT
    QML_ELEMENT                        // 自动注册,QML 类型名 = "SpeedSensor"
    QML_NAMED_ELEMENT(SpeedGauge)      // QML 侧叫 "SpeedGauge"
    Q_PROPERTY(double speed READ speed NOTIFY speedChanged)
    // ...
};
import MyModule           // Qt 6 自动从项目名推断模块名
SpeedGauge { id: gauge }

# 10.6.3 单例注册

class AppConfig : public QObject {
    Q_OBJECT
    QML_ELEMENT
    QML_SINGLETON                     // Qt 6 单例宏
    Q_PROPERTY(QString theme MEMBER m_theme CONSTANT)
private:
    QString m_theme = "dark";
};
Text { text: AppConfig.theme }    // ← 全局唯一实例,不需要 new

# 10.7 C++ 操作对象

QObject* root = engine.rootObjects().first();

// ① 按 objectName 查找
auto* label = root->findChild<QObject*>("speedLabel");
if (label) label->setProperty("text", "120 km/h");

// ② 按类型查找
auto* rects = root->findChildren<QQuickItem*>();
for (auto* r : rects) qDebug() << r->objectName();

// ③ 调用 QML 方法(QML 侧 function 定义的)
auto* controller = root->findChild<QObject*>("controller");
QMetaObject::invokeMethod(controller, "reset");

// ④ 调用带参数的 QML 方法
QMetaObject::invokeMethod(controller, "setRange",
                          Q_ARG(double, 0.0), Q_ARG(double, 200.0));

// ⑤ 读 QML 属性
double speed = root->property("speed").toDouble();

objectName vs id:

维度 objectName id
C++ 可见 ✅ findChild("...") ❌ 仅 QML 上下文内
QML 可见 ❌ ✅ id.doSomething()
唯一性 全局建议唯一 本文件内必须唯一
运行时可变 ✅ setObjectName(...) ❌

# 10.8 线程与生命周期

跨线程信号槽的自动处理——Qt::AutoConnection:

class SensorThread : public QThread {
    Q_OBJECT
protected:
    void run() override {
        while (running) {
            double data = readSensor();    // ← 独立线程
            emit newData(data);            // ← 跨线程 emit
        }
    }
signals:
    void newData(double value);
};

// main.cpp
auto* sensor = new SensorThread();
auto* model   = new DashboardModel();      // ← GUI 线程
connect(sensor, &SensorThread::newData,
        model,  &DashboardModel::appendData);  // Qt::AutoConnection
// ↑ 自动检测 sender/receiver 在不同线程
// → 信号排队到 receiver 的线程队列 → 安全

C++ 对象的 QML 所有权——这是跨语言交互最深的水:

// 场景 1: C++ 创建,传给 QML——默认 C++ 负责生命周期
auto* backend = new Backend();                          // C++ new
engine.rootContext()->setContextProperty("backend", backend);  // QML 引用
// backend 的生命周期由 C++ 管理——QQmlEngine 的 JS GC 不会 delete 它
// 原因: 未通过 QML JavaScript 引擎创建的 QObject,默认 CppOwnership

// 场景 2: QML 创建——QQmlEngine 管理生命周期
// QML: SpeedSensor { id: sensor }
// C++: auto* s = root->findChild<QObject*>("sensor");
// s 的生命周期由 QQmlEngine 管理——C++ 不要 delete
// 原因: 通过 QML new 的 QObject,默认 JavaScriptOwnership

// 场景 3: QML_ELEMENT + QML new——QQmlEngine JavaScript 所有权
// QML: Component { MyItem { } }
// QML 的 GC 管理 MyItem 的生命周期

// 场景 4: 手动控制所有权(混合管理)
auto* hybrid = new Backend();
QQmlEngine::setObjectOwnership(hybrid, QQmlEngine::CppOwnership);
engine.rootContext()->setContextProperty("hybrid", hybrid);
// 即使 QML 代码里用过 hybrid 后不再引用,JS GC 也不会清理它

所有权生命周期流程图——两种创建路径的完整对比:

C++ new → setContextProperty                     QML new { SpeedSensor { } }
    │                                                   │
    │ 默认 CppOwnership                                  │ 默认 JavaScriptOwnership
    │                                                   │
    ├─ QML 侧引用:                                        ├─ 属性父对象(this)即 QObject parent
    │  QQmlContext 持有指针                                │   → QObject 父子树管理
    │                                                   │
    ├─ QML 不再引用:                                       ├─ QML 不再引用:
    │  JS GC 标记收集,但 delete 被跳过                       │  JS GC 最终调用 delete
    │  (因为 ownership ≠ JavaScriptOwnership)               │
    │                                                   │
    └─ C++ 侧 delete backend                             └─ 自动销毁(不能手动 delete)
        → JS 侧的引用变为"野指针"
        → 如果再访问 → 崩溃

关键:setObjectOwnership 什么时候必须用?

场景 操作 原因
C++ 创建的对象在 QML 中动态创建 delegate setObjectOwnership(obj, JavaScriptOwnership) delegate 销毁时需要 GC 回收
C++ 缓存池管理对象,QML 只是"借用" setObjectOwnership(obj, CppOwnership) C++ 侧的 pool 负责 delete
QQmlListProperty 返回的对象 保持默认(parent = C++ owner) parent QObject 销毁时自动清理子对象

线程操作约束表:

操作 必须所在线程 违规后果
setProperty GUI 线程 崩溃
emit signal 任意线程 AutoConnection 自动安全
invokeMethod 目标对象线程 崩溃
Q_PROPERTY 读写 属性所在线程 不安全
beginInsertRows GUI 线程 断言失败

# 10.9 CAN 全栈实战

// SpeedController.h + SpeedController.cpp
class SpeedController : public QObject {
    Q_OBJECT
    QML_ELEMENT
    Q_PROPERTY(double speed READ speed WRITE setSpeed NOTIFY speedChanged)
    Q_PROPERTY(bool warning READ warning NOTIFY warningChanged)

public:
    double speed() const { return m_speed; }
    void setSpeed(double v) {
        if (qFuzzyCompare(v, m_speed)) return;
        m_speed = v;
        emit speedChanged(v);
        updateWarning();
    }
    bool warning() const { return m_warning; }

    Q_INVOKABLE void reset() { setSpeed(0); }

signals:
    void speedChanged(double newSpeed);
    void warningChanged(bool warn);

private:
    void updateWarning() {
        bool w = m_speed > 120;
        if (w != m_warning) { m_warning = w; emit warningChanged(w); }
    }

    double m_speed = 0;
    bool m_warning = false;
};

class CanReader : public QObject {
    Q_OBJECT
public slots:
    void onCanFrame(const QCanBusFrame& frame) {
        emit canDataReady(frame.frameId(), frame.payload().toHex());
    }
signals:
    void canDataReady(int canId, QString data);
};

// main.cpp
int main(int argc, char* argv[]) {
    QGuiApplication app(argc, argv);
    QQmlApplicationEngine engine;

    // C++ 创建业务对象,注入 QML
    auto* controller = new SpeedController();
    auto* canReader  = new CanReader();
    engine.rootContext()->setContextProperty("speedCtrl", controller);
    engine.rootContext()->setContextProperty("canReader", canReader);

    engine.load(QUrl("qrc:/main.qml"));
    return app.exec();
}
import QtQuick
import QtQuick.Controls

ApplicationWindow {
    width: 800; height: 480; visible: true; title: "仪表盘"

    // ───── 速度显示(绑定 C++ 属性) ─────
    Rectangle {
        id: bg
        anchors.fill: parent
        color: speedCtrl.warning ? "#FF5252" : "#0d0d1a"
    }

    Text {
        anchors.centerIn: parent
        text: speedCtrl.speed.toFixed(0) + " km/h"
        font.pixelSize: 72; color: "white"
    }

    // ───── CAN 数据列表 ─────
    ListView {
        anchors { bottom: parent.bottom; left: parent.left; right: parent.right; margins: 10 }
        height: 100; model: ListModel { id: canModel }; clip: true
        delegate: Text {
            text: "0x" + canId.toString(16) + ": " + data
            color: "#aaa"; font.family: "monospace"; font.pixelSize: 11
        }
    }

    // ───── CAN 数据信号 → QML 列表 ─────
    Connections {
        target: canReader
        function onCanDataReady(canId, data) {
            canModel.insert(0, { "canId": canId, "data": data })
            if (canModel.count > 50) canModel.remove(50)
        }
    }

    // ───── 按钮调 C++ 方法 ─────
    Button {
        anchors { top: parent.top; right: parent.right; margins: 10 }
        text: "重置"
        onClicked: speedCtrl.reset()     // ← Q_INVOKABLE
    }
}

案例知识融合:本案例把 QML/C++ 集成的全部路径聚合到一套代码——①Q_PROPERTY + NOTIFY 让 speed 和 warning 自动驱动 QML 绑定;②Q_INVOKABLE reset() 让 QML 按钮调 C++ 方法;③Connections 让 C++ 信号 canDataReady 驱动 QML 的 ListModel;④setContextProperty 注入两个 C++ 对象供 QML 全局引用。

跨章知识串连——本案例背后的每一行代码都对应前面章节的一个原理:

代码 背后原理 参考章节
Q_PROPERTY(double speed READ ... NOTIFY speedChanged) 依赖收集:QML 在首次求值时读到 speedCtrl.speed,QQmlEngine 订阅 speedChanged 信号 §4.2.1 依赖收集
emit speedChanged(v) 触发 QML 绑定重算 QQmlNotifier 沿依赖图向下推送脏标记 §4.3.1 通知链路
Connections { target: canReader; ... } QML 侧订阅 C++ 信号——Meta-Object 系统的跨语言桥 §7 模型视图 + §10.4
ListModel.insert(0, {...}) 触发 delegate 更新 ListView 的 delegate 复用池:不销毁 Text,只改绑定 §7.3.1 复用池
Button.onClicked → speedCtrl.reset() 事件穿透原理:MouseArea 命中测试 → 事件被 accepted → Q_INVOKABLE 调用 §6.2.4 分发机制
bg.color: speedCtrl.warning ? "red" : ... 三元表达式内含依赖收集 → warning 改变会触发单个绑定重算 §4.2 绑定原理

探索性诊断——为什么 CAN 数据显示卡顿?

场景:CAN 报文来得快(每秒 100 帧),列表 UI 明显卡。诊断链(不是直接结论):

  1. 是否 ListModel.insert 太频繁? → 每帧最多插 6 次(16ms/帧),但 100Hz 意味着一秒 100 次 → 触发 100 次 delegate 更新 → 检查 QSG_VISUALIZE=changes 是否红色区域大
  2. 信号跨线程了吗? → CanReader 若在独立 QThread,emit 是 Qt::QueuedConnection → 事件排队到 GUI 线程 → 如果 GUI 忙,队列堆积
  3. UI 阻塞了信号泵吗? → 检查是否有其他绑定表达式复杂(例如 speedCtrl.speed.toFixed(0) 每次触发 JS)→ 用 §14.3 Profiler 看 Binding 时间线
  4. 数据源本身有毛刺吗? → 用 qDebug() 打印 CAN 帧的时间戳分布——如果 100Hz 里有 200Hz 的突发 → 后端聚合处理

修复通常在后端聚合:C++ 里用 QTimer 每 100ms flush 一次批量 insert,配合 beginInsertRows/endInsertRows 单次通知——见 §7.5.2。

生产常见故障诊断——"C++ 改了数据,QML 还在显示旧值"

这是 QML/C++ 集成里最高频的一类 Bug,揪出根因有一套三层排查协议:

症状: setSpeed(120) 后 Text 仍然显示 "80"
  │
  ├─ 第 1 层:查 C++ 侧是否真的 emit 了
  │   加 qDebug() << "emit speedChanged(" << v << ")"
  │   → 如果日志没打 → setSpeed 根本没被调
  │   → 检查调用链路(CAN 数据到了吗?Q_INVOKABLE 被调了吗?)
  │
  ├─ 第 2 层:查 NOTIFY 信号是否注册到元对象表
  │   moc 生成的文件中搜索 "speedChanged"
  │   → 找不到?→ Q_PROPERTY 少写了 NOTIFY
  │   → 找到了但表中 notifier=nullptr?
  │      → 同名信号和 Q_PROPERTY 在同一个类吗?(MOC 要求 NOTIFY 信号在同一个类定义)
  │
  ├─ 第 3 层:查 QML 侧绑定是否存活
  │   在 QML 里加 onSpeedChanged: (v) => console.log("QML 收到:", v)
  │   → 日志打印了,但 Text 没变?
  │      → Text 的绑定被 JS 赋值覆盖了:text = "xxx" ← 这是绑定杀手
  │   → 日志没打印 → C++ 信号被某个 QObject 的 eventFilter 拦截了
  │      → 或者 connect 时 qt::QueuedConnection 队列堆积(见 §10.8)
  │
  └─ 旁路:查线程安全
     setSpeed 在哪个线程调的?
     → 非 GUI 线程直接调 → 崩溃或未定义行为
     → 必须通过信号 AutoConnection 桥接

最常见的根因(按频率排序):

  1. Text { text: speedCtrl.speed; Component.onCompleted: text = "0" } → JS 赋值杀死了绑定
  2. Q_PROPERTY 有 NOTIFY 信号,但信号定义在父类(MOC 要求当前类)→ 见 §10.2.1 完整声明
  3. setContextProperty 在 engine.load() 之后 → QML 解析时 context 为空 → 见 §10.1

# 10.10 新手陷阱

# 陷阱 说明 修复
1 setContextProperty 在 load 之后 QML 解析时符号未注入→undefined 永远在 engine.load() 之前
2 Q_PROPERTY 不加 NOTIFY QML 绑定不会自动更新 加 NOTIFY signal 并在 setter 里 emit
3 后台线程直接 setProperty 跨线程操作 QObject → crash 通过信号 Qt::AutoConnection 桥接
4 findChild 用 id 查找 id 不写入 QObject 元数据 用 objectName: "xxx",C++ 侧 findChild("xxx")
5 C++ delete QML 创建的对象 QQmlEngine 管理生命周期 让 QML 的 GC 负责

# 10.11 训练题

写一个温度传感器 QML 组件

C++ 侧提供 TemperatureSensor 类:Q_PROPERTY(double celsius) + NOTIFY + 切换华氏度的 Q_INVOKABLE。 QML 侧写一个组件显示摄氏度/华氏度,按钮切换单位。

参考答案
class TemperatureSensor : public QObject {
    Q_OBJECT
    Q_PROPERTY(double celsius READ celsius WRITE setCelsius NOTIFY celsiusChanged)
public:
    double celsius() const { return m_celsius; }
    void setCelsius(double v) { if(v!=m_celsius){ m_celsius=v; emit celsiusChanged(v); } }
    Q_INVOKABLE double fahrenheit() const { return m_celsius * 9.0/5.0 + 32; }
signals:
    void celsiusChanged(double v);
private:
    double m_celsius = 0;
};
TemperatureSensor { id: sensor }
Text { text: isF ? sensor.fahrenheit().toFixed(1)+"°F" : sensor.celsius.toFixed(1)+"°C" }
Button { text: "切换"; onClicked: isF = !isF }

# 10.12 思考题

  1. Q_PROPERTY 有 NOTIFY 却没有在 setter 里 emit——程序为什么能编译通过?QML 绑定在什么情况下会发现"数据变了但没收到信号"?
  2. setContextProperty vs qmlRegisterType:前者全局注入一个对象、后者让 QML 自己 new。两者在内存管理、生命周期、多窗口场景下各有什么优劣?
  3. MOC 的本质:Qt 的 moc 编译器在构建时扫描 Q_OBJECT 宏并生成 moc_*.cpp。简述这个生成文件里包含了什么——为什么 QML 能通过字符串 "speed" 读写属性?这和反射(如 Java Reflection)有什么异同?

# 10.13 速查表

概念 一句话
Q_PROPERTY C++ 属性暴露给 QML——READ/WRITE/NOTIFY
NOTIFY 属性变更信号——QML 绑定的基石(没有 NOTIFY = 绑定不会更新)
CONSTANT 属性永不改变——无信号成本
MEMBER Qt 6 直接读写成员变量——编译器生成 getter/setter
Q_INVOKABLE C++ 方法暴露给 QML——可被 JS 直接调用
setContextProperty 将 C++ QObject* 注入 QML 上下文——必须在 engine.load() 之前
qmlRegisterType 注册 C++ 类型为 QML 可实例化的类型
QML_ELEMENT Qt 6 宏——自动注册,替代手动 qmlRegisterType
QML_SINGLETON Qt 6 单例宏——全局唯一实例,无需 new
Connections QML 侧动态连接外部对象的信号
findChild C++ 反向访问 QML 对象树——用 objectName 而非 id
AutoConnection 跨线程信号槽的默认策略——自动判断队列/直连

核心哲学:

QML 与 C++ 的集成 = Q_PROPERTY(数据) + signal/slot(事件) + Q_INVOKABLE(方法)。

Q_PROPERTY 是 QML 绑定系统的对外接口,
NOTIFY 信号是它和 QQmlNotifier 的握手协议,
理解这两者就等于理解了 QML/C++ 交互的全部秘密。

下一篇:11. 自定义SceneGraph节点 (opens new window)

上次更新: 2026/07/12, 19:39:51
Canvas与自定义渲染
自定义SceneGraph节点

← Canvas与自定义渲染 自定义SceneGraph节点→

最近更新
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号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式