Qt QML ListView contentHeight behaviour
Asked Answered
N

3

7

I am having a very specific issue when using a QML ListView element in combination with its section properties.

I am using Qt 4.8.6, but I also have the same issue when I try this in Qt 5.3.1.

The following code can also be run in older versions of Qt by simply changing the import statement to

import QtQuick 1.0 (For < Qt 4.7.4)

or

import QtQuick 1.1 (For >= Qt 4.7.4)

Here is a standalone use case to demonstrate my problem:

import QtQuick 2.2

Rectangle {
    width: 800
    height: 800
    color: "black"

    property int pageNumber: 1
    property int totalPages: Math.ceil(animalListView.contentHeight/animalListView.height)

    Text {
        x: 2
        y: 90
        color: "Orange"
        text: "Expected height: " + (animalListView.count*70 + (50*10))
        font.pixelSize: 28
    }

    Text {
        x: 2
        y: 0
        color: "Orange"
        text: "Actual ContentHeight: " + animalListView.contentHeight
        font.pixelSize: 28
    }

    Text {
        x: 2
        y: 30
        color: "Orange"
        text: "Actual ChildrenRectHeight: " + animalListView.childrenRect.height
        font.pixelSize: 28
    }

    Text {
        x: 2
        y: 60
        color: "Orange"
        text: "Total model items (minus sections): " + animalListView.count
        font.pixelSize: 28
    }

    Rectangle {
        id: boundingRect
        width: 640
        height: 500
        x: 20
        y: 200
        radius: 10
        border.width: 1
        border.color: "green"
        color: "transparent"

        // The delegate for each section header
        Component {
            id: sectionHeaderDelegate
            Rectangle {
                width: parent.width
                height: 50 // this is the problem                
                color: "transparent"                

                Text {
                    anchors.left: parent.left
                    id: headerText
                    text: section
                    color: "red"
                }

                Rectangle {
                    anchors.fill: parent
                    border.color: "purple"                    
                    border.width: 1    
                    color: "transparent"
                }
            }
        }

        ListModel {
             id: animalsModel
             ListElement { name: "1Parrot"; size: "Small" }
             ListElement { name: "2Guinea pig"; size: "Small" }
             ListElement { name: "3Dog"; size: "Medium" }
             ListElement { name: "4Cat"; size: "Medium" }
             ListElement { name: "5Elephant"; size: "Medium" }
             ListElement { name: "6Parrot"; size: "Small" }
             ListElement { name: "7Guinea pig"; size: "Small" }
             ListElement { name: "8Dog"; size: "Medium" }
             ListElement { name: "9Cat"; size: "Medium" }
             ListElement { name: "10Elephant"; size: "Large" }
             ListElement { name: "11Parrot"; size: "Large" }
             ListElement { name: "12Guinea pig"; size: "Large" }
             ListElement { name: "13Dog"; size: "Large" }
             ListElement { name: "14Cat"; size: "Medium" }
             ListElement { name: "15Elephant"; size: "Large" }
             ListElement { name: "16Parrot"; size: "Small" }
             ListElement { name: "17Guinea pig"; size: "Small" }
             ListElement { name: "18Dog"; size: "Medium" }
             ListElement { name: "19Cat"; size: "Medium" }
             ListElement { name: "20Elephant"; size: "Large" }
        }

        ListView {
            id: animalListView
            anchors.fill: parent
            anchors.margins: 10
            clip: true
            interactive: true
            flickableDirection: Flickable.VerticalFlick
            boundsBehavior: Flickable.StopAtBounds
            model: animalsModel

            delegate: Item {
                width: parent.width
                height: 70
                    Text {
                        text: name
                        color: "green"
                    }

                    Rectangle {
                        anchors.fill: parent
                        border.color: "yellow"                    
                        border.width: 1    
                        color: "transparent"
                    }
                }

            section.property: "size"
            section.criteria: ViewSection.FullString
            section.delegate: sectionHeaderDelegate
        }
    }

    Rectangle {
        anchors.top: boundingRect.top
        anchors.left: boundingRect.right
        anchors.leftMargin: 20        
        width: 40
        height: 40
        color: "blue"

         MouseArea {
             anchors.fill: parent
             onClicked: {
                if (pageNumber > 1) {
                    animalListView.contentY -= animalListView.height;
                    animalListView.returnToBounds();
                    --pageNumber;
                }             
             }
         }

        enabled: (!animalListView.atYBeginning)
        visible: !(animalListView.atYBeginning && animalListView.atYEnd)

        Text {
            anchors.centerIn: parent
            font.family: "Wingdings 3"
            font.pixelSize: 40
            text: "Ç" // Up arrow
        }
    }

    Text {
        visible: totalPages > 1
        anchors.left: boundingRect.right
        anchors.verticalCenter: boundingRect.verticalCenter
        width: 100
        height: 20
        font.pixelSize: 18
        horizontalAlignment: Text.AlignHCenter
        color: "red"
        text: qsTr("%1 of %2").arg(pageNumber).arg(totalPages)
    }

    Rectangle {
        anchors.bottom: boundingRect.bottom
        anchors.left: boundingRect.right
        anchors.leftMargin: 20
        width: 40
        height: 40
        color: "orange"

         MouseArea {
             anchors.fill: parent
             onClicked: {
                if (pageNumber < totalPages) {
                    animalListView.contentY += animalListView.height;
                    ++pageNumber;
                }
             }
         }

        enabled: (!animalListView.atYEnd)
        visible: !(animalListView.atYBeginning && animalListView.atYEnd)

        Text {
            anchors.centerIn: parent
            font.family: "Wingdings 3"
            font.pixelSize: 40
            text: "È" // Down arrow
        }
    }

}

