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"
}
}
}