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

в 18:59, , рубрики: c++, c++11, QML, qt, Qt 5, qt quick, Qt Software, Программирование

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

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

Привязки

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

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

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");
  };
};

Если вы не знакомы с синтаксисом [&]{ ... }, то это лямбда-функции. Также я использую тот факт, что в 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 позволяет указать функцию, которая будет вызываться при изменении свойства.

Привязки Qt

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

property_qobject

Далее я ввожу property_qobject, который является базовой обёрткой 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();
}

Заключение

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

Автор: epicfailguy93

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js