如何将QAbstractItemModel序列化成QDataStream?

6

我已经创建了一个QAbstractItemModel,并且填充了数据。我的QTreeView小部件正确地显示了该模型中的每个数据。

现在,我想将该模型序列化到二进制文件中(稍后当然可以将该二进制文件加载回模型)。这可行吗?


你的模型是否可写?也就是说:你能够从一个空模型开始,仅使用 QAbstractItemModel 方法将其填充吗?如果可以,那么是可能的。否则,除非序列化/反序列化直接作用于您的内部数据,否则不可能。 - Kuba hasn't forgotten Monica
是的,我使用了这个例子:http://doc.qt.io/qt-5/qtwidgets-itemviews-editabletreemodel-example.html。在那里,QAbstractModel衍生的TreeModel在运行时通过文件进行构建。我想保存该树的状态。 - Ralf Wickum
2个回答

2
模型序列化的具体实现取决于模型的实现方式。一些需要注意的问题包括:
  • 某些可用模型可能没有实现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;
    }
};

必须实现这样一个类来满足特定模型的要求。上面给出的类通常对于基本模型已经足够。序列化器实例接受或默认构造特征类的实例。因此,特征可以具有状态。

在处理流和模型操作时,任何一方都可能失败。一个状态类捕获流和模型是否正常以及是否可以继续操作。当在初始状态中设置了忽略模型故障时,特征类报告的故障将被忽略,并且加载将继续进行。 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;

数据按以下顺序进行序列化:
  1. 模型配置,
  2. 模型数据,
  3. 水平标题数据,
  4. 垂直标题数据。
注意对流和流数据的版本进行版本控制。
    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());
}

0
与任何序列化操作相同,只需实现一种运算符或方法,按顺序将每个数据成员写入数据流中。
最好的方式是为您的类型实现这两个运算符:
QDataStream &operator<<(QDataStream &out, const YourType &t);
QDataStream &operator>>(QDataStream &in, YourType &t);

按照这种模式,您的类型将能够与Qt的容器类“即插即用”。

QAbstractItemModel不会(或不应该)直接持有数据,它只是底层数据结构的包装器。该模型仅用于为视图提供访问数据的接口。因此,实际上您不应序列化实际模型,而是底层数据。

关于如何序列化实际数据,这取决于您的数据格式,目前仍然是一个谜。但由于它是QAbstractItemModel,我假设它是某种树形结构,因此一般来说,您必须遍历树并序列化其中的每个对象。

请注意,当序列化单个对象时,序列化和反序列化是一个盲目的过程,但处理对象集合时,您可能需要考虑其结构并使用额外的序列化数据。如果您的树形结构类似于数组的数组,只要使用Qt的容器类,这将为您处理,您只需要实现项目类型的序列化,但对于自定义树形结构,您必须自己完成。


如果模型是可写的,并且不会从内部表示中丢失任何数据,那么肯定可以安全地序列化它。有很多这样有用的模型存在,与其单独处理每个内部表示,倒不如使用模型并可能使所有内部细节都能够安全地暴露给序列化。当然,我必须看看它在实践中的表现如何 :) - Kuba hasn't forgotten Monica
@KubaOber - 这在很大程度上取决于项目模型的结构。例如,我遇到的所有模型使用示例(除了我的模型)都是由一些非常琐碎的项结构组成的,这些项具有一组静态数据成员。但在我的工作中,我处理的恰好相反 - 模型项是“类型化”的 - 它们具有不同的结构、不同数量和类型的数据字段。 - dtech
此外,我认为模型不应该是数据,模型只是一种访问数据的格式,以驱动视图。这些应该是独立的设计层,彼此完全独立。这样设计就具有灵活性,并且可以轻松地移植到不同的模型-视图API中。这不是关于可能性,而是关于正确的编程实践。同样,仅仅因为模型可以用来存储数据并不意味着它应该这样做。就像你可以把核心逻辑放到GUI类中一样,但你真的不应该这样做。 - dtech
因此,最好的方法是将数据抽象到其自己的设计层中,并直接序列化/反序列化数据。通过模型进行序列化并不总是一个选项(在某些情况下,项目不是同构的),它也会更慢,并且过于向后兼容。在我的设计中,我甚至不实现实际类型序列化,因为序列化本身可以根据其方法和数据格式而变化。我使用专用的序列化器对象。我尽可能将核心逻辑与任何库和API分离。 - dtech

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接