第七节 C++ 与 QML 交互

本章是 QML 从"玩具"走向"产品"的关键:用 C++ 提供数据、业务逻辑、性能敏感计算,用 QML 负责界面。覆盖注册类型、属性/方法/信号暴露、context property、数据模型、C++ 调 QML 等全部主流姿势。

1. 引擎与启动流程

1.1 最小启动代码(Qt6 推荐)

cpp 复制代码
#include <QGuiApplication>
#include <QQmlApplicationEngine>

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);

    QQmlApplicationEngine engine;
    QObject::connect(&engine, &QQmlApplicationEngine::objectCreationFailed,
                     &app, []() { QCoreApplication::exit(-1); });   // Qt 6.4+
    engine.loadFromModule("appqmltutorial", "Main");                 // Qt 6.4+ 模块化加载
    // 传统写法(跨版本通用):
    // engine.load(QUrl(QStringLiteral("qrc:/Main.qml")));

    if (engine.rootObjects().isEmpty())
        return -1;
    return app.exec();
}

关键对象:

  • QGuiApplication:无窗口系统的 GUI 应用基类(不需要 QWidget)。
  • QQmlApplicationEngine:QML 引擎 + 资源加载器。它继承 QQmlEngine,额外管理 qrc 中的 qml 文件、根对象生命周期。
  • engine.rootObjects():加载后窗口对象(Window/ApplicationWindow)会出现在这里。

1.2 Qt6 模块化加载(可选)

CMake 中启用 qt_add_qml_module 后,QML 文件按 URI 导入:

javascript 复制代码
import appqmltutorial

并通过 engine.loadFromModule("appqmltutorial", "Main") 加载 Main.qml。适合中大型工程;小工程沿用 load(QUrl("qrc:/Main.qml")) 即可。

2. 向 QML 暴露 C++ 对象:两种主流方式

方式 适用 说明
Context Property 全局单例对象(配置、AppInfo、Controller) 简单直接,QML 里按名字直接用
注册类型(qmlRegisterType) 可多实例、需在 QML 中 new 的类型 更"QML 原生",支持属性/信号完整集成

经验:全局唯一服务(如网络管理器、数据库)用 context property;业务模型/控件用注册类型。

3. Context Property

3.1 C++ 侧

cpp 复制代码
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>

class AppInfo : public QObject {
    Q_OBJECT
    Q_PROPERTY(QString version READ version CONSTANT)
public:
    explicit AppInfo(QObject *parent = nullptr) : QObject(parent) {}
    QString version() const { return "1.2.3"; }
};

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);
    AppInfo info;
    QQmlApplicationEngine engine;
    engine.rootContext()->setContextProperty("appInfo", &info);   // 名字 → QML
    engine.load(QUrl(QStringLiteral("qrc:/Main.qml")));
    return app.exec();
}

3.2 QML 侧

javascript 复制代码
import QtQuick

Window {
    visible: true
    width: 400; height: 200
    title: "App " + appInfo.version        // 直接使用
}

注意:

  • context property 对象必须活得比 engine 长(栈上声明在 engine 之前,或使用智能指针)。
  • 名字建议小驼峰(appInfo),避免与类型名混淆。
  • Qt6 中仍推荐用 context property 暴露单例,但官方新趋势是 QML_ELEMENT + 单例注册(见 §7)。

4. 注册自定义类型

4.1 经典方式:qmlRegisterType(Qt5 风格,Qt6 仍可用)

cpp 复制代码
// person.h
#include <QObject>

class Person : public QObject {
    Q_OBJECT
    Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged)
    Q_PROPERTY(int age READ age WRITE setAge NOTIFY ageChanged)
public:
    explicit Person(QObject *parent = nullptr) : QObject(parent) {}

    QString name() const { return m_name; }
    void setName(const QString &n) { if (m_name != n) { m_name = n; emit nameChanged(); } }

    int age() const { return m_age; }
    void setAge(int a) { if (m_age != a) { m_age = a; emit ageChanged(); } }

