basysKom AnwendungsEntwicklung

Speedup your Qt/QML list scrolling on lowend devices
Essential Summary
Something that has traditionally been complicated to achieve in Qt/QML, especially on low end hardware, is high performant list scrolling with complex delegates. This has recently changed. In Qt 5.15, it is as simple as setting the new QML ListView property called reuseItems to true. For more details, have a look at the documentation. In this blog post, I will explain how you can implement this feature in Qt Versions prior to 5.15.

How scrolling in Qt/QML ListViews is implemented

In order to display something in a listview, you need to provide a data model and a delegate. The delegate defines how each data item from the model is displayed. By default, QML will not create all list entries (aka. delegates) upfront. Instead, the engine will create and show only visible entries as well as a few additional ones (for caching). This results in faster loading times and less memory usage compared to an approach where all entries are created upfront.

When scrolling, additional list entries are created on-demand. QML will create a delegate for each newly visible model entry. At the same time, delegates, that become invisible and move out of the cached range are destroyed.

Creating simple items is fast and cheap, whereas the creation of complex QML objects can become quite slow (e.g. items containing several text elements, buttons, icons and logic).

The QML ListView prior to Qt 5.15 gives you the cache buffer property to tweak the caching behavior. It allows you to adjust the pixel range, in which delegates will be created and not be destroyed. You pay for it with an increase in memory usage and loading time of your QML scene. Increasing the cache buffer is okay on small static lists, it won’t help you on lists with a dynamic amount of entries.

Since the issue is caused by the creation and destruction of those complex delegates, wouldn’t it be nice to keep the delegates and reuse them? While this is not a built-in option pre Qt 5.15, we can use the mechanics of QML, in combination with a little help of JavaScript, to achieve the same result!

In order to do that, we need to hook into the creation and destruction of delegates by the list view. This can be done via the Component.onCompleted and Component.onDestruction signals of the delegate. We will use this to create a custom delegate cache that will recycle delegates.

Let’s build a delegate cache

First, we add an item into our main.qml. Then we give the it the id elementCache, but you can call it however you like.
Said item also contains a JavaScript array called delegateCache as well as two functions called getDelegate and returnDelegate.

Component.onCompleted creates delegates upfront and pushes them into the cache. Note, if the cache is empty, additional delegates are created in the getDelegate function and reused on demand.

Item {
        id: elementCache

        visible: false

        property var delegateCache: []

        function getDelegate() {
            console.log("getDelegate, cache size", delegateCache.length)
            if (delegateCache.length > 0)
            {
                return delegateCache.pop()
            }
            else
            {
                return delegateComponent.createObject(elementCache)
            }
        }

        function returnDelegate( item ) {
            console.log("returnDelegate", item, "size", delegateCache.length)
            
            item.parent = elementCache
        
            /* 
                reset all properties of the delegate 
                this is important to get rid of bindings
                if you dont do this, you may experience crashes
                
                i.e.
                
                item.myProperty = ""
                item.myBindedProperty = false
            */
            item.anchors.fill = elementCache
            item.name = ""
            item.aStaticProperty = false

            delegateCache.push( item )
        }

        Component.onCompleted: {
            for (var i = 0; i < 10; ++i)
            {
                var element = delegateComponent.createObject(elementCache)
                delegateCache.push(element)
            }
        }

        Component {
            id: delegateComponent

            MyComplexDelegate {}
        }
    } 

Now let us build a ListView, that uses the cached delegates.

ListView {
    id: listView

    anchors.fill: parent
    model: myModel

    delegate: Item {
        id: container

        height: 40
        width: parent.width

        property Item item
        
        Connections {
            target: item
            ignoreUnknownSignals: true

            onButtonClicked: {
                console.log("HELLO WORLD")
            }
        }

        Component.onCompleted:
        {
            item = elementCache.getDelegate()
            item.parent = container
            item.anchors.fill = Qt.binding(function (){ return container })
            item.name = Qt.binding(function (){
                return model.NameRole
            })

            item.aStaticProperty = model.constantBoolRole
        }

        Component.onDestruction:
        {
            elementCache.returnDelegate(item)
        }
    }
} 

In Component.onCompleted of the container delegate, the code gets one cached delegate from our elementCache,by calling elementCache.getDelegate(). Next, we simply change the parent of the delegate to the container.
In the next steps, the code creates bindings and sets static properties.

In Component.onDestruction, the code calls elementCache.returnDelegate(item). This ensures that before the container is destroyed, the actual delegate will be pushed back into the cache (in returnDelegate the parent of the delegate will be set back to elementCache).

Working with Model Data

Getting static data into the delegate can be done by setting a property.
item.aStaticProperty = model.constantBoolRole

Getting dynamic data into the delegate can be done by creating a Qt.binding object.
item.name = Qt.binding(function (){ return model.NameRole})

Last we need to connect signals from within the pulled in delegate (such as button clicks) and populate them to the container, where we can setup the handler of the click.

This is done by adding a Connections object to the container. It is able to handle multiple signals.

Connections {
    target: item
    ignoreUnknownSignals: true

    onButtonClicked: {
        console.log("HELLO WORLD")
    }
} 

That’s it! A simple straightforward, JavaScript based, fast scrolling list caching mechanism. It works – no matter how complex your delegates are, even in Qt.5.2 🙂

If you have any questions or remarks, just drop a comment down below.

Picture of Jeremias Bosch

Jeremias Bosch

Jeremias Bosch consults in his position as Technical Project Manager our customers in building embedded HMI applications, as well as the implementation of next generation cloud projects. He is responsible for the system/software-architecture and the development within customer projects as well as the agile project management. He has over 12 years of experience in developing HMIs and Web Applications. He has delivered multiple large and medium scale Qt Quick and cloud applications in industries such as automotive, aerospace and manufacturing engineering. He holds a diploma of computer science from the University of Applied Sciences in Isny. Is a certifed SCRUM Master and Product Owner as well as a certifed Qt Developer.

2 Antworten

  1. Hi Arun,
    we already discussed in the ticket on github. The conclusion is that the time is spend in ‚onCompleted‘ of the delegate. Specifically on color change since this is causing color updates in 500 subsequent rectangles.

    The example here is made to be extrem – I guess that it’s unlikely that you ever build a delegate that contains 500 rectangles and those 500 rectangles changes their color on creation time.

    However there is a way to also improve performance here. You need to move the change of the color out of ‚onCompleted‘ and trigger the change later – i.e. by an external signal (scrolling is done) or by a timer.

    Once you have done so, scrolling should be smooth again.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

Weitere Blogartikel

basysKom Newsletter

We collect only the data you enter in this form (no IP address or information that can be derived from it). The collected data is only used in order to send you our regular newsletters, from which you can unsubscribe at any point using the link at the bottom of each newsletter. We will retain this information until you ask us to delete it permanently. For more information about our privacy policy, read Privacy Policy