How to serialize QAbstractItemModel to QDataStream?

I created a QAbstractItemModel and populated it with data. My QTreeView widget displays all the data in this model properly.

Now I would like to save this model serialized in a binary file (and later loading the heap binary file back into the model). Is it possible?

+5
source share
2 answers

The specifications for serializing a model to some extent depend on the implementation of the model. Some fixes include:

  • Ideally used models may not implement insertRows / insertColumns , preferring to use their own methods instead.

  • Models of type QStandardItemModel can have basic elements of various types. After deserialization, the prototype factory element will re-populate the model with clones of the same prototype type. To avoid this, the type identifier item't should be open for serialization, as well as a way to reconfigure the element of the correct type during deserialization.

    See one of the ways to implement it for the standard element model. The prototype polymorphic class can expose its type through the data role. After installing this role, he must recreate himself with the correct type.

Given this, a universal serializer is not possible.

Let's look at a complete example. The behavior required for this type of model should be represented by a feature class that parameterizes the serializer. Methods for reading data from a model take a pointer to a constant model. Methods that modify the model take a pointer to a mutable model and return false on failure.

 // https://github.com/KubaO/stackoverflown/tree/master/questions/model-serialization-32176887 #include <QtGui> struct BasicTraits { BasicTraits() {} /// The base model that the serializer operates on typedef QAbstractItemModel Model; /// The streamable representation of model configuration typedef bool ModelConfig; /// The streamable representation of an item data typedef QMap<int, QVariant> Roles; /// The streamable representation of a section of model header data typedef Roles HeaderRoles; /// Returns a streamable representation of an item data. Roles itemData(const Model * model, const QModelIndex & index) { return model->itemData(index); } /// Sets the item data from the streamable representation. bool setItemData(Model * model, const QModelIndex & index, const Roles & data) { return model->setItemData(index, data); } /// Returns a streamable representation of a model header data. HeaderRoles headerData(const Model * model, int section, Qt::Orientation ori) { Roles data; data.insert(Qt::DisplayRole, model->headerData(section, ori)); return data; } /// Sets the model header data from the streamable representation. bool setHeaderData(Model * model, int section, Qt::Orientation ori, const HeaderRoles & data) { return model->setHeaderData(section, ori, data.value(Qt::DisplayRole)); } /// Should horizontal header data be serialized? bool doHorizontalHeaderData() const { return true; } /// Should vertical header data be serialized? bool doVerticalHeaderData() const { return false; } /// Sets the number of rows and columns for children on a given parent item. bool setRowsColumns(Model * model, const QModelIndex & parent, int rows, int columns) { bool rc = model->insertRows(0, rows, parent); if (columns > 1) rc = rc && model->insertColumns(1, columns-1, parent); return rc; } /// Returns a streamable representation of the model configuration. ModelConfig modelConfig(const Model *) { return true; } /// Sets the model configuration from the streamable representation. bool setModelConfig(Model *, const ModelConfig &) { return true; } }; 

Such a class must be implemented to collect the requirements of a particular model. One of the above is often enough for basic models. The serializer instance accepts or sets the default instance of the feature class. Thus, properties can have a state.

Streaming and model operations may fail. The Status class captures whether the stream and model are supported, and whether it can continue. When IgnoreModelFailures set to its initial state, errors reported by the feature class are ignored, and loading continues despite them. QDataStream always interrupt save / load.

 struct Status { enum SubStatus { StreamOk = 1, ModelOk = 2, IgnoreModelFailures = 4 }; QFlags<SubStatus> flags; Status(SubStatus s) : flags(StreamOk | ModelOk | s) {} Status() : flags(StreamOk | ModelOk) {} bool ok() const { return (flags & StreamOk && (flags & IgnoreModelFailures || flags & ModelOk)); } bool operator()(QDataStream & str) { return stream(str.status() == QDataStream::Ok); } bool operator()(Status s) { if (flags & StreamOk && ! (s.flags & StreamOk)) flags ^= StreamOk; if (flags & ModelOk && ! (s.flags & ModelOk)) flags ^= ModelOk; return ok(); } bool model(bool s) { if (flags & ModelOk && !s) flags ^= ModelOk; return ok(); } bool stream(bool s) { if (flags & StreamOk && !s) flags ^= StreamOk; return ok(); } }; 

This class can also be implemented to make itself an exception instead of returning false . This would make it easier to read the serializer code, since each idiom if (!st(...)) return st would be replaced with a simpler st(...) . However, I decided not to use exceptions, as typical Qt code does not use them. To completely remove the syntax overhead for detecting feature methods and thread failures, you would need to use feature methods instead of returning false and use a thread wrapper that throws a crash.

