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

updating elements in a repeater?



  • Hi all -

    I know this code is coyote-ugly, but bear with me:

    Column {
        id: myColumn
        property var myArray: []
        property var cellXArray: [25, 25, 102];
        property var cellYArray: [105, 15, 125];
        property var cellColorArray: ['red', 'red', 'red']
        Repeater {
    
            model: 3//myColumn.myArray.length
            Bottle {
                cellX: myColumn.cellXArray[index]
                cellY: myColumn.cellYArray[index]
                cellColor: myColumn.cellColorArray[index]
            }
        }
    }
    

    When this component becomes active, it calls a function that includes this:

    myColumn.cellColorArray[0] = 'green'
    

    How do I get this change to be reflected in the first element of the repeater?

    Thanks...

    EDIT: oh: Bottle is just a component I made, based on a Rectangle.



  • Models of type number cannot be updated since they do not notify the change, those of type list are only updated if the entire list is replaced, so in your case it is better to use a ListModel:

        ListModel {
            id: bottlemodel
    
            ListElement {
                x: 25
                y: 105
                color: "red"
            }
    
            ListElement {
                x: 25
                y: 15
                color: "red"
            }
    
            ListElement {
                x: 102
                y: 125
                color: "red"
            }
    
        }
    
        Repeater {
            model: bottlemodel
    
            Rectangle {
                x: model.x
                y: model.y
                width: 20
                height: 20
                color: model.color
            }
    
        }
        Component.onCompleted: () => {
            bottlemodel.setProperty(0, "color", "green");
        }
    


  • Repeater.itemAt(index) will return the item, at which point the code can assign to its properties. Another option is to bind the property in the delegate to a property in something not managed by the Repeater. Creating a binding is more aesthetically pleasing to me. itemAt() should be more efficient if the goal is to change one item out of many.


  • Moderators

    See what @jeremy_k and @eyllanesc wrote. But as food for thought, doesn't this make more sense:

    Column {
        id: myColumn
        property list data: [
            QtObject {
                property int x: 25
                property int y: 105
                property string color: 'red'
            },
            QtObject {
                ...
            }
            ...
        ]
    
        Repeater {
            model: myColumn.data
            Bottle {
                cellX: modelData.x
                cellY: modelData.y
                cellColor: modelData.color
            }
        }
    }


  • Lots of good options here; thanks, guys.

    @eyllanesc: my OP might have been a bit misleading in that, when this view is presented, there will be lots more changes; I was just giving an example. There will be 16-19 bottles displayed, and each will have its color updated, and some other attributes set/updated. I don't know if this changes your opinion on how to approach this problem.

    I removed your Component.oncompleted in favor of something like this:

        onVisibleChanged: {
            var modelSize = bottleModel.count
            var i;
            var color;
            for (i = 0; i < modelSize; ++i) {
                color = (myColumn.myArray[i].volume > myColumn.myArray[i].amountNeeded) ? "green" : "red"
                bottleRepeater.itemAt(i).cellColor = color
        }
    

    This ensures a refresh whenever the view is activated.

    @jeremy_k I tried your first suggestion, and it works great. Could you possibly elaborate on your second suggestion? I'd like to hear more about it.

    @kshegunov your approach looks great, but I'm curious as to exactly what about it you prefer over the others.

    Thanks again...this has been very helpful.



  • @mzimmers What is myArray? I think that if you provide a minimal verifiable example you could give yourself a much more optimal answer. For example I suspect that "myArray" may be the Repeater model, and the color should not be a new property but just a binding something like: color: modelData.volume > modelData.amountNeeded ? "green" : "red"



  • @eyllanesc

        Column {
          id: myColumn
          property var myArray: []
    ...
        function updateArray() {
          var arr = []
          BottleList.getBottleList().forEach(function (bottle) {
            arr.push(bottle)
          }, this)
          myColumn.myArray = arr
        }
    

    It is (indirectly) constructed from a C++ list of stuct Bottle (not to be confused with the Bottle QML Component).

    Normally I try to give repeatable examples, but this application is a bit of a mare's nest, so I was trying to contain the information conveyed to what I felt was relevant.

    It occurs to me that all the data relevant to this view falls into 2 categories:

    1. data that is innate to the actual bottles (name, fill level, capacity, ingredient, etc)
    2. data that is only relevant to the UI (size on screen, position)

    I'm trying to keep these separate (maybe this isn't such a good idea)...anything from the first category, I try to get from my C++ model. Anything in the second category, I'm happy to contain within the QML code.



  • @mzimmers I think that if it is necessary that you provide the implementation of Bottle and that BottleList since the implementation of the view depends on the implementation of the data methods. In the case of Qt classes, it is not necessary to provide the source code because the docs replace it, but in your case we do not know the code. Provide the code and then we will point you to a better solution. On the other hand that is cellY, cellY since a Column does not have that attribute.



  • Sure:

    struct Bottle {
      Q_GADGET
      Q_PROPERTY(quint32 volume MEMBER m_volume)
      Q_PROPERTY(quint32 amountNeeded MEMBER m_amountNeeded)
      Q_PROPERTY(int position MEMBER m_position)
      Q_PROPERTY(QString name MEMBER m_name)
      Q_PROPERTY(ReagentBottleType bottleType MEMBER m_bottleType)
     public:
      quint32 m_volume;                // amount in bottle (in uL)
      quint32 m_amountNeeded;          // amount needed for synth (in uL)
      int m_position;                  // still figuring this one out
      QString m_name;                  // name of the reagent
      ReagentBottleType m_bottleType;  // bottle type.
    };
    ...
    typedef QVector<Bottle> Bottles;
    ...
    class BottleList : public QObject {
      Q_OBJECT
     private:
      Bottles m_bottleList;
      ...
    


  • @mzimmers The following is a trivial example:

    main.cpp

    #include <QGuiApplication>
    #include <QQmlApplicationEngine>
    #include <QQmlContext>
    
    struct Bottle {
        Q_GADGET
        Q_PROPERTY(quint32 volume MEMBER m_volume)
        Q_PROPERTY(quint32 amountNeeded MEMBER m_amountNeeded)
        Q_PROPERTY(int position MEMBER m_position)
        Q_PROPERTY(QString name MEMBER m_name)
    public:
        quint32 m_volume;                // amount in bottle (in uL)
        quint32 m_amountNeeded;          // amount needed for synth (in uL)
        int m_position;                  // still figuring this one out
        QString m_name;                  // name of the reagent
    };
    
    typedef QVector<Bottle> Bottles;
    
    class BottleManager: public QObject{
        Q_OBJECT
        Q_PROPERTY(QVariantList bottles READ bottles NOTIFY bottlesChanged)
    public:
        QVariantList bottles() const{
            return m_bottles;
        }
        void append(const Bottle & bottle){
            m_bottles << QVariant::fromValue(bottle);
            Q_EMIT bottlesChanged();
        }
        void clear(){
            m_bottles.clear();
            Q_EMIT bottlesChanged();
        }
    Q_SIGNALS:
        void bottlesChanged();
    private:
        QVariantList m_bottles;
    };
    
    int main(int argc, char *argv[])
    {
    #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
        QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
    #endif
    
        BottleManager manager;
        manager.append({100, 10, 100, "item1"});
        manager.append({10, 100, 10, "item2"});
        manager.append({100, 10, 10, "item3"});
        manager.append({100, 10, 10, "item4"});
        manager.append({10, 100, 10, "item5"});
    
        QGuiApplication app(argc, argv);
    
        QQmlApplicationEngine engine;
        engine.rootContext()->setContextProperty("bottlesmanager", &manager);
        const QUrl url(QStringLiteral("qrc:/main.qml"));
        QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
                         &app, [url](QObject *obj, const QUrl &objUrl) {
            if (!obj && url == objUrl)
                QCoreApplication::exit(-1);
        }, Qt::QueuedConnection);
        engine.load(url);
    
        return app.exec();
    }
    
    #include "main.moc"
    

    main.qml

    import QtQuick 2.12
    import QtQuick.Window 2.12
    
    Window {
        width: 640
        height: 480
        visible: true
        title: qsTr("Hello World")
        Column{
            Repeater{
                model: bottlesmanager.bottles
                Rectangle{
                    width: 100
                    height: 100
                    border.color: "black"
                    color: model.modelData.volume < model.modelData.amountNeeded ? "green": "red"
                    Text{
                        anchors.centerIn: parent
                        text: model.modelData.name
                    }
                }
            }
        }
    }
    

    Screenshot_20210311_142255.png



  • Thanks for the detailed example. I understand most of it, but...what is this for?

     QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
                         &app, [url](QObject *obj, const QUrl &objUrl) {
            if (!obj && url == objUrl)
                QCoreApplication::exit(-1);
        }, Qt::QueuedConnection);
    


  • @mzimmers hmm, that's part of the current Qt template for those kinds of projects. Previously it was verified that there is at least one rootObject but that does not guarantee that it works, so now it is preferred to verify using the objectCreated signal



  • @eyllanesc ah...OK, that's being taken care of elsewhere in the project setting, so I don't need to deal with that. (I do set the context property.)

    So, I guess the advantage of this approach is, no duplication of data?

    Nit: your comparison of volume/amountNeeded is backwards.


  • Moderators

    @mzimmers said in updating elements in a repeater?:

    @kshegunov your approach looks great, but I'm curious as to exactly what about it you prefer over the others.

    Because I assume that at some later time you/I/whoever are/am/is going to want to tie it with a C++ backend. So, I'd rather not stick to QtQuick items, but either directly expose an array of QObejcts or define a QAbstractItemModel and use that. It's not better, it's just that I've learned over the years that requirements have this peculiar property of changing themselves midway.

    @mzimmers said in updating elements in a repeater?:

    Thanks for the detailed example. I understand most of it, but...what is this for?

    QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
                         &app, [url](QObject *obj, const QUrl &objUrl) {
            if (!obj && url == objUrl)
                QCoreApplication::exit(-1);
        }, Qt::QueuedConnection);
    

    This is what is emitted when a quick item is created through a component that's loaded from a file (either your main file, or with a Loader). It's a dummy as it just kills the application if there's an error, but you could possibly attach there to handle the failure if you wish and if you allow your UI to, say, be edited without recompiling the application.

    @eyllanesc ah...OK, that's being taken care of elsewhere in the project setting, so I don't need to deal with that. (I do set the context property.)

    So, I guess the advantage of this approach is, no duplication of data?

    You're now being naive. ;)

    This:

    Text{
        anchors.centerIn: parent
        text: model.modelData.name
    }
    

    copies the QString (a shallow copy).



  • @kshegunov said in updating elements in a repeater?:

    Because I assume that at some later time you/I/whoever are/am/is going to want to tie it with a C++ backend. So, I'd rather not stick to QtQuick items, but either directly expose an array of QObejcts or define a QAbstractItemModel and use that.

    Agreed 100%. In this spirit, I'm trying to restructure my application like this:

    1. I have a struct Bottle, based on Q_GADGET. This contains information intrinsic to the bottle (size, dimensions, contents, etc.).
    2. I have a class ReagentManager that contains a private QVector of Bottles. An object of this class is registered as a context property to make it visible to the QML.
    3. One QML view contains a repeater for bottles, containing UI-specific information (size, screen location).

    So...while my ReagentManager class is visible to the QML, the QVector of Bottles is not. I can write Q_INVOKABLE access routines for each of them, but I'm curious as to whether there might be a better way of doing it.

    Thanks for any input.

    My C++ code maintains an instance of the class ReagentManager, so I'm confident that its contents are always current. Now: how best to do something like this:


  • Moderators

    @mzimmers said in updating elements in a repeater?:

    So...while my ReagentManager class is visible to the QML, the QVector of Bottles is not. I can write Q_INVOKABLE access routines for each of them, but I'm curious as to whether there might be a better way of doing it.

    Make the bottles QObject instead of them being Q_GADGET and expose their properties (look up the Q_PROPERTY docs and be sure to have the notification signals). After that the QML part remains pretty much the same, the change in the QObject is going to be reflected naturally into the QML scene without anything more than you binding the properties on creation.



  • @kshegunov thanks. I'm still a little UNclear on the binding details; what would be an example of a bind using your code above?


  • Moderators

    Bottle {
        cellX: modelData.x //< If modelData is QObject, this is a property binding
        cellY: modelData.y
        cellColor: modelData.color
    }
    


  • @kshegunov

    Trying this:

    struct Bottle : public QObject {
      Q_OBJECT
      Q_PROPERTY(quint32 volume MEMBER m_volume NOTIFY volumeChanged)
      Q_PROPERTY(quint32 minVolume MEMBER m_minVolume NOTIFY minVolumeChanged)
      Q_PROPERTY(quint32 amountNeeded MEMBER m_amountNeeded NOTIFY amountNeededChanged)
      Q_PROPERTY(int slotNumber MEMBER m_slotNumber)
      Q_PROPERTY(QString name MEMBER m_name)
      Q_PROPERTY(ReagentBottleType bottleType MEMBER m_bottleType)
    
     public:
      // needed to represent amounts in int, not float
      // because using floats causes a float-equal error
      // in the generated MOC file.
      quint32 m_volume;                // amount in bottle (in uL)
      quint32 m_minVolume;             // amount in bottle that can't be used (in uL)
      quint32 m_amountNeeded;          // amount needed for synth (in uL)
      int m_slotNumber;                // still figuring this one out
      QString m_name;                  // name of the reagent
      ReagentBottleType m_bottleType;  // bottle type.
    signals:
      void volumeChanged();
      void minVolumeChanged();
      void amountNeededChanged();
    };
    

    Getting this when I try to build:

    /home/mzimmers/git/KOL-UI/src/lib/change_consumables/reagent_manager.h:11: error: use of deleted function ‘QObject::QObject(const QObject&)’
    In file included from /home/mzimmers/git/KOL-UI/src/lib/change_consumables/reagent_manager.cpp:7:0:
    /home/mzimmers/git/KOL-UI/src/lib/change_consumables/reagent_manager.h:11:8: error: use of deleted function ‘QObject::QObject(const QObject&)’
     struct Bottle : private QObject {
            ^~~~~~
    

    Do I have to convert Bottle from a struct to a C++ class?



  • @mzimmers QObject is not copyable so you have to remove the copy constructor from Bottle.


  • Moderators

    @mzimmers said in updating elements in a repeater?:

    Do I have to convert Bottle from a struct to a C++ class?

    No, but you can't keep the objects in a QVariantList, because QObjects can't be copied. You need to switch to QList<QObject *>.

    PS.
    Additional notes:

    1. The notifier signals should report the new value of the property (look up the documentation examples).
    2. If you need to compare floats, then you should probably stick to defining your own setters and getters and registering them with READ/WRITE in the Q_PROPERTY definition instead of relying on MEMBER.


  • @kshegunov said in updating elements in a repeater?:

    No, but you can't keep the objects in a QVariantList, because QObjects can't be copied.

    I must be going crazy, but...where in my struct am I using QVariantList?

    Thanks for the note about including the new values in the signals.

    I'm avoiding floats for the reason you cited, plus a couple more.


  • Moderators

    @mzimmers said in updating elements in a repeater?:

    I must be going crazy, but...where in my struct am I using QVariantList?

    Nowhere, but how do you marshal the objects to QML?



  • @kshegunov said in updating elements in a repeater?:

    @mzimmers said in updating elements in a repeater?:

    I must be going crazy, but...where in my struct am I using QVariantList?

    Nowhere, but how do you marshal the objects to QML?

    Well, like this (I think this is what you're asking):

        Column {
          id: myColumn
          Repeater {
            id: bottleRepeater
            model: bottleModel
            Bottle {
              cellX: model.x
              cellY: model.y
              cellHeight: model.height
              cellWidth: model.width
              volume: model.volume
              bottleScaleFactor: scaleFactor
            }
    

    But if I'm doing something wrong in QML, why is the compiler giving me an error pointed at my struct?


  • Moderators

    @mzimmers said in updating elements in a repeater?:

    Well, like this (I think this is what you're asking):

    I mean from the C++ side. What is bottleModel and where does it come from?



  • From the same QML file:

      ListModel {
        id: bottleModel
    
        ListElement {
          // position 1
          x: 400
          y: 17
          height: 75
          width: 75
        }
        ...
    

  • Moderators

    This is confusing, I thought the data is supposed to come from C++.



  • I know, I'm doing a lousy job of explaining this. in C++:

    typedef QVector<Bottle> Bottles;
    class ReagentManager : public QObject {
      Q_OBJECT
     private:
      Bottles m_bottleList;
    }
    <in another file>
      ReagentManager m_reagentManager;
      engine->rootContext()->setContextProperty("reagentManager", &m_reagentManager);
    

    In QML:

        onVisibleChanged: {
          if (visible) {
            reagentManager.updateBottleList()
            rack.updateBottles()
          }
        }
    
        // update our QML array based on the C++ model.
        function updateBottles() {
          var modelSize = bottleModel.count
          var i
          var l_color
          var volume
          var minVolume
          var amountNeeded
          var name
    
          for (i = 0; i < modelSize; ++i) {
            name = reagentManager.getName(i)
            bottleRepeater.itemAt(i).cellText = name
    
            volume = reagentManager.m_volume
            minVolume = reagentManager.getMinVolume(i)
            amountNeeded = reagentManager.getAmountNeeded(i)
            l_color = ((volume - minVolume) >= amountNeeded) ? "green" : "red"
            bottleRepeater.itemAt(i).cellColor = l_color
          }
        }
    

    So, my QML function calls C++ routines to obtain the needed data. I'm trying to convert this to the approach you suggested; this is where I ran into the problem with the struct.

    I still don't see where the QVariantList comes into play, though.


  • Moderators

    typedef QVector<Bottle> Bottles;
    

    If Bottle is derived from QObject you can't keep it directly in a vector (can't copy the objects). You need to keep Bottle * there.



  • @kshegunov ah. OK, thanks for that. I'll look into making that change, and will report back.



  • I've changed my QVector to pointers (seems to be working).

    I think I see why my example is so confusing -- in my Bottle Repeater, I'm using a QML ListModel. This ListModel only contains UI-specific information (position, size). Additional informationmust come from an object (ReagentManager) that is exposed via a call to setContextProperty().

    So: do I need to change my model to the instance of ReagentManager, and find another way to access the information currently in my ListModel? Or is there a more clever way to do this?

    Thanks...


  • Moderators

    @mzimmers said in updating elements in a repeater?:

    So: do I need to change my model to the instance of ReagentManager, and find another way to access the information currently in my ListModel? Or is there a more clever way to do this?

    Why do you need the ListModel is the question now. Pass the objects vector from the context property (i.e the bottleManager object) directly to the repeater.



  • @kshegunov said in updating elements in a repeater?:

    Why do you need the ListModel is the question now. Pass the objects vector from the context property (i.e the bottleManager object) directly to the repeater.

    I'm using the list model for UI-specific information:

      ListModel {
        id: bottleModel
    
        ListElement {
          // position 1
          x: 400
          y: 17
          height: 75
          width: 75
        }
        ...
    

    I could probably put this data in arrays in QML. As I mentioned, I'd prefer not to put it into my C++ as it's purely UI data.

    From my definition of the ReagentManager class:

    class ReagentManager : public QObject {
      Q_OBJECT
     public:
      explicit ReagentManager(QObject *parent = nullptr);
      Bottles m_bottleList;
      Q_PROPERTY(Bottles bottleList MEMBER m_bottleList)
      ...
    

    And in my QML:

        function updateBottles() {
          var modelSize = bottleModel.count
          var i
          var volume
    
          for (i = 0; i < modelSize; ++i) {
            volume = reagentManager.bottleList[i].volume
        ...
    

    I'm getting an error "TypeError: Cannot read property '0' of undefined." Is my syntax wrong, or am I still not understanding how to access the property?

    Thanks...

    EDIT: looking at the QML Debugger Console, I see this message:

    QMetaProperty::read: Unable to handle unregistered datatype 'Bottles' for property 'ReagentManager::bottleList'
    

  • Moderators

    @mzimmers said in updating elements in a repeater?:

    I could probably put this data in arrays in QML. As I mentioned, I'd prefer not to put it into my C++ as it's purely UI data.

    You already have this data in the c++, you feed it to the JS engine as far as I can see. I'd just tie the cpp backend to the UI stuff and not mess with any intermediate stuff, much less using explicit JS code.

    Basically like this:

    Column {
          Repeater {
            model: reagentManager.bottleList //< This being a property that holds the list of bottles
            Bottle {
              cellX: modelData.x
              cellY: modelData.y //< Or use some other value, or nothing, however you decide to do it
              ...
              volume: modelData.volume //< This binds to the property of the QObject, so when the C++ changes value this updates
              ...
            }
         ...
    }
    

    @mzimmers said in updating elements in a repeater?:

    I'm getting an error "TypeError: Cannot read property '0' of undefined." Is my syntax wrong, or am I still not understanding how to access the property?
    Thanks...
    EDIT: looking at the QML Debugger Console, I see this message:

    From the method:

    https://doc.qt.io/qt-5/qqmllistproperty.html
    or
    you return QList<QObject *>
    or
    you return a QVariantList() << bottle1 << bottle2

    ... different possibilities.



  • I've changed my typedef from a QVector to a QList:

    typedef QList<Bottle *> Bottles;
    

    My C++ looks like this (stripped down) now:

    struct Bottle : public QObject {
      Q_OBJECT
      Q_PROPERTY(quint32 volume MEMBER m_volume)
     public:
      quint32 m_volume;                // amount in bottle (in uL)
     signals:
      void volumeChanged(quint32 m_volume);
    };
    typedef QList<Bottle *> Bottles;
    
    class ReagentManager : public QObject {
      Q_OBJECT
     private:
      Bottles m_bottleList;
     public:
      Q_INVOKABLE Bottles bottleList() { return m_bottleList; }
      void bottleListChanged();
    };
    

    And this line in QML:

            volume = reagentManager.bottleList[i].volume
    

    Now produces this error:

    qrc:/qml/Rack.qml:194: TypeError: Cannot read property 'volume' of undefined 
    

    So...I've kicked the can a little further down the street, but I'm not home yet. It seems that the QML can now access the QList (agree?), but not its properties. Do I need a getter for this? I thought the idea of getting the properties working properly was to eliminate the need for the getters/setters.



  • @mzimmers said in updating elements in a repeater?:

    volume = reagentManager.bottleList[i].volume

    bottleList is a function but you are accessing like an array

    volume = reagentManager.bottleList()[i]



  • @LeLev said in updating elements in a repeater?:

    @mzimmers said in updating elements in a repeater?:

    volume = reagentManager.bottleList[i].volume

    bottleList is a function but you are accessing like an array

    volume = reagentManager.bottleList()[i]

    Thanks...I thought this would work:

     volume = reagentManager.bottleList()[i].volume
    

    But I get this error:

    Error: Unknown method return type: Bottles
    

    This goes back to what kshegunov was talking about earlier, but I thought I'd fixed this by changing from a QVector to a QList:

    typedef QList<Bottle *> Bottles;
    
    class ReagentManager : public QObject {
      Q_OBJECT
     public:
      Q_INVOKABLE Bottles bottleList() { return m_bottleList; }
    

    I'm still missing something...



  • @mzimmers said in updating elements in a repeater?:

    I'm still missing something...

    i would return a QVariantList to qml instead of the QList<Bottle *>



  • @LeLev OK, but...how do I do that? I can't just cast it. Do I have to change my Bottles typedef?

    Thanks...



  • Q_INVOKABLE QVariant bottleList() { return QVariant::fromValue(m_bottleList); }


Log in to reply