„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