I am using the ListView to display a list of animal models, categorized by their size. In order to achieve this categorization in the view, I use the section.property, section.critiria and section.delegate properties of the ListView as implemented in the code given above.

(Note: Please ignore the fact that the model I supply to the ListView is not sorted, I understand that this will create numerous duplicate category entries in the ListView. This is beside the point here.)

When the number of models exceed the visible area of the ListView, I am using the property totalPages to calculate how many full ListView pages there are for navigation. The Up arrow and Down arrow buttons simply decrement and increment the content.Y of the ListView by the height of the ListView respectively.

The problem is that the contentHeight of the ListView does not remain static, it is dynamically changing and causing my totalPages property calculation to be incorrect.

It is interesting to note that this behavior occurs if and only if I set a height for my sectionHeaderDelegate rectangle. If I comment out the height statement (height: 50), the contentHeight of the ListView remains static, as expected - with the downside that the section headers/categories are now on top of the model text, which is not useful at all.

So my question is, why does the contentHeight of the QML ListView element dynamically change if and only if I use a section delegate who's height has been set to a non-zero value?

Also, I have left the following properties in the ListView for testing purposes, the ListView should be used with the Up/Down arrows:

          interactive: true
          flickableDirection: Flickable.VerticalFlick
          boundsBehavior: Flickable.StopAtBounds
Nadean answered 9/9, 2014 at 13:5 Comment(0)
O
5

I know this is ancient, but I'll answer it here anyway because I was looking for a solution;

If you have a fixed height for your items, you can set the height of the container dynamically by simply setting the value by formula:

MyContainerWithListItems {
height: MyModel.items.length * height
}

If you have variable height items it will be more difficult; the solution is probably to have an onChange event fire off a function which crawls through your items and manually adds up the heights.

Objectivity answered 26/5, 2016 at 19:54 Comment(0)
Q
3

It is because ListView is estimating its contentHeight by currently visible items. Check what happens when your sections cannot be grouped. ListView is avoiding instantiating every item so it doesn't know proper size of non visible content. Look at this thread.

Quondam answered 9/9, 2014 at 15:11 Comment(4)
The takeaway I got from the thread you link to is that "When items in a vertical ListView don’t have a fixed height, contentHeight is estimated." Ok, but in my case, all model items have the same implicit height and the section delegate has a fixed height as well. I'm not using variable heights for any of my model items or in the section delegate, yet I still have the same issue with contentHeight dynamically changing.Nadean
My issue purely stems from the usage of the section delegate. Setting the delegate's height to anything other than its implicit value causes the contentHeight to be calculated/estimated strangely, even if the section delegate has a fixed height!Nadean
Your section header has dynamic height because it can be draw with 50 pixels height if it is first in section group or is not draw when it is not first item in group. As I said look what happens when your sections cannot be grouped - every fixed size item has its own fixed size section header so nothing is dynamic.Quondam
Could you provide a diagram, or some visual representation of what you are trying to say? I don't quite follow, sorry :(Nadean
D
0

Declare a global property which stores the height of your listview:

property int propertyListViewHeight:0

Put the ListView inside a transparent item with the height equal to that global property propertyListViewHeight:

Item {
    id: rectangleEntities
    width: parent.width
    height: propertyListViewHeight
    ListView {
        id: listViewEntities
        anchors.fill: parent
        model: entityModel
        delegate: listDelegate
        focus: true
        boundsBehavior: Flickable.StopAtBounds
        highlightMoveDuration: 1000
        highlightMoveVelocity: 1000
    }
    
}

Add onCompleted signal in your delegate definition and then add the height of each item of your list to your global property propertyListViewHeight

Component { id: listDelegate Rectangle { id: container width: parent.width

        Component.onCompleted: {
            propertyListViewHeight += height
        }
        
    }
}
Dominicadominical answered 1/2, 2021 at 15:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.