Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to declare multiple QML properties without handling each one individually?

Tags:

properties

qt

qml

I've got a toy QML app (Qt 5.7) that consists of the following QML page plus some C++ code that allows the QML to subscribe to some state on an audio device and reflect that state in the QML GUI. The toy QML looks like this:

import QtQuick 2.6
import QtQuick.Controls 1.5
import QtQuick.Window 2.2

Item {
   id: deviceState

   property bool mute1: false
   property bool mute2: false
   property bool mute3: false
   property bool mute4: false

   Component.onCompleted: {
      // Call out to C++ land, to tell the server what 
      // control points we are interested in tracking the state of
      topLevelWindow.subscribeToControlPoints("Input 1-4 Mute", deviceState);
   }

   // Called by C++ land to tell us the current state of
   // a control point we previously subscribed to.
   function onControlPointValueUpdated(address, value) {
      if (address == "Input 1 Mute") {
         mute1 = value
      }
      else if (address == "Input 2 Mute") {
         mute2 = value
      }
      else if (address == "Input 3 Mute") {
         mute3 = value
      }
      else if (address == "Input 4 Mute") {
         mute4 = value
      }
   }

   // Absurdly minimalist example GUI
   Text {
      text: parent.mute4 ? "MUTED" : "unmuted"
   }
}

When I run this, my C++ code calls onControlPointValueUpdated() whenever one of the four mute-states on my audio device changes, and based on the 'address' argument in the call, one of the four muteN QML properties is changed. And, whenever the mute4 property is changed, the text in the Text area is updated to reflect that.

This all works fine, as far as it goes, but at some point I'm going to need to scale it up; in particular I will need to track hundreds or thousands of values rather than just four, and having to manually declare an individual, hand-named QML property for each value would quickly become arduous and difficult to maintain; and the implementation of onControlPointValueUpdated() would become very inefficient if it has to manually if/elseif its way through every property.

So my question is, is there some way in QML for me to declare an array (or better yet, a dictionary/map) of properties, so that I can declare a large number of QML properties at once? i.e. something like this:

property bool mutes[256]: {false}   // would declare mutes[0] through mutes[255], all default to false

... or if not, is there some other recommended approach to holding large amounts of observable state within a QML document? I like the ability to have my GUI widgets bind to a QML property to get automatically updated as necessary, but it seems like perhaps QML properties weren't intended to be used en masse?

like image 658
Jeremy Friesner Avatar asked Oct 29 '25 12:10

Jeremy Friesner


1 Answers

As soon as you get a large amount of data that has to be displayed (especially if it all shares a common format), you should think about using a QAbstractItemModel derivative.

Another thing to point out is that it's not a good idea to call into the QML of the user interface from C++, because it tends to limit what you can do in QML, and ties the QML to the C++.

For example, if you were to display the data in a list:

listview

The items in the view are created on demand, and there are a limited amount that are allocated at any one time, which helps when you have a lot of data and potentially complex delegates.

Here's how a simple, custom QAbstractListModel with custom roles looks:

main.cpp:

#include <QtGui>
#include <QtQuick>

class UserModel : public QAbstractListModel
{
public:
    UserModel() {
        for (int i = 0; i < 100; ++i) {
            mData.append(qMakePair(QString::fromLatin1("Input %1").arg(i), i % 5 == 0));
        }
    }

    enum Roles
    {
        NameRole = Qt::UserRole,
        MutedRole
    };

    QVariant data(const QModelIndex &index, int role) const Q_DECL_OVERRIDE
    {
        if (!index.isValid())
            return QVariant();

        switch (role) {
        case Qt::DisplayRole:
        case NameRole:
            return mData.at(index.row()).first;
        case MutedRole:
            return mData.at(index.row()).second;
        }

        return QVariant();
    }

    int rowCount(const QModelIndex &/*parent*/ = QModelIndex()) const Q_DECL_OVERRIDE
    {
        return mData.size();
    }

    int columnCount(const QModelIndex &/*parent*/ = QModelIndex()) const Q_DECL_OVERRIDE
    {
        return 1;
    }

    virtual QHash<int, QByteArray> roleNames() const Q_DECL_OVERRIDE
    {
        QHash<int, QByteArray> names;
        names[NameRole] = "name";
        names[MutedRole] = "muted";
        return names;
    }

private:
    QVector<QPair<QString, int> > mData;
};

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);

    QQmlApplicationEngine engine;
    UserModel userModel;
    engine.rootContext()->setContextProperty("userModel", &userModel);
    engine.load(QUrl(QStringLiteral("qrc:/main.qml")));

    return app.exec();
}

