Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

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:

    1. turn the internal QList<Message> into a QList<QSharedPointer<Message>>

    2. define a QMap<QString, QSharedPointer<Message>> to hold index ids to the individual messages

    3. when inserting, insert in the indexMap as well

    4. 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
    				}
    			}
    		}
    	}
    }