2
\$\begingroup\$

I'm asking your help to understand it the following code does something that should really be avoided.

The application I'm currently working on is an embedded HMI for a process controller who shares its status on a serial line. The application will be designed in QT (Quick). Stability of the application is essential (items manufactured in series).

I tried to take advantage from embedded ECMAScript interpreter of QT Quick and property binding automation; so I asked him to:

  • load QML from files for UI on runtime;
  • instantiate properties only in QML scripts;
  • Modify properties in response to UI actions (instead of calling Q_INVOKABLE functions that sends data to the control system)
  • use explicit binding as described below;
  • open up a window that lists all the properties of the backend object and allows to set communication parameters, inspect and change properties on the fly, and save list of properties and related configuration on a sqlite database.

I sent to an external developer (I am running out of time and I need help) a minimal (working) QT project example; simplified extracts of the code are listed below. The requirements and the architecture have been heavily criticized by the developer (to the extent that if I want the software written that way I have to do it by myself). While I understand some of his concerns (property initialization drifts from default use case), I don't see anything inherently wrong with the suggested approach, so I kindly ask anyone of you who has experience with QT/QML to help me understand if and where the approach I was triyng to discuss is really flawing, since I am still not getting the point and I need to understand if I am that wrong.

QML window: driving properties are defined in QML within the "BackEnd". Notice that properties are defined within BackEnd object and automatically generated signals on<Property>Changed are connected to slots by connectProperties() once component has been loaded.

import com.mydomain.backend 1.0

Window {
    property BackEnd backEnd : BackEnd {
        property string property1 : "default property1 value"
        property bool property2 : checkBox.checked
    }

    Column {
        Text {
            text: backEnd.property2
        }
        CheckBox {
            id: checkBox
        }
    }

    Component.onCompleted: {
        backEnd.connectProperties()
    }
}

C++ Backend. Callable connectProperties() function traverses MetaObject and connects methods from an object created on the fly

class BackEnd : public QObject
{
    Q_OBJECT
public:
    explicit BackEnd(QObject *parent = nullptr) : QObject(parent);
    ~BackEnd();
    Q_INVOKABLE void connectProperties()
    {
        const QMetaObject *mo = this->metaObject();
        for(int i = mo->propertyOffset(); i < mo->propertyCount(); ++i)
        {
            QMetaProperty property = mo->property(i);
            auto po = new PropertySlot(this, property);
            m_properties[property.name()] = po;
            po->connectSlot();
        }
    }
private:
    static QMap<QString, PropertySlot*> m_properties;
    friend class PropertySlot;
};

And here is the glue class (here the connectSlot ensures connection between source event and related slot)

class PropertySlot : public QObject {
        Q_OBJECT
    public:
        // Note: this is simplified, maybe the slot should be 
        // disconnected once destroyed event is reached.
        void PropertySlot(const QObject *source, const QMetaProperty &property)
        {
            this.source = source;
            this.name = property.name();
            // Here QT does not allow to mix QMetaMethod with functors, 
            // so I ended up with PropertySlot class to circumvent the limitation.
            QMetaObject *this_mo = PropertySlot::metaObject();
            connect(source, property.notifySignal(), this,
                   this_mo->method(this_mo->indexOfSlot("propertyChanged()")));
        }
    public slots:
        void propertyChanged()
        {
            // Send an event to a static function within I/O thread 
            // with property name and new value, ensure data is passed by copy.
        }
    private:
        const QObject *source;
        // Get by copy
        QString name;
};

The properties updated by the I/O thread would be nearly 100 with a refresh rate between 1 to 10 seconds each. Updated properties from UI to I/O are user event based.

Those are the highlighted issues (maybe you could find other points that I missed)

  • Loading QML for windows at runtime is not the "best practice" and it would not add any practical benefit.
  • Explicit binding made by traversing MetaObject is "forcing the code", "bad practice" and leads to application flaws.
  • This code adds unnecessary event loop hops and it will slow down the embedded application
  • Invokables do not require to run the event loop; property update chain in response to user action might be perceived as application slowness
  • Added complexity (and potential issues) without benefit (no reason to do it this way)
  • Property inspection shall be performed by logging. A single window that lists all the properties encourages laziness in unit testing.

