Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

Access List of Objects in QML



  • Hello,

    I build a class for multiple clients in my program to store the connection data of the multiple clients responding, to show in QML (mostly QString, bool, int). Each Client should be stored in its own Object.

    When I receive a response from a Client I create a new client-Object with the Data provided and store it in a QMap<QString, *client> with the IP (QString) as Key. This works quite fine and from C++ an I am able to access the Data.

    Now I am stuck at the point of trying to access the collected data in the QMap in QML.

    In the client-Class all Properties are setup with QProperty:

    #ifndef MOVIECLIENT_H
    #define MOVIECLIENT_H
    
    #include <QObject>
    #include <QMetaType>
    #include <QMap>
    #include <QDebug>
    
    class MovieClient : public QObject
    {
        Q_OBJECT
        Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged)
        Q_PROPERTY(QString address READ address WRITE setAddress NOTIFY addressChanged)
        Q_PROPERTY(QString version READ version WRITE setVersion NOTIFY versionChanged)
        Q_PROPERTY(int port READ port WRITE setPort NOTIFY portChanged)
    
    public:
        explicit MovieClient(QObject *parent = nullptr);
        MovieClient(const QString &name, const QString &version, const QString &address, const int &port, QObject *parent = nullptr);
        MovieClient(const MovieClient &other);
        ~MovieClient();
    
        QString name() const;
        void setName(const QString &name);
    
        QString address() const;
        void setAddress(const QString &address);
    
        QString version() const;
        void setVersion(const QString &version);
    
        int port() const;
        void setPort(const int &port);
    
    signals:
        void nameChanged();
        void addressChanged();
        void portChanged();
    
    private:
        // Connection-Information
        QString m_name;
        QString m_address;
        QString m_version;
        int m_port;
    
    public slots:
    };
    
    Q_DECLARE_METATYPE(MovieClient)
    #endif // MOVIECLIENT_H
    

    In the Receiver Class the QMap containing the MovieClients is located and on Message received a new Client ist created an added to the QMap. There I also used Q_PROPERTY to make the QMap know:

    #ifndef UDPRECEIVER_H
    #define UDPRECEIVER_H
    
    #include <QObject>
    #include <QHostAddress>
    #include <QUdpSocket>
    #include <QtNetwork>
    #include <QVariant>
    #include <QVariantMap>
    #include <QJsonDocument>
    #include "movieclient.h"
    #include <QMap>
    
    typedef QMap<QString, MovieClient*> movies;
    
    class UdpReceiver : public QObject
    {
        Q_OBJECT
        Q_PROPERTY(movies movieClientList MEMBER movieClients)
    
    public:
        explicit UdpReceiver(QObject *parent = nullptr);
        UdpReceiver(const UdpReceiver &receiver);
        ~UdpReceiver();
        Q_INVOKABLE void clearClientlist();
    
    
    signals:
        void dataReceived();
        void jsonClientListUpdated(QString jCList);
        void movieClientListCleared();
    
    private slots:
        void processPendingDatagrams();
    
    private:
        void createJSON();
        // QMap der Clients
        QMap<QString, MovieClient*> movieClients;
        QUdpSocket udpSocket4;
        QHostAddress groupAddress4;
    };
    
    Q_DECLARE_METATYPE(UdpReceiver)
    
    #endif // UDPRECEIVER_H
    

    in the Main.cpp i set the Context Property for the UdpReceiverClass to Access it from QML

    UdpReceiver uReceiver;
    qRegisterMetaType<MovieClient>("movieClient");
    qRegisterMetaType<movies>("movies");
    engine.rootContext()->setContextProperty("UdpReceiver", &uReceiver);
    

    and when I try to access now in QML the autocomplete goes up to:

    console.log(UdpReceiver.movieClientList)
    

    and there it stops, none of the geter/setter Methods are accessible.
    The console.log when i send this out states "QVariant(QMap<QString,MovieClients*>)" so at least it seem to know the structure but I don't get any real Data from within the QMap.

    Now I kinda wonder how I am able to access the Data or if my try to make the List of Clients is completely wrong. I hope someone can help me with my problem.

    And sorry for my bad English since it's not my native language, I hope you can understand my problem.

    Thanks in Advance



  • @Throndar
    To pass your QMap, define it as a QVariantMap instead (which is just a typedef for QMap<QString, QVariant>
    http://doc.qt.io/qt-5/qvariant.html#QVariantMap-typedef

    Then, there is an automatic conversion for it when passed to QML' JavaScript context.
    http://doc.qt.io/qt-5/qtqml-cppintegration-data.html#qvariantlist-and-qvariantmap-to-javascript-array-and-object

    So, you can get rid of your custom typedef altogether

    typedef QMap<QString, MovieClient*> movies; //<-- NO LONGER NEEDED
    qRegisterMetaType<movies>("movies"); //<-- NOT NEEDED
    

    and define movieClients as

     QVariantMap movieClients;
    

    and

     Q_PROPERTY(QVariantMap movieClientList MEMBER movieClients)
    

    You must then fill your map as e.g.:

    movieClients.insert("124.244.23.22", QVariant::fromValue(new MovieClient("client1", "v1", "124.244.23.22", 22, this)));
    

    (the important part above is the conversion to QVariant).

    Now, you can access the data in JavaScript as:

            var anObject = udpReceiver.movieClientList
    
            console.debug(Object.keys(anObject).length)
    
            for (var prop in anObject) {
                var client = anObject[prop]
                console.debug(client.name + " " + client.address + " " + client.port + " " + client.version)
            }
    

    Good luck.



  • Hello @Diracsbracket ,

    at first, I want to thank you very very much for your help. In QML I can reach the Variant List and this is really awesome :)

    But now I have three smaller new problems:

    1. I create a JSON Document of all the Clients I received to fill a Tableview to be able to choose one from a List. This function also emitted a SIGNAL every time a new entry in the JSON was made.
      With QMap I did this with a for-loop:
    QJsonArray jClients;
    QString jCList;
    
    for(auto client : movieClients.toStdMap())
                {
                    QJsonObject cEntry;
                    cEntry.insert("name", QJsonValue::fromVariant(client.second->name()));
                    cEntry.insert("ipaddress", QJsonValue::fromVariant(client.second->address()));
                    cEntry.insert("version", QJsonValue::fromVariant(client.second->version()));
                    cEntry.insert("port", QJsonValue::fromVariant(client.second->port()));
                    jClients.push_front(cEntry);
                }
        jCList = QJsonDocument(jClients).toJson();
        emit jsonClientListUpdated(jCList);
    

    It seems that I can't access the Properties this way. Is there a way to access the Values without the need of the key in the loop? (all the Answers I found online refered to QVariantMap["key"] but the key is set by the client responsing to a call, so i don't have that in the loop)

    1. When i start a new search, I have to clear the List of the exisiting Clients to make sure no Client is allready exisiting. I did this prior with
    qDeleteAll(movieClients);
    movieClients.clear();
    

    The QDeleteAll seems not to work with the QVariantMap since its no Pointer if I understood the Documentation correctly. Is there a safe way to clear out all the Items stored in the QVariantMap and Clear the QVariantMap itself?

    1. If i want to change some Values in the Objects with the UI, I tried to Change the Properties with the WRITE Q_Property specified in the Class of MovieClient
    console.debug(client.name + " " + client.address + " " + client.port + " " + client.version)
    client.setName("ClientsNewName")
    console.debug(client.name + " " + client.address + " " + client.port + " " + client.version)
    

    But this doesn't work this way. Is there a way to Change the Values of the Propertys after writing them to the QVariantMap or did I just forget something to Access them?

    At the Moment I'm still wondering if it would be a good Idea to make a QVariantMap to show the Stats on the UI with QML and keep the old QMap for C++ to be able to call the set-Function to change Properties. But keeping two complete Sets of identical Data seems kinda a waste of resources.

    Thanks in advance



  • @Throndar said in Access List of Objects in QML:

    At the Moment I'm still wondering if it would be a good Idea to make a QVariantMap to show the Stats on the UI with QML and keep the old QMap for C++

    I think you should consider storing your data in a model instead. This will make everything much simpler, including the interfacing between C++/QML and solve all of the problems you are currently facing.

    Have a look at:
    http://doc.qt.io/qt-5/qtquick-modelviewsdata-cppmodels.html#qabstractitemmodel-subclass



  • Hello @Diracsbracket

    thank you very much again for your help.

    I just tried the last four hours to adept the animal example on the Link you provided to create a ClientModel but I still can't get it working in my Program.

    I already tried the Youtube Tutorial Linked on the Documentation on Model in QML
    https://www.youtube.com/watch?v=9BcAYDlpuT8&feature=youtu.be
    this worked well but seemed to be used to populate a ListView (the only thing that I got to work on my Project without bigger problems with QJson)

    I also tried to work through the QT Documentation on http://doc.qt.io/qt-5/qabstractitemmodel.html but this got me even more confused since it States of a Model-index with rows and columns when I only try to build sets of simple Datatypes.

    Perhaps somebody has some additional source of information / or an Example on how to create a model for data to be used in QML Context? Everything I found so far referred to a ListView instead of a List of Objects

    I'm kinda wondering how hard it is to get a simple thing like a List of Objects with multiple Properties accessible in QML and would really appreciate additional Information to get me started.

    If there is a better / easier Way to store Information about (multiple) Clients (each containing multiple QString, bool, int) in a Struct / Vector or some other way, I am open for all suggestions. The only requirement is to read and write the properties and be able to access them in QML.

    Thanks in advance



  • @Throndar said in Access List of Objects in QML:

    I still can't get it working in my Program.

    Can you show us what you tried?
    Post your basic test code (C++, QML and main.cpp) using dummy (i.e. hard-coded) DB data. That's the only way to see what's missing.



  • Hello @Diracsbracket,

    thanks again for your help. This is the Code for the first four Properties (if everything works each Client should get about 10-20 Properties).

    At the Point of the Code below the manually added Clients are stored in the Model and the Listview shows them (the List is quite ugly, but all the Values are there).

    The most important things that don't work are:
    Since the MovieClientModel is defined in the main.cpp I have found no way to create MovieClients in my "ReceiverClass" and store them in the Model created in main.cpp (the Receiver gets the Response from the Clients and Creates a Client with the Data received) this Created Client have to be added to the model.

    The ListView shows the Data of the manual created Clients, but I still struggle to get Information back from the Model like "if there is a Client 3, show me all his Properties". I found no way to search the model for data contained in it.

    The next important thing and I think it is connected to the one above, I have no idea how to access QProperies in MovieClient Objects or the Functions to set Properties (to Change the Name for Example).

    And since the Listview is not Clickable I tried to use the Model in a TableView who showed all the Items quite nicely.
    With the "onClicked" event if try to get the Information there I could console.log the row but again, no further data. I used the same method that worked on the List generated with JSON:

    onClicked: {
                    console.log(row) // >> working, shows index
                    console.log("Name: " + view2.model.get(row).name) // >> not working
    }
    

    The Model and Model implementation i currently use:

    the MovieClientModel:

    #ifndef MOVIECLIENTMODEL_H
    #define MOVIECLIENTMODEL_H
    
    #include <QAbstractListModel>
    #include "movieclient.h"
    
    class MovieClientModel : public QAbstractListModel
    {
        Q_OBJECT
    public:
        enum MovieClientRoles {
            NameRole = Qt::UserRole,
            AddressRole = Qt::UserRole + 1,
            VersionRole = Qt::UserRole + 2,
            PortRole = Qt::UserRole + 3
        };
    
        MovieClientModel(QObject *parent = nullptr);
    
        void addMovieClient(const MovieClient &movieClient);
        int rowCount(const QModelIndex &parent = QModelIndex()) const;
        QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
    
    protected:
        QHash<int, QByteArray> roleNames() const;
    private:
        QList<MovieClient> m_movieClients;
    };
    
    #endif // MOVIECLIENTMODEL_H
    

    the MovieClientModel.cpp

    #include "movieclientmodel.h"
    
    MovieClientModel::MovieClientModel(QObject *parent)
        : QAbstractListModel (parent)
    {
    
    }
    
    void MovieClientModel::addMovieClient(const MovieClient &movieClient)
    {
        beginInsertRows(QModelIndex(), rowCount(), rowCount());
        m_movieClients << movieClient;
        endInsertRows();
    }
    
    int MovieClientModel::rowCount(const QModelIndex &parent) const
    {
        Q_UNUSED(parent);
        return m_movieClients.count();
    }
    
    QVariant MovieClientModel::data(const QModelIndex &index, int role) const
    {
        if (index.row() < 0 || index.row() >= m_movieClients.count())
            return QVariant();
    
        const MovieClient &movieClient = m_movieClients[index.row()];
        if(role == NameRole)
            return movieClient.name();
        else if(role == AddressRole)
            return movieClient.address();
        else if(role == VersionRole)
            return movieClient.version();
        else if(role == PortRole)
            return movieClient.version();
    
    }
    
    QHash<int, QByteArray> MovieClientModel::roleNames() const
    {
        QHash<int, QByteArray> roles;
        roles[NameRole] = "name";
        roles[AddressRole] = "address";
        roles[VersionRole] = "version";
        roles[PortRole] = "port";
        return roles;
    }
    

    in the Main.cpp:

    
        MovieClientModel model;
        model.addMovieClient(MovieClient("Client 1", "Version 1.2", "192.168.100.94", 2000));
        model.addMovieClient(MovieClient("Client 2", "Version 1.2", "192.168.100.98", 5000));
        model.addMovieClient(MovieClient("Client 3", "Version 1.2", "192.168.100.24", 1000));
    
        engine.rootContext()->setContextProperty("MovieClientModel", &model);
    

    and in QML:

    
            Button {
                id: modelbutton
                anchors.centerIn: parent
                text: "modelTest"
                onClicked: {
                    console.log(MovieClientModel.data(0, NameRole)) // >> not working, tried to Access data from the Model)
                }
            }
    
            ListView {
                id: movieClientModelList
                width: 200; height: 250
                
                model: MovieClientModel
                delegate: Text { text: "MovieClient: " + name + ", " + version + ", " + address + ", "  + port }            
            }
    


  • @Throndar said in Access List of Objects in QML:

    console.log(MovieClientModel.data(0, NameRole))

    You are not using the correct arguments for the data() function.

    QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const;
    

    0 is not a QModelIndex object. You can get the ModelIndex via the index(row, col, parent) property, e.g.

    movieClientModel.index(0,0)
    

    (Note that I renamed the instance by making it start with a lowercase char; the type name has uppercase first char.)

    The C++ enum item NameRole is not directly accessible in QML: You must register it using the Q_ENUM macro, but this requires you to register the MoveClientModel type too.
    http://doc.qt.io/qt-5/qtqml-cppintegration-data.html#enumeration-types

    When you do all the above, you can access the data as:

    console.log(movieClientModel.data(movieClientModel.index(0,0), MovieClientModel.NameRole))
    

    To avoid the hassle of the above syntax, I find it convenient to do this instead:
    Add the following function to your model

    Q_INVOKABLE QVariantMap get(int row);
    

    and implement is as follows:

    QVariantMap MovieClientModel::get(int row)
    {
        QVariantMap res;
        QModelIndex idx = index(row, 0);
    
        res["name"] = idx.data(NameRole);
        res["address"] = idx.data(AddressRole);
        res["version"] = idx.data(VersionRole);
        res["port"] = idx.data(PortRole);
    
        return res;
    }
    

    The above will get you the complete model row, with fields now easily accessible via their names, so in QML you can now do:

    console.log(movieClientModel.get(0).address)
    

    Good luck!



  • Hello @Diracsbracket,

    wow, I thank you so very much for your help.
    With the new Function to get the Object of the Index, I can access all the Data from the Entry and start filling my QML the for displaying the Properties.

    I only have two question if you could help me again.

    Is there a way to create MovieClients an add them to the MovieClientModel located in main from my Receiver class?

    With the Test-MovieClients in the Main hardcoded build everything works fine, but since the answer from the Clients comes via TCP I let my Receiver-Class do the dynamic MovieClient-Creation till now.
    In the Moment my Receiver-Class checks if there arrives Data on a specific address&port, parses it and if the message fits the expected message-definition from a Client it creates a client-object with the parameters received. Now I wonder how I can pass this MovieClient to the MovieClientModel on Creation.

    The other Question states the changing of a MovieClient.
    I think this could be done with a set-method similar to the get you provided to me. Or do you think it's better to delete the existing entry and create a new client with the new properties?



  • @Throndar said in Access List of Objects in QML:

    Is there a way to create MovieClients an add them to the MovieClientModel located in main from my Receiver class?

    Providing your Receiver object a reference to your model seems to be a straight-forward approach, no? Then you can invoke addMovieClient() from there.

    Or do you think it's better to delete the existing entry and create a new client with the new properties?

    That's completely up to you, but my first approach would be to edit the model entry for a given client rather than to delete and recreate it.



  • @Diracsbracket said in Access List of Objects in QML:

    Providing your Receiver object a reference to your model seems to be a straight-forward approach, no? Then you can invoke addMovieClient() from there.

    You are completely right (as always) and i really appreciate your help. At this spot, I am missing something. So far I used References for Function calls to let them work with the original Values. So I get what you intend to do.

    Just like:

    void myFunction(int &value){
            value = 10;
    }
    

    At first I tried to pass a reference to the MovieClientModel on the creation and changed to constructor on my Receiver, this resulted in all connect Statements to state "no matching function to call to connect".

    The next attempt was to set a "setModel(&ClientModelView)" Function, this resulted in the Error that MovieClientsModel in the Receiver cannot be assigned with "=" to the passed Value.

    Now I am completely unsure if I am at the right spot at all or if the passing of the Reference for Objects created in main.cpp is different from normal Functioncalls with a reference passed over.



  • @Throndar
    This thread is starting to look like a tutorial in programming...
    I think you should review your C++ basics, as you now really have all the info you need to accomplish what you need.



  • Hello @Diracsbracket

    if think you are right again.

    I never needed to pass References to own Classes or Models till now. So used the usual call of:

    UdpReceiver uReceiver(&model);
    
    with the Constructor of
    UdpReceiver(const MovieClientModel &model, QObject *parent = nullptr);
    

    after checking the Code again it seemed the Reason for the Errors where the CopyConstructor and Q_DECLARE_METATYPE Macro I used, trying to get the original QMap working in QML.

    After removing these two the Call with the Reference to the MovieClientModel seem ok, now just the initialization of with the Reference make some problems.

    Once again I thank you for your help and the Problems showed me that I need to clean up all the Stuff after trying something that didn't work and with the new AbstractModel I created with your tips i got huge step further.


Log in to reply