[SOLVED] What is the "right way" to signal specific instances of QML objects from C++?

Issue

Right up-front, I’ll apologize: This is a monster question, but I wanted to provide what I hope is all of the pertinent details.

I’ve got a QML-based GUI that I was tasked with taking over and developing from proof-of-concept to release. I believe the GUI is based on an example provided by QT (Automotive, maybe?). The GUI is being compiled for web-assembly (emscripten) and features a "back-end data-client" which communicates with our hardware controller via a socket and communicates with the GUI via signals. The GUI is accessed via web browser and communicates with the Data_Client via QWebSocket.

Simple Block Diagram

The GUI proof was initially created with a very "flat" hierarchy where every element is created and managed within a single ApplicationWindow object in a single "main" QML file. A Data_Client object is instantiated there and all the other visual elements are children (at various levels) of the ApplicationWindow:

ApplicationWindow {
id: appWindow

//various properties and stuff

Item {
    id: clientHolder
    property Data_Client client
}

ColumnLayout {
    id: mainLayout
    anchors.fill: parent
    layoutDirection: Qt.LeftToRight
//And so on...

The Data_Client C++ currently emits various signals in response to various things that happen in the controller application. In the main .QML the signals are handled as follows:

Connections {
        target: client 
    onNew_status_port_data:
        {
            textStatusPort.text = qdata;
        }
        onNew_status_data_act_on:
        {
            imageStatusData.source = "../imagine-assets/ledGoodRim.png";
        }
    //and so on...

What I’m trying to do is create a ChannelStatusPanel object that holds the various status fields and handles the updates to those fields (text, images, etc.) when it receives information from the Data_Client backend. There are multiple instances of this ChannelStatusPanel contained in a MainStatusPanel which is made visible or not from the main ApplicationWindow:

GUI Basic Layout

Having said all of that (Phew!), I come finally to my question(s). What is the correct way to signal a specific instance of the ChannelStatusPanel object from the Data_Client with the various data items needed to drive changes to the visual elements of the ChannelStatusPanel?

I thought I was being clever by defining a ChannelStatusObject to hold the values:

Item {
    id: channelStatusObject

    property int channel
    property int enabled    //Using the EnabledState enum 
    property string mode
    property int bitrate
    property int dataActivity   //Using the LedState enum
//and more...
    property int packetCount
}

In the ChannelStatusPanel.qml, I then created a ChannelStatusObject property and a slot to handle the property change:

property ChannelStatusObject statusObject

    onStatusObjectChanged: { 
//..do the stuff

From the Data_Client C++ I will get the information from the controller application and determine which "channel" I need to update. As I see it, I need to be able to do the following things:

  1. I need to determine which instance of ChannelStatusPanel I need to update. How do I intelligently get a reference to the instance I want to signal? Is that just accomplished through QObject::findChild()? Is there a better, faster, or smarter way?
  2. In the Data_Client C++, do I want to create an instance of ChannelStatusObject, set the various fields within it appropriately, and then set the ChannelStatusPanel instance’s ChannelStatusObject property equal to the newly created ChannelStatusObject? Alternatively, is there a mechanism to get a reference to the Panel’s ChannelStatusObject and set each of its properties (fields) to what I want? In C++, something like this:
QQmlComponent component(&engine, "ChannelStatusObject.qml");
QObject *statObj= component.create();

QQmlProperty::write(statObj, "channel", 1)
QQmlProperty::write(statObj, "bitrate", 5000);
QQmlProperty::write(statObj, "enabled", 0);

//Then something like using the pointer from #1, above, to set the Panel property
//QObject *channelPanel;
QQmlProperty::write(channelPanel, "statusObject", statObj)

Is there some other, more accepted or conventional paradigm for doing this? Is this too convoluted?

Solution

I would go about this using Qt’s model-view-controller (delegate) paradigm. That is, your C++ code should expose some list-like Q_PROPERTY of channel status objects, which in turn expose their own data as properties. This can be done using a QQmlListProperty, as demonstrated here.

However, if the list itself is controlled from C++ code — that is, the QML code does not need to directly edit the model, but only control which ones are shown in the view and possibly modify existing elements — then it can be something simpler like a QList of QObject-derived pointers. As long as you do emit a signal when changing the list, this should be fine:

class ChannelStatus : public QObject
{
    Q_OBJECT
public:
    Q_PROPERTY(int channel READ channel CONSTANT)
    Q_PROPERTY(int enabled READ enabled WRITE setEnabled NOTIFY enabledChanged)
    // etc.
};

class Data_Client : public QObject
{
    Q_OBJECT
public:
    Q_PROPERTY(QList<ChannelStatus*> statusList READ statusList NOTIFY statusListChanged)
    // ...
};

The ChannelStatus class itself must be registered with the QML type system, so that it can be imported in QML documents. Additionally, the list property type will need to be registered as a metatype, either in the main function or as a static variable. Otherwise, only lists of actual QObject pointers are recognised and you would have to provide yours as such.

qmlRegisterUncreatableType<ChannelStatus>("LibraryName", 1, 0, 
    "ChannelStatus", "Property access only.");
qRegisterMetaType<QList<ChannelStatus*>>();

You then use this property of the client on the QML side as the model property of a suitable QML component, such as a ListView or a Repeater inside a container like RowLayout. For example:

import LibraryName 1.0
ListView {
    model: client.statusList
    delegate: Column {
        Label { text: modelData.channel }
        Image { source: modelData.enabled ? "foo" : "bar" }
        // ...
    }
}

As you can see, the model data is implicitly attached to the delegate components. Any NOTIFYable properties will have their values automatically updated.

Answered By – sigma

Answer Checked By – Senaida (BugsFixing Volunteer)

Leave a Reply

Your email address will not be published. Required fields are marked *