目录

前言

整体过程

1. 编写QML暴露的C++类

继承QObject

暴露属性(Properties)

暴露方法(Methods)

暴露信号(Signals)

暴露枚举(Enums)

暴露嵌套的子对象

1. 设计 C++ 子对象

2.设计 C++ 父对象

3. 将父对象暴露给 QML

4. 在 QML 中访问嵌套子对象

2. 注册C++类或实例—>QML

注册为上下文属性 (setContextProperty):

注册为QML类型 (qmlRegisterType):

3. 在QML中访问和使用


前言

QML 作为一种灵活高效的界面开发语言已经越来越得到业界的认可。QML 负责界面,C++ 负责逻辑,这也是 Qt 官方推荐的开发方式。那么 QML 与 C++ 的交互必然是每一个Qt开发工程师需要掌握并且精通的。

Qt QML与C++后端通信的需求非常普遍,可以说是QML应用开发的核心和精髓

在实际项目中,几乎所有的复杂应用都会采用这种架构。QML主要负责构建前端的用户界面(UI),而C++则负责处理后端业务逻辑,比如:

  • 数据处理和管理: 数据库访问、文件读写、网络请求等。

  • 计算密集型任务: 复杂的算法运算、图像处理等。

  • 硬件交互: 与设备硬件(如串口、USB、传感器等)进行通信。

推荐一篇介绍的很详细且简单直白的文章(可以跟着这篇文章来进行QML与c++类交互实操):

Qt Quick QML 与 C++ 交互系列之一_qml的setcontextproperty-CSDN博客

整体过程

整个过程可以概括为:

  1. 设计C++类(继承QObject并使用宏) ->
  2. 在C++中注册到QML引擎(qmlRegisterType 或 setContextProperty) ->
  3. 在QML中无缝使用

这套流程为C++和QML之间提供了一个强大的、类型安全的通信桥梁,让您可以利用C++的高性能来处理后端逻辑,同时使用QML的便捷性来构建用户界面。

1. 编写QML暴露的C++类

继承QObject

首先,你的C++类必须继承自QObject。这是使用Qt元对象系统的前提,它提供了信号与槽机制、属性系统以及其他元编程功能。同时,在类的定义中,必须使用Q_OBJECT

Q_OBJECT宏告诉元对象编译器(MOC)为你的类生成额外的代码,以便支持元对象系统的功能。

暴露属性(Properties)

要将一个C++属性暴露给QML,需要使用Q_PROPERTY。这个宏允许你定义一个属性,并指定其在QML中可读、可写或可通知。

Q_PROPERTY(type name READ function WRITE function NOTIFY signal)

一个典型的Q_PROPERTY声明需要至少指定以下部分:

  • type: 属性的数据类型。

  • name: 属性的名称。

  • READ: 用于读取属性值的成员函数(getter)。

  • WRITE: (可选) 用于写入属性值的成员函数(setter)。

  • NOTIFY: (可选) 当属性值发生变化时发出的信号。这是非常重要的,虽然NOTIFY是可选但强烈推荐写上,当属性值改变时发出这个信号,QML中绑定的UI元素会自动更新

示例: 假设你有一个名为name的字符串属性需要暴露。

class Person : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged)

public:
    Person(QObject *parent = nullptr);

    QString name() const;
    void setName(const QString &name);

signals:
    void nameChanged();

private:
    QString m_name;
};

在这个例子中:

  • Q_PROPERTY宏将name属性暴露给QML。
  • READ函数name()用于获取属性值。
  • WRITE函数setName(const QString &)用于设置属性值。
  • NOTIFY信号nameChanged()在setName函数中被发出,通知QML该属性已更改。

在setName的实现中,你需要在值改变时发出nameChanged信号

void Person::setName(const QString &name)
{
    if (m_name == name)
        return;

    m_name = name;
    emit nameChanged();
}

暴露方法(Methods)

要将C++方法暴露给QML,需要使用Q_INVOKABLE宏或者将方法声明为public slots。

  • Q_INVOKABLE宏: 这是最推荐的方式。它使你的方法可以在QML中被调用。
     
  • public slots: 将方法声明为公共槽(slot)也可以使其被QML调用。不过,Q_INVOKABLE更清晰地表达了其意图,即为QML提供可调用的方法。
class MyClass : public QObject
{
    Q_OBJECT

public:
    Q_INVOKABLE void doSomething();
    Q_INVOKABLE int add(int a, int b);
};

暴露信号(Signals)

信号是C++与QML之间进行事件通信的关键。只要你的类继承了QObject并使用了Q_OBJECT宏,你就可以定义信号。QML可以直接连接到这些信号,并在信号发出时触发相应的JavaScript函数。

C++类中的信号向QML暴露的时候,无需额外的宏修饰来暴露,都是默认暴露的,只要这个类使用了 Q_OBJECT 宏,QML就可以直接识别并连接到这些信号,并在信号发出时触发相应的JavaScript函数。

