QXmlSerialization [Część 1. z 2. – Serializacja]

Co pewien czas, od 2 lat, pytałem się wujka Google czy może znalazłaby się jakaś prosta w użyciu biblioteka służąca serializacji obiektów do XML. Za każdym razem wujek karmił mnie tymi samymi linkami z tymi samymi odpowiedziami:
„Do it yourself!”
W końcu nadszedł ten dzień. Dzień, w którym postanowiłem zakończyć szukanie i rozpocząć działanie!

Wzorując się nieco na https://github.com/flavio/qjson rozpocząłem swą pracę od stworzenia QObjectHelper – klasy pomocniczej, pośredniczącej pomiędzy XMLem, a QObjectem. Wstępnie zacząłem od bardziej oczywistej i prostszej części – serializacji:

QObjectHelper::QObject2QVariantHash

QVariantHash QObjectHelper::QObject2QVariantHash(const QObject *object, const QStringList &ignoredProperties)
{
    QVariantHash result;

    const QMetaObject *metaobject = object->metaObject();
    for (int i=0; i< metaobject->propertyCount(); ++i)
    {
        QMetaProperty metaproperty = metaobject->property(i);
        const char *name = metaproperty.name();

        if (ignoredProperties.contains(QLatin1String(name)) || (!metaproperty.isReadable()))
          continue;

        QVariant value = object->property(name);

        if(value.type() == QVariant::UserType)
        {
            QObject * obj = qvariant_cast(value);
            result[name] = QObject2QVariantHash(obj);
        }
        else
        {
            result[name] = value;
        }
    }

    return result;
}


Podany kod w prosty sposób dzięki użyciu QMetaObject oraz QMetaProperty odczytuje wszystkie możliwe, zadeklarowane property – dokładnie jak Reflection w .NET. Jedyną właściwą różnicą pomiędzy kodem @flavio, a moim (oprócz użycia QVariantHash zamiast QVariantMap jest dodanie magicznego warunku:
if(value.type() == QVariant::UserType)
{
    QObject * obj = qvariant_cast(value);
    result[name] = QObject2QVariantHash(obj);
}
W ten oto sposób jestem w stanie dodać rekurencyjnie QHash (który również jest QVariantem!) do głównego hasha obiektu. Co to daje? Wszystkie klasy dziedziczące po QObject, które są własnościami klasy, którą chcemy zserializować, również zostaną uwzględnione.

Idąc dalej – kolejnym krokiem była konwersja z QVariantHash do QDomDocument:

XMLSerializer::CreateDomElement
QDomElement XMLSerializer::CreateDomElement(const QString &name, const QVariantHash &hash, QDomDocument &document)
{
    QDomElement element = document.createElement(name);

    for(int hashElementIndex = 0; hashElementIndex < hash.count(); hashElementIndex++)
    {
        QDomElement tag;
        QVariant variant = hash.values().at(hashElementIndex);

        if(variant.type() == QVariant::Hash)
        {
			//if QVariant is QHash we can assume that it is a QVariantHash
            QDomDocument temp;
            QVariantHash variantHash = qvariant_cast(variant);
			//recursively create inner child element of current tag
            tag = CreateDomElement(hash.keys().at(hashElementIndex), variantHash, temp);
            element.appendChild(tag);
        }
        else if(variant.toString() != "")
        {
			//any other option is treated as a simple string
            tag = document.createElement(hash.keys().at(hashElementIndex));
            element.appendChild(tag);
            QDomText text = document.createTextNode(variant.toString());
            tag.appendChild(text);
        }
    }

    return element;
}
Z racji tego, że rekurencja jest przyjaciółką wszystkich programistów, to i tu znalazła swoje miejsce 😉

I w końcu ostatni element serializacji: XMLSerializer::Serialize
{
    QVariantHash hash = QObjectHelper::QObject2QVariantHash(objectToSerialize);

    QDomDocument doc;

    //adding header element
    QDomProcessingInstruction header = doc.createProcessingInstruction("xml", "version=\"1.0\" encoding=\"UTF-8\"");
    doc.appendChild(header);

    //filling root tag element
    QDomElement root = XMLSerializer::CreateDomElement(
                objectToSerialize->metaObject()->className(),
                hash,
                doc);

    doc.appendChild(root);

    return doc;
}


O tyle, o ile w C# domyślnie serializator dodaje deklarację nagłówka XML dodatkowo uzupełniając go o domyślnie nieuznawany przez Qt encoding UTF-16, to w samym Qt deklarację trzeba dodać samemu… Odpowiedzialny za to jest następujący fragment kodu:
//adding header element
QDomProcessingInstruction header = doc.createProcessingInstruction("xml", "version=\"1.0\" encoding=\"UTF-8\"");
doc.appendChild(header);


To tyle jeśli chodzi o serializację QObject do XML. Teraz demo z serii

How to use it?

Oto przykładowy obiekt, który da się zserializować:
#ifndef TESTOBJECT2_H
#define TESTOBJECT2_H

#include 

class TestObject2 : public QObject
{
    Q_OBJECT

    Q_PROPERTY(QString TestString READ TestString WRITE setTestString NOTIFY TestStringChanged)

public:
    Q_INVOKABLE explicit TestObject2(QObject *parent = 0) : QObject(parent) {} //needed to deserialize

    void setTestString(const QString &string)
    {
        if (string != m_TestString) {
            m_TestString = string;
            emit TestStringChanged();
        }
    }

    QString TestString() const {
        return m_TestString;
    }

signals:
    void TestStringChanged();

private:
    QString m_TestString;
};
Q_DECLARE_METATYPE(TestObject2*) //needed to deserialize

#endif // TESTOBJECT2_H


Szczególną uwagę należy zwrócić na:
Q_PROPERTY(QString TestString READ TestString WRITE setTestString NOTIFY TestStringChanged)
Co prawda w tym przypadku nadmiarowe są WRITE i NOTIFY, ale tak definiuje się „wypasione” property w Qt. Jak widać getter, setter oraz event wysyłany na zmianę property (przydatny w QML, o którym kiedyś parę słów napiszę) jest definiowany oddzielnie. Mając doświadzenie z C# i Resharpera, gdzie tworzenie property z backing field i eventem PropertyChanged zajmuje dosłownie 2 sekundy, nieco się przeraziłem. Na prędce napisałem więc pomocnego regexa: https://regex101.com/r/yM3fL8/1, który generuje cały (prawie) potrzebny kod za nas.

Ostatecznie serializacja wygląda w następujący sposób:
#include 
#include 

#include "XMLSerializer.h"
#include "TestObject2.h"
#include "QObjectHelper.h"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    //INIT
    TestObject2* test = new TestObject2();

    //DEMO 1
    QDomDocument testDoc = XMLSerializer::Serialize(test);
    qDebug() << testDoc.toString();

    return a.exec();
}


Et voilà!

Następny wpis poświęcę deserializacji - metodzie, która przysporzyła mi nieco więcej problemów 😉

PS Oto link do repo biblioteki na githubie: https://github.com/Dorrro/QXmlSerialization

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *