Saving ListModel to file
-
Hello everyone,
I am looking for approaches to saving and loading ListModel to any kind of file. Currently I have a ListModel located in qml, which gets data from c++ signals as custom qml types.
I thought of moving ListModel to c++ by inheriting QAbstractListModel, but then I won't be able to write custom class objects to file using QDataStream because it doesn't allow classes inherited from QObject. My custom class needs to inherit QObject so I can register it as qml type and use it on qml side.
I am new to the subject, so the question may sound silly, but what are the approaches to solving my problem? -
Can you provide some code to explain the problem?
The answer generally is to save and restore the basic data types that make up the entries in the model, rather than the layout of the objects in memory. This might be through providing a function in the model that exports or imports state as a list. In cases where adding to the model implementation isn't possible, walking it with QAbstractItemModel::data() or ListMode.get() can work. For large data sets, fetching each entry in sequence can be painfully slow.
Going into more detail is easier with an implementation to discuss.
-
Thanks for reply!
Let me provide some code:
PacketData is a class that represents a single row in ListView:class PacketData : public QObject { Q_OBJECT QML_ELEMENT public: explicit PacketData(QObject *parent = nullptr); int number; QString sourceIp, destIp, protocol, length, fullData; QTime timestamp; Q_INVOKABLE int getNumber(){ return number; } Q_INVOKABLE QTime getTimestamp(){ return timestamp; } Q_INVOKABLE QString getSourceIp(){ return sourceIp; } Q_INVOKABLE QString getDestIp(){ return destIp; } Q_INVOKABLE QString getProtocol(){ return protocol; } Q_INVOKABLE QString getLength(){ return length; } Q_INVOKABLE QString getFullData() { return fullData; } };There's a class called SnifferWrapper that creates an object of PacketData and emits a signal packetDeserialized, I don't think that implementation of this class is important, I will just show how I emit and connect signal:
// signal: void packetDeserialized(PacketData *packet); ... // creating an object of PacketData: PacketData *packet = new PacketData(); // filling it with data ... // emitting signal emit packetDeserialized(packet); packet->deleteLater(); // calling deleteLater, because later in PacketsView I will copy members of this obect to listModel ... // somewhere in main.qml: SnifferWrapper{ id: snifferWrapper // connecting to addPacketToView slot onPacketDeserialized: { packetsView.addPacketToView(packet) } }Then I have PacketsView.qml file - here I create ListModel and ListView:
ListModel{ id: listModel } ... // This function acts as a slot for packetDeserialized signal: function addPacketToView(packet){ listModel.append({ number: packet.getNumber(), timestamp: Qt.formatTime(packet.getTimestamp(), "hh:mm:ss"), source: packet.getSourceIp(), destination: packet.getDestIp(), protocol: packet.getProtocol(), length: packet.getLength(), fullData: packet.getFullData() }); } ... // Later in code there's a ListView, but I don't think it's important for the topicThere's a naming mistake as function addPacketToView actually adds packet to model, please don't get confused.
How exactly should I implement model in c++ and then be able to save it? For example, if I have a class with a list:
QList<PacketData *> listI won't be able to use QDataStream because PacketData inherits QObject. Also if I make PacketData class not inherit QObject, I won't be able to register it as qml type and use it in qml.
I might be missing or not understanding something here. Also sorry for bad english
-
Seeing the code helps a lot. Unless there is a significant portion missing, doing it this way is adding code complexity and copying overhead without a benefit. In particular, there's no need for the intermediate packet QObject representation.
If this was a QML only project, using ListModel and copying data into it might be reasonable. As there's already a C++ portion, adding a QAbstractListModel shouldn't be difficult.
Here's a rough sketch of a C++ model. It won't compile, or perform even basic error checking, but will hopefully convey the general idea.
class PacketModel : public QAbstractListModel { Q_OBJECT QVector<packet> m_packets; enum class roles { NumberRole = Qt::UserRole, LengthRole }; public: struct packet { int number; int length; }; // Load packets into an existing model void restorePackets(QVector<packet> packets) { beginResetModel(); m_packets = packets; endResetModel(); } // Connect to a signal that delivers packet data, or directly invoked from the UI thread. void appendPacket(packet p) { beginInsertRows(QModelIndex(), packets.size(), packets.size()); m_packets.push_back(p); endInsertRows(); } // QAbstractListModel functions int QAbstractItemModel::rowCount(const QModelIndex &parent) const override { return m_packets.length(); } QVariant data(const QModelIndex &index, int role) override { auto packetsIndex = index.row(); const packet &p = m_packets.at(packetsIndex); switch (role) { case roles.NumberRole: return p.number; case roles.LengthRole; return p.length; default: return QAbstractListModel::data(index, role); } } QHash<int, QByteArray> QAbstractItemModel::roleNames() const override { return QHash<int, QByteArray> { {roles.NumberRole, "number"}, {roles.LengthRole, "length"} }; } };listview.qml:
ListView { model: packetModel delegate: Text { text: "packet number " + model.number + " with length " model.length } } -
Seeing the code helps a lot. Unless there is a significant portion missing, doing it this way is adding code complexity and copying overhead without a benefit. In particular, there's no need for the intermediate packet QObject representation.
If this was a QML only project, using ListModel and copying data into it might be reasonable. As there's already a C++ portion, adding a QAbstractListModel shouldn't be difficult.
Here's a rough sketch of a C++ model. It won't compile, or perform even basic error checking, but will hopefully convey the general idea.
class PacketModel : public QAbstractListModel { Q_OBJECT QVector<packet> m_packets; enum class roles { NumberRole = Qt::UserRole, LengthRole }; public: struct packet { int number; int length; }; // Load packets into an existing model void restorePackets(QVector<packet> packets) { beginResetModel(); m_packets = packets; endResetModel(); } // Connect to a signal that delivers packet data, or directly invoked from the UI thread. void appendPacket(packet p) { beginInsertRows(QModelIndex(), packets.size(), packets.size()); m_packets.push_back(p); endInsertRows(); } // QAbstractListModel functions int QAbstractItemModel::rowCount(const QModelIndex &parent) const override { return m_packets.length(); } QVariant data(const QModelIndex &index, int role) override { auto packetsIndex = index.row(); const packet &p = m_packets.at(packetsIndex); switch (role) { case roles.NumberRole: return p.number; case roles.LengthRole; return p.length; default: return QAbstractListModel::data(index, role); } } QHash<int, QByteArray> QAbstractItemModel::roleNames() const override { return QHash<int, QByteArray> { {roles.NumberRole, "number"}, {roles.LengthRole, "length"} }; } };listview.qml:
ListView { model: packetModel delegate: Text { text: "packet number " + model.number + " with length " model.length } }@jeremy_k
Thank you for such a detailed reply!
You definitely solved my problem. Let me leave minimal working code for someone who has same problem:Packet class:
class Packet { public: Packet(); int number, length; };PacketModel class:
#include <QAbstractListModel> #include <QObject> #include <QDataStream> #include <QFile> #include <packet.h> class PacketModel : public QAbstractListModel { Q_OBJECT QList<Packet> m_packets; enum roles { NumberRole = Qt::UserRole, LengthRole }; public: PacketModel(); int rowCount(const QModelIndex &parent) const { return m_packets.length(); } QVariant data(const QModelIndex &index, int role) const { auto packetsIndex = index.row(); const Packet &p = m_packets.at(packetsIndex); switch(role){ case NumberRole: return p.number; case LengthRole: return p.length; default: return data(index, role); } } QHash<int, QByteArray> roleNames() const{ return QHash<int, QByteArray> { { NumberRole, "number"}, { LengthRole, "length"} }; } Q_INVOKABLE void add(){ // Temporary method simulating insertion from other class Packet p; p.number = 1; p.length = 2; beginInsertRows(QModelIndex(), m_packets.size(), m_packets.size()); m_packets.push_back(p); endInsertRows(); } Q_INVOKABLE void saveModel(){ QFile file("snifff.txt"); file.open(QIODevice::WriteOnly); QDataStream stream(&file); for(auto i : m_packets){ stream << i.number << i.length; } file.close(); } Q_INVOKABLE void loadModel(){ QFile file("snifff.txt"); file.open(QIODevice::ReadOnly); QDataStream stream(&file); beginResetModel(); m_packets.clear(); while(!stream.atEnd()){ Packet p; stream >> p.number >> p.length; m_packets.push_back(p); } endResetModel(); file.close(); } };PacketModel class needs to be registered as qml type as well:
qmlRegisterType<PacketModel>("PacketModel", 1, 0, "PacketModel");Using model in qml:
import QtQuick 2.12 import QtQuick.Window 2.12 import QtQuick.Controls 2.12 import PacketModel 1.0 Window { width: 640 height: 480 visible: true title: qsTr("Hello World") ListView{ width: parent.width height: parent.height id: listView model: packetModel delegate: Text{ text: number + ", " + length } } PacketModel{ id: packetModel } Button { text: "Add" anchors.right: parent.right onClicked: { packetModel.add() } } Button { text: "Save" anchors.bottom: parent.bottom anchors.left: parent.left onClicked: { packetModel.saveModel() } } Button{ text: "Load" anchors.right: parent.right anchors.bottom: parent.bottom onClicked: { packetModel.loadModel() } } }