Evan Krause
Evan Krause

Reputation: 187

QML ListView: Binding loop detected for property "height"

I have a QML ListView, and I'm trying to dynamically add elements to it. I want the background rectangle to also scale dynamically as elements are added/removed from the ListView. Right now I get a binding loop, and I understand what they are but I can't figure out where it's coming from. I played around changing the code a bit and I was able to get rid of the binding loop one time but then the ListView couldn't be scrolled. Anyone have any ideas?

import QtQuick 2.15
import QtQuick.Window 2.0

Window {
  visible: true
  width: 800
  height: 800

      Rectangle {
          id: listContainer
          height: childrenRect.height
          width: parent.width
          color: "transparent"
          anchors {
              top: parent.top
              topMargin: 30
              left: parent.left
              leftMargin: 45
          }

          ListView {
              anchors.top: parent.top
              anchors.left: parent.left
              anchors.right: parent.right
              model: myModel
              height: childrenRect.height
              header:
                  Text {
                    z: 2
                    height: 50
                    text: "HEADER"
                    color: "black"

              }
              delegate:  Component {
                  Item {
                      Text {
                          id:  userName;
                          text: name;
                          color: "black";
                          font.pixelSize: 50
                          anchors {
                              left: parent.left
                              leftMargin: 20
                          }
                      }
                      
                      Rectangle {
                          height: 1
                          color: 'black'
                          width: listContainer.width
                          anchors {
                              left:  userName.left
                              top:  userName.top
                              topMargin: - 12
                              leftMargin: -15
                          }
                      }
                  }
              }
              spacing: 80
          }
      }

      ListModel {
          id: myModel
      }

      /* Fill the model with default values on startup */
      Component.onCompleted: {
          for (var i = 0; i < 100; i++) {
              myModel.append({
                  name: "Big Animal : " + i
              })
          }
      }
}

EDIT: As suggested by @Aditya, the binding loop can be removed by having a static ListView height, but I don't want it to be that way. I'm using the rectangle as a background for the ListView and I want it to scale according to the ListView. For example, if I only add two elements, I want the rectangle to also scale for those two elements and not cover the entire screen. This causes a problem:

import QtQuick 2.15
import QtQuick.Window 2.0

Window {
  visible: true
  width: 800
  height: 800

      Rectangle {
          id: listContainer
          height: childrenRect.height
          width: parent.width
          color: "yellow"
          anchors {
              top: parent.top
              topMargin: 30
              left: parent.left
              leftMargin: 45
          }

          ListView {
              anchors.top: parent.top
              anchors.left: parent.left
              anchors.right: parent.right
              model: myModel
              height: 800//childrenRect.height

              header:
                  Text {
                    z: 2
                    height: 50
                    text: "HEADER"
                    color: "black"

              }
              delegate:  Component {
                  Item {
                      Text {
                          id:  userName;
                          text: name;
                          color: "black";
                          font.pixelSize: 50
                          anchors {
                              left: parent.left
                              leftMargin: 20
                          }
                      }

                      Rectangle {
                          height: 1
                          color: 'black'
                          width: listContainer.width
                          anchors {
                              left:  userName.left
                              top:  userName.top
                              topMargin: - 12
                              leftMargin: -15
                          }
                      }
                  }
              }
              spacing: 80
          }
      }

      ListModel {
          id: myModel
      }

      /* Fill the model with default values on startup */
      Component.onCompleted: {
          for (var i = 0; i < 2; i++) {
              myModel.append({
                  name: "Big Animal : " + i
              })
          }
      }
}

I also tried separating the header from ListView into a different component and anchoring the listview below it and that worked. The only problem was it could not be scrolled with the listview. Worst case, I could make a scrolling animation for it but that seems like an inefficient solution and I'd like to know why this doesn't work.

Upvotes: 0

Views: 2395

Answers (2)

Aditya
Aditya

Reputation: 449

The binding loop is originating from the ListView's height: childrenRect.height statement. It looks like the ListView needs to be a fixed height, or at least not dependent on childrenRect. It is most likely how the ListView element knows that the view should be scrollable to view elements below.

It really depends on what you're trying to achieve with setting the height to match childrenRect, but in my case, ListView height is changing based on the children (per your desire presumably). With a 100 items the height came out to be 7970. With 5 items in the model, the result was 350. You can check this by adding a debug or console.log() with onHeightChanged However, as a result of this scaling, the ListView is assumed to be big enough to view the entire data set regardless of the window parent container size.

