пятница, 23 декабря 2011 г.

XML сериализация для QT

Я не удержался и взялся изобретать этот велосипед. Или лучше сказать - небольшое велосипедное колесо. После написания кода для первой серии я нашёл вот такую штуку. Но это не то, о чём мечталось. А хотелось иметь что-то похожее на .net xml serialization. Поиски готовых решений я немедленно  прекратил, поскольку появилось серьёзное опасение, что поиски могут увенчаться успехом. Также с самого начала я почуял, что такая штука для QT никому не нужна. Но и эти предчувствия я отважно проигнорировал.

 XML сериализация для подклассов QObject сегодня будет возможна благодаря таким фичам как мета-объекты и свойства, а также QT DOM. А именно, мы можем перечислять все свойства объекта и обращаться к свойству по строковому имени. На этом высокие технологии заканчиваются.

Для примера сварганим какой-нибудь класс, представляющий иерархичные объекты, и поробуем его сериализовать в xml, а потом десериализовать обратно. Пусть это будет записная книжка. И пусть в сегодня она будет состоять из одной записи :) Потому что поддержку коллекций я откладываю на потом.

Свойства объекта
Свойствами может обладать подкласс QObject, снабжённый макроопределением Q_OBJECT. Этот макрос нужен кьютовскому moc компилятору. Как известно, этот компилятор генерирует для каждого класса с макросом Q_OBJECT дополнительный код для поддержки в том числе механизма динамических свойств, о коих и речь.
Чтобы определить в классе свойство, требуется использовать макроопределение Q_PROPERTY в параметрах которого задаются имя свойства, функции доступа и прочее не обязательное, нас не интересующее.

Вот наш первый класс

class Note : public QObject
{
 Q_OBJECT
 Q_PROPERTY(QString title READ title WRITE setTitle)
 Q_PROPERTY(QString message READ message WRITE setMessage)
 Q_PROPERTY(QDateTime changeDate READ changeDate WRITE setChangeDate)
 Q_PROPERTY(int mark READ mark WRITE setMark)

public:
 Note(QObject *parent = 0) :
  QObject(parent),
  mChangeDate(QDateTime::currentDateTime()),  
  mMark(0)
 {  
 }
 void setMessage(const QString &v) { mMessage = v; }
 QString message() const { return mMessage; }

 void setTitle(const QString &v) { mTitle = v; }
 QString title() const { return mTitle; }

 void setChangeDate(const QDateTime &v) { mChangeDate = v; }
 QDateTime changeDate() const { return mChangeDate; }

 void setMark(int m) { mMark = m; }
 int mark() { return mMark; }

private:
 QString mMessage;
 QString mTitle;
 QDateTime mChangeDate;
 int mMark;
};

Этот класс представляет собой одну запись с заголовком и датой изменения. Еще я долго придумывал, как употребить тип int и bool для иллюстрации. Придумал только целочисленное свойство.



Простая сериализация в XML
 Теперь переходим к превращениям. Допустим, у нас есть ссылка типа QObject* на некий объект со свойствами. Сделаем из него xml. Для этого будем использовать DOM модель.

QDomDocument doc;
QDomElement root = doc.createElement(object->metaObject()->className());
doc.appendChild(root);
 Этот кусок кода создаёт документ и добавляет в него тег. Имя тэга - имя класса нашего объекта. Имя класса доступно благодаря meta object system от QT. В частности ради этого мы проделали тот огромный труд по наследованию от QObject и добавлению макроса Q_OBJECT. Теперь осталось пробежаться по всем свойствам объекта и нагенерировать тэгов!


for(int i = 0; i < object->metaObject()->propertyCount(); i++)
{
    QMetaProperty prop = object->metaObject()->property(i);
    QString propName = prop.name();
    if(propName == "objectName")
        continue;
    QDomElement el = doc.createElement(propName);
    QVariant value = object->property(propName.toAscii().data());
    QDomText txt = doc.createTextNode( value.toString() );
    el.appendChild(txt);
    root.appendChild(el);
}
Здесь пропускаем свойство objectName, которое есть у всех объектов по умолчанию. Дальше сохраним XML в файл:

QFile output("note.xml");
QTextStream stream(&output);
doc.save(stream, 2);
return true;

Простая десериализация
Для десиарелизации уже придётся один раз напрячься и вспомнить шаблоны C++. Еще добавим одно требование к классам десиарелизуемых объектов: они должны иметь конструктор без параметров. Это нужно для удобства реализации нашей поделки с помощью шаблонов. Цель сделать так, чтобы пользователь, зная класс объекта, имел возможность восстановить объект класса из XML файла. Если схема XML файла не соответствует классу, то просто будет неполная десериализация, то есть возможно не все поля создаваемого объекта будут заполнены и возможно не все тэги XML будут задействованы. Примерный вид шаблонной функции:


template<class T>
T* deserialize(QIODevice *input)
{
    T* object = new T();
    if(_deserializeObject(input, object))
        return object;
    delete object;
    return NULL;
}

