Bind QML Values across an Arbitrary Number of Elements

Wait 5 sec.

Bind QML Values across an Arbitrary Number of ElementsA while back I was writing a program that could instantiate an arbitrary number of windows containing controls synchronized across them. How a control would be synchronized would depend on a condition that determined which window instances would be linked to other instances. There are a few ways this could be implemented. In this entry I'll share my approach, in which I used a singleton C++ class serving as a message broker to bind properties across window instances. Properties bound across multiple instances of a window The App: Display to Light PanelsThe software I wrote this for is a small app that allows you to have many windows open, all synchronized to display a single color on a per-monitor basis. The idea behind this is for users to adjust the light that comes off their monitors and use it to illuminate their faces when recording a video or taking pictures. By having individual windows be synchronized, users can continue to interact with the computer through their displays (rather inconveniently), while simultaneously using them to illuminate themselves.This is, by no means, a replacement for a proper recording setup. Some of you will know that a light source placed in front of the subject can serve as either a nice "fill light" or a "scary spotlight", depending on the height and size of the source. Therefore, this should be complemented with other sources of light; ideally ambient light and a top light, to achieve a nice look. If the app seems useful to you, there's a link to it at the end of this article. That's enough gaffer speak for today. Let's talk about the code.The CodeWhen writing code, one of my main concerns is always long-term maintainability. For that reason, I prefer to connect different parts of code in explicit, and easy to follow ways. One of such ways is passing values through a hierarchy of components; that is generally easy to track and produces well performing code. However, that approach can become unviable when connecting dynamically instantiated items to other dynamically instantiated items. A better solution in this instance is to use signals and slots to interconnect the items via a message broker class, done in C++. Each item would have a model or backend class in C++ and those classes would have the message broker in common. Lastly, the properties would be exposed to QML through Q_PROPERTY and updated via your control's signal handlers.Message Broker SingletonThe message broker needs to be a singleton. That way there's only one instance of the broker in memory and all instantiated objects interface with the same broker. Our message broker only needs to provide the signals that will be used for routing properties. The actual connections that the routing involves are to be done from the outside. As such, a broker class would look like this: // internalmessagebroker.hpp// Singleton broker class contains signals that serve as pipes// for different parts of a program to communicate with each other.#pragma once#include class InternalMessageBroker : public QObject{ Q_OBJECT// Hide regular constructorprivate: InternalMessageBroker() = default;public: // Disable copy constructor InternalMessageBroker(const InternalMessageBroker& obj) = delete; InternalMessageBroker& operator=(InternalMessageBroker const&) = delete; static std::shared_ptr instance() { static std::shared_ptr sharedPtr{new InternalMessageBroker}; return sharedPtr; }// This is where all the signals would gosignals: void broadcastAPropertyChange(int value, bool broadcast);}; Connecting PropertiesThen we have the class or classes that would connect the properties together. On my Display Panels app, all controls and visual features come from QML, meaning I only have to concern myself with interconnecting the properties. To that end, I've created a class based on QObject and instantiated it within the delegate of a QML Instantiator. This class is only for managing property data, so I refer to it as a model class, called PropertiesModel: // Main.qml// Here are 3 windows, each with a PropertiesModel,// that allows them all to share a binding to aProperty// across all window instantiatons.import QtQuickimport QtQuick.Windowimport QtQuick.Layoutsimport QtQuick.Controlsimport com.kdab.exampleItem { Instantiator { model: 3 delegate: Window { PropertiesModel { id: propertiesModel aProperty: 1 } ColumnLayout { anchors.fill: parent Text { text: propertiesModel.aProperty } Slider { id: hueSlider value: propertiesModel.aProperty Layout.fillWidth: true onMoved: { propertiesModel.aProperty = value; } } } } }} Each property of our class is to be declared using a Q_PROPERTY macro with a getter, a setter, and a notifier. The getter and the notifier have nothing out of the ordinary... // An ordinary getterint PropertiesModel::aProperty(){ return m_aProperty;} // An ordinary notifier, form propertiesmodel.hsignals: void aPropertyChanged(); However, the setter must be able to distinguish between when its call is the product of a user interaction and when it comes from the message broker. We accomplish this by having the setter accept a boolean argument (broadcast) that will be used to determine whether the value being set should be sent through the message broker or notified back to QML for the UI to be updated. When a setter is first called, the value being set should be broadcasted and only on its way back should the UI be updated.There are a few ways we could make sure that the value is always broadcasted first. We could set broadcast to true by default, or have two setter functions: a private one taking in both the value and broadcast arguments and a public one that only takes-in the value and calls the private function with broadcast set to true. Another option is to have broadcast be an enum with only two possible values. That would produce more readable code, however, I didn't worry about that in my code because broadcast is only to be used on calls to the private setter.Upon the private setter being called by the public setter, it will emit the broker's broadcast signal and that signal will in turn call the calling private setter for a second time (as well as the private setters of all other instances of PropertiesModel; those being called for the first time). When the private setter calls the broker that calls back to the private setter, it also passes broadcast set to false. All other instances will then evaluate the value of broadcast to be false determining that the UIs should be updated and preventing an infinite loop. // Private setter implementation for a property that's being broadcastedvoid PropertiesModel::setAProperty(const int value, const bool broadcast){ if (broadcast) emit m_broker.get()->broadcastAPropertyChange(value, false); else if (m_currentScreen == screenName) { m_aProperty = value; emit screenSaturationChanged(); }}// Publicly exposed setter prevents the broadcast argument from being// specified by other callersvoid PropertiesModel::setAProperty(const int value){ // Always broadcast properties not being set by the message broker setAProperty(value, true);} To complete this loop as described, we need to connect the signal from the broker to the private setter of the PropertiesModel class whenever a new copy is instantiated. The best place to accomplish that is from the class' constructor, like so: // The constructor is used to connect signals from the broker to setter propertiesPropertiesModel::PropertiesModel(QObject* parent) : QObject { parent }{ m_mb = InternalMessageBroker::instance(); // Connections take place after the class has been instantiated // and its QML properties parsed. QTimer::singleShot(0, this, [this] () { connect(m_mb.get(), &InternalMessageBroker::aPropertyChange, this, &ScreenModel::setAProperty); });} QTimer to Delay InitializationIf you've been reading the blocks of code that accompany this article, you may be wondering "Why is there a singleShot QTimer there?" When the PropertiesModel class is instantiated via QML, any connected properties that have values assigned from QML code will trigger the Q_PROPERTY's setter function. This will happen once per instantiation. If the broker were connected, it would broadcast the value set to all currently instantiated instances every single time that a new instance is added. To prevent that, we must not make the connection to the broker until after a class has been fully initialized. A single shot timer can be used to accomplish that; setting its delay to 0 will ensure that it is run immediately after all properties have been evaluated.In the end, this is what the PropertyModel header would look like: // propertiesmodel.h#pragma once#include "internalmessagebroker.hpp"#include class PropertiesModel : public QObject { Q_OBJECT QML_ELEMENT Q_PROPERTY(int aProperty READ aProperty WRITE setAProperty NOTIFY aPropertyChanged FINAL)public: explicit PropertiesModel(QObject* parent = nullptr); int aProperty(); void setAProperty(const int value);signals: void aPropertyChanged();private: void setAProperty(const int value, const bool broadcast); std::shared_ptr m_mb; int m_aProperty;}; Conditional propagationIf a condition must be met for the propagated value to result in an update, then, in addition to value and broadcast, you should also broadcast any other values that are required to for such condition to be met. Pass those as arguments to the setter's and the broker's signal. Condition validation would then take place within the base case of the private setter. Here's a commented snippet of what that looks like on the Display to Light Panels app that this article was based on: // In the original code what here I named broadcast used to be named spread.void ScreenModel::setScreenHue(const int hue, const bool spread=true, const QString &screenName="s"){ if (spread) emit m_mb.get()->spreadHueChange(hue, false, m_currentScreen); // The incoming value is only accepted if screenName matches the screen // that the window is at and discarded otherwise. else if (m_currentScreen == screenName) { m_screens[m_currentScreen].hue = hue; emit screenHueChanged(); }} Properties bound across instances of a window Real World ExampleFor a real world application, you can read the code for Display to Light Panels, the app that inspired this article, at: https://github.com/Cuperino/Display-to-Light-PanelsIf you need help solving architecture problems, such as this one, reach out to us and we will gladly find ways in which we can help.The post Bind QML Values across an Arbitrary Number of Elements appeared first on KDAB.