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
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()); }