【问题标题】:How do I properly serialize and deserialize a QList class in QT using QDatastream?如何使用 QDatastream 在 QT 中正确序列化和反序列化 QList 类?
【发布时间】:2018-12-26 10:57:25
【问题描述】:

我正在尝试序列化自定义类 Layer* 并使用 QDataStream 将其读回。现在,Layer 是一个带有虚方法的抽象类,被不同类型的层继承:RasterLayerTextLayerAdjustmentLayer 等。

我有一个QList<Layer*> layers,它跟踪所有图层,对图层所做的任何调整都会在列表中更新。我需要将 QList 序列化和反序列化为其原始状态,并恢复各个层(不同类型)的属性。

这里是layer.h

#ifndef LAYER_H
#define LAYER_H

#include <QString>
#include <QImage>
#include <QDebug>
#include <QListWidgetItem>
#include <QGraphicsItem>
#include <QPixmap>

class Layer : public QListWidgetItem
{

public:

    enum LayerType{
        RASTER,
        VECTOR,
        TEXT,
        ADJUSTMENT
    };

    Layer(QString name, LayerType type);

    ~Layer();
    inline void setName(QString &name) { _name = name; }
    inline QString getName() { return _name; }
    inline LayerType getType() { return _type; }

    virtual void setSceneSelected(bool select) = 0;
    virtual void setLayerSelected(bool select) = 0;
    virtual void setZvalue(int z) = 0;
    virtual void setParent(QGraphicsItem *parent) = 0;

protected:
    QString _name;
    LayerType _type;
};

#endif // LAYER_H

这是由 RasterLayer 类扩展的:

#ifndef RASTERLAYER_H
#define RASTERLAYER_H

#include <QGraphicsPixmapItem>
#include <QPainter>
#include <QGraphicsScene>

#include "layer.h"

    class RasterLayer : public Layer, public QGraphicsPixmapItem
    {
    public:
        RasterLayer(const QString &name, const QImage &image);
        RasterLayer();
        ~RasterLayer();

        void setLocked(bool lock);
        void setSceneSelected(bool select);
        void setLayerSelected(bool select);
        void setZvalue(int z);
        void setParent(QGraphicsItem *parent);
        inline QPixmap getPixmap() const { return pixmap(); }
        inline QPointF getPos() const { return QGraphicsPixmapItem::pos(); }
        inline void setLayerPos(QPointF pos) { setPos(pos);}
        inline void setLayerPixmap(QPixmap pixmap) { setPixmap(pixmap); }

        friend QDataStream& operator<<(QDataStream& ds, RasterLayer *&layer)
        {
            ds << layer->getPixmap() << layer->getName() << layer->getPos();
            return ds;
        }

        friend QDataStream& operator>>(QDataStream& ds, RasterLayer *layer)
        {
            QString name;
            QPixmap pixmap;
            QPointF pos;

            ds >> pixmap >> name >> pos;

            layer->setName(name);
            layer->setPixmap(pixmap);
            layer->setPos(pos);

            return ds;
        }

    protected:
        void paint(QPainter *painter,
                   const QStyleOptionGraphicsItem *option,
                   QWidget *widget);

    private:
        QImage _image;
    };

    #endif // RASTERLAYER_H

我目前正在尝试像这样测试RasterLayer 的序列化反序列化:

QFile file(fileName);

file.open(QIODevice::WriteOnly);
QDataStream out(&file);

Layer *layer = paintWidget->getItems().at(1);
// Gets the second element in the list

RasterLayer *raster = dynamic_cast<RasterLayer*> (layer);
out << raster;
file.close();

现在,正如您在此处看到的那样,我专门将 Layer* 转换为 RasterLayer* 以进行序列化,这很有效,因为到目前为止我只处理了一种类型的层。所以我的第一个问题是:

如何将此序列化过程推广到所有类型的层?