signals:
    void nameChanged();
    void ageChanged();

private:
    QString m_name;
    int m_age = 0;
};
cpp 复制代码
// main.cpp
#include "person.h"
#include <QQmlEngine>

qmlRegisterType<Person>("MyApp.Models", 1, 0, "Person");
// 注册到命名空间 MyApp.Models 1.0,QML 中类型名 Person
javascript 复制代码
import MyApp.Models 1.0        // Qt5 必须写版本号;Qt6 可省略或写 1.0

Person {
    name: "Alice"
    age: 30
}

4.2 Qt6 推荐方式:QML_ELEMENT 自动注册

在 CMake 中启用 qt_add_qml_module 后,C++ 类型可自动注册

cpp 复制代码
// person.h
#include <QtQml/qqmlregistration.h>   // QML_ELEMENT 宏

class Person : public QObject {
    Q_OBJECT
    QML_ELEMENT                       // 自动注册到当前 QML 模块(URI 由 CMake 决定)
    Q_PROPERTY(...)
    ...
};

CMake:

复制代码
qt_add_qml_module(appqmltutorial URI MyApp VERSION 1.0 QML_FILES Main.qml SOURCES person.h person.cpp)

QML 中:

javascript 复制代码
import MyApp

Person { name: "Alice"; age: 30 }

QML_ELEMENT 需要类在当前模块的 SOURCES 里编译,并由 qt_add_qml_module 生成类型注册。这是 Qt6 官方主推路径,可避免手写 qmlRegisterType 与版本号不匹配问题。

4.3 其他注册宏

作用
QML_ELEMENT 注册为可实例化类型
QML_NAMED_ELEMENT(CustomName) 注册并自定义 QML 类型名
QML_SINGLETON 注册为单例(需配合 QML_ELEMENT)
QML_ANONYMOUS 注册但不在 QML 中直接使用(如继承基类)
QML_UNCREATABLE("reason") 注册但禁止在 QML 中 new(如抽象类)

5. Q_PROPERTY:属性系统详解

Q_PROPERTY 是 QML 与 C++ 之间数据联动的核心。四种组件:

部分 含义
READ 读取函数(必填)
WRITE 写入函数(可选;缺省则属性只读)
NOTIFY 变化信号(推荐必填:QML 绑定依赖它!)
CONSTANT 常量(不随生命周期变化)
FINAL 不可在 QML 中重写
cpp 复制代码
Q_PROPERTY(QString title READ title WRITE setTitle NOTIFY titleChanged)
Q_PROPERTY(int count READ count CONSTANT)

缺少 NOTIFY 的后果:QML 中绑定 text: obj.count 只在首次求值,之后 C++ 改 count,界面不更新。

Qt 6 还可以用 Q_PROPERTY(... REVISION 1) 控制版本,配合 QML_REVISION;日常不常用。

6. Q_INVOKABLE:可调用方法

cpp 复制代码
class Calculator : public QObject {
    Q_OBJECT
    QML_ELEMENT
public:
    Q_INVOKABLE int add(int a, int b) { return a + b; }          // QML 可调
    Q_INVOKABLE void startTask(const QString &name);             // 无返回值也可
    // 普通 public 方法 QML 看不到(除非用 slots/信号)
public slots:
    void doThing() { }                                          // slots 也可被 QML 调用
};

QML:

javascript 复制代码
Calculator {
    id: calc
    Component.onCompleted: console.log(calc.add(2, 3))   // 5
}

推荐统一用 Q_INVOKABLE(显式、与 slots 语义区分)。

7. 信号:C++ ↔ QML 双向

7.1 C++ 发信号 → QML 收

cpp 复制代码
class Worker : public QObject {
    Q_OBJECT
    Q_PROPERTY(int progress READ progress NOTIFY progressChanged)
public:
    Q_INVOKABLE void start() { /* ... */ emit progressChanged(); }
signals:
    void progressChanged();
    void finished(QString message);        // 带参信号
};
javascript 复制代码
Worker {
    onProgressChanged: progressBar.value = progress
    onFinished: (message) => console.log("完成:", message)
}

