How to achieve better caching control for ListView? - qt

I tried to play with cacheBuffer, but it's only help me to increase count of cached delegates, when I want to disable caching at all.
Now with zero caching buffer my example (only one item stretched on all ListView) behaves like this:
At the start ListView creates two delegates: currently visible and
next one.
When I scrolling list forward it creates and keep up to 4 delegates without beginning destroying them.
When I start scrolling list backward it begin immediately destroying delegates without looking on cacheBuffer.
If you replace "height: root.height" to "height: listView.height", it will create delegates for all model items at the start.
Is this behaviour normal? Can I change it some way?
You can tried it yourself:
import QtQuick 2.11
import QtQuick.Controls 2.4
import QtQuick.Window 2.11
import Qt.labs.calendar 1.0
Window {
id: root
visible: true
width: 640
height: 480
title: qsTr("Hello World")
ListView {
id: listView
anchors.fill: parent
snapMode: ListView.SnapOneItem
cacheBuffer: 0
model: 10
delegate: Rectangle {
width: parent.width
height: root.height
// height: listView.height
border.color: "black"
Text {
anchors.centerIn: parent
text: modelData
}
Component.onCompleted: {
console.log("Delegate completed")
}
Component.onDestruction: {
console.log("Delegate destruction")
}
}
}
}

Replace
delegate: MyVeryComplexDelegate {
}
by
delegate: Loader {
width: expectedDelegateWidth
height: expectedDelegateHeight // Otherwise you might create all...
sourceComponent: MyVeryComplexDelegate {
}
active: someCriteriaYouFeelGoodAbout()
}
Now you will only have simple Loaders in your cache and you can decide which ones of those in the cache are active.
Probably better: Have parts of the MyVeryComplexDelegate loaded as the ListView wants, and just hide the most complex parts behind a Loader that turns active only if you really need the full complexity.
On your strange findings as far as I can explain them:
Regarding the difference between root.height and listView.height, the explanation is an issue that is subject to many questions:
While root.height references the property height of the window, which you have explicitly set, listView.height is determined by anchors.fill: parent, which results in setting the height to root.contentItem.height - and that is initially 0. Therefore the delegates, initially all have a height of 0, all of them would fit in the view and therefor have to be created, even if you load as lazy as possible. Later they will resize together with the root.contentItem and some will be destroyed again.
You can see that, when monitoring the height changes of your delegates and your ListView
The next thing is, that even if the delegate really fills the ListView from the beginning, a second delegate is instantiated. The reason for that is, the condition used by the ListView, when to create new delegates. For that the sum of heights - the displacement of the first has to be larger than the ListView. That is not fulfilled when it is equal to the height.
Increase the height of your delegate by a fraction of a pixel, and you are good.
height: root.height + 0.0001

Related

QtQuick - what is meant by "parent item"?

The following QML code:
Window {
id: window
width: 450
height: 700
visible: true
StackView {
id: mainStack
property Item itemTest: Item {
id: itemTest
ColumnLayout {
id: mainLayout
width: mainStack.width
ScrollView {
id: scrollview
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ColumnLayout{
id: colLayout
anchors.fill: scrollview
}
}
}
}
initialItem: itemTest
anchors.fill: parent
}
}
outputs "QML ColumnLayout: Cannot anchor to an item that isn't a parent or sibling."
Replacing "anchors.fill: scrollview" by "anchors.fill: parent" in the above code makes this message disappear but then the ColumnLayout does not seem to fill the ScrollView.
Given this behaviour, I come to the conclusion that the ScrollView in this QML file isn't actually the parent of "colLayout", which comes against my first intuition about the way parenting works in QML.Can someone explain to me what is meant exactly by the keyword "parent" in QML ? Many thanks in advance.
The issue is that Controls use the concept of a contentItem. While the ScrollView itself is a Control which itself is in turn an Item, the children are parented to a different Item called contentItem.
More info here:
https://doc.qt.io/qt-5/qml-qtquick-controls2-control.html#contentItem-prop
See also the comment there:
Note: Most controls use the implicit size of the content item to calculate the implicit size of the control itself. If you replace the content item with a custom one, you should also consider providing a sensible implicit size for it (unless it is an item like Text which has its own implicit size).
You don't want to grow your ColumnLayout to match the contentItem, the contentItem will automatically resize to fit the implicit size of the ColumnLayout.
If the effect you are trying to get is to match the size of the ColumnLayout to that of the ScrollView, then use something like:
ColumnLayout{
id: colLayout
implicitWidth: scrollview.width
implicitHeight: scrollview.height
height: implicitHeight
width: implicitWidth
}
But in that case, why bother using a ScrollView? Normally you would allow the ColumnLayout to manage its implicit size normally based on it's children. When the contentItem ends up overflowing the ScrollView, then it starts to automatically scroll.

