6

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

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

4

2 回答 2

2

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

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

序列化器保留一个特征实例,也可以传一个来使用。

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());
}
于 2015-09-10T05:26:03.227 回答
0

The same way you serialize anything, just implement an operator or method which writes each data member to a data stream in sequence.

The preferable format is to implement those two operators for your types:

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

Following that pattern will allow your types to be "plug and play" with Qt's container classes.

QAbstractItemModel does not (or should not) directly hold the data, it is just a wrapper to an underlying data structure. The model only serves to provide an interface for a view to access the data. So in reality you shouldn't serialize the actual model, but the underlying data.

As of how to serialize the actual data, it depends on the format of your data, which as of now remains a mystery. But since it is a QAbstractItemModel I assume it is a tree of some sort, so generally speaking, you have to traverse the tree and serialize every object in it.

Make a note that when serializing a single object, the serialization and deserialization are a blind sequence, but when dealing with a collection of objects, you may have to account for its structure with extra serialization data. If your tree is something like an array of arrays, as long as you use Qt's container classes this will be taken care of for you, all you will need is to implement the serialization for the item type, but for a custom tree you will have to do it yourself.

于 2015-08-31T06:47:22.340 回答