Finally, we have a common serializer parameterized by the feature class. Most model operations are delegated to the feature class. Several operations performed directly on the model are as follows:

  • bool hasChildren(parent)
  • int rowCount(parent)
  • int columnCount(parent)
  • QModelIndex index(row, column, parent)
 template <class Tr = BasicTraits> class ModelSerializer { enum ItemType { HasData = 1, HasChildren = 2 }; Q_DECLARE_FLAGS(ItemTypes, ItemType) Tr m_traits; 

The headers for each orientation are serialized based on the row / column counts of the root element.

  Status saveHeaders(QDataStream & s, const typename Tr::Model * model, int count, Qt::Orientation ori) { Status st; if (!st(s << (qint32)count)) return st; for (int i = 0; i < count; ++i) if (!st(s << m_traits.headerData(model, i, ori))) return st; return st; } Status loadHeaders(QDataStream & s, typename Tr::Model * model, Qt::Orientation ori, Status st) { qint32 count; if (!st(s >> count)) return st; for (qint32 i = 0; i < count; ++i) { typename Tr::HeaderRoles data; if (!st(s >> data)) return st; if (!st.model(m_traits.setHeaderData(model, i, ori, data))) return st; } return st; } 

The data for each element is serialized recursively, ordered by depth, columns before rows. Any item can have children. Element flags are not serialized; ideally, this behavior should be parameterized in features.

  Status saveData(QDataStream & s, const typename Tr::Model * model, const QModelIndex & parent) { Status st; ItemTypes types; if (parent.isValid()) types |= HasData; if (model->hasChildren(parent)) types |= HasChildren; if (!st(s << (quint8)types)) return st; if (types & HasData) s << m_traits.itemData(model, parent); if (! (types & HasChildren)) return st; auto rows = model->rowCount(parent); auto columns = model->columnCount(parent); if (!st(s << (qint32)rows << (qint32)columns)) return st; for (int i = 0; i < rows; ++i) for (int j = 0; j < columns; ++j) if (!st(saveData(s, model, model->index(i, j, parent)))) return st; return st; } Status loadData(QDataStream & s, typename Tr::Model * model, const QModelIndex & parent, Status st) { quint8 rawTypes; if (!st(s >> rawTypes)) return st; ItemTypes types { rawTypes }; if (types & HasData) { typename Tr::Roles data; if (!st(s >> data)) return st; if (!st.model(m_traits.setItemData(model, parent, data))) return st; } if (! (types & HasChildren)) return st; qint32 rows, columns; if (!st(s >> rows >> columns)) return st; if (!st.model(m_traits.setRowsColumns(model, parent, rows, columns))) return st; for (int i = 0; i < rows; ++i) for (int j = 0; j < columns; ++j) if (!st(loadData(s, model, model->index(i, j, parent), st))) return st; return st; } 

The serializer stores an instance of the attributes, it can also be passed for use.

 public: ModelSerializer() {} ModelSerializer(const Tr & traits) : m_traits(traits) {} ModelSerializer(Tr && traits) : m_traits(std::move(traits)) {} ModelSerializer(const ModelSerializer &) = default; ModelSerializer(ModelSerializer &&) = default; 

Data is serialized in the following order:

  • model configuration
  • model data
  • horizontal header data
  • vertical header data.

Attention is given to version control of both streaming and streaming data.

  Status save(QDataStream & stream, const typename Tr::Model * model) { Status st; auto version = stream.version(); stream.setVersion(QDataStream::Qt_5_4); if (!st(stream << (quint8)0)) return st; // format if (!st(stream << m_traits.modelConfig(model))) return st; if (!st(saveData(stream, model, QModelIndex()))) return st; auto hor = m_traits.doHorizontalHeaderData(); if (!st(stream << hor)) return st; if (hor && !st(saveHeaders(stream, model, model->rowCount(), Qt::Horizontal))) return st; auto ver = m_traits.doVerticalHeaderData(); if (!st(stream << ver)) return st; if (ver && !st(saveHeaders(stream, model, model->columnCount(), Qt::Vertical))) return st; stream.setVersion(version); return st; } Status load(QDataStream & stream, typename Tr::Model * model, Status st = Status()) { auto version = stream.version(); stream.setVersion(QDataStream::Qt_5_4); quint8 format; if (!st(stream >> format)) return st; if (!st.stream(format == 0)) return st; typename Tr::ModelConfig config; if (!st(stream >> config)) return st; if (!st.model(m_traits.setModelConfig(model, config))) return st; if (!st(loadData(stream, model, QModelIndex(), st))) return st; bool hor; if (!st(stream >> hor)) return st; if (hor && !st(loadHeaders(stream, model, Qt::Horizontal, st))) return st; bool ver; if (!st(stream >> ver)) return st; if (ver && !st(loadHeaders(stream, model, Qt::Vertical, st))) return st; stream.setVersion(version); return st; } }; 

To save / load the model using the main features:

 int main(int argc, char ** argv) { QCoreApplication app{argc, argv}; QStringList srcData; for (int i = 0; i < 1000; ++i) srcData << QString::number(i); QStringListModel src {srcData}, dst; ModelSerializer<> ser; QByteArray buffer; QDataStream sout(&buffer, QIODevice::WriteOnly); ser.save(sout, &src); QDataStream sin(buffer); ser.load(sin, &dst); Q_ASSERT(srcData == dst.stringList()); } 
+1
source

Just as you serialize something, just implement an operator or method that writes each data item in sequence to the data stream.

The preferred format is to implement these two statements for your types:

 QDataStream &operator<<(QDataStream &out, const YourType &t); QDataStream &operator>>(QDataStream &in, YourType &t); 

After this template, your types will be โ€œplugged in and played backโ€ using the Qt container classes.

QAbstractItemModel does not contain (or should not) directly store data, it is just a shell of the underlying data structure. The model is intended only to provide an interface for accessing data. Therefore, in fact, you should not serialize the actual model, but the data underlying it.

How to serialize the actual data, it depends on the format of your data, which at the moment remains a mystery. But since this is a QAbstractItemModel , I assume this is some kind of tree, so generally speaking, you need to cross the tree and serialize every object in it.

Note that when serializing a single object, serialization and deserialization are a blind sequence, but when working with a collection of objects, you may need to consider its structure with additional serialization data. If your tree is something like an array of arrays while you use the Qt container classes, this will take care of you, you only need to implement serialization for the item type, but for the custom tree you will have to do it yourself.

0
source

All Articles