1
votes

I have two problems with the following code.

  1. If I'm using padding in the QML Column I get this message:

    QML Column: possible QQuickItem::polish() loop
    

    and the application becomes unresponsive. Also if I don't use anchors the problem does not appear, but the Rectangle inside the Column won't be stretched.

  2. If I'm using anchors the Column's implicitWidth and impliciHeight will be zero which will result that the Rectangles won't be shown.

The Qt documentation says this:

Also, since a Column automatically positions its children vertically, a child item within a Column should not set its y position or vertically anchor itself using the top, bottom, anchors.verticalCenter, fill or centerIn anchors.

Which means horizontal anchoring (left/right) is not prohibited.

Any idea what could be wrong?

Rectangle {
    anchors.fill: parent
    color: "green"
    Rectangle {
        anchors.centerIn: parent
        implicitWidth: col.implicitWidth
        implicitHeight: col.implicitHeight
        color: "blue"
        Column {
            spacing: 10
            //padding: 10 // causes: QML Column: possible QQuickItem::polish() loop
            id: col
            Rectangle {
                anchors.left: parent.left
                anchors.right: parent.right
                implicitWidth: 100
                implicitHeight: 25
            }
            Rectangle {
                //anchors.left: parent.left // uncommenting these anchors will result that the column's implicitWidth and implicitHeight will be 0
                //anchors.right: parent.right
                implicitWidth: 200
                implicitHeight: 25
            }
            Component.onCompleted: console.log("column, imp width: " + implicitWidth + ", imp height: " + implicitHeight)
        }
    }
}
4
The padding works if you specify a width for the column. It appears to fail if trying to use the implicitWidth of the children. That might be a bug.JarMan
What's the reason for wanting to specify horizontal anchors and implicitWidth at the same time?JarMan
implicitWidth (just like sizeHint in Qt widgets) tells the item's preferred width. I have a several elements with different implicitWidths and I want to display them with the same visible width (using the widest) underneath each other, hence I'm using horizontal anchoring. A single item in a Column (which is pointless) will set the Column's implicitWidth and the padding is correct too.Ponzifex
Makes sense. Does a ColumnLayout work any better for you?JarMan
The computer says no! QML Rectangle: Detected anchors on an item that is managed by a layout. This is undefined behavior; use Layout.alignment instead. But I have a solution and I will post it soon.Ponzifex

4 Answers

1
votes

As discussed in comments, Ponzifex's code can be improved:

Remove reparenting and instead create a new default property alias like this:

Rectangle 
{ 
   default property alias data2: col.data

   data: 
   [ 
      Column 
      { 
        id: col; 
        onChildrenChanged: { ...calculate sizes, add bindings, etc... }
      } 
   ]
} 

How this works:

  • When in QML code you are nesting objects in another object, you are adding them to a property that is marked as default inside the parent object
  • For Item and thus Rectangle the property called data is marked as default
    • That property contains a combined list of visual children and resources of Item / Rectangle
    • Thus normally nesting visual elements in Rectangle causes them to be added as that Rectangle's visual children
    • So normally Rectangle { Text {}; Timer{} } ...
    • ...is equivalent to: Rectangle { data: [ Text {}, Timer{} ] }
  • I changed that by creating a new property called data2 and setting it as default for the Rectangle
    • data2 is not related to data so its elements are not added to Rectangle's visual children list
    • Instead I made data2 an alias to data property of your Column
    • An alias property is just that - an alias - another name of the property, in this case - as a property of another object - but both name "point" to the same actual property an thus Column's list of visual children
    • So all QML elements nested inside the Rectangle are added as visual children of the Column
  • However, now I have a problem: I cannot just nest the Column into Rectangle in QML code because it would mean that the Column needs to be added as it's own child (which makes no sense)
    • So I have to assign Rectangle's actual data property (which is no longer default so I have to write its name explicitly) thus adding the Column as a visual child of the Rectangle
  • Now whenever Rectangle's nested elements are added or removed (including by a Repeater), the Column's data property changes, but since we are adding visual children children property also changes
    • So we can trigger recalculations and rebinding when Column's onChildrenChanged signal fires (or onDataChanged if you want to also trigger on non-visual children aka resources)
    • You can skip elements which already have your bindings or just rebind them

