QML 列:可能的 QQuickItem::polish() 循环

QML Column: possible QQuickItem::polish() loop

下面的代码有两个问题。

  1. 如果我在 QML 列中使用填充,我会收到此消息:

    QML Column: possible QQuickItem::polish() loop
    

    并且应用程序变得无响应。另外,如果我不使用锚点,问题就不会出现,但 Column 内的 Rectangle 不会被拉伸。

  2. 如果我使用锚点,列的 implicitWidth 和 impliciHeight 将为零,这将导致不显示矩形。

Qt 文档是这样说的:

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.

这意味着不禁止水平锚定(left/right)。

知道哪里出了问题吗?

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)
        }
    }
}

填充

您将元素附加到列的左右边缘, 但是然后告诉列它应该将其元素放置在距该边界 10 像素的位置。

然后他们开始互相“打架”,每个人都引起布局更新,从而互相触发。

您需要放置一个中间元素来处理这样的填充:

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

锚点

让我们看看到底发生了什么

我在每个元素中插入了一些调试代码:

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)

它会在任何属性更改时打印一个字符串

我的 window 是 500x500

初始布局 - 这对所有情况都保持不变:

// 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

好的,所以锚点还没有应用,列还没有计算它的大小,所以元素简单地假设 h=ih w=iw

之后我们看到不同的结论:

顶部和底部矩形的锚点评论:

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

结果:

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

按预期工作:列根据组合的子项大小计算其大小, 然后周围的元素也假定 size

只有顶部矩形的锚未注释:

// 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

结果:

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

均未注释:

// 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

结果:绿色window

结论

列宽取决于最大元素宽度,但元素宽度锚定在列上,因此存在先有鸡还是先有蛋的问题,但由于它是间接的,并且还会导致初始零大小持续存在,Qt 无法检测到绑定循环,并且相反,元素保持折叠状态。

这实际上意味着 QtQuick 不够“智能”,无法在这种情况下正确定位项目。您必须为其中一项或列指定实际宽度。

ColumnLayout 更聪明一些,因为它可以指定最小、最大和首选大小,因此您应该使用它而不是 Column。我知道你已经知道如何使用它了,所以我不会在这里详细介绍。

或者,命令式代码可用于确定元素的最大宽度并将 col 的宽度设置为其宽度。如果需要,它还可以设置其他元素的宽度。

QML Column 更像是一个定位器,在我的例子中,它不太适合调整其子项的大小。

对 ColumnLayout 进行了试验,这在一定程度上解决了问题,但产生了很多警告消息,因为 ColumnLayout 不是直接的,而是从 QQuickLayout 派生的,其中检查了锚点并转储了此警告消息:“检测到托管项目上的锚点通过布局。这是未定义的行为;请改用 Layout.alignment。"

最后,我在 QML 中做了一个解决方法,它在 implicitHeight 大于零的元素之间使用统一的填充和间距。

它可以用作常规 QML 元素。

这是根据 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);
                    }
                }
            }
        }
    ]
}

未锚定到列的示例使用。取消注释锚线以查看预期行为。

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"
        }
    }
}

结果:

如评论中所述,Ponzifex 的代码可以改进:

删除 reparenting 并创建一个新的默认 属性 别名,如下所示:

Rectangle 
{ 
   default property alias data2: col.data

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

这是如何工作的:

  • 当您在 QML 代码中将 object 嵌套在另一个 object 中时,您将它们添加到 属性 中,该 default 在 parentobject
  • 对于 Item 因此 Rectangle 调用 data 的 属性 被标记为 default
    • 属性 包含视觉 children 和 Item / Rectangle
    • 资源的组合列表
    • 因此通常在 Rectangle 中嵌套视觉元素会导致它们被添加为 Rectangle 的视觉元素 children
    • 所以通常 Rectangle { Text {}; Timer{} } ...
    • ...等同于:Rectangle { data: [ Text {}, Timer{} ] }
  • 我通过创建一个名为 data2 的新 属性 并将其设置为 default 来更改 Rectangle
    • data2data 无关,因此其元素未添加到 Rectangle 的视觉 children 列表
    • 相反,我为 Column
    • data 属性 创建了一个别名 data2
    • alias 属性 就是 - 别名 - 属性 的另一个名称,在这种情况下 - 作为另一个 [=105= 的 属性 ] - 但两个名称“指向”相同的实际 属性 因此 Column 的视觉列表 children
    • 所以嵌套在 Rectangle 中的所有 QML 元素都添加为 Column
    • 的视觉 children
  • 但是,现在我遇到了一个问题:我不能只将 Column 嵌套到 QML 代码中的 Rectangle 中,因为这意味着需要添加 Column 作为它自己的child(没有意义)
    • 所以我必须分配 Rectangle 的实际 data 属性(这不再是默认值,所以我必须明确地写下它的名字)从而添加 Column 作为 Rectangle
    • 的视觉效果 child
  • 现在,无论何时添加或删除 Rectangle 的嵌套元素(包括 Repeater),Columndata 属性 都会更改, 但由于我们添加了视觉 children children 属性 也发生了变化
    • 所以我们可以在 ColumnonChildrenChanged 信号触发时触发重新计算和重新绑定(或者 onDataChanged 如果你想在 non-visual children 又名 resources)
    • 您可以跳过已经绑定的元素或重新绑定它们

据我所知,这应该是有效且受支持的 QML 语法 - 只是不是您通常使用的语法 - 所以可以在生产代码中使用它,也许可以用注释解释正在发生的事情

我做了一个纯 C++ 的解决方案,在我们的项目环境中运行良好。

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);
}

此类型必须在某处注册,例如: main.cpp

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

QML 片段:

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"
        }
    }
}