Why is loop created in this case?

This examples gives me property binding errors:
file:///home/user/qmltests/layouts.qml:22:4: QML Label: Binding loop detected for property "font.pixelSize"
file:///home/user/qmltests/layouts.qml:22:4: QML Label: Binding loop detected for property "font.pixelSize"
file:///home/user/qmltests/layouts.qml:18:4: QML Label: Binding loop detected for property "font.pixelSize"
Code:
import QtQuick 2.11
import QtQuick.Controls 2.4
import QtQuick.Layouts 1.11
Page {
id: root
width: 400
height: 200
StackLayout {
id: main_container
Layout.fillWidth:true
Layout.fillHeight:true
ColumnLayout {
id: sub_container
Layout.fillWidth:true
Layout.fillHeight:true
Label {
text: "One"
font.pixelSize: sub_container.height*0.2
}
Label {
text: "Two"
font.pixelSize: sub_container.height*0.2
}
}
}
}
By logic, this shouldn't happen, because I am copying the width and height down to lower level components by using Layout.fillWidth=true and layout.fillHeight=true
To fix this error, I have to copy the heigth from the root element:
import QtQuick 2.11
import QtQuick.Controls 2.4
import QtQuick.Layouts 1.11
Page {
id: root
width: 400
height: 200
StackLayout {
id: main_container
Layout.fillWidth:true
Layout.fillHeight:true
ColumnLayout {
id: sub_container
Layout.fillWidth:true
Layout.fillHeight:true
Label {
text: "One"
font.pixelSize: root.height*0.2
}
Label {
text: "Two"
font.pixelSize: root.height*0.2
}
}
}
}
Why aren't width and height propagated from the root elements down to children layouts?
How can I reference sub_container.width and sub_container.height (because it is known before items are laid out) without getting binding loop error? I don't want to reference the root item because due to complexity there may be many layouts inside root item and in order to lay out components in a scalable way I need to know the width and height of the parent layout.
If you use layouts, the elements they manage must not change their size based
on size given by the layout. To do what you wish to do, you shouldn’t be using a layout, but anchors, since you want to manage the child sizes manually. The loop is there because the layout uses the size of your item to resize itself, that your item then uses to resize itself, endlessly. If you don’t need that functionality, it will interfere – as you have seen. The reason it worked via root is that root’s size is not managed by the layout: it’s fixed. And that’s what you wanted all along, isn’t it?
Another approach would be for the label not to change its size hint based on font size, so that the layout wouldn’t react to the font size change.
TL;DR: Layouts size themselves based on child sizes, thus there’s a loop if the child sizes itself based on the layout’s size.

How to improve ListView performance during scrolling in qt/qml

I am facing serious issues while scolling ListView when there is costly delegate.
When i am scrolling listview, its jerky.
Some once please help on this.
Here is my sample code
/LIstview with some dummy costly delegate, I tried using loader to load the delegate then its a bit better but still its jerky/
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Window 2.2
ApplicationWindow {
visible: true
width: Screen.width
height: Screen.height
ListView {
width: Screen.width
height: Screen.height
model: 500
spacing: 10
highlightMoveVelocity: 50
flickDeceleration: 500
delegate: Loader{
asynchronous: true
sourceComponent: Image {
width: Screen.width
asynchronous: true
height: index %2 === 0 ? 500: 200
source: "file:///home/Downloads/4k.jpg"
///Just for show
Image {
anchors.fill: parent
asynchronous: true
source: "file:///home/Downloads/4k.jpg"
}
Image {
anchors.fill: parent
asynchronous: true
source: "file:///home/Downloads/4k.jpg"
}
Image {
anchors.fill: parent
asynchronous: true
source: "file:///home/Downloads/4k.jpg"
}
Image {
anchors.fill: parent
asynchronous: true
source: "file:///home/Downloads/4k.jpg"
}
Image {
anchors.fill: parent
asynchronous: true
source: "file:///home/Downloads/4k.jpg"
}
Image {
anchors.fill: parent
asynchronous: true
source: "file:///home/Downloads/4k.jpg"
}
Image {
anchors.fill: parent
asynchronous: true
source: "file:///home/Downloads/4k.jpg"
}
}
}
}
}
Since it is unclear what is exactly is costly about your model/delegate, I cannot give exact advice, but:
If you have to create a list that is very long, containing items that require some rather costly code to run to fully show them:
You could run the costly code in a background thread, then update the display as the calculations are finished. In the meantime, you can present a non-finished version of the data. Once the calculations for one item are finished, you update the display.
This could be improved even more by caching the result.
Make use of QAbstractListModel's fetchMore functionality to fill the list slowly as the scrolling progresses. Here is a good example.
Both approaches can be combined, of course.
And naturally C++ is required for the solutions, but that's rather unavoidable once the limitations of QML are hit.
Maybe try increasing the cacheBuffer property. And remember - the value is pixels you want to pre-load.
Other than that, streamlining the delegate is the way to go, but that would require your actual use case rather than just some dummy costly delegate.