main.qml:

import QtQuick 2.3
import QtQuick.Layouts 1.1
import QtQuick.Window 2.2

Window {
    width: 300
    height: 300
    visible: true

    ListView {
        id: listView
        anchors.fill: parent
        anchors.margins: 10
        model: userModel
        spacing: 20

        delegate: ColumnLayout {
            width: ListView.view.width

            Text {
                text: name
                width: parent.width / 2
                font.pixelSize: 14
                horizontalAlignment: Text.AlignHCenter
            }

            RowLayout {
                Rectangle {
                    width: 16
                    height: 16
                    radius: width / 2
                    color: muted ? "red" : "green"
                }

                Text {
                    text: muted ? qsTr("Muted") : qsTr("Unmuted")
                    width: parent.width / 2
                    horizontalAlignment: Text.AlignHCenter
                }
            }
        }
    }
}

You can easily swap the ListView out with a GridView, for example. Best of all, the C++ knows nothing about the QML.

You can read more about using C++ models with Qt Quick here.


Looking at the image you posted in the comments, that type of UI could be made in a couple of ways. You could use the approach above, but using a Loader in the delegate. The Loader can determine which type of component to show based on some data in the model. However, it looks like the amount of data displayed on your UI, while large in quantity, might indeed be better off without a view.

You could still use a model (e.g. with a Repeater like @peppe mentioned), but you could also get away with just listing off each "panel" (I dunno what they're called) in a similar way to how you're currently doing it. It looks like most of the data in one panel is the same, so each one could be its own class (untested code follows):

#include <QtGui>
#include <QtQuick>

class Panel : public QObject
{
    Q_OBJECT
    Q_PROPERTY(bool muted READ muted WRITE setMuted NOTIFY mutedChanged)

    Panel() { /* stuff */ }

    // getters
    // setters

signals:
    // change signals

private:
    // members
};

class WeirdPanel : public QObject
{
    Q_OBJECT
    Q_PROPERTY(int level READ level WRITE setLevel NOTIFY levelChanged)

    WeirdPanel() { /* stuff */ }

    // getters
    // setters

signals:
    // change signals

private:
    // members
};

class Board : public QObject
{
    Q_OBJECT
    Q_PROPERTY(Panel *panel1 READ panel1 CONSTANT)
    Q_PROPERTY(Panel *panel2 READ panel2 CONSTANT)
    Q_PROPERTY(Panel *panel3 READ panel3 CONSTANT)
    // etc.
    Q_PROPERTY(WeirdPanel *weirdPanel READ weirdPanel WRITE setWeirdPanel NOTIFY weirdPanelChanged)

public:

    Board() {
    }

    // getters
    // setters

signals:
    // change signals

private:
    Panel panel1;
    Panel panel2;
    Panel panel3;
    Panel panel4;
    WeirdPanel weirdPanel;
};

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);

    QQmlApplicationEngine engine;
    Board board;
    engine.rootContext()->setContextProperty("board", &board);
    engine.load(QUrl(QStringLiteral("qrc:/main.qml")));

    return app.exec();
}

main.qml:

import QtQuick 2.3
import QtQuick.Layouts 1.1
import QtQuick.Window 2.2

Window {
    width: 300
    height: 300
    visible: true

    RowLayout {
        anchors.fill: parent

        Panel {
            id: panel1
            panel: board.panel1
        }

        Panel {
            id: panel2
            panel: board.panel2
        }

        Panel {
            id: panel3
            panel: board.panel3
        }

        WeirdPanel {
            id: weirdPanel
            panel: board.weirdPanel
        }
    }
}

Then, Panel.qml might just be a column of buttons and whatnot:

import QtQuick 2.0
import QtQuick.Layouts 1.1
import QtQuick.Controls 2.0

Rectangle {
    property var panel

    color: "#444"

    implicitWidth: columnLayout.implicitWidth
    implicitHeight: columnLayout.implicitHeight

    ColumnLayout {
        anchors.fill: parent

        Text {
            text: panel.name
            width: parent.width / 2
            font.pixelSize: 14
            horizontalAlignment: Text.AlignHCenter
        }

        RowLayout {
            Rectangle {
                width: 16
                height: 16
                radius: width / 2
                color: panel.muted ? "red" : "green"
            }

            Text {
                text: panel.muted ? qsTr("Muted") : qsTr("Unmuted")
                width: parent.width / 2
                horizontalAlignment: Text.AlignHCenter
            }
        }
    }
}
like image 136
Mitch Avatar answered Nov 01 '25 14:11

Mitch