Pull to refresh

Properties framework для Qt

Reading time 7 min
Views 23K


Проблема

В Qt существует замечательная вещь — Q_PROPERTY, которая позволяет добавить необходимое свойство к любому QObject классу. Но в некоторых случаях пользоваться ими неудобно.

Например, у вас есть приложение, в котором существуют (или можно создавать) много различных объектов и, которые необходимо настраивать (выдавать диалог со свойствами выбранного объекта). Примером таких приложений может быть почти любая инженерная программа, редактор векторной графики, Visual Studio в конце концов.


Итак, существует множество объектов, которые пользователь может «настраивать». Логично иметь для этого какой-то универсальный механизм, ведь иначе придется для каждого объекта реализовывать свой собственный диалог настройки. Q_PROPERTY для этого не совсем подходит. Во-первых, нет стандартного виджета для редактирования Q_PROPERTY. Во-вторых, даже если вы воспользуетесь сторонним виджетом для этих целей, то наряду с вашими свойствами, будут показываться стандартные (как минимум objectName). Далее, Q_PROPERTY не поддерживает иерархии (иногда полезно сгруппировать связанные свойства); нет description (текстовая строка, объясняющая данное свойство); нет никаких средств для настройки внешнего вида Q_PROPERTY в GUI (для улучшения usability).

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

Решение

Пытаясь сделать удобные свойства, я создал проект QtnProperty. Далее я расскажу чем хорош этот проект и как этой библиотекой пользоваться.
Прежде всего библиотека состоит из нескольких частей:
  • QtnPropertyCore — библиотека с классами, не связанными с GUI
  • QtnPropertyWidget — виджеты для редактирования свойств, плюс делегаты для кастомизации внешнего вида и редактирования различных свойств
  • QtnPEG — приложение (подобное moc генератору), которое создает С++ класс набора свойств PropertySet из *.pef файла (это более простой способ описания свойств, по синтаксису подобен QML)
  • QtnPropertyTests — тесты для библиотеки QtnPropertyCore
  • QtnPropertyDemo — демо приложение, где можно редактировать свойства с помощью QtnPropertyWidget, а также из скрипта и задавая текст в виде строк «SuperProperty.SubProperty = value»

QtnPropertyCore

Прежде всего, библиотека определяет два основных класса QtnProperty и QtnPropertySet. Первый является базовым для всех типов свойств (например QtnPropertyBool или QtnPropertyFloat), а второй представляет группу свойств. И QtnProperty и QtnPropertySet имеют общего родителя QtnPropertyBase, который, в свою очередь, является потомком QObject. В QtnPropertyBase классе определено всё, что есть общего между QtnProperty и QtnPropertySet:
  1. QString name — имя property или propertyset (совпадает с objectName)
  2. QString description — текстовое описание, которое может показыватся в нижней панели QtnPropertyWidget
  3. qint32 id — уникальный ID свойства среди других в группе (используется при сохранении в бинарном виде)
  4. QtnPropertyState state — состояние свойства (например QtnPropertyStateImmutable — нередактируемое свойство или QtnPropertyStateInvisible — невидимое)
  5. load/save — сохранение/загрузка через QDataStream
  6. fromStr/toStr — конвертирование значения свойства из/в QString
  7. fromVariant/toVariant — конвертирование значения свойства из/в QVariant
  8. propertyWillChange — сигнал, который срабатывает перед изменением свойства (при изменении не только значения но и других атрибутов — name, description, id, state и т.п.)
  9. propertyDidChange — сигнал, который срабатывает после изменением свойства (симметричен propertyWillChange)

QtnPropertySet класс содержит еще список дочерних свойств и методы работы с ними. Для каждого типа свойства (например целочисленного) существует два класса (QtnPropertyInt и QtnPropertyIntCallback). Первый хранит значение в классе, а второй вызывает некоторую функцию для получения или сохранения значения.
Вот список свойств, который реализован непосредственно в модуле QtnPropertyCore:
  1. QtnPropertyBool — представляет булево значение.
  2. QtnPropertyInt — представляет знаковое целочисленное значение.
  3. QtnPropertyUInt — представляет беззнаковое целочисленное значение.
  4. QtnPropertyFloat — представляет вещественное значение с плавающей точкой.
  5. QtnPropertyDouble — представляет вещественное значение двойной точности.
  6. QtnPropertyEnum — представляет значение из перечисления.
  7. QtnPropertyEnumFlags — представляет комбинацию значений из перечисления.
  8. QtnPropertyQString — представляет строковое значение.
  9. QtnPropertyQPoint — представляет значение QPoint.
  10. QtnPropertyQSize — представляет значение QSize.
  11. QtnPropertyQRect — представляет значение QRect.
  12. QtnPropertyQFont — представляет значение QFont.
  13. QtnPropertyQColor — представляет значение QColor.

Все численные свойства (QtnPropertyInt, QtnPropertyUInt, QtnPropertyFloat, QtnPropertyDouble) имеют три дополнительных метода:
  1. minValue — минимально допустимое значение.
  2. maxValue — максимально допустимое значение.
  3. stepValue — шаг изменения значения.

QtnPropertyWidget

В этом модуле содержатся два виджета и делегаты свойств.
Класс QtnPropertyView реализует древовидный виджет для набора свойств.
Класс QtnPropertyWidget является составным и содержит QtnPropertyView и QLabel в качестве нижней панели.