You do not need to scale the ListView height to match the contents; that is what it is built for. It allows scrolling because the contents are too big to be shown within its limited height.

I was able to achieve get rid of the binding loop and be able to scroll by simply changing the statement to a static value, which is the parent height of 800 as an example:

Window {
  visible: true
  width: 800
  height: 800

      Rectangle {
          id: listContainer
          height: childrenRect.height
          width: parent.width
          color: "transparent"
          anchors {
              top: parent.top
              topMargin: 30
              left: parent.left
              leftMargin: 45
          }

          ListView {
              anchors.top: parent.top
              anchors.left: parent.left
              anchors.right: parent.right
              model: myModel
              height: 800//childrenRect.height

              header:
                  Text {
                    z: 2
                    height: 50
                    text: "HEADER"
                    color: "black"

              }
              delegate:  Component {
                  Item {
                      Text {
                          id:  userName;
                          text: name;
                          color: "black";
                          font.pixelSize: 50
                          anchors {
                              left: parent.left
                              leftMargin: 20
                          }
                      }

                      Rectangle {
                          height: 1
                          color: 'black'
                          width: listContainer.width
                          anchors {
                              left:  userName.left
                              top:  userName.top
                              topMargin: - 12
                              leftMargin: -15
                          }
                      }
                  }
              }
              spacing: 80
          }
      }

      ListModel {
          id: myModel
      }

      /* Fill the model with default values on startup */
      Component.onCompleted: {
          for (var i = 0; i < 100; i++) {
              myModel.append({
                  name: "Big Animal : " + i
              })
          }
      }
}

Edit:

I feel like you're trying to just secure a background for a scalable ListView. Having a static background as a container works but not very well for modern unser interfaces - any bounce effects or such will not move the rectangle. You could achieve this by anchoring the rectangle to the ListView element but it is a very roundabout way. Instead, you could just set a rectangle to style each element of the ListView delegate instead.

delegate:  Component {
            Item {
                Rectangle{
                    width: listContainer.width
                    height: userName.height+13
                    //add 13 to adjust for margin set below
                    anchors {
                        left:  userName.left
                        top:  userName.top
                        topMargin: - 12
                        leftMargin: -15
                        //just copying from the other rectangle below
                    }
                    gradient: Gradient { 
                        //I am just using gradient here for a better understanding of spacing. You could use color.
                        GradientStop { position: 0.0; color: "aqua" }
                        GradientStop { position: 1.0; color: "green" }
                    }
                }
                Text {
                    id:  userName;
                    text: name;
                    color: "black";
                    font.pixelSize: 50
                    anchors {
                        left: parent.left
                        leftMargin: 20
                    }
                }
                Rectangle {
                    height: 1
                    color: 'black'
                    width: listContainer.width
                    anchors {
                        left:  userName.left
                        top:  userName.top
                        topMargin: - 12
                        leftMargin: -15
                    }
                }


            }
        }

This will make sure that the rectangle background behind the ListView will look like it is scrolling with the items. In reality we have broken one rectangle into multiple and just set each element with one. You can also use this type of styling to achieve alternate colors in your list for example.

Upvotes: 1

Amfasis
Amfasis

Reputation: 4208

You are probably also biting yourself with the Item as the top-level in the delegate, since that doesn't give any implicit size, which the ListView uses to calculate the scrolling needs. You can simply use Text directly as the delegate (you don't need the Component either) and put the line/rectangle inside. If doing so you can use the contentHeight property of ListView to size the background.

Furthermore, I would suggest to have the ListView as the top level and do any styling secondary, with which I mean, put the background Rectangle inside.

import QtQuick 2.12
import QtQuick.Window 2.12

Window {
    width: 640
    height: 480
    visible: true
    title: qsTr("Hello World")

    ListView {
        id: listView
        model: 3
        anchors.fill: parent

        Rectangle { //background
            color: "yellow"
            z: -1
            width: listView.width
            height: listView.contentHeight
        }

        delegate: Text {
            text: "name" + index
            color: "black";
            font.pixelSize: 50
            leftPadding: 20

            Rectangle {
                height: 1
                color: 'black'
                width: listView.width
                y: - 12
                x: -15
            }
        }
        spacing: 80

    }
}

Btw, if you are going to put the ListView in some RowLayout or something, you probably also want implicitHeight: contentHeight in the ListView.

Upvotes: 2

Related Questions