My points are:

  • QML is based on ECMAScript, and as such it is a scripting language. I don't see any difference (apart from the fact that a plain text file can be modified by the user, but that's what a scripting language is designed for) of loading the QML file from and URI located within the application binary and loading it from a local file.
  • The benefit of a scripting language is that it can be modified without recompilation; experiments can be performed on the device and it would ease development phase. Application is reconfigurable and it can be used in multiple future cases.
  • The risk of an unwanted change to text files in production is minimal on our embedded device; even in this case files might be signed to detect changes.
  • QML scripts are simple and they do not require recompilation, I can give it to a 'junior' developer and he could make adjustments with less efforts and see the results immediately, so that we can focus on glue logic instead of UI
  • Explicit binding avoids duplication of property declaration inside C++ classes, I/O thread and calls within QML. The code is reduced since a single slot is attached to each event generated when QML script is loaded. While the explicit binding is not endorsed by a large number of QT examples, the described approach uses standard functions and I don't see any potential break.
  • While working with control systems, it's very hard to set up unit testing from sequential phases without a phisical test hardware, and an overall view of variable values from within a single table would be of benefit. Logging does not give shapshot of status. UI might not show all the properties.
  • I see added complexity and risk in defining variable and properties in up to 4 different places: the QML code, the C++ backend, the I/O layer and the control board firmware. While the suggested approach adds some development efforts (and cost) the ability to reconfigure the application with reduced efforts would be a long term benefit.

The code above is tested in a simple use case and it works. Any comment is well appreciated.

\$\endgroup\$
0
\$\begingroup\$

I've developed my first QML application some months ago, so I'm far from an expert in QML. Here are some of my thoughts:

Loading a QML file from disk vs. embedding it

Nothing wrong about either of those.

Modify properties in response to UI actions (instead of calling Q_INVOKABLE functions that sends data to the control system)

That's called two-way bindings. The can be fine, but often leads to weird problems. If you only have a dumb value-store, it should be fine. Read this:

https://stackoverflow.com/questions/28250710/qt-5-4-qml-prevent-binding-loop

 Architecture

Here's how I implemented my application (sorry for any bugs, I don't have the source code here):

QML:

CheckBox {
    checked: Settings.everythingOk
    onCheckedChanged: Settings.everythingOk = checked
}

C++ (not sure about the syntax)

class Settings: public QObject {
    Q_OBJECT
    Q_PROPERTY(bool everythingOk MEMBER m_everythingOk NOTIFY everythingOkChanged)

signals:
    void everythingOkChanged();

private:
    bool m_everythingOk = false;
}

and then I exposed Settings as a global singleton to QML. (It's also possible to expose it as a normal object if multiple instances are required.)

To me this seems much simpler.

Yes, it's slightly more to write, but not much. Your trick is clever, but it's not as future-proof as using a more standard approach.

\$\endgroup\$
  • \$\begingroup\$ Thank you for your kind reply. I get the point about circular dependency. Having a unique "driving" property instead of a "two way" control makes a lot of sense (checkBox.checked property in my code, this differs from what C#/WPF does with its properties). I kindly ask you to help me understand why I should consider my code not so "future-proof"; I thought that if my code is not going to work it would be in a next major QT release, where many other things might break... \$\endgroup\$ – Fab Jan 26 '18 at 15:53
  • \$\begingroup\$ @Fab First of all a big caveat: I don't completely understand what the advantages of your architecture are, so I might be coming from a wrong angle. I don't think the code will stop working, but in my opinion it's not future proof in 2 ways: (1) it's an uncommon design, every developer who's going to work on it will first have to understand the design, (2) It's possible that this design works in your test project but has disadvantages / problems you will only find later. A standard architecture is a safer choice. \$\endgroup\$ – Georg Schölly Jan 26 '18 at 19:37
  • \$\begingroup\$ Your points sound reasonable and fair. I understand that the perspective is important (I was trying to simplify management of a system where more than 100 properties have to be kept in sync with a PLC; I was trying to reduce code duplication and locations where the same properties have to be declared by name. I could have done it by using dynamic properties (setProperty) but I did not find a way to bind them to QML. BTW I'll think about different ways to reach the same purpose with a more standard approach. Thank you! \$\endgroup\$ – Fab Jan 27 '18 at 20:01
  • \$\begingroup\$ @Fab I've made good experience with code generation, e.g. use Python to generate your C++ backend. \$\endgroup\$ – Georg Schölly Feb 1 '18 at 8:27

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.