二十一、C++与QML交互之模型、类型与生命周期
本讲详解QT Quick中C++与QML交互的核心技巧,涵盖C++模型复用、自定义类型跨语言传递及对象生命周期管理,助力构建健壮应用。
二十一、C++与QML交互之模型、类型与生命周期-MakerLi

C++与QML交互(三)——模型、自定义类型与生命周期管理


一、课程概述

当你需要构建学生成绩管理、商品列表这类复杂数据驱动的QT Quick应用时,C++与QML的深度交互是核心能力。本讲深入探讨三大关键主题:如何让QML复用C++的数据模型、如何传递自定义复杂数据、如何妥善管理对象生命周期避免内存泄漏或崩溃,帮你打通C++逻辑与QML界面的协作壁垒。


学习目标:掌握C++模型在QML中的绑定方法;实现自定义数据类型跨语言传递;建立清晰的对象所有权管理策略,写出健壮的混合编程代码。


二、在QML中使用C++数据模型(QAbstractItemModel)

QAbstractItemModel是Qt模型/视图框架的核心,把它暴露给QML后,ListView、TableView等组件就能直接显示和编辑C++管理的数据,无需重复拷贝数据。


2.1 核心步骤

  1. 创建C++模型类:继承QAbstractListModel(列表场景)或QAbstractTableModel(表格场景),实现rowCount、data、roleNames三个必要虚函数;
  2. 注册模型到QML:通过qmlRegisterType注册类型(允许QML创建实例),或用setContextProperty直接传递已创建的实例;
  3. QML绑定使用:将视图的model属性绑定到C++模型,通过自定义角色读取数据。

2.2 代码示例:水果列表模型

// mylistmodel.h
#include <QAbstractListModel>
#include <QStringList>
#include <QColor>

class MyListModel : public QAbstractListModel
{
    Q_OBJECT
public:
    enum RoleNames {
        NameRole = Qt::UserRole + 1,
        ColorRole
    };

    explicit MyListModel(QObject *parent = nullptr);

    // 必须实现的虚函数
    int rowCount(const QModelIndex &parent = QModelIndex()) const override;
    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
    QHash<int, QByteArray> roleNames() const override;

    // 新增:动态添加数据的方法(需通知QML视图刷新)
    Q_INVOKABLE void addFruit(const QString &name, const QColor &color);

private:
    QStringList m_fruitNames;
    QList<QColor> m_fruitColors;
};


// mylistmodel.cpp
#include "mylistmodel.h"

MyListModel::MyListModel(QObject *parent) : QAbstractListModel(parent)
{
    // 初始化数据
    m_fruitNames << "苹果" << "香蕉" << "橙子" << "葡萄";
    m_fruitColors << Qt::red << Qt::yellow << Qt::green << Qt::magenta;
}

int MyListModel::rowCount(const QModelIndex &parent) const
{
    Q_UNUSED(parent);
    return m_fruitNames.size();
}

QVariant MyListModel::data(const QModelIndex &index, int role) const
{
    if (!index.isValid() || index.row() >= m_fruitNames.size())
        return QVariant();

    switch (role) {
    case NameRole: return m_fruitNames.at(index.row());
    case ColorRole: return m_fruitColors.at(index.row());
    default: return QVariant();
    }
}

QHash<int, QByteArray> MyListModel::roleNames() const
{
    // 映射角色名到QML可识别的字符串
    QHash<int, QByteArray> roles;
    roles[NameRole] = "name";
    roles[ColorRole] = "color";
    return roles;
}

void MyListModel::addFruit(const QString &name, const QColor &color)
{
    // 必须调用begin/end系列函数,通知QML视图数据变化
    beginInsertRows(QModelIndex(), rowCount(), rowCount());
    m_fruitNames.append(name);
    m_fruitColors.append(color);
    endInsertRows();
}


// main.cpp注册模型
#include <QQmlApplicationEngine>
#include "mylistmodel.h"

int main(int argc, char *argv[])
{
    QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
    QGuiApplication app(argc, argv);

    // 注册模型类型到QML,命名空间MyModels,版本1.0
    qmlRegisterType<MyListModel>("MyModels", 1, 0, "MyListModel");

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

    return app.exec();
}


// main.qml使用模型
import QtQuick 2.15
import QtQuick.Controls 2.15
import MyModels 1.0

ApplicationWindow {
    width: 300; height: 400
    visible: true

    MyListModel { id: fruitModel }

    ListView {
        anchors.fill: parent
        model: fruitModel
        delegate: Rectangle {
            width: ListView.view.width
            height: 50
            color: model.color // 绑定自定义角色
            Text {
                text: model.name
                anchors.centerIn: parent
                font.pixelSize: 18
            }
        }
    }

    Button {
        anchors.bottom: parent.bottom
        text: "添加草莓"
        onClicked: fruitModel.addFruit("草莓", Qt::darkRed)
    }
}


三、自定义数据类型的传递

当需要在C++和QML之间传递学生、商品这类复杂自定义数据时,需要将类型注册为Qt元类型,让Qt能识别并传递它。