每种类型的层都有不同的序列化方式,因为每个层都有不同的属性。此外,这里的铸造感觉有点代码味道,可能是一个糟糕的设计选择。因此,将整个层列表序列化并调用其相应的重载运算符将是预期的场景。

我的第二个问题是:

如何正确反序列化数据? 以下是我目前如何序列化个人RasterLayer

QFile newFile(fileName);
newFile.open(QIODevice::ReadOnly);
QDataStream in(&newFile);

RasterLayer *layer2 = new RasterLayer;
in >> layer2;
paintWidget->pushLayer(layer2);
ui->layerView->updateItems(paintWidget->getItems());

首先,在这种情况下,我不认为序列化指针是我应该做的事情,但我不确定还能做什么或如何做得更好。其次,反序列化在这里有效,但它并没有完全达到我期望的效果。尽管我在重载运算符中使用了设置器,但实际上并没有正确更新图层。我需要调用构造函数来创建一个新层。

我试过这个:Serialization with Qt,但我不太确定如何让Layer* 将其转换为Layer,对其进行序列化、反序列化,然后再将其转换回Layer*。 所以我需要添加第三步:

RasterLayer *layer3 = new RasterLayer(layer2->getName(), layer2->getPixmap().toImage());
layer3->setPos(layer2->pos());

然后将layer3 推送到列表中以使其真正起作用。根据这篇文章:https://stackoverflow.com/a/23697747/6109408,我真的不应该在运算符重载函数中执行new RasterLayer...(否则我会在地狱中煎熬),我正在遵循那里给出的第一个建议,这不是很好在我的情况下工作很多,我不知道正确的方法。

另外,我如何对Layer*s 的一般 QList 进行反序列化,而不必创建新的特定层实例并将反序列化数据注入它们?虽然这很相似:Serialize a class with a Qlist of custom classes as member (using QDataStream),但答案不够清楚,我无法理解。

我有一个关于中间值持有者类的想法,我将使用它来序列化各种层,并让它根据层的类型创建和注入参数,但我不确定这是否可行.

感谢您帮助我。