Why does my QML background transparency break depending on width setting?

I'm running into some strange QML behavior. Basically, I have a TabBar header with several tabs running across it. I'd like the background element to be mostly the same for each of them, but some of them I want to be able to dynamically change the color of. So I have a component:
Component {
id: standardBackground
Rectangle {
opacity: parent.parent.checked ? 0 : (parent.parent.pressed ? 0.8 : 1)
color: tabColor
}
}
And for each TabButton, I'm doing:
TabButton {
text: qsTr("Tab 1")
background: Loader { sourceComponent: standardBackground }
height: 60
}
This works perfectly, but I'm running into some really strange errors. First off, running it this way gives me the following QML warning:
QML TabButton: Binding loop detected for property "implicitWidth"
So I figured I could fix this by adding: width: parent.width to the Rectangle in my component. This does silence the warning, but for some reason, it makes it so that the first tab will always be transparent regardless of whether or not it's clicked. This only affects the first tab. I have no clue why this would happen.
However, when I set width: <anything>, then this fixes both problems: No warnings and correct transparency. Playing around with different settings for the width causes no noticeable changes, as long as it's positive. So I have it set to 1. If I set it to 0, I get the same "implicit width" warnings.
So a couple different questions:
Why does the transparency of the component break when I set width: parent.width?
Why can I set width to any constant value without it affecting the GUI at all?
Is there a better way of silencing the warning about implicit width?
Here is my full code (simplified to less tabs):
import QtQuick 2.6
import QtQuick.Layouts 1.3
import QtQuick.Controls 2.0
import QtQuick.Controls.Material 2.0
import QtQuick.Controls.Universal 2.0
import Qt.labs.settings 1.0
import QtQuick.VirtualKeyboard 2.1
import QtQuick.VirtualKeyboard.Settings 2.1
import "DataEntry"
ApplicationWindow {
id: window
width: 1280
height: 1024
visible: true
title: "Hello World"
property var tabColor: "#353637"
property var dummy: InputContext.focus
Settings {
id: settings
property string style: "Universal"
}
Component {
id: standardBackground
Rectangle {
opacity: parent.parent.checked ? 0 : (parent.parent.pressed ? 0.8 : 1)
color: tabColor
width: 1
}
}
header: TabBar {
id: bar
width: parent.width
height: 60
TabButton {
text: qsTr("Tab 1")
background: Loader { sourceComponent: standardBackground }
height: 60
}
TabButton {
text: qsTr("Tab 2")
background: Loader {
sourceComponent: standardBackground
function getTabColor(error){
if (error)
return '#cccc00'
return window.tabColor
}
property var tabColor: getTabColor(hasError)
}
height: 60
}
}
StackLayout {
id: viewStack
width: parent.width
anchors.fill: parent
currentIndex: bar.currentIndex
tab1 {
}
tab2 {
}
}
}
As we are on SO I tend to answer only one question. For you, I choos the question for the binding loop.
The reason for that binding loop is documented here.
You do not specify a size for the Loader so the implicit width of the Loader is set to the width specified by the loaded Item. Here you set the size to be the same as the Loader's size. Now this would not be a problem, and the result would just be 0
Now we stir in the Button which also has an implicitSize set to its styling items. Here the Loader is instantiated widht width 0 and then resized to fill the implicitWidth of the Button which is (without a sized background) depending on the text and the paddings.
And now we update the round. So, the implicitWidth of the Rectangle is depending on the width of the Loader whose implicitWidth is depending on the Rectangles width. Further the Loaders width is depending on the Buttons width, which is depending on its implicitWidth and which is in turn depending on its childrenRect.width...
A binding loop is easily detected even if there are no direct problems, as the system is stabilizing in the first iteration.

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.

Resources