As far as I know, this is supposed to be valid and supported QML syntax - just not the one you usually use - so it's ok to use it in production code, perhaps with a comment explaining what is happening

1
votes

Padding

You attach elements to left and right edges of the column, but then tell the column it should position its elements 10 pixels away from that border.

They then start to "fight" each other by each causing layout update and thus triggering each other.

You need to place an intermediate element to handle padding like that:

Column{ padding: 10; Column{ id: col; Rectangle{}; Rectangle{}; } }

Anchors

Let's see what's actually happening

I inserted some debugging code in each element:

property string name: "..."//I named them: "green_rect", "blue_rect", "col", "top_rect", "bottom_rect"
onWidthChanged: console.log(name + " changed: w=" + width)
....
property string mydbgstr: "top_rect w=" + width + " h=" + height + " iw=" + implicitWidth + " ih=" + implicitHeight
onMydbgstrChanged: console.log(mydbgstr)

It prints a string when any of the properties changes

My window is 500x500

Initial layout - this remains the same for all cases:

// property change notifications removed since they are not interesting yet
qml: bottom_rect w=200 h=25 iw=200 ih=25
qml: top_rect w=100 h=25 iw=100 ih=25
qml: col w=0 h=0 iw=0 ih=0
qml: blue_rect w=0 h=0 iw=0 ih=0
qml: green_rect w=0 h=0 iw=0 ih=0

okay so anchors are not applied yet, and the column hasn't calculated its size yet, so elements simply assume h=ih w=iw

After that we see different conclusions:

Both top and bottom rectangles' anchors commented:

qml: col changed: w=200
qml: col changed: h=60 
qml: col changed: iw=200
qml: blue_rect changed: w=200
qml: blue_rect changed: iw=200
qml: blue_rect w=200 h=0 iw=200 ih=0

qml: col changed: ih=60
qml: col w=200 h=60 iw=200 ih=60
qml: blue_rect changed: h=60
qml: blue_rect changed: ih=60
qml: blue_rect w=200 h=60 iw=200 ih=60

qml: green_rect changed: w=500
qml: green_rect w=500 h=500 iw=0 ih=0
qml: green_rect changed: h=500

Result:

▀▀▀▀▀
▀▀▀▀▀▀▀▀▀▀

Works as expected: column calculates its size based on combined children sizes, then surrounding element also assumes that size

Only top rect's anchors uncommented:

// it appears that top rect adjusted itself to fit still-zero-width column
qml: top_rect changed: w=0   col.w=0 //uh oh, top_rect is now zero-sized
qml: top_rect w=0 h=25 iw=100 ih=25

// here col probably performed layout and determined its width based on bottom rect
// however for some reason its own signal got delayed (isn't shown)
// probably because children get priority

// top_rect anchors react to column layout:
qml: top_rect changed: w=200   col.w=200  //top_rect is properly sized again
qml: top_rect w=200 h=25 iw=100 ih=25

// here col appears to react to the first layout change: 
qml: col changed: w=200
qml: col changed: h=25 // height excludes top_rect which was zero-size at that point
qml: col changed: iw=200 // bottom_rect retained its size so col isn't zero-sized 

//...and so surrounding elements are updated
qml: blue_rect changed: w=200
qml: blue_rect changed: iw=200
qml: blue_rect w=200 h=0 iw=200 ih=0

//...next col decides to update its implicitHeight 
qml: col changed: ih=25
qml: col w=200 h=25 iw=200 ih=25
//...which causes a second layout of surroundings:
qml: blue_rect changed: h=25
qml: blue_rect changed: ih=25
qml: blue_rect w=200 h=25 iw=200 ih=25

