Property Bindings and Declarative Syntax in C++
QtQuick and QML form a really nice language to develop user interfaces.
The QML Bindings are very productive and convenient. The declarative syntax is really a pleasure to work with.
Would it be possible to do the same in C++?
In this blog post, I will show a working implementation of property bindings in pure C++.
Disclaimer: This was done for the fun of it and is not made for production.
Bindings
The goal of bindings is to have one property which depends on other properties. When its dependencies are changed, the property is automatically updated.
Here is an example inspired from the QML documentation.
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"); }; };
If you are not familiar with the [&]{ ... }
syntax, this is a
lambda function.
I'm also using the fact that in C++11, you can initialize the
members directly in the declaration.
Now, we'll see how this property class works. At the end I will show a cool demo of what you can do.
The code is using lots of C++11 constructs. It has been tested with GCC 4.7 and Clang 3.2.
Property
I have used my knowledge from QML and the QObject system to build something similar with C++ bindings.
The goal is to make a proof of concept. It is not optimized. I just wanted to have comprehensible code for this demo.
The idea behind the property
class is the same as in QML. Each property keeps a list of its dependencies.
When a binding is evaluated, all access to the property will be recorded as dependencies.
property<T>
is a template class. The common part is put in a base class: 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; };
Then we have the implementation of the class 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
It is also desirable to be notified when a property is changed, so we can for example call update()
.
The property_hook
class lets you specify a function which will be called when the property changes.
Qt bindings
Now that we have the property
class, we can build everything on top of that.
We could build for example a set of widgets and use those. I'm going to use Qt Widgets for that.
If the QtQuick elements had a C++ API, I could have used those instead.
The property_qobject
I introduce a
property_qobject
which is basically wrapping a property in a QObject. You initialize it by passing a
pointer to the QObject and the string of the property you want to track, and voilà.
The implementation is not efficient and it could be optimized by sharing the QObject rather than having one for each property. With Qt5 I could also connect to lambda instead of doing this hack, but I used Qt 4.8 here.
Wrappers
Then I create a wrapper around each class I'm going to use that expose the properties in a property_qobject
A Demo
Now let's see what we are capable of doing:
This small demo just has a line edit which lets you specify a color and few sliders to change the rotation and the opacity of a graphics item.
Let the code speak for itself.
We need a Rectangle object with the proper bindings:
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()); } };
Then we can proceed and declare a window object with all the subwidgets:
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
You can clone the code repository and try it for yourself.
Perhaps one day, a library will provide such property bindings.
Woboq is a software company that specializes in development and consulting around Qt and C++. Hire us!
If you like this blog and want to read similar articles, consider subscribing via our RSS feed (Via Google Feedburner, Privacy Policy), by e-mail (Via Google Feedburner, Privacy Policy) or follow us on twitter or add us on G+.
Article posted by Olivier Goffart on 28 February 2013
Click to subscribe via RSS or e-mail on Google Feedburner. (external service).
Click for the privacy policy of Google Feedburner.
Google Analytics Tracking Opt-Out
Loading comments embeds an external widget from disqus.com.
Check disqus privacy policy for more information.