【问题标题】:How to serialize a QAbstractItemModel into QDataStream?如何将 QAbstractItemModel 序列化为 QDataStream?
【发布时间】:2015-11-17 13:22:12
【问题描述】:

我已经设置了一个 QAbstractItemModel 并用数据填充了它。我的 QTreeView 小部件正确显示了该模型中的所有数据。

现在,我想将序列化的模型存储在二进制文件中(随后将该二进制文件加载回模型中)。这可能吗?

【问题讨论】:

  • 你的模型是可写的吗?如:您可以从一个空模型开始并使用 only QAbstractItemModel 方法来填充它吗?如果是这样,那么它是可能的。否则,它不是,除非反序列化直接作用于您的内部数据。
  • 是的。我用了这个例子:doc.qt.io/qt-5/…。 QAbstractModel 派生 TreeModel 在运行时由文件构建。我想保存那个树的状态。

标签: qt serialization qabstractitemmodel qstandarditemmodel qdatastream


【解决方案1】:

模型序列化的细节在某种程度上取决于模型的实现。一些问题包括:

  1. 完全可用的模型可能不会实现insertRows/insertColumns,而是更喜欢使用自定义方法。

  2. 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;

数据按以下顺序序列化:

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

【讨论】:

    【解决方案2】:

    与序列化任何东西的方式相同,只需实现一个运算符或方法,将每个数据成员按顺序写入数据流。

    最好的格式是为您的类型实现这两个运算符:

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

    遵循该模式将允许您的类型与 Qt 的容器类“即插即用”。

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

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

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

    【讨论】:

    • 如果模型是可写的,并且不会从内部表示中丢失任何数据,那么当然可以安全地序列化它。有很多像这样的有用模型,使用模型的工作量更少,并且可能使所有内部细节都可以安全地暴露给序列化,而不是单独处理每个内部表示。当然,我必须看看它在实践中的效果如何:)
    • @KubaOber - 这在很大程度上取决于项目模型结构。例如,从字面上看,我遇到的 100% 的模型使用示例(不是我自己的)都是由非常琐碎的项目结构组成的,项目具有少量数据成员的静态集合。但在我的工作中,我处理的恰恰相反——模型项是“类型化的”——它们具有不同的结构、不同的数据字段数量和类型。
    • 此外,我认为模型不应该是数据,模型只是为了驱动视图而访问数据的一种格式。这些应该是独立的设计层,彼此完全独立。这种方式设计很灵活,可以很容易地移植到不同的模型视图 API。这不是关于什么是可能的,而是关于正确的编程实践。同样,仅仅因为模型可用于存储数据并不意味着它应该。就像您可以将核心逻辑放入 GUI 类中一样,但您确实不应该这样做。
    • 因此,最好的办法是把数据抽象在自己的设计层,直接对数据进行序列化/反序列化。通过模型进行序列化并不总是一种选择(如果项目不是同构的),它也会更慢,并且整体上过于落后。在我的设计中,我什至没有沿类型本身实现实际的类型序列化,因为序列化本身的方法和数据格式可能会有所不同。我使用专用的序列化程序对象。我尽可能让核心逻辑与任何库和 API 分离。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-08-12
    • 1970-01-01
    • 1970-01-01
    • 2013-11-01
    • 2012-04-25
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多