【问题讨论】:

    标签: c++ qt serialization qdatastream


    【解决方案1】:

    我希望下面的例子能给你一个大致的概念:

    #include <iostream>
    #include <fstream>
    #include <list>
    
    class A{
        int a=0;
    public:
        virtual int type(){return 0;}
        virtual void serialize(std::ostream& stream)const{
            stream<<a<<std::endl;
        }
        virtual void deserialize(std::istream& stream){
            stream>>a;
        }
    
        friend std::ostream& operator <<(std::ostream& stream, const A& object){
            object.serialize(stream);
            return stream;
        }
        friend std::istream& operator >>(std::istream& stream, A& object){
            object.deserialize(stream);
            return stream;
        }
    
        virtual ~A(){}
    };
    
    class B : public A{
      int b=1;
    public:
      virtual int type(){return 1;}
      virtual void serialize(std::ostream& stream)const{
          A::serialize(stream);
          stream<<b<<std::endl;
      }
      virtual void deserialize(std::istream& stream){
          A::deserialize(stream);
          stream>>b;
      }
    };
    
    class C : public A{
      int c=2;
    public:
      virtual int type(){return 2;}
      virtual void serialize(std::ostream& stream)const{
          A::serialize(stream);
          stream<<c<<std::endl;
      }
      virtual void deserialize(std::istream& stream){
          A::deserialize(stream);
          stream>>c;
      }
    };
    
    std::ostream& operator <<(std::ostream& stream, const std::list<A*>& l){
        stream<<l.size()<<std::endl;
        for(auto& a_ptr: l){
            stream<<a_ptr->type()<<std::endl;
            stream<<*a_ptr;
        }
    }
    std::istream& operator >>(std::istream& stream, std::list<A*>& l){
        l.clear();
        int size, type;
        stream>>size;
        A* tmp;
        for(int i =0; i<size; ++i){
            stream>>type;
            if(type==0){
               tmp = new A;
            }
            if(type==1){
               tmp = new B;
            }
            if(type==2){
               tmp = new C;
            }
            stream>>(*tmp);
            l.push_back(tmp);
        }
        return stream;
    }
    
    
    int main(){
        A* a = new A;
        A* b = new B;
        A* c = new C;
        std::list<A*> List{ a, b, c };
        std::list<A*> List2;
        std::ofstream ofs("D:\\temp.txt");
        ofs<<List;
        ofs.flush();
        ofs.close();
    
        std::ifstream ifs("D:\\temp.txt");
        ifs>>List2;
        std::cout<<List2;
        for(auto& a_ptr : List2){
            delete a_ptr;
        }
        delete c;
        delete b;
        delete a;
        return 0;
    }
    

    编辑:在我没有考虑到序列化列表时我们应该编写列表大小和元素类型以成功反序列化的事实,所以我修改了示例。

    【讨论】:

    • 您好,感谢您的意见。你能再详细一点吗?我还需要一种正确反序列化数据的方法。请多解释一点,以便更清楚一点。另外,我想根据我可以读取的序列化数据创建一个新列表。如果你能给我一个例子,那就太好了。
    • 您在基类中定义 > 运算符。运算符使用虚函数进行序列化和反序列化。然后在派生类中提供这些函数的实现(如果需要)。因此,您可以在派生类上使用 >。 (如果我是正确的,编译器将执行转换为基类并自行推断出正确的序列化/反序列化函数)
    • 这看起来是个不错的处理方法,我会尝试并回复您。谢谢你。 :D
    • @2dsharp 我修改了我的答案(添加了反序列化)。起初我没有考虑序列化时应该存储列表大小和元素类型的事实。
    • 非常感谢,这段代码实际上可以正常工作,而无需我进行很多更改。我所要做的就是重载 QList&lt;Layer*&gt; 的运算符并进行迭代。
    【解决方案2】:

    满足您的需求:典型的实现方式是利用多态性。

    基类(QListWidgetItem)有一个接口来执行序列化和反序列化。我们可以利用它来实现(反)序列化指向派生类型的指针。序列化调用在派生类中实现的接口来序列化特定于派生的数据。反序列化首先使用特定类型的工厂来创建派生类型的实例,然后才调用在派生类中实现的反序列化接口 - 使用基类的运算符。

    一旦实现了基类型的序列化和反序列化,QListQVariant (!) 应该也能正常工作。

    您不应该实现自己的类型存储 - QListWidgetItem 已经为您提供了它!

    Layer 类是派生自 QGraphicsItem 的类的抽象基础。 typeId()typeName() 方法利用元类型类型系统。派生类应该将typeId(不是type()!)传递给Layer的构造函数。

    // https://github.com/KubaO/stackoverflown/tree/master/questions/stream-qwidgetlistitem-51403419
    #include <QtWidgets>
    
    class Layer : public QListWidgetItem {
    public:
       virtual QGraphicsItem *it() = 0;
       const QGraphicsItem *it() const { return const_cast<Layer*>(this)->it(); }
       int typeId() const {
          if (type() < UserType)
             return QMetaType::UnknownType;
          return type() - QListWidgetItem::UserType + QMetaType::User;
       }
       const char *typeName() const { return QMetaType::typeName(typeId()); }
       void write(QDataStream&) const override;
       void read(QDataStream&) override;
       QListWidgetItem *clone() const override final;
    
       void setZValue(int z) { it()->setZValue(z); }
       void setParentItem(Layer *parent) { it()->setParentItem(parent->it()); }
       void setParentItem(QGraphicsItem *parent) { it()->setParentItem(parent); }
       void setSelected(bool sel) { it()->setSelected(sel); }
       void setPos(const QPointF &pos) { it()->setPos(pos); }
    
       Layer(const Layer &);
       QString name() const { return m_name; }
       void setName(const QString &n) { m_name = n; }
       ~Layer() override = default;
    protected:
       using Format = quint8;
       Layer(const QString &name, int typeId);
       static void invalidFormat(QDataStream &);
       template <typename T> T &assign(const T& o) { return static_cast<T&>(assignLayer(o)); }
    private:
       QString m_name;
       Layer& assignLayer(const Layer &);
    };
    

    it() 助手提供对派生的QGraphicsItem* 类型的访问。实现的基础比较简单。

    Layer::Layer(const Layer &o) : Layer(o.name(), o.typeId()) {}
    
    Layer::Layer(const QString &name, int typeId) :
       QListWidgetItem(nullptr, typeId - QMetaType::User + QListWidgetItem::UserType),
       m_name(name)
    {}
    
    QListWidgetItem *Layer::clone() const {
       const QMetaType mt(typeId());
       Q_ASSERT(mt.isValid());
       return reinterpret_cast<QListWidgetItem*>(mt.create(this));
    }
    
    Layer &Layer::assignLayer(const Layer &o) {
       Q_ASSERT(o.type() == type());
       const QMetaType mt(typeId());
       Q_ASSERT(mt.isValid());
       this->~Layer();
       mt.construct(this, &o);
       return *this;
    }
    

    对数据进行版本控制以确保向后兼容很重要:较新版本的软件应该能够读取旧版本写入的数据。因此,每个类都维护自己的格式指示符。这将Layer 类的格式与派生类的格式分离。数据类型保存为文本,以确保在可能更改类型 ID 的情况下具有可移植性。

    void Layer::write(QDataStream &ds) const {
       ds << typeName() << (Format)0 << m_name << it()->pos();
       QListWidgetItem::write(ds);
    }
    
    void Layer::read(QDataStream &ds) {
       QByteArray typeName_;
       Format format_;
       QPointF pos_;
       ds >> typeName_ >> format_;
       if (typeName_.endsWith('\0')) typeName_.chop(1);
       Q_ASSERT(typeName_ == typeName());
       if (format_ >= 0) {
          ds >> m_name >> pos_;
          setPos(pos_);
          QListWidgetItem::read(ds);
       }
       if (format_ >= 1)
          invalidFormat(ds);
    }
    
    void Layer::invalidFormat(QDataStream &ds) {
       ds.setStatus(QDataStream::ReadCorruptData);
    }
    

    Qt 已经为QListWidgetItem 的引用提供了流操作符。我们需要提供流操作符来处理指向该类型的指针。输出运算符立即转发给引用输出运算符。输入运算符查看存储在流中的对象的类型,使用该类型查找元类型 id,并使用QMetaType::create() 对其进行实例化。然后,它转发给引用输入操作符。

    QDataStream &operator<<(QDataStream &ds, const Layer *l) {
       return ds << *l;
    }
    
    QByteArray peekByteArray(QDataStream &ds) {
       qint32 size;
       auto read = ds.device()->peek(reinterpret_cast<char*>(&size), sizeof(size));
       if (read != sizeof(size))
          return ds.setStatus(QDataStream::ReadPastEnd), QByteArray();
       if (ds.byteOrder() == QDataStream::BigEndian)
          size = qFromBigEndian(size);
       auto buf = ds.device()->peek(size + 4);
       if (buf.size() != size + 4)
          return ds.setStatus(QDataStream::ReadPastEnd), QByteArray();
       if (buf.endsWith('\0')) buf.chop(1);
       return buf.mid(4);
    }
    
    QDataStream &operator>>(QDataStream &ds, Layer *&l) {
       auto typeName = peekByteArray(ds);
       int typeId = QMetaType::type(typeName);
       QMetaType mt(typeId);
       l = mt.isValid() ? reinterpret_cast<Layer*>(mt.create()) : nullptr;
       if (l)
          ds >> *l;
       else
          ds.setStatus(QDataStream::ReadCorruptData);
       return ds;
    }
    

    一旦Layer抽象基类设置好了,实现派生类就很简单了:

    class RasterLayer : public Layer, public QGraphicsPixmapItem {
    public:
       QGraphicsItem *it() override { return this; }
       int type() const override { return Layer::type(); }
       RasterLayer &operator=(const RasterLayer &o) { return assign(o); }
       void write(QDataStream &) const override;
       void read(QDataStream &) override;
       RasterLayer(const RasterLayer &);
       RasterLayer(const QString &name = {});
    };
    Q_DECLARE_METATYPE(RasterLayer)
    
    // implementation
    
    static int rasterOps = qRegisterMetaTypeStreamOperators<RasterLayer>();
    
    RasterLayer::RasterLayer(const RasterLayer &o) :
       Layer(o),
       QGraphicsPixmapItem(o.pixmap())
    {}
    
    RasterLayer::RasterLayer(const QString &name) : Layer(name, qMetaTypeId<RasterLayer>()) {}
    
    void RasterLayer::write(QDataStream &ds) const {
       Layer::write(ds);
       ds << Format(0) << pixmap();
    }
    
    void RasterLayer::read(QDataStream &ds) {
       Layer::read(ds);
       Format format_;
       QPixmap pix_;
       ds >> format_;
       if (format_ >= 0) {
          ds >> pix_;
          setPixmap(pix_);
       }
       if (format_ >= 1)
          invalidFormat(ds);
    }
    

    同样:

    class VectorLayer : public Layer, public QGraphicsPathItem {
    public:
       QGraphicsItem *it() override { return this; }
       int type() const override { return Layer::type(); }
       VectorLayer &operator=(const VectorLayer &o) { return assign(o); }
       void write(QDataStream &) const override;
       void read(QDataStream &) override;
       VectorLayer(const VectorLayer &);
       VectorLayer(const QString &name = {});
    };
    Q_DECLARE_METATYPE(VectorLayer)
    
    // implementation
    
    static int vectorOps = qRegisterMetaTypeStreamOperators<VectorLayer>();
    
    VectorLayer::VectorLayer(const VectorLayer &o) :
       Layer(o),
       QGraphicsPathItem(o.path())
    {}
    
    VectorLayer::VectorLayer(const QString &name) : Layer(name, qMetaTypeId<VectorLayer>()) {}
    
    void VectorLayer::write(QDataStream &ds) const {
       Layer::write(ds);
       ds << Format(0) << path();
    }
    
    void VectorLayer::read(QDataStream &ds) {
       Layer::read(ds);
       Format format_;
       QPainterPath path_;
       ds >> format_;
       if (format_ >= 0) {
          ds >> path_;
          setPath(path_);
       }
       if (format_ >= 1)
          invalidFormat(ds);
    }
    

    rasterOpsvectorOps 是虚拟变量,用于为输入 main() 之前的类型注册流运算符。它们没有其他用途。这些流操作符注册用于将类型连接到QVector

    现在我们可以编写一个测试工具来演示支持的流式操作。

    #include <QtTest>
    
    class LayerTest : public QObject {
       Q_OBJECT
       QBuffer buf;
       QDataStream ds{&buf};
    
    private slots:
       void initTestCase() {
          buf.open(QIODevice::ReadWrite);
       }
    
       void testClone() {
          RasterLayer raster("foo");
          QScopedPointer<QListWidgetItem> clone(raster.clone());
          auto *raster2 = static_cast<RasterLayer*>(clone.data());
    
          QCOMPARE(raster2->type(), raster.type());
          QCOMPARE(raster2->name(), raster.name());
       }
    
       void testValueIO() {
          ds.device()->reset();
          RasterLayer raster("foo");
          VectorLayer vector("bar");
          ds << raster << vector;
    
          ds.device()->reset();
          RasterLayer raster2;
          VectorLayer vector2;
          ds >> raster2 >> vector2;
    
          QCOMPARE(raster2.name(), raster.name());
          QCOMPARE(vector2.name(), vector.name());
       }
    
       void testPointerIO() {
          ds.device()->reset();
          RasterLayer raster("foo");
          VectorLayer vector("bar");
          ds << &raster << &vector;
    
          ds.device()->reset();
          Layer *raster2 = {}, *vector2 = {};
          ds >> raster2 >> vector2;
    
          QVERIFY(raster2 && vector2);
          QCOMPARE(raster2->typeId(), qMetaTypeId<RasterLayer>());
          QCOMPARE(vector2->typeId(), qMetaTypeId<VectorLayer>());
          QCOMPARE(raster2->name(), raster.name());
          QCOMPARE(vector2->name(), vector.name());
          delete raster2;
          delete vector2;
       }
    
       void testValueContainerIO() {
          ds.device()->reset();
          QVector<RasterLayer> rasters(2);
          QList<VectorLayer> vectors;
          vectors << VectorLayer() << VectorLayer();
          ds << rasters << vectors;
    
          ds.device()->reset();
          rasters.clear();
          vectors.clear();
          ds >> rasters >> vectors;
    
          QCOMPARE(rasters.size(), 2);
          QCOMPARE(vectors.size(), 2);
       }
    
       void testPointerConteinerIO() {
          ds.device()->reset();
          RasterLayer raster;
          VectorLayer vector;
          QList<Layer*> layers;
          layers << &raster << &vector;
          ds << layers;
    
          ds.device()->reset();
          layers.clear();
          QVERIFY(layers.isEmpty());
          ds >> layers;
          QCOMPARE(layers.size(), 2);
          QVERIFY(!layers.contains({}));
          qDeleteAll(layers);
       }
    
       void testVariantIO() {
          ds.device()->reset();
          RasterLayer raster;
          VectorLayer vector;
          auto vr = QVariant::fromValue(raster);
          auto vv = QVariant::fromValue(vector);
          ds << vr << vv;
    
          ds.device()->reset();
          vv.clear();
          vr.clear();
          QVERIFY(vr.isNull() && vv.isNull());
          ds >> vr >> vv;
          QVERIFY(!vr.isNull() && !vv.isNull());
          QCOMPARE(vr.userType(), qMetaTypeId<RasterLayer>());
          QCOMPARE(vv.userType(), qMetaTypeId<VectorLayer>());
       }
    
       void testVariantContainerIO() {
          ds.device()->reset();
          QVariantList layers;
          layers << QVariant::fromValue(RasterLayer())
                 << QVariant::fromValue(VectorLayer());
          ds << layers;
    
          ds.device()->reset();
          layers.clear();
          ds >> layers;
          QCOMPARE(layers.size(), 2);
          QVERIFY(!layers.contains({}));
          QCOMPARE(layers.at(0).userType(), qMetaTypeId<RasterLayer>());
          QCOMPARE(layers.at(1).userType(), qMetaTypeId<VectorLayer>());
       }
    };
    
    QTEST_MAIN(LayerTest)
    #include "main.moc"
    

    完整的可编译示例到此结束。

    【讨论】:

    • 您好,感谢您的详细回答。我的版本似乎对Layer 使用new 创建RasterLayer 的实例有问题。在 layer.h 中包含 rasterlayer.h 会产生循环依赖。你能告诉我如何解决这个问题吗?
    • 您不需要在 layer.h 中包含 rasterlayer.h,因为实现属于 layer.cpp,而不是 layer.h。我还更新了答案以遵循 QListWidgetItem 的约定。
    • 您好,非常有帮助。虽然我看到这里添加了很多额外的代码来使我的版本与 QListWidgetItem 兼容。但我有点难以理解 MetaType 流运算符之类的要求和用法。
    • @2dsharp 我现在提交了一个完整的例子。您也可以从 github 获取它。它演示了派生类型是如何相当简单的,并且还演示了它们的全部使用。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-01-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-09-25
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多