Что бы поменять внешний вид или поведение некоторого отдельного свойства, были введены делегаты. Например, для QtnPropertyBool существуют два делегата: QtnPropertyDelegateBoolCheck и QtnPropertyDelegateBoolCombobox. Первый отображает checkbox, а второй — combobox с двумя настраиваемыми значениями (для значений true и false). Можно создавать свои делегаты и выставлять их в качестве дефолтных, тогда все свойства данного типа будут отображаться с помощью нового делегата. Иногда легче и удобнее создавать новый тип делегата, чем новый тип свойства. Например, свойство для хранения пути к файлу реализовано через специальный делегат QtnPropertyDelegateQStringFile для QtnPropertyQString.

Как пользоваться

Для примера рассмотрим небольшой набор свойств для текстового редактора.
Вот что мы хотим показать пользователю.

А вот что нужно сделать программисту, что бы получить такой property set:
    QtnPropertySet* textProperties = new QtnPropertySet(owner);

    QtnPropertyBool* enableWrapping = new QtnPropertyBool(textProperties);
    enableWrapping->setName(tr("enableWrapping"));
    enableWrapping->setDescription(tr("Enable/disable text wrapping"));
    enableWrapping->setValue(true);

    QtnPropertyQColor* textColor = new QtnPropertyQColor(textProperties);
    textColor->setName(tr("textColor"));
    textColor->setDescription(tr("Foreground text color"));
    textColor->setValue(QColor(0, 0, 0));

    QtnPropertySet* Tabulation = new QtnPropertySet(textProperties)
    Tabulation->setName(tr("Tabulation"));
    Tabulation->setDescription(tr("Tabulation settings"));

    QtnPropertyBool* replaceWithSpaces = new QtnPropertyBool(Tabulation);
    replaceWithSpaces->setName(tr("replaceWithSpaces");
    replaceWithSpaces->setDescription(tr("Automatically replace tabs with spaces"));
    replaceWithSpaces->setValue(false);

    QtnPropertyUInt* tabSize = new QtnPropertyUInt(Tabulation);
    tabSize->setName(tr("tabSize"));
    tabSize->setDescription(tr("Number of spaces to be placed."));
    tabSize->setState(QtnPropertyStateImmutable);
    tabSize->setValue(4);
    tabSize->setMinValue(1);
    tabSize->setMaxValue(10);

    // создаём обработчик сигнала propertyDidChange
    auto lambda =  [tabSize, replaceWithSpaces](const QtnPropertyBase* changedProperty,
                             const QtnPropertyBase* firedProperty,
                             QtnPropertyChangeReason reason) {
        // если изменилось значение
        if (reason & QtnPropertyChangeReasonValue)
        {
            if (*replaceWithSpaces)
                // делаем tabSize изменяемым
                tabSize->removeState(QtnPropertyStateImmutable);
            else
                // делаем tabSize не изменяемым
                tabSize->addState(QtnPropertyStateImmutable);
    };
    QObject::connect(replaceWithSpaces, &QtnProperty::propertyDidChange, lambda);

    // и еще код для настройки делегата replaceWithSpaces
    ...

Чтобы облегчить жизнь программистам (или даже дать возможность описывать свойства не-программистам) был разработан небольшой кодо-генератор QtnPEG.
Вот так выглядит описание нашего примера в Text.pef файле:
property_set Text
{
    Bool enableWrapping
    {
        description = "Enable/disable text wrapping";
        value = true;
    }

    QColor textColor
    {
        description = "Foreground text color";
        value = QColor(0, 0, 0);
    }

    property_set Tabs Tabulation
    {
        description = "Tabulation settings";
        Bool replaceWithSpaces
        {
            description = "Automatically replace tabs with spaces";
            value = false;

            // устанавливаем специальный делегат
            delegate ComboBox
            {
                //  назначаем текст для значения true
                labelTrue = "On";
                //  назначаем текст для значения false
                labelFalse = "Off";
            }

            // определяем код слота для сигнала replaceWithSpaces.propertyDidChange
            slot propertyDidChange
            {
                tabSize.switchState(QtnPropertyStateImmutable, !replaceWithSpaces);
            }
        }

        UInt tabSize
        {
            description = "Number of spaces to be placed.";
            state = QtnPropertyStateImmutable;
            value = 4;
        }
    }
}

Достаточно просто и лаконично. QtnPEG создает два файла: Text.peg.h и Text.peg.cpp, где находятся два класса. QtnPropertySetText и QtnPropertySetTabs — классы наследники от QtnPropertySet.
В сгенерированных классах можно увидеть следующую декларацию подсвойств, например для QtnPropertySetText:
    // start children declarations
    QtnPropertyBool& enableWrapping;
    QtnPropertyQColor& textColor;
    QtnPropertySetTabs& Tabulation;
    // end children declarations

Таким образом программист в C++ коде может работать с этим классом, как со структурой с полями. Например:
void doSomething(const QtnPropertySetText& textParams, QString& text)
{
    if (textParams.Tabulation.replaceWithSpaces)
    {
        QString spaces(QChar::Space, textParams.Tabulation.tabSize);
        text.replace(QChar::Tabulation, spaces);
    } 
}

Итог

Надеюсь я смог дать некоторое представление о моем проекте.
Буду рад, если кому-то он пригодится, особенно там, где есть много настроек и разрабатывать GUI для всего и вся накладно. Если, по вашему мнению, для полноценного использования QtnProperty не хватает какой-то функциональности, прошу комментировать.
Tags:
Hubs:
+41
Comments 9
Comments Comments 9

Articles