class MyClass : public QObject
{
    Q_OBJECT

signals:
    void dataReady(const QString &data);
};

在C++代码中,你可以通过emit关键字来发出信号:

void MyClass::fetchData()
{
    // 假设获取了数据
    QString result = "Hello from C++!";
    emit dataReady(result);
}

在QML中,你可以通过on<SignalName>语法来连接这个信号:

// 假设MyClass的实例名为myObject
MyClass {
    id: myObject

    onDataReady: {
        console.log("Received data:", data);
    }
}

暴露枚举(Enums)

要将C++枚举暴露给QML,你需要将枚举类型放在一个继承自QObject的类中,并使用Q_ENUM宏

#include <QObject>

class DataManager : public QObject
{
    Q_OBJECT

public:
    explicit DataManager(QObject *parent = nullptr);

    // 将枚举放在一个QObject子类中
    enum Status {
        Idle,
        Loading,
        Ready,
        Error
    };
    Q_ENUM(Status) // 注册这个枚举

    // 可以在属性或方法中使用这个枚举类型
    Q_PROPERTY(Status currentStatus READ currentStatus NOTIFY currentStatusChanged)
    Status currentStatus() const;
    
signals:
    void currentStatusChanged();
    
private:
    Status m_currentStatus;
};

QML代码:

Item {
    id: root

    DataManager {
        id: manager
        // 访问枚举值
        onCurrentStatusChanged: {
            if (manager.currentStatus === DataManager.Ready) {
                console.log("Data is ready!");
            }
        }
    }
}

暴露嵌套的子对象

1. 设计 C++ 子对象

首先,作为嵌套的子对象,它必须符合所有暴露给 QML 的基本要求:

  • 必须继承自 QObject。
  • 必须在类定义中包含 Q_OBJECT 宏。
  • 使用 Q_PROPERTY、Q_INVOKABLE 或 Q_SIGNAL 宏来暴露它自己的属性、方法和信号。

例如,一个网络配置子对象的类:

// networksettings.h
#include <QObject>
#include <QString>

class NetworkSettings : public QObject {
    Q_OBJECT
    Q_PROPERTY(QString serverUrl READ serverUrl WRITE setServerUrl NOTIFY serverUrlChanged)
    Q_PROPERTY(int port READ port WRITE setPort NOTIFY portChanged)

public:
    explicit NetworkSettings(QObject* parent = nullptr);

    QString serverUrl() const;
    void setServerUrl(const QString& url);

    int port() const;
    void setPort(int p);

signals:
    void serverUrlChanged();
    void portChanged();

private:
    QString m_serverUrl;
    int m_port;
};

在对应的.cpp 实现文件中,你需要提供相应的  setter 方法。

2.设计 C++ 父对象

接下来,需要设计一个父类来包含和管理这个子对象。父类也必须遵循暴露给 QML 的基本原则。

关键在于:

  • 将子对象作为父类的成员变量。
     
  • 通过 Q_PROPERTY 将这个成员变量暴露给 QML。这个属性的类型就是子对象的类名,并且属性的 READ 函数返回一个指向该子对象的指针。
// appsettings.h
#include <QObject>
#include "networksettings.h"

class AppSettings : public QObject {
    Q_OBJECT
    // 暴露子对象,注意其类型是 NetworkSettings*
    // CONSTANT 表示它在运行时不会改变
    Q_PROPERTY(NetworkSettings* network READ network CONSTANT)

public:
    explicit AppSettings(QObject* parent = nullptr);
    ~AppSettings();

    NetworkSettings* network() const;

private:
    NetworkSettings* m_network;
};

3. 将父对象暴露给 QML

这里选择在c++中创建对应的对象实例,然后将这个父类对象暴露给QML,当然如果你需要在QML中多次创建这个父类对象的话,你也可以选择将父类暴露给QML,然后在QML中可以创建多个对象。

// main.cpp
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include "appsettings.h"

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

    // 1. 创建父对象实例
    AppSettings appSettings;

    // 2. 将父对象实例暴露给 QML
    engine.rootContext()->setContextProperty("appSettings", &appSettings);

    const QUrl url(u"qrc:/myapp/main.qml"_qs);
    engine.load(url);

    return app.exec();
}

4. 在 QML 中访问嵌套子对象

现在,在 QML 中,你可以通过父对象的名字 appSettings 访问其暴露的属性 network,然后继续访问 network 的属性和方法,形成一个清晰的层级调用。

// main.qml
import QtQuick
import QtQuick.Controls

Window {
    width: 640
    height: 480
    visible: true

    Column {
        spacing: 10
        anchors.centerIn: parent

        Label {
            text: "Server URL: " + appSettings.network.serverUrl
        }
        
        Label {
            text: "Port: " + appSettings.network.port
        }
        
        Button {
            text: "Change URL"
            onClicked: {
                appSettings.network.setServerUrl("https://new.server.com");
            }
        }
    }
}

