- PVSM.RU - https://www.pvsm.ru -

Привязка свойств и декларативный синтаксис в C++

Привязка свойств и декларативный синтаксис в C++ QtQuick и QML образуют по-настоящему хороший язык для разработки пользовательских интерфейсов. Привязки QML очень производительны и удобны. Декларативный синтаксис действительно приятен в работе. Возможно ли сделать то же самое на C++? В этом посте я покажу рабочую реализацию привязки свойств на чистом C++.

Внимание: это было сделано для забавы, а не для использования в реальном проекте.

Привязки

Привязки служат для создания свойств, зависящих от других свойств. При изменении зависимостей значение свойства обновляется автоматически.

Вот пример, на написание которого меня вдохновил код из документации QML [1].

int calculateArea(int width, int height) {
  return (width * height) * 0.5;
}

struct rectangle {
  property<rectangle*> parent = nullptr;
  property<int> width = 150;
  property<int> height = 75;
  property<int> area = [&]{ return calculateArea(width, height); };

  property<std::string> color = [&]{
    if (parent() && area > parent()->area)
      return std::string("blue");
    else
      return std::string("red");
  };
};

Если вы не знакомы с синтаксисом [&]{ ... }, то это лямбда-функции [2]. Также я использую тот факт, что в C++11 можно инициализировать члены структуры или класса прямо во время объявления.
Теперь посмотрим, как работает класс свойства. В конце я покажу пример его использования.
В коде используется множество конструкций C++11, он был протестирован с GCC 4.7 и Clang 3.2.

Свойства

Я использовал свои знания QML и мета-объектной системы Qt для того, чтобы создать что-то похожее в виде C++ привязок.
Цель состоит лишь в демонстрации proof of concept. Код не оптимизирован, я старался сделать его как можно более понятным.
Идея в том, что можно написать класс property, поведение которого будет аналогичным свойствам в QML. Каждое свойство будет хранить список своих зависимостей. При обработке привязки все входящие в неё property будут отмечены как зависимости.
property<T> — шаблонный класс. Общая часть будет помещена в родительский класс property_base.

class property_base
{
  /* Множество свойств, зависящих от текущего
     Когда свойство изменится, все зависимые свойства будут обновлены */
  std::unordered_set<property_base *> subscribers;

  /* Множество свойств, от которых зависит текущее */
  std::unordered_set<property_base *> dependencies;

public:
  virtual ~property_base()
  { clearSubscribers(); clearDependencies(); }

  // это свойство должно быть переопределено
  virtual void evaluate() = 0;
   
  // [...]
protected:
  /* Эта функция вызывается производным классом после того, как свойство было изменено
    Стандартная реализация переопределяет все свойства, зависящие от текущего */
  virtual void notify() {
    auto copy = subscribers;
    for (property_base *p : copy) {
      p->evaluate();
    }
  }

  /* Эта функция вызывается производным классом при получении доступа к свойству
     Здесь происходит регистрация всех зависимостей */
  void accessed() {
    if (current && current != this) {
      subscribers.insert(current);
      current->dependencies.insert(this);
    }
  }

  void clearSubscribers() {
      for (property_base *p : subscribers)
          p->dependencies.erase(this);
      subscribers.clear();
  }
  void clearDependencies() {
      for (property_base *p : dependencies)
          p->subscribers.erase(this);
      dependencies.clear();
  }

  /* Вспомогательный класс */
  struct evaluation_scope {
    evaluation_scope(property_base *prop) : previous(current) {
      current = prop;
    }
    ~evaluation_scope() { current = previous; }
    property_base *previous;
  };
private:
  friend struct evaluation_scope;
  /* thread_local */ static property_base *current;
};

Далее мы реализуем класс property.

template <typename T>
struct property : property_base {
  typedef std::function<T()> binding_t;

  property() = default;
  property(const T &t) : value(t) {}
  property(const binding_t &b) : binding(b) { evaluate(); }

  void operator=(const T &t) {
      value = t;
      clearDependencies();
      notify();
  }
  void operator=(const binding_t &b) {
      binding = b;
      evaluate();
  }

  const T &get() const {
    const_cast<property*>(this)->accessed();
    return value;
  }

  // автоматическое приведение
  const T &operator()() const { return get();  }
  operator const T&() const { return get(); }

  void evaluate() override {
    if (binding) {
      clearDependencies();
      evaluation_scope scope(this);
      value = binding();
    }
    notify();
  }

protected:
  T value;
  binding_t binding;
};

property_hook

Желательно также получать уведомления, когда свойство изменяется, поэтому мы можем, например, вызывать update(). Класс property_hook [3] позволяет указать функцию, которая будет вызываться при изменении свойства.