Действие разворачивается в функции _deserializeObject Снова итерация по всем свойствам объекта. Для каждого свойства в XML документе ищется соответствующий тэг. Если тэг найден, то свойству задаётся значение из содержимого тэга.

bool _deserializeObject(QIODevice* input, QObject* object)
{
    QDomDocument doc;
    if (!doc.setContent(input))
        return false;
    QDomElement root = doc.documentElement();
    for(int i = 0; i < object->metaObject()->propertyCount(); i++)
    {
        QMetaProperty prop = object->metaObject()->property(i);
        QString propName = prop.name();
        if(propName == "objectName")
            continue;
        QDomNodeList nodeList = element.elementsByTagName(propName);
        if(nodeList.length() < 1)
            continue;
        QDomNode node = nodeList.at(0);
        QVariant value = object->property(propName.toAscii().data());
        QString v = node.toElement().text();
        object->setProperty(propName.toAscii().data(), QVariant(v));
    }
    return true; 
}
Да, и как обычно, всю грязную работу за нас делает класс QVariant. Он, соответственно своему названию, может хранить почти всё, что угодно, без усилий и еще больше, если потребуется.
Ну вот мы и закончили работу с плоскими структурами. Но этого недостаточно для сносной поделки.

Иерархичные объекты
Добавим класс, содержащий объекты класса Note. В предыдущем примере свойства простых типов магическим образом восстанавливались из строк. Такое не пройдёт с пользовательскими типами. Пусть класс NoteBook содержит одну запись Note. Необходимо создать свойство для этого поля.

class NoteBook : public QObject
{
 Q_OBJECT
 Q_PROPERTY(QObject* note READ note WRITE setNote)

public:
 NoteBook(QObject *parent = 0)
  : QObject(parent)
 {
  mNote = new Note(this);
 }

 void setNote(QObject *v) 
 { 
  delete mNote;
  v->setParent(this);
  mNote = dynamic_cast<Note*>(v); 
 }
 Note* note() const { return mNote; }

private:
 Note* mNote;

};

Как уже было сказано, QVariant хорош, однако он не готов хранить и красиво отддавать ссылку на QObject, который мы вознамерились использовать для свойства. Надо прописать для этого специальную мантру в заголовочном файле после декларации класса NoteBook:
Q_DECLARE_METATYPE(QObject*)
Это даёт нам возможность использовать функции вроде qVariantValue и qRegisterMetaType, весьма полезные в дальнейшем. Последняя регистрирует новый тип для QVariant так, чтобы можно было выполнить проверку, содержится ли в переменной QVariant объект нашего типа. Например:
int QOBJECT_STAR_ID = qRegisterMetaType<QObject*>("QObject*");
Теперь в наш "алгоритм" сериализации можем внести проверку, является ли очередное поле ссылкой на QObject.

if(value.type() == QOBJECT_STAR_ID)
{
    QObject *object1 = qVariantValue<QObject*>(value);
    ...
}
Если является, то придётся рекурсивно сериализовать это поле.
Аналогично для десериализации.

В итоге, если оформить всё в виде класса с таким интерфейсом

class Serializer
{
public:
    bool serialize(QObject *object, QIODevice *output);
    template<class T>
    T* deserialize(QIODevice *input)
    {
      ...
    }
};
то может получиться вот такой симпатичный код:

Note *n = new Note;
n->setMessage("Hello!");
n->setMark(1);
n->setTitle("My note");
NoteBook *b = new NoteBook;
b->setNote(n);
QString fname = "notebook.xml";
QFile f(fname);
Q_ASSERT(f.open(QIODevice::WriteOnly));
Serializer s;
Q_ASSERT(s.serialize(b, &f));
f.close();
Q_ASSERT(f.open(QIODevice::ReadOnly));
NoteBook *b1 = s.deserialize(&f);
Q_ASSERT(b1->note()->title().compare(b->note()->title()) == 0);
Эта программа порождает вот такой XML файл

<NoteBook>
  <note>
    <title>My note</title>
    <message>Hello!</message>
    <changeDate>2011-12-23T02:15:57</changeDate>
    <mark>1</mark>
  </note>
</NoteBook>

В следующей серии добавим сериализацию коллекций

2 комментария:

  1. bool Serialize(QObject *object) {
    QDomDocument doc;
    QDomElement root = doc.createElement(object->metaObject()->className());
    doc.appendChild(root);

    for(int i = 0; i < object->metaObject()->propertyCount(); i++)
    {
    QMetaProperty prop = object->metaObject()->property(i);
    QString propName = prop.name();
    if(propName == "objectName")
    continue;
    QDomElement el = doc.createElement(propName);
    QVariant value = object->property(propName.toAscii().data());
    QDomText txt;
    if (object->metaObject()->property(i).typeName() != "QByteArray") {
    txt = doc.createTextNode( value.toString() );
    } else {
    txt = doc.createTextNode(object->metaObject()->property(i).typeName());
    }
    el.appendChild(txt);
    root.appendChild(el);
    }

    позволит сериализовать поля типа QByteArray
    может кому будет интересно

    ОтветитьУдалить
  2. Этот комментарий был удален автором.

    ОтветитьУдалить