Как сериализовать QAbstractItemModel в QDataStream?

Я настроил QAbstractItemModel и заполнил ее данными. Мой виджет QTreeView правильно отображает все данные в этой модели.

Теперь я хотел бы сохранить эту модель, сериализованную в двоичном файле (и позже, конечно, загрузить этот двоичный файл обратно в модель). Это возможно?


person Ralf Wickum    schedule 24.08.2015    source источник
comment
Ваша модель доступна для записи? Например: можете ли вы начать с пустой модели и использовать только QAbstractItemModel методы для ее заполнения? Если так, то это возможно. В противном случае это не так, если только десериализация не работает непосредственно с вашими внутренними данными.   -  person Kuba hasn't forgotten Monica    schedule 02.09.2015


Ответы (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, сбои, о которых сообщает класс свойств, игнорируются, и загрузка продолжается, несмотря на них. 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());
}
person Kuba hasn't forgotten Monica    schedule 10.09.2015

Точно так же, как вы сериализуете что-либо, просто реализуйте оператор или метод, который последовательно записывает каждый член данных в поток данных.

Предпочтительным форматом является реализация этих двух операторов для ваших типов:

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

Следование этому шаблону позволит вашим типам быть "подключи и работай" с классами контейнеров Qt.

QAbstractItemModel не содержит (или не должен) напрямую хранить данные, это просто оболочка базовой структуры данных. Модель служит только для предоставления интерфейса для доступа к данным. Так что на самом деле вы должны сериализовать не фактическую модель, а базовые данные.

Что касается того, как сериализовать фактические данные, это зависит от формата ваших данных, который на данный момент остается загадкой. Но поскольку это QAbstractItemModel, я предполагаю, что это какое-то дерево, поэтому, вообще говоря, вам нужно пройти по дереву и сериализовать каждый объект в нем.

Обратите внимание, что при сериализации одного объекта сериализация и десериализация являются слепой последовательностью, но при работе с набором объектов вам, возможно, придется учитывать его структуру с помощью дополнительных данных сериализации. Если ваше дерево представляет собой что-то вроде массива массивов, пока вы используете классы контейнеров Qt, об этом позаботятся за вас, все, что вам нужно, это реализовать сериализацию для типа элемента, но для пользовательского дерева у вас будет сделать это самостоятельно.

person dtech    schedule 31.08.2015
comment
Если модель доступна для записи и не теряет никаких данных из внутреннего представления, то, безусловно, ее можно безопасно сериализовать. Существует так много полезных моделей, подобных этой, что меньше работы с моделью и, возможно, сделать так, чтобы все внутренние детали можно было безопасно подвергнуть сериализации, а не работать с каждым внутренним представлением по отдельности. Конечно, я должен увидеть, насколько хорошо это будет работать на практике :) - person Kuba hasn't forgotten Monica; 02.09.2015
comment
@KubaOber - это во многом зависит от структуры модели предмета. Например, буквально 100% примеров использования моделей, с которыми я столкнулся (которые не были моими собственными), состоят из действительно тривиальной структуры элементов, элементов со статическим набором из нескольких элементов данных. Но в своей работе я имею дело с полной противоположностью — элементы модели типизированы — у них разная структура, разное количество и тип полей данных. - person dtech; 02.09.2015
comment
Кроме того, я не думаю, что модель должна быть данными, модель — это просто формат доступа к данным с целью управления представлением. Это должны быть отдельные слои дизайна, полностью независимые друг от друга. Таким образом, дизайн является гибким и может быть легко перенесен на другую модель-представление API. Дело не в том, что возможно, а в правильной практике программирования. Точно так же только потому, что модель может использоваться для хранения данных, не означает, что она должна использоваться. Точно так же вы могли бы поместить основную логику в классы GUI, но на самом деле не должны этого делать. - person dtech; 02.09.2015
comment
Таким образом, лучший подход — хранить данные абстрагированно на своем собственном уровне проектирования и напрямую сериализовать/десериализовать данные. Сериализация через модель не всегда возможна (в случаях, когда элементы не изоморфны), она также будет медленнее и в целом слишком отсталой. В моем дизайне я даже не реализую фактическую сериализацию типов по самому типу, поскольку сама сериализация может различаться по своему подходу и формату данных. Я использую специальные объекты сериализатора. Я держу основную логику максимально отделенной от какой-либо библиотеки и API. - person dtech; 02.09.2015