QML Repeater is not drawn correctly after model is changed

Viewed 456

I'm writing a QML application to draw brown blocks inside a blue Rectangle. The application performs this task using a ColumnLayout and a Repeater to draw an arbitrary number of blocks (4 by default):

I'm attempting to change the model of the Repeater dynamically when the user clicks on the screen to force the UI to draw a different amount of blocks. Whenever the desired amount of blocks is changed via blockCount, it triggers the recalculation of blockHeight, the height of each block, so that a smaller amount of blocks can occupy more space on the screen. At least that is the theory!

For debugging purposes, clicking on the screen sets blockCount to 2.

Here's a sample image with the expected result on the Left and the current result on the Right:

As you can see on the image above, when the click happens and rectId.blockCount = 2 is executed, it appears to trigger a sequence of calls that:

  • Ends up changing the model of the Repeater before blockHeight is recalculated;
  • Or the anchors for the ColumnLayout are reset, for some bizarre reason;
  • Or something else is going on;

I'm trying to understand what's causing this behavior and also looking for an approach that allows the application to change the number of blocks dynamically while being able to draw them correctly!

What am I missing?

import QtQuick 2.12
import QtQuick.Window 2.12
import QtQuick.Layouts 1.15

Window {
    id: wndId
    property int wndWidth: 200
    property int wndHeight: 300

    visible: true
    width: wndWidth
    height: wndHeight
    title: qsTr("Testing ColumnLayout")

    Rectangle {
        id: rectId

        property int borderWidth: 5  // width of the blue frame surrounding the window
        property int blockCount: 4   // number of blocks to be drawn using Repeater
        property int blocksSpace: 8  // minimum space between the blocks

        width: wndId.wndWidth
        height: wndId.wndHeight
        border.color: "blue"
        border.width: borderWidth

        // size of each inner rectangle is computed dinamically: changing blockCount should update blockHeight
        property int blockWidth: rectId.width - (rectId.borderWidth * 4)
        property int blockHeight: updateBlockHeight()

        function updateBlockHeight(numBlocks)
        {
            if (numBlocks === undefined)
                numBlocks = rectId.blockCount;

            var newHeight = (rectId.height - ((rectId.borderWidth + rectId.blocksSpace)*2) - (rectId.blocksSpace * (numBlocks-1))) / numBlocks;
            print("updateBlockHeight: newHeight=", newHeight);
            return newHeight;
        }

        Component.onCompleted: print("Outter Rectangle w=" + rectId.width + " h=" + rectId.height)

        // draw blocks on top of each other with some space between them
        ColumnLayout {
            spacing: rectId.blocksSpace
            Layout.alignment: Qt.AlignBottom
            anchors.bottom: parent.bottom
            anchors.bottomMargin: rectId.borderWidth + rectId.blocksSpace
            anchors.left: rectId.left
            anchors.leftMargin: rectId.borderWidth*2

            Repeater {
                id: repId
                model: rectId.blockCount

                // each block size is calculated dinamically
                Rectangle {
                    id: blockId
                    color: "brown"
                    width: rectId.blockWidth
                    height: rectId.blockHeight

                    // Debug:
                    Component.onCompleted: {
                        print("Inner Rectangle")
                        print("  blockCount=" + rectId.blockCount);
                        print("  blockId.width=" + blockId.width + " blockId.height=" + blockId.height)
                        print("  blockWidth=" + rectId.blockWidth + " blockHeight=" + rectId.blockHeight)
                    }

                    Component.onDestruction: print("~Inner Rectangle")

                } // inner Rectangle

                Component.onCompleted: print("Repeater")
                Component.onDestruction: print("~Repeater")
            } // Repeater

        } // ColumnLayout

        MouseArea {
            anchors.fill: parent
            onClicked: {
                print("Mouse clicked!");

                // since repId uses blockCount as the model, any change to it should automatically recreate the elements of the Repeater
                // here we force blockHeight to be recalculated before the model is changed
                rectId.blockHeight = rectId.updateBlockHeight(2)

                // and finally we change the number of blocks, forcing the Repeater to redraw the model correctly
                rectId.blockCount = 2;
                print("blockHeight= " + rectId.blockHeight);
            }
        }

    } // outter Rectangle

} // Window

2 Answers

Why are you calculating the blockHeight and blockWidth while you can leverage the power of ColumnLayout?

Use the Layout.fillWidth and Layout.fillHeight properties to signal the ColumnLayout that the blocks should fill the entire width & height, evenly distributed. And then set the correct size to the ColumnLayout and it will do the calculations you try to program itself.

import QtQuick 2.12
import QtQuick.Window 2.12
import QtQuick.Layouts 1.3