7.2 QML 发信号 → C++ 收

方式一:QML 定义信号,C++ 用 QObject::connect 连接 QML 对象:

javascript 复制代码
// Main.qml
Rectangle {
    signal userAction(string action, int value)
    Button { onClicked: userAction("submit", 42) }
}
cpp 复制代码
// main.cpp 加载后
QObject *root = engine.rootObjects().first();
QObject::connect(root, "2userAction(QString,int)", ...);
// 更常见:用 lambda + QMetaObject
QObject::connect(root, SIGNAL(userAction(QString,int)),
                 &handler, SLOT(onUserAction(QString,int)));

方式二(更规范):QML 调用 C++ 可调用方法,把"事件"变成"方法调用"。

javascript 复制代码
class Backend : public QObject {
    Q_OBJECT
    QML_ELEMENT
public:
    Q_INVOKABLE void onUserAction(const QString &action, int value);
};
javascript 复制代码
Backend {
    id: backend
}
Button { onClicked: backend.onUserAction("submit", 42) }

推荐优先用"QML → Q_INVOKABLE",类型安全、好调试;只有需要 C++ 监听 QML 内部状态变化时才用信号连接。

8. C++ 调用 QML

8.1 找到 QML 对象(findChild)

cpp 复制代码
QObject *root = engine.rootObjects().first();
QObject *textItem = root->findChild<QObject*>("titleLabel");   // QML 中 objectName

QML 侧给对象设置 objectName:

javascript 复制代码
Text { objectName: "titleLabel"; text: "old" }

8.2 读写属性(QMetaObject)

cpp 复制代码
// 读
QVariant v = textItem->property("text");
// 写(会触发 QML 侧绑定联动)
textItem->setProperty("text", "new title");

8.3 调用 QML 方法(invokeMethod)

javascript 复制代码
// Main.qml
function showMessage(msg) { console.log(msg) }
cpp 复制代码
QMetaObject::invokeMethod(textItem, "showMessage",
                          Q_ARG(QString, "hello from C++"));

C++ 调 QML 建议"少而精":把需要被 C++ 控制的逻辑收敛为少数几个 QML 函数/属性,避免散落。

8.4 修改 QML 属性时注意绑定

setProperty 会破坏该属性的绑定(等同 JS 赋值)。如果 QML 侧该属性有绑定且需要保留,让 C++ 修改绑定的"源头"属性,而不是直接 setProperty 目标。

9. 数据模型暴露

9.1 简单场景:QStringList / QVariantList

javascript 复制代码
Q_PROPERTY(QVariantList items READ items CONSTANT)

QVariantList items() const { return {"a", "b", "c"}; }
javascript 复制代码
ListView {
    model: backend.items
    delegate: Text { text: modelData }
}

9.2 对象列表:QObjectList

cpp 复制代码
Q_PROPERTY(QVariantList persons READ persons NOTIFY personsChanged)
// persons() 返回 QList<QObject*> 或 QVariantList(每个元素是 Person*)

ListView {
    model: backend.persons
    delegate: Column {
        Text { text: modelData.name }      // modelData 是 Person 对象
        Text { text: modelData.age }
    }
}

9.3 专业做法:QAbstractListModel

适合大数据量 + 动态增删。子类化 QAbstractListModel,实现 rowCount/data/roleNames:

cpp 复制代码
// personlistmodel.h
#include <QAbstractListModel>

class PersonListModel : public QAbstractListModel {
    Q_OBJECT
public:
    enum Roles { NameRole = Qt::UserRole + 1, AgeRole };

    int rowCount(const QModelIndex &parent = QModelIndex()) const override;
    QVariant data(const QModelIndex &index, int role) const override;
    QHash<int, QByteArray> roleNames() const override;

    Q_INVOKABLE void addPerson(const QString &name, int age);   // QML 可调用
    Q_INVOKABLE void removeAt(int row);

private:
    QVector<QPair<QString,int>> m_items;
};
cpp 复制代码
// personlistmodel.cpp
int PersonListModel::rowCount(const QModelIndex &parent) const {
    return parent.isValid() ? 0 : m_items.size();
}