3.1 核心步骤

  1. 定义数据类型:如果需要信号槽通知属性变化,用Q_OBJECT标记类;如果仅用于数据传递,用更轻量的Q_GADGET,同时用Q_PROPERTY声明可被QML访问的属性。
  2. 注册元类型:用Q_DECLARE_METATYPEqRegisterMetaType宏,让Qt的QVariant能存储该类型,支持信号槽传递。
  3. 注册到QML:用qmlRegisterType允许QML创建实例;如果仅需传递不能创建,用qmlRegisterUncreatableType

3.2 示例:自定义学生类型

// student.h
#include <QObject>
#include <QString>
#include <QColor>

// 用Q_OBJECT支持信号槽,属性变化时通知QML
class Student : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged)
    Q_PROPERTY(int score READ score WRITE setScore NOTIFY scoreChanged)
    Q_PROPERTY(QColor gradeColor READ gradeColor NOTIFY scoreChanged) // 计算属性
public:
    explicit Student(QObject *parent = nullptr);
    Student(const QString &name, int score, QObject *parent = nullptr);

    QString name() const;
    void setName(const QString &name);
    int score() const;
    void setScore(int score);
    QColor gradeColor() const; // 根据分数返回对应颜色

signals:
    void nameChanged();
    void scoreChanged();

private:
    QString m_name;
    int m_score;
};

// 声明为元类型,支持QVariant存储
Q_DECLARE_METATYPE(Student*)


// student.cpp
#include "student.h"

Student::Student(QObject *parent) : QObject(parent), m_score(0) {}
Student::Student(const QString &name, int score, QObject *parent)
    : QObject(parent), m_name(name), m_score(score) {}

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

int Student::score() const { return m_score; }
void Student::setScore(int score) {
    if (m_score != score) {
        m_score = score;
        emit scoreChanged();
    }
}

QColor Student::gradeColor() const {
    if (m_score >= 90) return Qt::darkGreen;
    if (m_score >= 60) return Qt::blue;
    return Qt::red;
}


// main.cpp注册类型
#include "student.h"

int main(int argc, char *argv[])
{
    // ... 其他初始化代码
    qRegisterMetaType<Student*>("Student*");
    // 注册到QML命名空间MyTypes
    qmlRegisterType<Student>("MyTypes", 1, 0, "Student");
    // ...
}


四、对象生命周期管理

C++和QML混合编程时,对象所有权是最容易踩坑的地方,处理不当会导致内存泄漏或程序崩溃,核心要明确“谁负责回收对象”。


4.1 所有权基础规则

  • C++创建的对象:如果有父QObject,父对象被删除时会自动回收它;如果没有父对象,必须手动用delete或智能指针清理,否则会内存泄漏。
  • QML创建的对象:默认由QML引擎的垃圾回收器管理,当对象不再被使用或引擎关闭时,会自动删除对应的C++对象。

4.2 手动调整所有权:QQmlEngine::setObjectOwnership

我们可以用这个函数手动指定对象所有权:

  • QQmlEngine::CppOwnership:C++端保留所有权,QML不会自动删除它,适合全局单例、管理器这类长期存在的对象。
  • QQmlEngine::JavaScriptOwnership:所有权转移给QML引擎,垃圾回收时自动删除,适合临时创建的数据对象。

4.3 最佳实践

  1. 全局对象:通过setContextProperty注入的管理器、单例,设置为CppOwnership,在应用退出时手动清理。
  2. QML创建的组件:保持默认的JavaScriptOwnership,让引擎自动管理生命周期。
  3. 避免循环引用:如果C++持有QML对象的引用,用QPointer替代普通指针,它会在对象销毁后自动置为nullptr,避免悬空指针。
  4. 动态创建对象:C++中创建后传递给QML的对象,记得设置JavaScriptOwnership,让QML负责回收。

4.4 示例:安全传递对象所有权

// manager.h
#include <QObject>
#include "student.h"

class DataManager : public QObject
{
    Q_OBJECT
public:
    explicit DataManager(QObject *parent = nullptr);
    Q_INVOKABLE Student* createStudent(const QString &name, int score);
};


// manager.cpp
#include "manager.h"
#include <QQmlEngine>

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

Student* DataManager::createStudent(const QString &name, int score)
{
    Student* stu = new Student(name, score);
    // 将所有权转移给QML引擎,确保对象被正确回收
    QQmlEngine::setObjectOwnership(stu, QQmlEngine::JavaScriptOwnership);
    return stu;
}


// main.cpp注入管理器
DataManager manager;
engine.rootContext()->setContextProperty("dataManager", &manager);
// manager是栈对象,由C++管理,不需要转移所有权


五、综合示例与总结

结合以上知识点,我们可以构建一个完整的学生成绩列表应用:

  • StudentModel:继承QAbstractListModel,存储Student对象,提供添加学生的方法;
  • Student:自定义数据类型,包含姓名、分数及计算属性;
  • DataManager:提供创建学生的接口,处理对象所有权;
  • QML视图:用ListView绑定模型,显示学生信息,通过按钮调用管理器添加学生。

核心总结

C++负责核心数据逻辑和模型管理,QML负责界面展示和交互,两者协作的关键在于:

  1. 用模型/视图架构实现数据复用,避免数据冗余;
  2. 规范注册自定义类型,打通跨语言数据传递;
  3. 明确对象所有权边界,用QPointer、setObjectOwnership等工具避免内存问题。

掌握这些技能,你就能轻松构建复杂、健壮的QT Quick应用了!