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

accessing elements of a ListModel



  • Hi all -

    I need to extract a QML Text type (or equivalent) from one of two ListModels.

    The view represents bottles in a rack. The size of the rack is determined from an environment variable, and can be one of two values (16 or 19 bottles). But...the rack isn't always full.

    I have 2 ListModels for the positioning and sizing of the bottle slots:

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

    Model selection is as follows:

    Column {
      id: bottles
      Repeater {
    	id: bottleRepeater
    	model: (nbrBottlesInRack === 16) ? bottleModel_16 : bottleModel_19
      ...
      }
    

    A function reads a list of bottles from a ViewModel:

    function updateBottles() {
    var listSize = nbrBottlesInRack
    var i
    var bottle
    
    for (i = 0; i < listSize; ++i) {
      bottle = reagentManager.bottleList[i]
      if (bottle === undefined) {
    	bottleRepeater.itemAt(i).cellText = "??"
            bottleRepeater.itemAt(i).cellColor = "white"
      } else {
         ...
      }
      ...
    }
    

    Sometimes, the number of bottles returned won't match the number of slots in the rack. For these slots, I'm currently coloring them in white, and giving them a label of "??" as you can see.

    The issue: I'd like to add a default text field to each element in the models. Something like this (but I know this won't work):

        ListElement {
          // position 1
          x: 400
          y: 17
          height: 75
          width: 75
          Text {
              id: text
              text: qsTr("ETH")
          }
        }
    

    So, two questions:

    • how do I add a text field to a ListElement?
    • how do I access this field in my updateBottles() function above?

    Thanks...



  • Why aren't you modifying the source data (ie the ListModel)?
    If you modify the value using itemAt of the repeater you may be breaking the binding to the data in the ListModel.
    ListElements are not Items and cannot hold Items.



  • I am confused as to what you are trying to accomplish with using itemAt.



  • @fcarney said in accessing elements of a ListModel:

    I am confused as to what you are trying to accomplish with using itemAt.

    I'm using itemAt() to access the elements of the repeater. It seems to work...not a good idea?



  • @mzimmers said in accessing elements of a ListModel:

    ListElement {
    // position 1
    x: 400
    y: 17
    height: 75
    width: 75
    Text {
    id: text
    text: qsTr("ETH")
    }
    }

    Why is using:

     ListElement {
          // position 1
          x: 400
          y: 17
          height: 75
          width: 75
          text: qsTr("ETH")
        }
    

    not an option?



  • @fcarney said in accessing elements of a ListModel:

    Why aren't you modifying the source data (ie the ListModel)?
    If you modify the value using itemAt of the repeater you may be breaking the binding to the data in the ListModel.
    ListElements are not Items and cannot hold Items.

    That's essentially what my 2nd question was: how do I refer to the model from a function? More specifically, [how] can I loop through the members of the list, and modify them? I don't know the syntax for doing this.



  • @Diracsbracket I'm sure that is a good option. My problem isn't how to populate the model; it's how to extract information from individual elements from within a loop.



  • @mzimmers said in accessing elements of a ListModel:

    My problem isn't how to populate the model;

    I'm afraid I don't get it indeed. As far as I understand it, your problem is exactly how to populate (in this case, modify) the model. As @fcarney said, the way to go is to update the model itself.
    And you can access elements of the model using the get() method of the model.



  • @Diracsbracket thank you for that -- I actually discovered the get() function just after my previous post (doh).

    So...that's how I extract or modify the data in the ListModel. Now...how do I get that data to display? Currently, unless I update the repeater (which fcarney said I shouldn't do), I don't seem to pick up the text in the model.

    Thanks again...



  • @mzimmers
    If the model data is updated, the repeater delegates should automatically refresh as well, assuming that you nowhere have broken the bindings.

    import QtQuick 2.15
    import QtQuick.Controls 2.15
    import QtQuick.Window 2.15
    
    Window {
        width: 640
        height: 480
        title: 'UmTreeView'
        visible: true
    
        ListModel {
            id: lmodel
            ListElement{
                txt: "text1"
            }
            ListElement{
                txt: "text2"
            }
        }
    
        Column {
            Repeater {
                model: lmodel
    
                delegate: Text {
                    text: txt
                }
            }
        }
    
        Button {
            y: 100
            text: "Button"
            onClicked: {
                for (var i=0; i<lmodel.count;++i)
                {
                    lmodel.get(i).txt = "Hello"
                }
            }
        }
    }
    


  • @Diracsbracket OK, I follow that (I think). Your example gave me the idea to do this:

    ListModel {
    id: bottleModel_19
    
      ListElement {
        // position 1
    	bottleLabel: "ETH"
      }
      ...
      
    Column {
      id: bottles
      Repeater {
        id: bottleRepeater
        model: (nbrBottlesInRack === 16) ? bottleModel_16 : bottleModel_19
        Bottle {
          cellText: model.bottleLabel
    

    It seems to work OK...does it look all right to you?



  • @mzimmers said in accessing elements of a ListModel:

    cellText: model.bottleLabel

    The properties of the ListElement should be accessible as just their names:

    cellText: bottleLabel
    

    The names of the properties should be showing up as accessible properties within the delegate Bottle.



  • @fcarney that worked for the bottleLable property, but not the others. Perhaps because the other properties' names are the same as properties local to Bottle, and the interpreter gets confused?

    On the subject of eliminating my use of itemAt() in the repeater: how do you propose I re-implement this?

    function updateBottles() {
      ...
      for (i = 0; i < listSize; ++i) {
        bottle = reagentManager.bottleList[i]
        if (bottle === undefined) {
          bottleRepeater.itemAt(i).cellColor = Theme.neutralLight
        } else {
          bottleRepeater.itemAt(i).cellColor = Theme.dark
        }
        ...
    

    This function is called whenever the view becomes visible, to update the bottle data from a C++ Q_PROPERTY.

    EDIT:

    I've replaced this functionality as follows:

      Column {
      Repeater {
        Bottle {
        cellColor: rack.getColor(index)
    
    function getColor(i) {
      var l_color
      var volume
      var minVolume
      var amountNeeded
      var bottle
    
      bottle = reagentManager.bottleList[i]
      if (bottle === undefined) {
        l_color = Theme.neutralLight
      } else {
        volume = bottle.volume
        minVolume = bottle.minVolume
        amountNeeded = bottle.amountNeeded
        l_color = ((volume - minVolume) >= amountNeeded) ? "green" : "red"
      }
      return l_color
    }
    

    This eliminates the write to the repeater that fcarney said is a bad idea, but I get the impression I'm still not really doing this right and/or making unnecessary work for myself. Thoughts?

    Thanks...

    EDIT 2:

    So, I changed my Bottle delegate (is that the correct term?) to look like this:

            Bottle {
              cellX: model.x
              cellY: model.y
              cellHeight: model.height
              cellWidth: model.width
              cellText: (reagentManager.bottleList[index] !== undefined)
                        ? reagentManager.bottleList[index].name
                        : bottleLabel
              cellColor: rack.getColor(index)
              bottleScaleFactor: scaleFactor
            }
    

    I believe that I can now do away with the updateBottles() function completely. How does this look to the experienced people here?



  • @mzimmers said in accessing elements of a ListModel:

    if (bottle === undefined) {
    bottleRepeater.itemAt(i).cellColor = Theme.neutralLight
    } else {
    bottleRepeater.itemAt(i).cellColor = Theme.dark
    }

    Bottle {
      cellColor: reagentManager.bottleList[index] === undefined ? Theme.neutralLight : Theme.dark
    }
    

    Assuming a 1:1 relationship to repeater and bottleList.
    All properties in Bottle should be set from information flowing into it in a declarative manner.
    QML is a declarative language. Writing functions to set things is sometimes unavoidable, but should be minimized.



  • @fcarney noted. The logic is actually a little more complicated than I showed in my example. Here's the real function:

    function getColor(i) {
            var l_color
            var volume
            var minVolume
            var amountNeeded
            var bottle
    
            bottle = reagentManager.bottleList[i]
            if (bottle === undefined) {
                l_color = Theme.neutralLight
            } else {
                volume = bottle.volume
                minVolume = bottle.minVolume
                amountNeeded = bottle.amountNeeded
                l_color = ((volume - minVolume) >= amountNeeded) ? "green" : "red"
            }
            return l_color
    

    I'd be happy to do away with this if it didn't make the QML too wordy...



  • Are you taking data from 2 sources? If so you can build up a ListModel and combine multiple sources of data using a function. Then use that model as the model for your repeater.

    The set function of ListModel takes a js object as input. Any time the lists change you can rebuild that list which will automatically update all the Bottles in the repeater.



  • @fcarney said in accessing elements of a ListModel:

    Are you taking data from 2 sources?

    Yes I am: I have the positions and dimensions in a QML ListModel. The "real" information about the bottle (size, contents, expiration date, etc) comes from a view model.

    If so you can build up a ListModel and combine multiple sources of data using a function. Then use that model as the model for your repeater.

    I like the sound of this. How do I put this in a loop, so I don't have to do something like this?

      ListModel {
        id: bottleModel_19
    
        ListElement {
          // position 1
          x: 407
          y: 18
          height: 78
          width: 78
          bottleLabel: (reagentManager.bottleList[index] !== undefined)
                       ? reagentManager.bottleList[index].name
                       : "ETH"
        }
        ...
    


  • No, use the methods for ListModel like insert.

    onChangeOfSourceList: {
      bottleModel_19.clear()
      for(var count=0; count<x; ++count){ 
        // insert, get, and set use js objects
        bottleModel_19.insert(count, {
          "x":list1[count].whatever,
          ...  // repeat for items from both lists
        })
      }
    }
    

    Edit: Maybe append is better. Also clear list each time you recreate the list.



  • Does onChangeOfSourceList correspond to the signal I emit when I've updated the list? Here's my property:

      Q_PROPERTY(Bottles bottleList
                     MEMBER m_bottleList
                         NOTIFY bottleListChanged)
    

    I tried onbottleListChanged, but this isn't correct.



  • Yes, something that emits a signal that you are calling when you update lists.
    It will be:

    onBottleListChanged
    

    Notice the B gets capitalized. Is there another signal that can trigger this too? Then you will need to create a function that gets called in each instance.

    function updateListModel(){
      ...
    }
    onBottleListChanged: updateListModel()
    onOtherListChanged: updateListModel()
    


  • @fcarney to which component is the onBottleListChanged: applied? I've tried putting this in several places in my QML file, and the editor warns of an invalid property name. Am I supposed to put this in a Connections object?

    I don't have another signal for a bottle list update. Earlier you asked:

    Are you taking data from 2 sources?

    Yes I am. I put some data in the QML ListModels. This is data pertaining to the UI itself (screen location of the representation, size, etc.) "Real" data about the bottles (label, contents, fill level) is kept in a C++ object. This seemed like a natural breakdown for the data; is using two sources a bad idea?

    Thanks...



  • @mzimmers said in accessing elements of a ListModel:

    Q_PROPERTY(Bottles bottleList
    MEMBER m_bottleList
    NOTIFY bottleListChanged)

    What object has this in it?



  • @fcarney

    class ReagentManager : public QObject {
      Q_OBJECT
     public:
      Q_PROPERTY(Bottles bottleList
                     MEMBER m_bottleList
                         NOTIFY bottleListChanged)
    ...
    class ChangeConsumables : public QObject {
      Q_OBJECT
      ReagentManager m_reagentManager;
    ...
    }
    engine->rootContext()->setContextProperty("reagentManager", &m_reagentManager);
    


  • @mzimmers said in accessing elements of a ListModel:

    engine->rootContext()->setContextProperty("reagentManager", &m_reagentManager);

    Then its a connection on the reagentManager.



  • @fcarney the reagentManager generates the signal, but who consumes it?



  • onBottleListChanged: updateListModel()

    You call the function that rebuilds your ListModel. Don't you want that to update when it changes? Also, you said you had two lists of data. Does it have a signal too?



  • @fcarney said in accessing elements of a ListModel:

    onBottleListChanged: updateListModel()

    Oh, OK...I see my problem now. I was trying to use this line of code from within the repeater. I see now that it should go in the containing Rectangle.

    You call the function that rebuilds your ListModel. Don't you want that to update when it changes? Also, you said you had two lists of data. Does it have a signal too?

    I have two sources of data: one in C++ and one in a QML ListModel. I actually have 2 QML ListModels, but only use one (which one to use is determined at runtime by an environment variable). The only signal is emitted by the C++ update function; nothing in the ListModels.

    As it turns out, I've been able to eliminate the use of the QML update function entirely (though I still need the getColor routine() above, so I won't need to use this feature here, but this has been very educational. Thank you for all the help.


Log in to reply