qml: green_rect changed: w=500
qml: green_rect changed: h=500
qml: green_rect w=500 h=500 iw=0 ih=0

//This is apparently col getting the second update of top_rect:
qml: col changed: h=60 //height now includes non-zero-sized top_rect
qml: col changed: ih=60
qml: col w=200 h=60 iw=200 ih=60

//...so blue_rect is changed yet again:
qml: blue_rect changed: h=60
qml: blue_rect changed: ih=60
qml: blue_rect w=200 h=60 iw=200 ih=60

Result:

▀▀▀▀▀▀▀▀▀▀
▀▀▀▀▀▀▀▀▀▀

Both uncommented:

// col is zero-sized still so children cling to its zero-size
qml: bottom_rect changed: w=0   col.w=0
qml: bottom_rect w=0 h=26 iw=200 ih=26
qml: top_rect changed: w=0   col.w=0
qml: top_rect w=0 h=24 iw=100 ih=24

// because all children are zero-sized, col is also zero-sized so it doesn't attempt to do anything

// because col is zero-sized, blue_rect also remains zero-sized

qml: green_rect changed: w=500
qml: green_rect changed: h=500
qml: green_rect w=500 h=500 iw=0 ih=0

Result: green window

Conclusion

Column width depends on largest element width but element width is anchored to column so it has a chicken-and-egg-problem but since it's indirect and also causes initial zero size to persist, Qt cannot detect a binding loop and instead the elements remain collapsed.

This effectively means that QtQuick is not "smart" enough to properly position items in this case. You have to either specify actual width for one of the items or the column.

ColumnLayout is a bit smarter in that it can have minimum, maximum and preferred sizes specified so you should probably use it instead of Column. I understand you already figured out how to use it so I won't go into details here.

Alternatively, imperative code could be used to determine largest of the elements' width and set col's width to that. It can also set other elements' width if desired.

1
votes

QML Column is more like a positioner and in my case it is not very good in resizing its children.

Experimented with ColumnLayout which somewhat solves the issue, but produces a lot of warning messages because ColumnLayout is not directly, but derived from QQuickLayout where anchoring is checked and dumps this warning message: "Detected anchors on an item that is managed by a layout. This is undefined behavior; use Layout.alignment instead."

Finally, I have made a workaround in QML which utilizes uniform padding and spacing between the elements whose implicitHeight is larger than zero.

It can be used as a regular QML element.

This is a modified answer based on the suggestions of Jack White.

MyColumn.qml:

import QtQuick 2.12

Rectangle
{
    default property alias data2: container.data

    property int spacing: 0
    property int padding: 0

    implicitWidth: container.implicitWidth + 2*padding
    implicitHeight: container.implicitHeight + 2*padding

    data:
    [
        Item
        {
            id: container

            property int spacing: parent.spacing

            function implicitHeightOfChildren() {
                var total = 0
                for (var i=0;i<children.length;i++)
                    total += children[i].implicitHeight
                return total
            }

            function widestChild() {
                var max = 0
                for (var i=0;i<children.length;i++)
                    if(children[i].implicitWidth > max)
                        max = children[i].implicitWidth
                return max
            }

            function calculateSpacing() {
                var itemsWithHeight = 0
                for (var i = 0; i < children.length; i++)
                    if(children[i].implicitHeight > 0)
                        itemsWithHeight++
                return (itemsWithHeight > 0 ? (itemsWithHeight - 1) * spacing : 0)
            }

            anchors.top: parent.top
            anchors.topMargin: parent.padding
            anchors.left: parent.left
            anchors.leftMargin: parent.padding
            anchors.right: parent.right
            anchors.rightMargin: parent.padding

            implicitWidth: widestChild()
            implicitHeight: implicitHeightOfChildren() + calculateSpacing()

            onChildrenChanged:
            {
                for (var i=0;i<children.length;i++) {
                    if(i === 0) {
                        children[i].anchors.top = Qt.binding(function() { return children[i].parent.top });
                    } else {
                        children[i].anchors.top = Qt.binding(function() { return children[i-1].bottom });
                        children[i].anchors.topMargin = (children[i-1].implicitHeight > 0 ? spacing : 0);
                    }
                }
            }
        }
    ]
}