QVariant PersonListModel::data(const QModelIndex &index, int role) const {
    if (!index.isValid() || index.row() >= m_items.size()) return {};
    const auto &item = m_items.at(index.row());
    if (role == NameRole) return item.first;
    if (role == AgeRole) return item.second;
    return {};
}

QHash<int, QByteArray> PersonListModel::roleNames() const {
    return { {NameRole, "name"}, {AgeRole, "age"} };   // QML 中直接用 name / age
}

void PersonListModel::addPerson(const QString &name, int age) {
    beginInsertRows(QModelIndex(), m_items.size(), m_items.size());
    m_items.append({name, age});
    endInsertRows();
}

注册:

cpp 复制代码
qmlRegisterType<PersonListModel>("MyApp.Models", 1, 0, "PersonListModel");
// 或 QML_ELEMENT + import MyApp

QML:

javascript 复制代码
import MyApp.Models 1.0

ListView {
    model: PersonListModel {
        id: personModel
    }
    delegate: Row {
        spacing: 8
        Text { text: name }
        Text { text: age }
    }

    Button {
        onClicked: personModel.addPerson("New", 20)   // Q_INVOKABLE 直接操作
    }
}

关键点:

  • roleNames() 必须实现:否则 QML 不知道 role 名,delegate 里 name 无效。
  • 增删必须用 beginInsertRows/endInsertRows 等信号包围,视图才能正确刷新。
  • 修改单项用 dataChanged 通知。
  • C++ 线程中操作模型必须经队列回到 GUI 线程(用 QMetaObject::invokeMethod 或信号)。

9.4 树形:QAbstractItemModel

QML 6 的 TreeView 支持 QAbstractItemModel;实现 index/parent/data 三件套 + roleNames 即可。写法与列表模型类似,篇幅原因不展开。

10. 完整示例:C++ 后端 + QML 界面

10.1 工程(CMake + Qt6,兼容 Qt5 只需改 import 与 find_package)

复制代码
cmake_minimum_required(VERSION 3.16)
project(CppQmlDemo VERSION 0.1 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)                       # Qt5 需要;Qt6 qt_standard_project_setup 也会开启

find_package(Qt6 REQUIRED COMPONENTS Core Qml Quick QuickControls2)

qt_add_executable(cppqmldemo main.cpp backend.h backend.cpp qml.qrc)
target_link_libraries(cppqmldemo PRIVATE Qt6::Core Qt6::Qml Qt6::Quick Qt6::QuickControls2)

10.2 backend.h / backend.cpp

cpp 复制代码
// backend.h
#pragma once
#include <QObject>

class Backend : public QObject {
    Q_OBJECT
    Q_PROPERTY(int count READ count NOTIFY countChanged)
    Q_PROPERTY(QString statusText READ statusText NOTIFY statusTextChanged)
public:
    explicit Backend(QObject *parent = nullptr);

    int count() const { return m_count; }
    QString statusText() const { return m_statusText; }

    Q_INVOKABLE void increment();
    Q_INVOKABLE void reset();

signals:
    void countChanged();
    void statusTextChanged();
    void levelUp();                       // C++ → QML 信号

private:
    int m_count = 0;
    QString m_statusText = QStringLiteral("就绪");
};
cpp 复制代码
// backend.cpp
#include "backend.h"

Backend::Backend(QObject *parent) : QObject(parent) {}

void Backend::increment() {
    m_count++;
    emit countChanged();
    if (m_count == 10) {
        m_statusText = QStringLiteral("达到 10!");
        emit statusTextChanged();
        emit levelUp();
    }
}

void Backend::reset() {
    m_count = 0;
    m_statusText = QStringLiteral("已重置");
    emit countChanged();
    emit statusTextChanged();
}

10.3 main.cpp

