恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
将C++ 类型属性暴露给 QML
首页
资讯中心
/
将C++ 类型属性暴露给 QML
将C++ 类型属性暴露给 QML
发布时间:2026/10/8 20:12:26
前言用 Qt 写界面时一个典型的合作模式是C 负责数据模型和业务逻辑QML 负责界面。问题随之而来——QML 怎么才能看到 C 里的那个Person类并且拿到它的name、age初学者最常见的误解是只要把 C 类写出来QML 自然就能用。事实完全不是这样。QML 引擎不认识 C 的类型系统它只认识 Qt 的元对象系统meta-object system一个由mocMeta-Object Compiler在编译期生成的、运行期可查询的属性/方法/信号清单。一个类如果没进这套系统哪怕它是public的、哪怕你#include了它的头文件QML 里也完全看不见它。第二个误解是属性写上了就完事了。属性确实会出现在 QML 里但如果没有配套的NOTIFY 信号QML 的绑定binding只会取一次初始值之后 C 改了数据界面上还是老样子——这是新手最常遇到的数据变了界面不变。本文以Qt 6为主同时在对照处标出 Qt 5 的写法从元对象系统讲起依次说清属性、可调用方法、枚举的暴露方式再讲类型注册、对象所有权和线程约束。文中所有 API 名称均取自 Qt 官方文档写作环境没有 Qt 工具链示例未经过编译验证请以你实际安装的 Qt 版本头文件为准。一、QML 看见的是元对象不是 C 类型要让一个类被 QML 使用它必须满足两个条件继承自QObject或其派生类并且在类体第一行写上Q_OBJECT宏经过moc处理。用 qmake 时HEADERS里的头文件会自动送去 moc用 CMake 时包含Q_OBJECT的头文件也必须列进qt_add_executable/qt_add_qml_module的源文件列表里否则 moc 不会跑链接阶段会缺一堆staticMetaObject、qt_metacall之类的符号。Q_OBJECT宏展开后会往类里插入元对象相关的声明。它不能用在模板类上——moc 不处理模板。需要模板化的 QObject时只能写成普通的 QObject 派生类或者用Q_GADGET配合值类型。moc会把类里的这些东西收集成元数据声明被 moc 收集成QML 侧怎么用Q_PROPERTY(...)属性表obj.name、obj.name xsignals:区的信号信号表onNameChanged: { ... }处理器Q_INVOKABLE标记的成员函数可调用方法表obj.greeting()public slots:区的成员函数槽表同样可调用obj.doSomething()Q_ENUM(...)标记的枚举枚举表Person.MaleQ_CLASSINFO(...)附加类信息通过className等访问反过来说不加任何标记的 public 成员函数QML 是调不到的。这是方法明明存在却报Property xxx of object is not a function的根本原因。二、暴露属性Q_PROPERTY 与 NOTIFY一个完整的可暴露类型长这样// person.h —— Qt 6 #ifndef PERSON_H #define PERSON_H #include QObject #include QString #include QtQml/qqmlregistration.h // QML_ELEMENT 需要这个头 class Person : public QObject { Q_OBJECT QML_ELEMENT // Qt 6让 QML 里可以直接写 Person { } 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 value) { if (m_name value) // 值没变就不发信号避免无谓的绑定重算 return; m_name value; emit nameChanged(); // 通知 QML属性变了请重算绑定 } int age() const { return m_age; } void setAge(int value) { if (m_age value) return; m_age value; emit ageChanged(); } Q_INVOKABLE QString greeting() const { return QStringLiteral(你好) m_name; } signals: void nameChanged(); void ageChanged(); private: QString m_name; int m_age 0; }; #endif // PERSON_H关于Q_PROPERTY的语法几个要点基本形式是Q_PROPERTY(类型 名字 READ 读函数 WRITE 写函数 NOTIFY 通知信号)。NOTIFY后面跟的信号必须无参数并且声明在signals:区。写成nameChanged(const QString )会被 moc 拒绝。WRITE可以省略这时属性在 QML 里就是只读的。也可以用MEMBER关键字直接绑定一个成员变量让 moc 自动生成读写函数Q_PROPERTY(int age MEMBER m_age NOTIFY ageChanged)。代价是你没法在写入路径上插入校验或副作用moc 生成的就是一次直来直去的赋值。属性类型必须是元对象系统认识的类型int、bool、double、QString、QVariant、QObject派生类指针、用Q_ENUM注册过的枚举以及用Q_DECLARE_METATYPE注册过的自定义类型。std::string不是——用了它moc 会在生成阶段直接报错。加了NOTIFY的属性才具备可绑定的语义。没有NOTIFY的属性QML 只在初始化时取一次值。QML_ELEMENT是 Qt 6 引入的注册宏配合 CMake 的qt_add_qml_module使用不需要再手写qmlRegisterType。Qt 5 没有这个宏必须用下面第三节的注册函数。三、暴露方法与枚举Q_INVOKABLE加在成员函数声明的最前面把函数放进元对象的方法表Q_INVOKABLE int nextAge() { setAge(m_age 1); return m_age; } Q_INVOKABLE bool save(const QString path) const;返回值和参数类型同样必须被元对象系统认识。想让一个函数返回自定义结构体得先让那个结构体成为可被QVariant承载的类型。枚举用Q_ENUM注册写在枚举定义之后同一个类里class Person : public QObject { Q_OBJECT QML_ELEMENT public: enum Gender { Male, Female, Other }; Q_ENUM(Gender) // 注册进元对象系统 // ... };QML 里就能这样用Person { id: p; gender: Person.Female }注意枚举必须定义在带Q_OBJECT的类里Q_ENUM才有意义定义在命名空间里的枚举要用Q_ENUM_NS且该命名空间必须带Q_NAMESPACE。四、注册类型、所有权与线程约束Qt 6QML_ELEMENT qt_add_qml_moduleCMake 里大致是这样以官方文档为准不同 Qt 6 小版本参数略有增减cmake_minimum_required(VERSION 3.21) project(people LANGUAGES CXX) find_package(Qt6 REQUIRED COMPONENTS Quick QuickControls2) qt_standard_project_setup() qt_add_executable(apppeople main.cpp) qt_add_qml_module(apppeople URI People VERSION 1.0 SOURCES person.h person.cpp QML_FILES Main.qml ) target_link_libraries(apppeople PRIVATE Qt6::Quick Qt6::QuickControls2)URI People就是 QML 侧import People时用的模块名。因为类上写了QML_ELEMENTPerson会自动注册到这个模块下QML 里import People之后直接写Person { }即可。注意person.h必须出现在SOURCES里否则 moc 不会处理它。Qt 5qmlRegisterTypeQt 5 里在main()中手动注册必须在引擎加载 QML 之前调用#include QGuiApplication #include QQmlApplicationEngine #include QUrl #include person.h int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); // Qt 5 的注册方式签名是 qmlRegisterTypeT(uri, major, minor, qmlName) qmlRegisterTypePerson(People, 1, 0, Person); QQmlApplicationEngine engine; engine.load(QUrl(QStringLiteral(qrc:/Main.qml))); if (engine.rootObjects().isEmpty()) return -1; return app.exec(); }写成qmlRegisterTypePerson(People, 1, 0, Person)之后QML 里import People 1.0就能看到Person。Qt 6 仍然保留了这些函数所以上面这段在 Qt 6 里也能编只是官方更推荐QML_ELEMENT。QML 侧import QtQuick import People Window { width: 320; height: 160; visible: true Person { id: person name: 张三 age: 30 } Text { anchors.centerIn: parent // 绑定一旦 name 或 age 变化NOTIFY 信号会触发这里重算 text: person.greeting() / person.age } }Qt 6 推荐用不带版本号的import QtQuick版本无关导入。Qt 5 需要写版本号例如import QtQuick 2.15。所有权谁负责 deleteQML 引擎对QObject有一套所有权规则QML 里创建的对象Person { }默认是QQmlEngine::JavaScriptOwnership由 QML 的垃圾回收负责销毁它的parent通常是它的 QML 父项。C 里new出来、没有 parent、又交给 QML 引用的对象默认是QQmlEngine::CppOwnershipQML 不会删它得你自己管。可以用QQmlEngine::setObjectOwnership(obj, QQmlEngine::CppOwnership)显式指定。经验法则是谁new的谁负责不要让 C 和 QML 都以为自己该删。线程改属性必须在对象所属线程QObject有线程亲和性thread affinity一个对象属于创建它的那个线程只能从那个线程调用它的方法、改它的属性。而 QML 引擎运行在主GUI线程上QML 里创建的对象也就绑在主线程。所以一个后台线程里直接person-setAge(18)是数据竞争属于未定义行为并且大概率让 QML 场景图崩溃。正确做法是发一个跨线程信号让槽函数在主线程里执行Qt 会根据接收者的线程亲和性自动选用排队连接// 工作线程里 emit ageReady(18); // 信号 // 主线程里 Person 的槽 void Person::onAgeReady(int v) { setAge(v); } // 连接类型用 Qt::AutoConnection 即可顺带说一句volatile在这里帮不上任何忙——它既不提供原子性也不建立 happens-before 关系不能用来做线程同步。常见坑点1. 忘了Q_OBJECTclass Person : public QObject { Q_PROPERTY(QString name READ name) // ❌ 没有 Q_OBJECTmoc 不生成元数据 public: QString name() const; }; class Person : public QObject { Q_OBJECT // ✅ 必须是类体第一条 Q_PROPERTY(QString name READ name NOTIFY nameChanged) };症状编译能过运行时 QML 报Cannot assign to non-existent property name。2. 属性没有NOTIFY界面不刷新Q_PROPERTY(int age READ age WRITE setAge) // ❌ 绑定只算一次 Q_PROPERTY(int age READ age WRITE setAge NOTIFY ageChanged) // ✅ 数据变化会推给 QML3. setter 里不加值没变就返回void setName(const QString v) { m_name v; emit nameChanged(); } // ❌ 每次都发信号 // 如果 QML 里有 name: object.name 这类双向绑定会来回震荡 void setName(const QString v) { // ✅ 先比再改 if (m_name v) return; m_name v; emit nameChanged(); }4. 用 QML 不认识的自定义 C 类型做属性Q_PROPERTY(std::string name READ name WRITE setName) // ❌ std::string 不在元对象系统里 Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged) // ✅ 用 QString5.qmlRegisterType调用得太晚QQmlApplicationEngine engine; engine.load(QUrl(QStringLiteral(qrc:/Main.qml))); // ❌ 已经加载了 qmlRegisterTypePerson(People, 1, 0, Person); // 再注册也来不及 qmlRegisterTypePerson(People, 1, 0, Person); // ✅ 注册必须在 load 之前 QQmlApplicationEngine engine; engine.load(QUrl(QStringLiteral(qrc:/Main.qml)));症状QML 报module People is not installed。6. 在带Q_OBJECT的类上套模板template typename T class Holder : public QObject { Q_OBJECT }; // ❌ moc 不支持模板类 class PersonHolder : public QObject { Q_OBJECT }; // ✅ 老老实实写具体类7. 把 C 侧new出来的无父对象交给 QML 后不管// ❌ C new、无 parent、又 setContextProperty 给 QML // QML 不会删程序退出时泄漏对象销毁后 QML 里的引用又成了空壳 QQmlContext *ctx engine.rootContext(); ctx-setContextProperty(person, new Person()); // ✅ 明确所有权并且让 C 持有它 Person *p new Person(app); // 交给 app 做父对象生命周期跟随 app ctx-setContextProperty(person, p);另外QQmlContext::setContextProperty会把对象放进全局上下文Qt 6 里已不推荐在大型项目中使用更推荐注册类型或单例。8. 从工作线程修改属性// ❌ 工作线程 std::thread([p]{ p-setAge(18); }).detach(); // 数据竞争UB通常直接崩 // ✅ 发信号回主线程由排队连接在对象所属线程执行 emit ageReady(18);总结想暴露什么加什么关键约束数据字段Q_PROPERTY(类型 名字 READ … WRITE … NOTIFY …)类型必须被元对象系统认识数据变化的通知signals:里的无参信号与NOTIFY一一对应成员函数Q_INVOKABLE或放进public slots:参数和返回值类型同样受限枚举Q_ENUM(枚举名)枚举必须定义在带Q_OBJECT的类里类本身Qt 6 用QML_ELEMENTQt 5 用qmlRegisterType必须在 QML 加载前完成注册所有权QQmlEngine::setObjectOwnership默认 CppOwnership 时由 C 负责释放跨线程更新信号 排队连接QObject 只能被它所属线程访问一句话记住QML 看到的是moc生成的元数据不是你写的 C 类。属性要能被绑定就必须有NOTIFY类型要在 QML 里可用就必须注册跨线程改数据就必须回到对象所属的线程——这三条覆盖了绝大多数明明写了却不起作用的情况。