QML Column: possible QQuickItem::polish() loop - qt

I have two problems with the following code.
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.
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)
}
}
}

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

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.

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:

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

Related

QML Flickable with TextArea: property binding for contentY is overwritten - by whom?

I'm making a terminal widget. I want the Flickable to scroll down to the latest input when TextArea.text is updated. My code looks like as follows.
ColumnLayout {
anchors.fill: parent
Flickable {
id: scroller
clip: true
contentY: contentHeight - height
onContentYChanged: {
console.log("contentY:", contentY)
}
TextArea.flickable: TextArea {
id: textArea
Layout.fillWidth: true
}
Layout.fillWidth: true
Layout.fillHeight: true
}
RowLayout {
id: prompt
Label {
text: " > $ "
}
TextField {
id: textInput
Layout.fillWidth: true
}
}
}
When I run this, I see that contentY is being overwritten right after it's set by my binding:
qml: contentY: 1498
qml: contentY: 0
qml: contentY: 1517
qml: contentY: 0
I checked to make sure my binding is not setting it to 0. I tried to debug the binding loop using export QT_LOGGING_RULES="qt.qml.binding.removal.info=true", that came out clean. I had a look at the source for Flickable and I don't think any of the methods there are the culprit.
Is binding contentY the right way to do what I want? Why is my binding is not being respected?
The issue is that contentY is going to be overwritten constantly by the Flickable as the content inside of it moves around. You can't place a binding on it because as you can see, it will be immediately overwritten with a static value that updates as the user interacts with the flickable.
What you need to do instead is something like
onContentHeightChanged: Qt.callLater(() => contentY = contentHeight - height)
Now, whenever the text area grows, it will jump the contentY via an immediate assignment instead of trying to rely on a binding. The callLater ensures that it happens after the flickable resets contentY to 0 on its own due to the height change.

QML - How to drag a ToolButton?

