模型序列化的细节在某种程度上取决于模型的实现。一些问题包括:
-
完全可用的模型可能不会实现insertRows/insertColumns,而是更喜欢使用自定义方法。
-
QStandardItemModel 之类的模型可能具有不同类型的基础项。反序列化后,原型项目工厂将使用一种原型类型的克隆重新填充模型。为防止这种情况发生,必须为序列化公开项目的非类型标识符,并提供一种在反序列化时重建正确类型的项目的方法。
让我们看看标准项目模型的一种实现方式。原型多态项类可以通过数据角色公开其类型。设置此角色后,它应该使用正确的类型重新创建自己。
鉴于此,通用序列化程序是不可行的。
那么,让我们看一个完整的例子。给定模型类型所需的行为必须由对序列化程序进行参数化的特征类表示。从模型中读取数据的方法采用常量模型指针。修改模型的方法采用非常量模型指针,失败返回false。
// 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's configuration
typedef bool ModelConfig;
/// The streamable representation of an item's data
typedef QMap<int, QVariant> Roles;
/// The streamable representation of a section of model's header data
typedef Roles HeaderRoles;
/// Returns a streamable representation of an item's data.
Roles itemData(const Model * model, const QModelIndex & index) {
return model->itemData(index);
}
/// Sets the item's 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's 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's 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's configuration.
ModelConfig modelConfig(const Model *) {
return true;
}
/// Sets the model's configuration from the streamable representation.
bool setModelConfig(Model *, const ModelConfig &) {
return true;
}
};
必须实现这样的类来捕获特定模型的要求。上面给出的一个对于基本模型来说通常就足够了。序列化程序实例采用或默认构造特征类的实例。因此,特征可以具有状态。
在处理流和模型操作时,两者都可能失败。 Status 类捕获流和模型是否正常,以及是否可以继续。当IgnoreModelFailures 设置为初始状态时,traits 类报告的失败将被忽略,并且加载继续进行。 QDataStream 失败总是中止保存/加载。
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();
}
};
这个类也可以实现为将自身作为异常抛出,而不是返回false。这将使序列化程序代码更易于阅读,因为每个if (!st(...)) return st 惯用语都将被更简单的st(...) 替换。尽管如此,我还是选择不使用异常,因为典型的 Qt 代码不使用它们。要完全消除检测特征方法和流失败的语法开销,需要抛出特征方法而不是返回false,并使用失败时抛出的流包装器。
最后,我们有一个通用的序列化器,由一个特征类参数化。大多数模型操作都委托给特征类。直接在模型上执行的少数操作是:
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;
每个方向的标题都根据根项目的行/列计数进行序列化。
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;
}
每个项目的数据都是递归序列化的,按深度优先、列在行之前排序。任何项目都可以有孩子。项目标志未序列化;理想情况下,这种行为应该在特征中参数化。
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;
}
序列化器保留一个traits实例,也可以传一个来使用。
public:
ModelSerializer() {}
ModelSerializer(const Tr & traits) : m_traits(traits) {}
ModelSerializer(Tr && traits) : m_traits(std::move(traits)) {}
ModelSerializer(const ModelSerializer &) = default;
ModelSerializer(ModelSerializer &&) = default;
数据按以下顺序序列化:
- 模型配置,
- 模型数据,
- 水平标题数据,
- 垂直标题数据。
注意流和流数据的版本控制。
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;
}
};
使用基本特征保存/加载模型:
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());
}