Window {
    id: wndId
    property int wndWidth: 200
    property int wndHeight: 300

    visible: true
    width: wndWidth
    height: wndHeight
    title: qsTr("Testing ColumnLayout")

    Rectangle {
        id: rectId

        property int borderWidth: 5  // width of the blue frame surrounding the window
        property int blockCount: 4   // number of blocks to be drawn using Repeater
        property int blocksSpace: 8  // minimum space between the blocks

        width: wndId.wndWidth
        height: wndId.wndHeight
        border.color: "blue"
        border.width: borderWidth

        Component.onCompleted: print("Outter Rectangle w=" + rectId.width + " h=" + rectId.height)

        // draw blocks on top of each other with some space between them
        ColumnLayout {
            spacing: rectId.blocksSpace
            Layout.alignment: Qt.AlignBottom

            anchors.fill: parent
            anchors.margins: rectId.borderWidth * 2

            Repeater {
                id: repId
                model: rectId.blockCount

                // each block size is calculated dinamically
                Rectangle {
                    id: blockId
                    color: "brown"
                    Layout.fillWidth: true
                    Layout.fillHeight: true

                    // Debug:
                    Component.onCompleted: {
                        print("Inner Rectangle", index)
                        print("  blockCount=" + rectId.blockCount);
                        print("  blockId.width=" + blockId.width + " blockId.height=" + blockId.height)
                        print("  blockWidth=" + rectId.blockWidth + " blockHeight=" + rectId.blockHeight)
                    }

                    Component.onDestruction: print("~Inner Rectangle")

                } // inner Rectangle

                Component.onCompleted: print("Repeater")
                Component.onDestruction: print("~Repeater")
            } // Repeater

        } // ColumnLayout

        MouseArea {
            anchors.fill: parent
            onClicked: {
                print("Mouse clicked!");

                // and finally we change the number of blocks, forcing the Repeater to redraw the model correctly
                rectId.blockCount = 2;
                print("blockHeight= " + rectId.blockHeight);
            }
        }

    } // outter Rectangle

} // Window

EDIT for keeping blockHeight calculation

If you insist on keeping the calculation since it is more difficult in the real world (fair enough), I would suggest to use implicitHeight and implicitWidth. This works because the Layout engine does not trigger on changes on width/height since it is supposed to set these himself, it does however monitor the implicit sizes:

import QtQuick 2.12
import QtQuick.Window 2.12
import QtQuick.Layouts 1.3

Window {
    id: wndId
    property int wndWidth: 200
    property int wndHeight: 300

    visible: true
    width: wndWidth
    height: wndHeight
    title: qsTr("Testing ColumnLayout")

    Rectangle {
        id: rectId

        property int borderWidth: 5  // width of the blue frame surrounding the window
        property int blockCount: 4   // number of blocks to be drawn using Repeater
        property int blocksSpace: 8  // minimum space between the blocks

        width: wndId.wndWidth
        height: wndId.wndHeight
        border.color: "blue"
        border.width: borderWidth

        // size of each inner rectangle is computed dinamically: changing blockCount should update blockHeight
        property int blockWidth: rectId.width - (rectId.borderWidth * 4)
        property int blockHeight: updateBlockHeight()

        function updateBlockHeight(numBlocks)
        {
            var newHeight = (rectId.height - ((rectId.borderWidth + rectId.blocksSpace)*2) - (rectId.blocksSpace * (rectId.blockCount-1))) / rectId.blockCount;
            print("updateBlockHeight: newHeight=", newHeight);
            return newHeight;
        }

        Component.onCompleted: print("Outter Rectangle w=" + rectId.width + " h=" + rectId.height)

        // draw blocks on top of each other with some space between them
        ColumnLayout {
            spacing: rectId.blocksSpace
            Layout.alignment: Qt.AlignBottom
            anchors.fill: parent
            anchors.margins: rectId.borderWidth * 2

            Repeater {
                id: repId
                model: rectId.blockCount

                // each block size is calculated dinamically
                Rectangle {
                    id: blockId
                    color: "brown"
                    implicitWidth: rectId.blockWidth
                    implicitHeight: rectId.blockHeight
                    onXChanged: print("x[",index,"]=", x)
                    onYChanged: print("y[",index,"]=", y)

                    // Debug:
                    Component.onCompleted: {
                        print("Inner Rectangle", index)
                        print("  blockCount=" + rectId.blockCount);
                        print("  blockId.width=" + blockId.width + " blockId.height=" + blockId.height)
                        print("  blockWidth=" + rectId.blockWidth + " blockHeight=" + rectId.blockHeight)
                    }

                    Component.onDestruction: print("~Inner Rectangle")

                } // inner Rectangle

                Component.onCompleted: print("Repeater")
                Component.onDestruction: print("~Repeater")
            } // Repeater

        } // ColumnLayout

        MouseArea {
            anchors.fill: parent
            onClicked: {
                print("Mouse clicked!");

                // and finally we change the number of blocks, forcing the Repeater to redraw the model correctly
                rectId.blockCount = 2;
                print("blockHeight= " + rectId.blockHeight);
            }
        }

    } // outter Rectangle

} // Window

Also, I refactored the updateBlockHeight function, it's not needed to explicitly set it, the QML engine is so smart it will even reevaluate the binding when one of the parameters in the function changes!

(From my comments)

With Qt 5.15.1 on Linux get the expected output. With Qt 5.15 get the output you indicate for what is probably a bug that has been fixed in Qt 5.15.1.

Related