Table of Contents
Qt

Speedup your Qt/QML list scrolling on lowend devices

Qt

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.

2 Responses

  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.

Leave a Reply

Your email address will not be published. Required fields are marked *

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.
Share on facebook
Share on twitter
Share on linkedin
Share on reddit
Share on xing
Share on email
Share on stumbleupon
Share on whatsapp
Share on pocket

Read more

OPC UA open62541
OPC UA
Frank Meerk├Âtter
Qt OPC UA: Logging improvements in Qt 6.1

So far programs using Qt OPC UA with the open62541 back-end produced quite a bit of chatter on stdout originating from the open62541 stack itself. Unfortunately there was no simple way to get rid of these low-level logs. We now provide a way to control this behavior.

Read More ┬╗
Pyhton
Python Qt
Sumedha Widyadharma
Using Shiboken2 to create Python bindings for a Qt library

With the release of Qt 5.12, Qt for Python is officially supported and can be used to write full-fledged Qt applications using Python as the main programming language.

This prompted us to also take a closer look at the bindings (the Python module is called PySide2) and also the underlying technology, namely the binding generator called Shiboken2.

Read More ┬╗
IoT Cloud
Azure
Heike Ziegler
IoT: Getting started with cloud and modern IoT and IIoT from scratch

IoT and IIoT applications are special compared to other kinds of cloud applications as they have to deal with devices existing “outside” of data centers.

The following series of articles provide an end-to-end overview of what Microsoft Azure offers to handle some of the challenges involved in connecting an IoT Device with the Cloud.

By working through this series you will learn about the major concepts involved in getting your IoT/IIoT device connected to Microsoft Azure. In our examples we will feature Qt, Node.Js, Protobuf from Google and much more to get you started.

Read More ┬╗