The QT documentation has this tutorial.
I initially followed it exactly, and it works. I then made two modifications:
I replaced the ListView with a GridView (that works without #2).
I attempted to add a ToolButton to my delegate inside the Rectangle "content" like so:
Rectangle {
id: content
ToolButton {
id: toolButton
icon.color = "transparent"
icon.source = "image://Loader/iconName"
}
Drag.active: dragArea.held
Drag.source: dragArea
Drag.hotSpot.x: width / 2
Drag.hotSpot.y: height / 2
}
This does not work, the ToolButton appears to be processing the mouse movements and not propagating the messages (I can click the button, but I can not drag it)? This is actually somewhat expected to be honest.
So that said, does anyone have a good way of dragging ToolButtons around? Or is it just accepted that you can't do that? I have tried various combinations of Rectangles and MouseAreas but I can't seem to do one without breaking the other (ie either the drag fails or the button fails).
You can move the MouseArea as a child of the ToolButton to manage the drag with pressAndHold, and propagate the click to keep the button behavior:
Rectangle {
id: content
ToolButton {
id: toolButton
// bind the visual state of the button to the MouseArea
background: Rectangle {
color: marea.pressed
? Qt.darker("blue")
: marea.containsMouse
? Qt.lighter("blue")
: "blue" // use your desired colors
}
MouseArea {
id: marea
property bool held: false
drag.target: held ? content : undefined
drag.axis: Drag.YAxis
anchors.fill: parent
hoverEnabled: true
onPressAndHold: held = true
onReleased: held = false
onClicked: toolButton.clicked() // propagate clicked event to the ToolButton
}
}
// ...
}

How can I get the drop effect of a DragArea to animate towards the DropArea that received it?

Hope this makes some sense as a question. In my app, I have a DragArea defined which I use to start dragging things over top of various Rectangles that each contain a DropArea. Everything is working fine in my code except for a cosmetic effect that I would like to change.
In QML, when you start dragging from a DragArea and eventually drop, the animation effect is such that the thing you're dragging animates (while fading out) back to the spot from which you started dragging. This happens even when you drop over a DropArea that successfully captures the drop.
What I would like to do is have the drop effect animate towards the DropArea that received the drop - so that it appears I am dragging-and-dropping things into the Rectangle. Is there any way to do this?
I'm guessing that this in some way involves the .source and .target properties of these areas, but no luck so far in having any effect on where the drop animation goes.
By default, QML will give you no cosmetic behavior for drag and drop whatsoever. The drag target will begin at the drag start location, and will end wherever it is dropped, regardless of whether the drag is accepted or not.
Thus I assume the behavior you describe is implemented in your user code, which you have not disclosed. Regardless, what you want to do is quite easy, it involves tracking the position the drag originates at and it ends at, so you can use the two coordinates to animate the position.
In the following example the red rectangle can be dragged, and if dropped outside of a drop area it will animate from its current to its initial position, whereas if dropped in the yellow rectangle, it will animate from its initial to its drop position.
Window {
width: 600
height: 600
visible: true
Rectangle {
width: 200
height: 200
color: "yellow"
DropArea {
anchors.fill: parent
onEntered: drag.source.accepted = true
onExited: drag.source.accepted = false
}
}
Rectangle {
id: rect
width: 50
height: 50
color: "red"
x: parent.width * 0.5
y: parent.height * 0.5
Drag.active: mouseArea.drag.active
property point begin
property point end
property bool accepted : false
MouseArea {
id: mouseArea
anchors.fill: parent
drag.target: parent
onPressed: rect.begin = Qt.point(rect.x, rect.y)
onReleased: {
rect.end = Qt.point(rect.x, rect.y)
aX.from = rect.accepted ? rect.begin.x : rect.end.x
aX.to = rect.accepted ? rect.end.x : rect.begin.x
aY.from = rect.accepted ? rect.begin.y : rect.end.y
aY.to = rect.accepted ? rect.end.y : rect.begin.y
anim.start()
}
ParallelAnimation {
id: anim
NumberAnimation { id: aX; target: rect; property: "x"; duration: 200 }
NumberAnimation { id: aY; target: rect; property: "y"; duration: 200 }
}
}
}
}

QML: referencing root window by parent reference is unreliable

Qt/QML question. Using Qt 5.7.
Take the following simple QML program that displays a red rectangle and a blue rectangle aligned vertically. Click handlers for both rectangles attempt to change the color of the parent host window. But with a subtle difference. The red rectangle references the host window directly by it's id (rootWindow). The blue click handler changes color via a parent reference.
The former case works fine. The latter case does not work. It seems like the root window is treated specially and isn't directly part of the parent/child hierarchy, even if the Rectangles are logically nested in the code that way.
Can someone explain the rule around this?
import QtQuick 2.7
import QtQuick.Window 2.2
Window {
visible: true
width: 640
height: 480
title: qsTr("Hello World")
id: rootWindow
color: "#ffffee"
Rectangle {
id: rect1; width: 50; height: 50; color:"red"
MouseArea {
anchors.fill: parent;
onClicked: {
print("rect1 clicked");
rootWindow.color = "green"; // works fine
}
}
}
Rectangle {
id: rect2; width: 50; height: 50; color:"blue"
anchors.top: rect1.bottom
MouseArea {
anchors.fill: parent;
onClicked: {
print("rect2 clicked");
rect2.parent.color = "pink"; // does not work
}
}
}
}
If you add the following line to the onClicked handler, you'll see that its parent isn't the Window:
print(rect2.parent)
Output:
qml: QQuickRootItem(0x18b18147bc0)
This is explained not-so-visibly in the documentation for Window:
If you assign an Item to the data list, it becomes a child of the Window's contentItem, so that it appears inside the window. The item's parent will be the window's contentItem, which is the root of the Item ownership tree within that Window.
The window itself isn't an item, so it uses contentItem instead so that child items can have a parent.
However, in Qt 5.7, Window got an attached property that can be used to access the window of an item:
rect2.Window.window.color = "pink";
Whichever item comes before the Window.window part will be the item that the attached property is used on. You could use it on any item in this scene (e.g. the MouseArea), as they all belong to the same window.
Note that attached properties create a QObject-derived object for each unique item they're used on, so be mindful of how you use them, especially in items that are created in very large numbers.

How do I get a Loader to no longer fill its parent?

I have a Loader object in my main QML file. I load different QML sources in the loader at run time depending on the current context.
My steps are like this:
Load a login.qml file and set anchors.centerIn: parent on the Loader.
After successfully logging in, I load task.qml and then set anchors.fill: parent on the Loader.
After the user logs out, I want to redirect back to login.qml, and I set anchors.centerIn: parent again on the loader.
I would expect this would cause the Loader to be centered in the parent and no longer fill it. However, this is not the case. The Loader still fills the parent.
How do I change the anchors on an Item so it is centered again and no longer fills its parent?
You don't need to set anchors.centerIn again. Instead, you need to set anchors.fill and width and height to undefined so it defaults to the implicit size and again uses the anchors.centerIn property.
Here's a working example:
import QtQuick 2.0
import QtQuick.Controls 1.0
Item {
width: 800
height: 600
Rectangle {
id: rect
anchors.centerIn: parent
implicitWidth: 500
implicitHeight: 200
color: "red"
Button {
text: "Toggle Full Size"
anchors.centerIn: parent
onClicked: {
if (rect.anchors.fill) {
rect.anchors.fill = undefined
rect.width = undefined
rect.height = undefined
} else {
rect.anchors.fill = rect.parent
}
}
}
}
}
Alternatively, a simpler solution might be to make your Loader always fill its parent and instead add an extra Item at the top level of your login.qml file so the login view is centered in this Item. That would remove the necessity to change the anchor on the Loader.

Resources