通过这种方式,你可以将复杂的 C++ 功能模块化,并以一种自然、分层的方式在 QML 中进行访问和管理,极大地提高了代码的组织性和可维护性。

2. 注册C++类或实例—>QML

在这一步,尼需要让QML引擎知道您的C++类或对象存在。有两种主要方法:

  • 注册为上下文属性 (setContextProperty):如果您的C++类是一个单例对象,或者您只需要在QML中访问一个已经创建好的特定实例,那么这种方法更简单直接。

  • 注册为QML类型 (qmlRegisterType):如果您的C++类是一个通用的组件或数据模型,并且可能需要在QML中多次实例化,那么这种方法最合适。这使得您的C++类就像一个内置的QML类型一样,可以在QML文档中直接创建和使用。

册为上下文属性 (setContextProperty)

使用QQmlContext::setContextProperty,这种方法是把一个具体的C++对象实例注册到QML的上下文中。这个对象在QML中以一个特定的名称(比如"manager")全局可访问,就像一个单例一样。

适用场景: 当你需要一个全局管理器或控制器时,这种方法非常适合。例如,一个BackendManager类型对象负责所有网络请求、数据处理和系统状态管理,你希望在任何QML文件中都能直接访问它。

特点:这个实例在 C++ 代码中创建,并且在 QML 整个生命周期中是唯一的。QML 无法使用这个方法来创建新的实例。这就像是你在 QML 中创建了一个全局的单例对象。

// main.cpp
QQmlApplicationEngine engine;
BackendManager manager; // 创建一个C++对象实例
engine.rootContext()->setContextProperty("backendManager", &manager);
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));

代码解释

创建 QML 引擎 (QQmlApplicationEngine)。

创建一个自定义的 C++BackendManager类型 后端对象 (manager)。

把这个 C++ 对象通过 setContextProperty 注册到 QML 上下文,起名 "backendManager"。

加载并运行 main.qml,此时 QML 就能直接调用 backendManager 的方法或属性。

backendManager 被注册到了根上下文里,根上下文是整个 QML 引擎的“全局作用域”上下文。

在这个上下文里注册的属性(比如这里的 backendManager),会被该引擎加载的所有 QML 文件 共享。所以不管是 main.qml 还是 Page.qml,都可以直接用 backendManager,因为它是挂在根上下文的。

// main.qml
Text {
    // 直接访问 C++ 对象的属性
    text: backendManager.status
    // 调用 C++ 对象的方法
    onClicked: backendManager.doSomething()
}

注册为QML类型 (qmlRegisterType)

使用qmlRegisterType 这种方法是把一个C++类注册到QML的类型系统中,让QML能够像创建内置类型(如Rectangle)一样,动态地创建这个类的实例。

适用场景: 当你需要创建可重用的组件、数据模型或服务时,这种方法是首选。

这里我们省略一下MyCppClass类的定义

// main.cpp
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include "mycppclass.h" // 包含你的 C++ 类头文件

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

    QQmlApplicationEngine engine;

    // 注册 C++ 类
    // 参数依次为:
    // 1. QML 模块的 URI (例如 "com.mycompany.controls")
    // 2. 主版本号 (例如 1)
    // 3. 次版本号 (例如 0)
    // 4. QML 中使用的类型名称 (例如 "MyCppClass")
    qmlRegisterType<MyCppClass>("com.mycompany.controls", 1, 0, "MyCppClass");

    engine.load(QUrl(QStringLiteral("qrc:/main.qml")));

    return app.exec();
}

在 QML 中使用 C++ 类

在 QML 文件中,你需要通过 import 语句导入你的模块,然后就可以像使用任何其他 QML 类型一样使用自定义的 C++ 类了。

// main.qml
import QtQuick 2.15
import QtQuick.Window 2.15
import com.mycompany.controls 1.0 // 导入你注册的 QML 模块

Window {
    width: 640
    height: 480
    visible: true
    title: qsTr("C++ QML Example")

    // 实例化 C++ 类
    MyCppClass {
        id: myObject
        name: "World" // 直接设置属性值
    }

    Column {
        anchors.centerIn: parent
        spacing: 10

        Text {
            // 通过 myObject 访问 C++ 属性
            text: "Hello from QML, " + myObject.name
            font.pixelSize: 24
        }

        Button {
            text: "Change Name"
            onClicked: {
                // 在 QML 中调用 C++ 方法
                myObject.setName("Gemini");
            }
        }
        
        Button {
            text: "Say Hello"
            onClicked: {
                // 调用 C++ 方法
                myObject.sayHello();
            }
        }
    }
}

3. 在QML中访问和使用

一旦C++类或对象被注册,您就可以在QML中像操作其他QML元素一样,直接访问其属性、调用其方法和连接其信号。

  • 访问属性:直接使用点号.运算符访问,例如 myObject.name。
     
  • 调用方法:直接调用,例如 myObject.doSomething()。
     
  • 连接信号:使用 on<SignalName> 的形式连接信号,例如 onNameChanged: { ... }。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