3

I am trying to serialize a custom class Layer* and read it back using QDataStream. Now, Layer is an abstract class with virtual method which is inherited by different kinds of layers: RasterLayer, TextLayer, AdjustmentLayer etc.

I have a QList<Layer*> layers which keeps track of all the layers, and any adjustments made to a layer are updated in the list. I need to serialize and deserialize the QList to its original state and restore the properties of the individual layers (of different types).

Here is 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

This is extended by a RasterLayer class:

#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

I am currently trying to test serialization-deserialization of a RasterLayer like this:

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

Now, as you can see here, I am specifically casting Layer* to a RasterLayer*to be serialized, and this works since I have worked on only one type of layer till now. So my first question is:

How do I generalize this serialization process to all types of layers?

Every type of layer will have a different way of serialization since each hold different properties. Also, the casting here feels like a bit of code smell and a possible bad design choice. So, having something like serialize the entire list of layers calling their corresponding overloaded operators would be the expected scenario.

My second question is:

How do I deserialize the data properly? Here's how I am currently serializing an individual 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());

Firstly, I don't think serializing to a pointer is something I should be doing in this case, but I am not sure what else to do or how to do better yet. Secondly, the deserialization works here, but it doesn't quite do what I would be expecting it to do. Although I am using setters in the overloaded operators, it's really not updating the layer properly. I need to call the constructor to make a new layer out of it.

I have tried this: Serialization with Qt but I am not quite sure how to have a Layer* convert it to a Layer, serialize it, deserialize it and then convert it back to Layer*. So I need to add a third step:

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

and then push layer3 to the list to actually make it work. According to this post: https://stackoverflow.com/a/23697747/6109408, I really shouldn't be doing a new RasterLayer... inside the operator overloading function (or else I will fry in hell), and I am following the first suggestion given there, which isn't very much working in my case and I don't know the right way to do it.

Also, how do I deserialize this for a general QList of Layer*s instead of having to create new specific layer instances and injecting them with deserialized data? Although this is similar: Serialize a class with a Qlist of custom classes as member (using QDataStream), the answers weren't clear enough for me to understand.

I have had an idea about an intermediate value holder class that I will use to serialize all sorts of Layers and let that create and inject the parameters depending upon the type of Layer it is, but I am not sure if that will work.

Thanks for helping me out.

twodee
  • 571
  • 4
  • 23

2 Answers2

3

I hope the following example will give you general idea:

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

Edit: at I did not consider the fact that when serializing list we should write size of list and type of elements for succesfull deserialization, so I modified example.

Andrew Kashpur
  • 706
  • 5
  • 13
  • Hi, thanks for your input. Could you please be a bit more elaborate? I also require a way to properly deserialize the data. Please explain a bit more so that its a little bit clearer. Also, I would like to create a new list based on the serialized data that I can read. If you could provide me an example for that, it would be great. – twodee Jul 18 '18 at 14:21
  • you define << and >> operators in base class. operators use virtual functions serialize and deserialize. Then you provide implementations to these functions ( if required ) in derived classes. And so you can use << and >> on derived classes. (The compiler will perform cast to base class and deduce correct serialize/deserialize function by itself if I am correct) – Andrew Kashpur Jul 18 '18 at 14:31
  • That looks like a nice way to deal with it, I will try that out and get back to you. Thank you. :D – twodee Jul 18 '18 at 14:35
  • @2dsharp I modified my answer (added desirialization). at first I did not consider the fact that size of list and type of elements should be stored when serializing. – Andrew Kashpur Jul 18 '18 at 15:16
  • Thank you very much, this code actually worked without me having to change a lot of things. All I had to do was overload the operators for `QList` and put the iteration. – twodee Jul 18 '18 at 22:11
3

To address your need: The typical way to do it is to exploit the polymorphism.

The base class (QListWidgetItem) has an interface to perform serialization and deserialization. We can leverage it to implement (de)serialization of pointers to derived types. Serialization invokes an interface implemented in derived classes to serialize derived-specific data. Deserialization first uses a type-specific factory to create an instance of the derived type, and only then invokes the deserialization interface implemented in the derived class - using the base class's operator.

Once the serialization and deserialization of the base type is implemented, QList and QVariant (!) should work as well.

You should not be implementing your own type storage - QListWidgetItem already provides it for you!

The Layer class is the abstract foundation for the classes that derive from QGraphicsItem. The typeId() and typeName() methods leverage the metatype type system. The derived class should pass a typeId (not type()!) to Layer's constructor.

// 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 &);
};

The it() helper provides access to the derived QGraphicsItem* type. The basics of the implementation are relatively simple.

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

It is important to version the data to ensure backwards-compatibility: newer versions of the software should be able to read data written by older versions. Thus each class maintains its own format indicator. This decouples the format of the Layer class from that of the derived classes. The data types are saved as text to ensure portability in light of potentially changing type ids.

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 already provides the stream operators for references to QListWidgetItem. We need to provide stream operators that handle the pointers to that type. The output operator immediately forwards to the reference-taking output operator. The input operator peeks the type of the object stored in the stream, uses the type to look up the metatype id, and instantiates it using QMetaType::create(). Then, it forwards to the reference-taking input operator.

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

Once the Layer abstract base class is set up, it is simple to implement derived classes:

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

And, similarly:

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

The rasterOps and vectorOps are dummy variables used to register stream operators for the types before main() is entered. They serve no other purpose. Those stream operator registrations are used to interface the types to QVector.

Now we can write a test harness that demonstrates the streaming operations that are supported.

#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"

This concludes the complete, compileable example.

Kuba hasn't forgotten Monica
  • 88,505
  • 13
  • 129
  • 275
  • Hi, thanks for the detailed answer. My version seems to have a problem for `Layer` to create an instance of `RasterLayer` using `new`. Including rasterlayer.h in layer.h would create a cyclic dependency. Could you tell me how to go about this problem? – twodee Jul 18 '18 at 15:45
  • You don't need to include rasterlayer.h in layer.h, because the implementations belong in `layer.cpp`, not `layer.h`. I've also updated the answer to follow the conventions of `QListWidgetItem`. – Kuba hasn't forgotten Monica Jul 18 '18 at 16:04
  • Hi, it was very helpful. Although I see a lot of extra code added here to make my version compatible with QListWidgetItem. But I am kind of having a hard time understanding the requirements and usages of the things like MetaType stream operators and stuff. – twodee Jul 18 '18 at 22:12
  • @2dsharp I've now submitted a complete example. You can also fetch it from github. It demonstrates how the derived types are fairly simple, and also demonstrates the use of it all. – Kuba hasn't forgotten Monica Jul 18 '18 at 23:55