cpp 复制代码
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include "backend.h"

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);
    Backend backend;

    QQmlApplicationEngine engine;
    engine.rootContext()->setContextProperty("backend", &backend);  // context property
    engine.load(QUrl(QStringLiteral("qrc:/Main.qml")));

    if (engine.rootObjects().isEmpty())
        return -1;
    return app.exec();
}

10.4 Main.qml

javascript 复制代码
import QtQuick
import QtQuick.Controls

ApplicationWindow {
    visible: true
    width: 400; height: 260
    title: "C++ ↔ QML Demo"

    Column {
        anchors.centerIn: parent
        spacing: 16

        Text {
            anchors.horizontalCenter: parent.horizontalCenter
            text: backend.count
            font.pixelSize: 48
            font.bold: true
        }

        Label {
            anchors.horizontalCenter: parent.horizontalCenter
            text: backend.statusText
            color: backend.count >= 10 ? "green" : "gray"
        }

        Row {
            anchors.horizontalCenter: parent.horizontalCenter
            spacing: 8
            Button { text: "+1";        onClicked: backend.increment() }
            Button { text: "重置";       onClicked: backend.reset() }
        }
    }

    Connections {
        target: backend
        function onLevelUp() {
            console.log("C++ 发来的信号:升级了!")
        }
    }
}

11. 生命周期、线程与所有权

  • 所有权 :QML 创建的对象由 QML 引擎管理;context property / 栈对象由 C++ 管理。不要 delete QML 创建的对象
  • 对象释放顺序:engine 先销毁 QML 对象,再销毁 context property(如果声明在 main 中,注意声明顺序:Backend backend; 应早于 engine,否则 engine 析构时 backend 已死)。
  • 线程:QML 运行在主线程。C++ 后台线程改属性/发信号前,用 QMetaObject::invokeMethod(obj, ..., Qt::QueuedConnection) 或信号自动跨线程(auto connection 在线程间自动队列化)。
  • 避免在 QML 中直接持有 C++ 裸指针:用 id + context property 模式即可。

12. 最佳实践总结

关注点 建议
数据源 业务状态放 C++(Q_PROPERTY + NOTIFY),UI 状态放 QML
绑定 C++ 属性必须带 NOTIFY 信号
方法 用 Q_INVOKABLE 暴露业务动作
信号方向 C++ 主动通知用信号;QML 主动请求用 Q_INVOKABLE 调用
列表 简单用 QVariantList / QObjectList;大数据量用 QAbstractListModel
注册 新工程 Qt6 优先 QML_ELEMENT;兼容 Qt5 用 qmlRegisterType
单例 context property(简单)或 QML_SINGLETON(规范)
调试 engine.rootObjects() 为空时先查 qrc 路径与 import
相关推荐
qq_401700411 小时前
TCP 粘包问题深度解析:嵌入式网络开发的常见坑与解决方案
服务器·网络·qt·网络协议·tcp/ip
晓晓_za8986681 小时前
Geo 优化源码二次开发:自定义地域规则改造实操文档
java·开发语言·搜索引擎·ci/cd·矩阵
ShineWinsu2 小时前
对于TRAE中配置Qt的解析
开发语言·c++·ide·vscode·qt·ai·trae
wy3136228212 小时前
Git——ignore让项目的 target 目录真正被忽略(忽略其他文件也是同样的道理)
开发语言·git
格林威2 小时前
C#图像分块处理:图像按行或按块(Tile)切分,多个 CPU 核心同时处理不同的区域
开发语言·图像处理·人工智能·机器学习·计算机视觉·c#·工业相机
小小龙学IT2 小时前
第一节 QML 背景介绍
c++·qt·js
leisoo80972 小时前
财报舞弊预警系统实战用Python挖掘应收存货异常因子 IG50免费开源股票数据API接口
开发语言·python
君顾12 小时前
AI新零售线上商城系统实战:架构设计与开发全流程指南
java·开发语言·零售
wind100322 小时前
Outdated Visual C++ Redistributable 过时 VC++ 运行库报错修复
开发语言·c++