Example use without anchoring to the column. Uncomment the anchor lines to see the expected behavior.

MyColumn {
    color: "red"
    padding: 10
    spacing: 10
    Repeater {
        model: 3
        Rectangle {
            //anchors.left: parent.left
            //anchors.right: parent.right
            implicitWidth: 100 + 25 * index
            implicitHeight: 25
            color: "black"
        }
    }
}

Result:

using anchors vs not using anchors

0
votes

I have made a pure C++ solution which works well in our project environment.

TheColumn.h

#ifndef THECOLUMN_H
#define THECOLUMN_H

#include <QQuickItem>

class TheColumn : public QQuickItem
{
    Q_OBJECT
    Q_PROPERTY(qreal spacing READ spacing WRITE setSpacing NOTIFY spacingChanged)

public:
    explicit TheColumn(QQuickItem *parent = nullptr);
    ~TheColumn();

    qreal spacing() const;
    void setSpacing(qreal r);

    void positionItems();
    void componentComplete() override;
    void itemChange(ItemChange, const ItemChangeData &) override;

Q_SIGNALS:
    void spacingChanged();

public slots:
    void onChildImplicitWidthChanged();
    void onChildImplicitHeightChanged();

private:
    qreal mSpacing;
};

#endif // THECOLUMN_H

TheColumn.cpp

#include "thecolumn.h"

TheColumn::TheColumn(QQuickItem *parent) : QQuickItem{ parent }, mSpacing{ 0.0 }
{
    setFlag(ItemHasContents, true);
}

TheColumn::~TheColumn()
{
}

qreal TheColumn::spacing() const
{
    return mSpacing;
}

void TheColumn::setSpacing(qreal s)
{
    if (mSpacing == s) return;
    mSpacing = s;

    Q_EMIT spacingChanged();

    if(isComponentComplete())
        positionItems();
}

void TheColumn::positionItems()
{
    qreal maxImplicitWidth = 0.0;
    qreal totalImplicitHeight = 0.0;

    QList<QQuickItem*> children = childItems();
    for (int i = 0; i < children.count(); ++i) {
        QQuickItem *child = children.at(i);

        child->setY(totalImplicitHeight);

        if(child->implicitWidth() > maxImplicitWidth)
            maxImplicitWidth = child->implicitWidth();
        if(child->implicitHeight() > 0) {
            totalImplicitHeight += child->implicitHeight();
            totalImplicitHeight += mSpacing;
        }
    }

    totalImplicitHeight -= mSpacing;

    setImplicitWidth(maxImplicitWidth);
    setImplicitHeight(totalImplicitHeight);
}

void TheColumn::componentComplete()
{
    positionItems();
    QQuickItem::componentComplete();
}

void TheColumn::onChildImplicitWidthChanged()
{
    positionItems();
}

void TheColumn::onChildImplicitHeightChanged()
{
    positionItems();
}

void TheColumn::itemChange(ItemChange change, const ItemChangeData &value)
{
    if(change == ItemChildAddedChange) {
        QObject::connect(value.item, &QQuickItem::implicitWidthChanged, this, &TheColumn::onChildImplicitWidthChanged);
        QObject::connect(value.item, &QQuickItem::implicitHeightChanged, this, &TheColumn::onChildImplicitHeightChanged);
    }
    QQuickItem::itemChange(change, value);
}

This type must be registered for somewhere, for example: main.cpp

qmlRegisterType<TheColumn>("TheColumn", 1, 0, "TheColumn");

QML snippet:

import TheColumn 1.0

//boilerplate QML code

TheColumn {
    spacing: 10
    Repeater {
        model: 3
        Rectangle {
            anchors.left: parent.left
            anchors.right: parent.right
            implicitWidth: 100 + 25 * index
            implicitHeight: 25
            color: "black"
        }
    }
}