Wiki de QtFR.org

Des tutos, des articles et des trucs et astuces

Outils pour utilisateurs

Outils du site


property_bindings_et_syntaxe_declarative_en_c

Property Binding et Syntaxe Déclarative en C++


Préambule

Ce tutoriel est la traduction libre de l'article présent ici et mis à disposition sur QtFR par tof ici. L'utilisation de la première personne dans cet article désignera ici l'auteur original et non pas moi, le rédacteur de cette traduction.


QtQuick et QML forment un langage très élégant lorsqu'il s'agit de développer des IHM1). Les associations de propriété en QML2) sont très efficaces et très pratiques, et la syntaxe déclarative est très agréable à l'usage.

Est il possible de faire la même chose en C++ ? Dans cet article, je montrerai une implémentation fonctionnelle des QML Bindings en C++ pur.

Attention : Ceci a été fait et pensé à des fins d'expérimentation et n'est en aucun cas destiné à être implémenté dans du code en production.

Bindings

Le but des associations de propriétés est d'avoir une propriété dont la valeur dépend de la valeur ou de l'état d'autres propriétés. Quand ses propriétés liées sont mises à jour, la propriété “bindée” ou liée est aussi mise à jour automatiquement.

L'exemple ci-dessous est inspiré de la documentation 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");
  };
};

Si vous n'êtes pas familiers de la syntaxe [&]{ … }, sachez que c'est le code représentatif d'une fonction lambda. J'utilise aussi le fait qu'en C++11, il est possible d'initialiser les membres directement dans la déclaration de ces derniers.

Maintenant nous allons voir comment fonctionnent ces propriétés de classe. A la fin je ferai une petite démonstration de ce qu'il est possible de faire ici.
Le code utilise un bon nombre de mécanismes de C++11. Il a été testé avec GCC 4.7 et Clang 3.2.

Propriétés

J'ai utilisé ma connaissance de QML et le système de QOBject pour mettre en place quelque chose de similaire avec des associations purement C++. Le but de la manoeuvre était de créer un prototype démontrant qu'il était possible d'arriver au bout du problème. Ce dernier est livré tel quel et non optimisé, et ne sert qu'à des fins de compréhension et de démonstration.
L'idée sous-jacente derrière la classe property est la même qu'en QML. Chaque propriété garde une liste des dépendances. Lorsqu'une propriété liée est évaluée, tous les accès à cette dernière seront enregistrés comme des dépendances.

property<T> est une classe template. La partie commune à toutes les propriétés est placée dans la classe de base property_base.

class property_base
{
  /* Set of properties which are subscribed to this one.
     When this property is changed, subscriptions are refreshed */
  std::unordered_set<property_base *> subscribers;
 
  /* Set of properties this property is depending on. */
  std::unordered_set<property_base *> dependencies;
 
public:
  virtual ~property_base()
  { clearSubscribers(); clearDependencies(); }
 
  // re-evaluate this property
  virtual void evaluate() = 0;
 
  // [...]
protected:
  /* This function is called by the derived class when the property has changed
     The default implementation re-evaluates all the property subscribed to this one. */
  virtual void notify() {
    auto copy = subscribers;
    for (property_base *p : copy) {
      p->evaluate();
    }
  }
 
  /* Derived class call this function whenever this property is accessed.
     It register the dependencies. */
  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();
  }
 
  /* Helper class that is used on the stack to set the current property being evaluated */
  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;
};

Puis vient l'implémentation de la classe 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;
  }
 
  //automatic conversions
  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

Il est aussi souhaitable d'être informé lorsqu'une propriété change de valeur ou d'état, et de pouvoir lier le changement de la propriété à un appel de fonction, comme une fonction update().

La classe property_hook permet de spécifier une fonction qui sera appelée lorsque la propriété changera d'état.

Bindings Qt

Maintenant que nous avons la classe property, nous pouvons construire les autres éléments autour de cette classe. On pourrait par exemple construire un ensemble de widgets et les utiliser. Pour cela je vais utiliser les widgets de Qt Widgets. Si les éléments de Qt Quick avaient une API C++ je les aurais utilisés ici mais ce n'est pas le cas.

Le ''property_qobject''

Ici, j'introduis un property_qobject, qui va basiquement encapsuler une propriété dans un QObject. On l'initialise en lui passant un pointeur vers le QObject de la propriété et le nom de la propriété que l'on veut lier, et voilà 3).

Cette implémentation n'est pas efficace et elle pourrait être optimisée en partageant le QObject des propriétés liées, plutôt que d'en avoir un par propriété. Avec Qt5 il est possible de faire un connect vers une fonction lambda plutôt qu'utiliser ce “hack”, mais c'est Qt 4.8 que j'utilise ici.

Wrappers

Ensuite, je crée un wrapper4) autour de chaque classe que je vais utiliser. Ce dernier expose les propriétés de la classe qu'il encapsule en utilisant des property_qobject.

Démonstration

Maintenant voyons ce qu'on est capables de faire.

Cette petite démo comprend juste un lineEdit qui permet de spécifier une couleur et quelques sliders qui influent sur la rotation et l'opacité d'un GraphicsItem.

Laissons le code parler pour lui.

Nous avons besoin ici d'un objet Rectangle avec les bons liens de propriétés :

struct GraphicsRectObject : QGraphicsWidget {
  // bind the QObject properties.
  property_qobject<QRectF> geometry { this, "geometry" };
  property_qobject<qreal> opacity { this, "opacity" };
  property_qobject<qreal> rotation { this, "rotation" };
 
  // add a color property, with a hook to update when it changes
  property_hook<QColor> color { [this]{ this->update(); } };
private:
  void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget*) override {
    painter->setBrush(color());
    painter->drawRect(boundingRect());
  }
};

Ensuite nous pouvons déclarer un objet window avec tous ses widgets enfants :

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 the items.  Not really as good as real layouts, but it demonstrates bindings
    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); 
    };
 
    // Some proper default value
    colorEdit.text = QString("blue");
    rotationSlider.minimum = -180;
    rotationSlider.maximum = 180;
    opacitySlider.minimum = 0;
    opacitySlider.maximum = 100;
    opacitySlider.value = 100;
 
    scene.addItem(&rectangle);
 
    // now the 'cool' bindings
    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();
}

Conclusion

Vous pouvez cloner le code depuis le dépôt et tester par vous-même. Il se peut qu'un jour dans le futur une librairie propose une fonctionnalité similaire pour lier les propriétés des objets entre eux.

1) Interface Homme Machine
2) QML Bindings
3) NDLR : en français dans le texte
4) objet qui en encapsule un autre
property_bindings_et_syntaxe_declarative_en_c.txt · Dernière modification: 2015/08/19 11:48 par givememyname