Making QAbstractListModel items individually accessible
-
Hi again,
Writing a simple message list viewer, where the message has a title, a message text, and a unique identifier I faced a problem trying to make the individual messages stored in the QAbstractListModel derived MessageList accessible by their identifier from QML.
I tried to implement the QML invokable index operator on the list model which would perform a lookup on a map that's associating identifier strings to QList::iterator of the certain object, but that requires the Message class to derive from QObject, which by design is non-copyable and what happens if the list model changes, but the map is untouched and the iterators are corrupted? I have that feeling of doing it totally wrong again...
Could someone help me design a working structure to elegantly solve this kind of problem? The complete source code is attached below
main.qml:
import QtQuick 2.7 import QtQuick.Controls 2.0 import QtQuick.Layouts 1.0 ApplicationWindow { visible: true width: 640 height: 480 title: qsTr("Messages") Rectangle { id: menu color: Qt.rgba(0.9, 0.9, 0.9, 1) height: 32 anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right Text { anchors.left: parent.left anchors.margins: 8 anchors.verticalCenter: parent.verticalCenter //THIS DOESNT YET WORK //text: "at id 'a' there is: " + MessageList["a"].title } } ListView { model: MessageList anchors.top: menu.bottom anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom clip: true delegate: Rectangle { height: 32 Column { anchors.fill: parent anchors.margins: 8 spacing: 2 Text { text: title + " (" + identifier + ")" font.bold: true } Text { text: message } } } } }
MessageList.cpp:
#ifndef MESSAGELIST_HPP #define MESSAGELIST_HPP #include "Message.hpp" #include <QObject> #include <QAbstractListModel> #include <QModelIndex> #include <QList> #include <QHash> #include <QVariant> #include <QMap> class MessageList : public QAbstractListModel { Q_OBJECT public: enum Roles { IdentifierRole, TitleRole, MessageRole, }; typedef QMap<QString, QList<Message>::iterator> IdMap; protected: QList<Message> _list; IdMap _byId; public: MessageList(); int rowCount(const QModelIndex &parent) const; QHash<int, QByteArray> roleNames() const; QVariant data(const QModelIndex &index, int role) const; bool insert( const QList<Message>& messages, int position = 0 ); Q_INVOKABLE QVariant operator[](const QString& identifier) const; bool reset(); }; #endif // MESSAGELIST_HPP
MessageList.cpp:
#include "MessageList.hpp" #include <QObject> #include <QModelIndex> #include <QVariant> #include <QHash> #include <QByteArray> MessageList::MessageList() : QAbstractListModel(nullptr) { } int MessageList::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent) return _list.size(); } QHash<int, QByteArray> MessageList::roleNames() const { QHash<int, QByteArray> roles; roles[IdentifierRole] = "identifier"; roles[TitleRole] = "title"; roles[MessageRole] = "message"; return roles; } QVariant MessageList::data(const QModelIndex &index, int role) const { if(!index.isValid() || index.row() >= _list.size() || index.row() < 0 ) { return QVariant(); } switch(role) { case IdentifierRole: return _list.at(index.row()).identifier(); break; case TitleRole: return _list.at(index.row()).title(); break; case MessageRole: return _list.at(index.row()).message(); break; default: return QVariant(); } } bool MessageList::insert( const QList<Message>& messages, int position ) { beginInsertRows(QModelIndex(), position, position + messages.size() - 1); for(int row = 0; row < messages.size(); ++row) { _list.insert(position, messages.at(row)); QList<Message>::iterator listItr(_list.begin()); listItr += position; _byId.insert(messages.at(row).identifier(), listItr); } endInsertRows(); return true; } bool MessageList::reset() { beginResetModel(); _list.clear(); endResetModel(); return true; } QVariant MessageList::operator[](const QString& identifier) const { IdMap::const_iterator itr(_byId.find(identifier)); if(itr != _byId.constEnd()) { return QVariant(QVariant::fromValue(*(*itr))); } return QVariant(); }
Message:
#ifndef MESSAGE_HPP #define MESSAGE_HPP #include <QObject> #include <QString> class Message : public QObject { Q_OBJECT Q_PROPERTY(QString identifier READ identifier NOTIFY identifierChanged) Q_PROPERTY(QString title READ title NOTIFY titleChanged) Q_PROPERTY(QString message READ message NOTIFY messageChanged) protected: QString _identifier; QString _title; QString _message; public: Message(); Message( const QString& identifier, const QString& title, const QString& message ); const QString& identifier() const; const QString& title() const; const QString& message() const; signals: void identifierChanged(); void titleChanged(); void messageChanged(); }; Q_DECLARE_METATYPE(Message) #endif // MESSAGE_HPP
Message.cpp:
#include "Message.hpp" #include <QObject> #include <QString> Message::Message() : QObject(nullptr) { } Message::Message( const QString& identifier, const QString& title, const QString& message ) : QObject(nullptr), _identifier(identifier), _title(title), _message(message) { } const QString& Message::identifier() const { return _identifier; } const QString& Message::title() const { return _title; } const QString& Message::message() const { return _message; }
-
After hours of google, trial and error I'm proud to finally have solved it myself as I am still pretty new to Qt!
To help people with a similar problem I'll quickly sum up all I had to do to make it work:
-
turn the internal QList<Message> into a QList<QSharedPointer<Message>>
-
define a QMap<QString, QSharedPointer<Message>> to hold index ids to the individual messages
-
when inserting, insert in the indexMap as well
-
define a method in the QAbstractListModel derived MessageList which simply returns a QVariantMap
P.S. Below is the new working version of the example app, in case anyone notices any misconceptions, please let me know
MessageList.hpp:
#ifndef MESSAGELIST_HPP #define MESSAGELIST_HPP #include "Message.hpp" #include <QObject> #include <QAbstractListModel> #include <QModelIndex> #include <QList> #include <QHash> #include <QVariant> #include <QMap> #include <QSharedPointer> class MessageList : public QAbstractListModel { Q_OBJECT public: typedef QSharedPointer<Message> MessagePointer; typedef QList<MessagePointer> MessagePointerList; typedef QMap<QString, MessagePointer> IndexMap; enum Roles { IdentifierRole, TitleRole, MessageRole, }; protected: MessagePointerList _list; IndexMap _indexMap; public: MessageList(); int rowCount(const QModelIndex& parent) const; QHash<int, QByteArray> roleNames() const; QVariant data(const QModelIndex& index, int role) const; bool insert( const QList<Message>& messages, int position = 0 ); bool reset(); Q_INVOKABLE QVariantMap get(const QString& identifier) const; const Message& at(int index) const; }; #endif // MESSAGELIST_HPP
MessageList.cpp:
#include "MessageList.hpp" #include <QObject> #include <QModelIndex> #include <QVariant> #include <QHash> #include <QByteArray> MessageList::MessageList() : QAbstractListModel(nullptr) { } int MessageList::rowCount(const QModelIndex& parent) const { Q_UNUSED(parent) return _list.size(); } QHash<int, QByteArray> MessageList::roleNames() const { QHash<int, QByteArray> roles; roles[IdentifierRole] = "identifier"; roles[TitleRole] = "title"; roles[MessageRole] = "message"; return roles; } QVariant MessageList::data(const QModelIndex& index, int role) const { if(!index.isValid() || index.row() >= _list.size() || index.row() < 0 ) { return QVariant(); } switch(role) { case IdentifierRole: return _list.at(index.row())->identifier(); break; case TitleRole: return _list.at(index.row())->title(); break; case MessageRole: return _list.at(index.row())->message(); break; default: return QVariant(); } } bool MessageList::insert( const QList<Message>& messages, int position ) { beginInsertRows(QModelIndex(), position, position + messages.size() - 1); for(int row = 0; row < messages.size(); ++row) { IndexMap::const_iterator indexMapItr(_indexMap.constFind(messages.at(row).identifier())); if(indexMapItr == _indexMap.constEnd()) { MessagePointer newMessage(new Message(messages.at(row))); _list.insert(position, newMessage); _indexMap.insert(messages.at(row).identifier(), newMessage); } } endInsertRows(); return true; } bool MessageList::reset() { beginResetModel(); _list.clear(); __indexMap.clear(); endResetModel(); return true; } QVariantMap MessageList::get(const QString& identifier) const { QVariantMap result; IndexMap::const_iterator indexMapItr(_indexMap.constFind(identifier)); if(indexMapItr != _indexMap.constEnd()) { result["identifier"] = QVariant(indexMapItr->data()->identifier()); result["title"] = QVariant(indexMapItr->data()->title()); result["message"] = QVariant(indexMapItr->data()->message()); } return result; } const Message& MessageList::at(int index) const { return *(_list.at(index).data()); }
main.cpp:
#include <QGuiApplication> #include <QQmlApplicationEngine> #include <QQmlContext> #include "Message.hpp" #include "MessageList.hpp" int main(int argc, char *argv[]) { QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QGuiApplication app(argc, argv); MessageList messageList; messageList.insert(QList<Message> { Message("a", "first message", "this is a sample text message of the first message"), Message("b", "second message", "another sample text message of the second message"), Message("c", "third message", "yet a third text message sample"), Message("d", "fouth message", "last sample message") }); QQmlApplicationEngine engine; engine.rootContext()->setContextProperty("MessageList", &messageList); engine.load(QUrl(QLatin1String("qrc:/main.qml"))); return app.exec(); }
main.qml:
import QtQuick 2.7 import QtQuick.Controls 2.0 import QtQuick.Layouts 1.0 ApplicationWindow { visible: true width: 640 height: 480 title: qsTr("Messages") Rectangle { id: menu color: Qt.rgba(0.9, 0.9, 0.9, 1) height: 32 anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right Text { anchors.left: parent.left anchors.margins: 8 anchors.verticalCenter: parent.verticalCenter text: "at id 'b' there is: " + MessageList.get("b").title } } ListView { model: MessageList anchors.top: menu.bottom anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom clip: true delegate: Rectangle { height: 32 Column { anchors.fill: parent anchors.margins: 8 spacing: 2 Text { text: title + " (" + identifier + ")" font.bold: true } Text { text: message } } } } }
-