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.2 属性暴露
- 10.3 方法暴露
- 10.4 跨语言通信
- 10.5 上下文注入
- 10.6 类型注册
- 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"
}
思考题:
unit用CONSTANT——如果以后需求变成"用户可切换英里/公里",应该怎么改?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 明显卡。诊断链(不是直接结论):
- 是否 ListModel.insert 太频繁? → 每帧最多插 6 次(16ms/帧),但 100Hz 意味着一秒 100 次 → 触发 100 次 delegate 更新 → 检查 QSG_VISUALIZE=changes 是否红色区域大
- 信号跨线程了吗? →
CanReader若在独立 QThread,emit是Qt::QueuedConnection→ 事件排队到 GUI 线程 → 如果 GUI 忙,队列堆积 - UI 阻塞了信号泵吗? → 检查是否有其他绑定表达式复杂(例如
speedCtrl.speed.toFixed(0)每次触发 JS)→ 用 §14.3 Profiler 看 Binding 时间线 - 数据源本身有毛刺吗? → 用
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 桥接
最常见的根因(按频率排序):
Text { text: speedCtrl.speed; Component.onCompleted: text = "0" }→ JS 赋值杀死了绑定- Q_PROPERTY 有 NOTIFY 信号,但信号定义在父类(MOC 要求当前类)→ 见 §10.2.1 完整声明
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 思考题
Q_PROPERTY有 NOTIFY 却没有在 setter 里 emit——程序为什么能编译通过?QML 绑定在什么情况下会发现"数据变了但没收到信号"?setContextPropertyvsqmlRegisterType:前者全局注入一个对象、后者让 QML 自己 new。两者在内存管理、生命周期、多窗口场景下各有什么优劣?- 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++ 交互的全部秘密。