Привязки Qt

Теперь у нас есть класс свойств, на основе которых мы можем построить что угодно. Я буду использовать их в связке с Qt Widgets.

property_qobject

Далее я ввожу property_qobject [4], который является базовой обёрткой property в QObject. Он инициализируется передачей указателя на QObject и строкой свойства, которую вы хотите отслеживать.
Реализация не является эффективной и может быть оптимизирована путём обмена QObject вместо создания по одному для каждого свойства. С Qt 5 я бы мог использовать лямбда-функции вместо этого хака, но здесь я использовал Qt 4.8.

Обёртки

Теперь я создаю обёртки вокруг каждого класса, в котором будут использованы свойства property_qobject.

Демонстрация

Давайте посмотрим, на что мы способны:
Этот маленький пример содержит line edit, который позволяет вам задать цвет, и два слайдера, задающие поворот и прозрачность графического элемента.

Привязка свойств и декларативный синтаксис в C++

Пусть код говорит сам за себя.

Нам нужен прямоугольник с соответствующими привязками:

struct GraphicsRectObject : QGraphicsWidget {
  // привязываем свойства QObject 
  property_qobject<QRectF> geometry { this, "geometry" };
  property_qobject<qreal> opacity { this, "opacity" };
  property_qobject<qreal> rotation { this, "rotation" };

  // добавляем свойство цвета с привязкой для обновления, когда оно изменяется
  property_hook<QColor> color { [this]{ this->update(); } };
private:
  void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget*) override {
    painter->setBrush(color());
    painter->drawRect(boundingRect());
  }
};

Теперь мы объявляем объект окна со всеми подвиджетами:

struct MyWindow : Widget {
  LineEdit colorEdit {this};

  Slider rotationSlider {Qt::Horizontal, this};
  Slider opacitySlider {Qt::Horizontal, this};

  QGraphicsScene scene;
  GraphicsView view {&scene, this};
  GraphicsRectObject rectangle;

  ::property<int> margin {10};

  MyWindow() {
    // компоновка элементов; реализано не так хорошо, как через настоящие Layout, зато продемонстрированы привязки
    colorEdit.geometry = [&]{ return QRect(margin, margin,
                                             geometry().width() - 2*margin,
                                             colorEdit.sizeHint().height()); };
    rotationSlider.geometry = [&]{ return QRect(margin,
                                                  colorEdit.geometry().bottom() + margin,
                                                  geometry().width() - 2*margin,
                                                  rotationSlider.sizeHint().height()); };
    opacitySlider.geometry = [&]{ return QRect(margin,
                                                 rotationSlider.geometry().bottom() + margin,
                                                 geometry().width() - 2*margin,
                                                 opacitySlider.sizeHint().height()); };
    view.geometry = [&]{
        int x = opacitySlider.geometry().bottom() + margin;
        return QRect(margin, x, width() - 2*margin, geometry().height() - x - margin); 
    };

    // зададим значения по умолчанию
    colorEdit.text = QString("blue");
    rotationSlider.minimum = -180;
    rotationSlider.maximum = 180;
    opacitySlider.minimum = 0;
    opacitySlider.maximum = 100;
    opacitySlider.value = 100;

    scene.addItem(&rectangle);

    // дальше - наши привязки
    rectangle.color = [&]{ return QColor(colorEdit.text);  };
    rectangle.opacity = [&]{ return qreal(opacitySlider.value/100.); };
    rectangle.rotation = [&]{ return rotationSlider.value(); };
  }
};

int main(int argc, char **argv)
{
    QApplication app(argc,argv);
    MyWindow window;
    window.show();
    return app.exec();
}

Заключение

Вы можете клонировать репозиторий [5] и попробовать использовать всё это сами.
На данный момент библиотека представляет собой лишь небольшой прототип; возможно, когда-нибудь она будет дописана до приемлемого состояния.

Автор: epicfailguy93

Источник [6]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/programmirovanie/28520

Ссылки в тексте:

[1] документации QML: http://qt-project.org/doc/qt-4.8/propertybinding.html#property-binding

[2] лямбда-функции: http://stackoverflow.com/questions/7627098/what-is-a-lambda-expression-in-c11

[3] property_hook: http://woboq.com/blog/property-bindings-in-cpp/code/src/property.h.html#property_hook

[4] property_qobject: http://woboq.com/blog/property-bindings-in-cpp/code/src/property_qobject.h.html#property_qobject

[5] клонировать репозиторий: https://github.com/woboq/property_bindings

[6] Источник: http://habrahabr.ru/post/171295/