本章是 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 |