Sharing State Across QML Components Using a C++ Singleton Model

When developing complex Qt Quick applications, maintaining synchronized state across multiple QML components often requires a shared data model. Declaring independent ListModel instances within separate QML files leads to data fragmentation. A robust solution involves implementing a C++ backend model derived from QAbstractListModel managed as a singleton.

This approach centralizes data storage in C++ while exposing standard model interfaces to QML. The implementation requires subclassing QAbstractListModel and overriding core virtual functions such as rowCount, data, and roleNames. Additionally, custom methods for manipulation (add, update, remove) should be marked as Q_INVOKABLE to allow calls from QML.

C++ Model Implementation

The header file defines the singleton accessor and the data structure. Using a static instance pointer ensures only one model exists throughout the application lifecycle.

#ifndef SHAREDTAGMODEL_H
#define SHAREDTAGMODEL_H

#include <QObject>
#include <QAbstractListModel>
#include <QJsonObject>
#include <QVariantMap>

class SharedTagModel : public QAbstractListModel
{
    Q_OBJECT
public:
    enum TagRoles {
        NameRole = Qt::UserRole + 1,
        StatusRole
    };

    static SharedTagModel* getInstance();

    // QAbstractItemModel interface
    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;

    Q_INVOKABLE void addEntry(const QVariantMap &entry);
    Q_INVOKABLE QJsonObject getEntry(int index) const;
    Q_INVOKABLE void updateEntry(int index, const QJsonObject &entry);
    Q_INVOKABLE void removeEntry(int index);
    Q_INVOKABLE int entryCount() const;
    Q_INVOKABLE int findIndex(const QJsonObject &criteria);

private:
    struct TagEntry {
        QString name;
        QString status;
    };
    QList<TagEntry> m_entries;

    explicit SharedTagModel(QObject *parent = nullptr);
    static SharedTagModel* s_instance;

    SharedTagModel(const SharedTagModel&) = delete;
    SharedTagModel& operator=(const SharedTagModel&) = delete;
};

#endif // SHAREDTAGMODEL_H

The source file implements the logic. Note the use of beginInsertRows and endInsertRows to notify the view of changes. Bounds checking uses logical OR to correctly identify invalid indices.

#include "SharedTagModel.h"

SharedTagModel* SharedTagModel::s_instance = nullptr;

SharedTagModel* SharedTagModel::getInstance()
{
    if (!s_instance) {
        s_instance = new SharedTagModel();
    }
    return s_instance;
}

SharedTagModel::SharedTagModel(QObject *parent)
    : QAbstractListModel(parent)
{
}

int SharedTagModel::rowCount(const QModelIndex &parent) const
{
    if (parent.isValid())
        return 0;
    return m_entries.size();
}

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

    const TagEntry &entry = m_entries.at(index.row());
    if (role == NameRole) return entry.name;
    if (role == StatusRole) return entry.status;
    return QVariant();
}

QHash<int, QByteArray> SharedTagModel::roleNames() const
{
    QHash<int, QByteArray> roles;
    roles[NameRole] = "name";
    roles[StatusRole] = "status";
    return roles;
}

void SharedTagModel::addEntry(const QVariantMap &entry)
{
    beginInsertRows(QModelIndex(), m_entries.size(), m_entries.size());
    TagEntry newEntry;
    newEntry.name = entry.value("name").toString();
    newEntry.status = entry.value("status").toString();
    m_entries.append(newEntry);
    endInsertRows();
}

QJsonObject SharedTagModel::getEntry(int index) const
{
    if (index < 0 || index >= m_entries.size())
        return QJsonObject();

    const TagEntry &entry = m_entries.at(index);
    QJsonObject obj;
    obj["name"] = entry.name;
    obj["status"] = entry.status;
    return obj;
}

void SharedTagModel::updateEntry(int index, const QJsonObject &entry)
{
    if (index < 0 || index >= m_entries.size())
        return;

    TagEntry &target = m_entries[index];
    if (entry.contains("name"))
        target.name = entry["name"].toString();
    if (entry.contains("status"))
        target.status = entry["status"].toString();

    emit dataChanged(createIndex(index, 0), createIndex(index, 0));
}

void SharedTagModel::removeEntry(int index)
{
    if (index < 0 || index >= m_entries.size())
        return;

    beginRemoveRows(QModelIndex(), index, index);
    m_entries.removeAt(index);
    endRemoveRows();
}

int SharedTagModel::entryCount() const
{
    return m_entries.size();
}

int SharedTagModel::findIndex(const QJsonObject &criteria)
{
    bool hasName = criteria.contains("name");
    bool hasStatus = criteria.contains("status");
    if (!hasName && !hasStatus) return -1;

    for (int i = 0; i < m_entries.size(); ++i) {
        bool nameMatch = !hasName || (m_entries[i].name == criteria["name"].toString());
        bool statusMatch = !hasStatus || (m_entries[i].status == criteria["status"].toString());
        
        if ((hasName && nameMatch) || (hasStatus && statusMatch)) {
            return i;
        }
    }
    return -1;
}

Registration and Usage

In main.cpp, the singleton instance is exposed to the QML engine context. This makes the model globally accessible to any QML component without explicit imports or property bindings.

#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include "SharedTagModel.h"

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

    engine.rootContext()->setContextProperty("TagModel", SharedTagModel::getInstance());

    engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
    return app.exec();
}

Within QML, the model behaves like a standard list model. Multiple components can read or modify the same underlying data structure simultaneously.

import QtQuick 2.15
import QtQuick.Window 2.15

Window {
    visible: true
    width: 640
    height: 480
    title: "Shared Model Demo"

    Component.onCompleted: {
        // Populate data centrally
        for (var i = 0; i < 5; ++i) {
            TagModel.addEntry({
                "name": "Tag_" + i,
                "status": "active"
            });
        }

        // Verify access
        console.log("Total items:", TagModel.entryCount());
        var first = TagModel.getEntry(0);
        first["status"] = "updated";
        TagModel.updateEntry(0, first);
    }

    ListView {
        anchors.fill: parent
        model: TagModel
        delegate: Text {
            text: name + " (" + status + ")"
            color: status === "active" ? "green" : "gray"
        }
    }
}

Tags: Qt QML C++ singleton model-view

Posted on Sun, 05 Jul 2026 16:33:21 +0000 by rtconner