{"id":3898,"date":"2020-12-10T10:04:19","date_gmt":"2020-12-10T09:04:19","guid":{"rendered":"https:\/\/www.basyskom.de\/?p=3898"},"modified":"2024-01-04T17:19:43","modified_gmt":"2024-01-04T16:19:43","slug":"speedup-your-qt-qml-list-scrolling-on-lowend-devices","status":"publish","type":"post","link":"https:\/\/www.basyskom.de\/en\/speedup-your-qt-qml-list-scrolling-on-lowend-devices\/","title":{"rendered":"Speedup your Qt\/QML list scrolling on lowend devices"},"content":{"rendered":"<div data-elementor-type=\"wp-post\" data-elementor-id=\"3898\" class=\"elementor elementor-3898\" data-elementor-post-type=\"post\">\n\t\t\t\t\t\t<section class=\"elementor-section elementor-top-section elementor-element elementor-element-be9e83d elementor-section-boxed elementor-section-height-default elementor-section-height-default wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-equal-height-no\" data-id=\"be9e83d\" data-element_type=\"section\" data-e-type=\"section\">\n\t\t\t\t\t\t<div class=\"elementor-container elementor-column-gap-default\">\n\t\t\t\t\t<div class=\"elementor-column elementor-col-100 elementor-top-column elementor-element elementor-element-54bd592\" data-id=\"54bd592\" data-element_type=\"column\" data-e-type=\"column\">\n\t\t\t<div class=\"elementor-widget-wrap elementor-element-populated\">\n\t\t\t\t\t\t<div class=\"elementor-element elementor-element-7325ed3 elementor-widget elementor-widget-text-editor\" data-id=\"7325ed3\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"text-editor.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t\t\t<h2>How scrolling in Qt\/QML ListViews is implemented<\/h2><p>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.<\/p><p>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.<\/p><p>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).<\/p><p>The QML ListView prior to Qt 5.15 gives you the <a href=\"https:\/\/doc.qt.io\/qt-5\/qml-qtquick-listview.html#cacheBuffer-prop\" target=\"_blank\" rel=\"noopener\">cache buffer property<\/a> 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&#8217;t help you on lists with a dynamic amount of entries.<\/p><p>Since the issue is caused by the creation and destruction of those complex delegates, wouldn&#8217;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!<\/p><p>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 <code data-no-translation=\"\">Component.onCompleted<\/code> and <code data-no-translation=\"\">Component.onDestruction<\/code> signals of the delegate. We will use this to create a custom delegate cache that will recycle delegates.<\/p>\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-f846ad4 elementor-view-default elementor-widget elementor-widget-icon\" data-id=\"f846ad4\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"icon.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t<div class=\"elementor-icon-wrapper\">\n\t\t\t<div class=\"elementor-icon\">\n\t\t\t<i aria-hidden=\"true\" class=\"hm hm-bowl\"><\/i>\t\t\t<\/div>\n\t\t<\/div>\n\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-1d2701c elementor-widget elementor-widget-text-editor\" data-id=\"1d2701c\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"text-editor.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t\t\t<h2>Let&#8217;s build a delegate cache<\/h2>\n<p>First, we add an item into our <code data-no-translation=\"\">main.qml<\/code>. Then we give the it the id <code data-no-translation=\"\">elementCache<\/code>, but you can call it however you like.<br>Said item also contains a JavaScript array called <code data-no-translation=\"\">delegateCache<\/code> as well as two functions called <code data-no-translation=\"\">getDelegate<\/code> and <code data-no-translation=\"\">returnDelegate<\/code>.<\/p>\n<p><code data-no-translation=\"\">Component.onCompleted<\/code> creates delegates upfront and pushes them into the cache. Note, if the cache is empty, additional delegates are created in the <code data-no-translation=\"\">getDelegate <\/code>function and reused on demand.<\/p>\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-3e672e9 elementor-widget elementor-widget-elementor-syntax-highlighter\" data-id=\"3e672e9\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"elementor-syntax-highlighter.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t<pre data-no-translation=\"\"><code class='language-qml' data-no-translation=\"\">Item {\r\n        id: elementCache\r\n\r\n        visible: false\r\n\r\n        property var delegateCache: []\r\n\r\n        function getDelegate() {\r\n            console.log(&quot;getDelegate, cache size&quot;, delegateCache.length)\r\n            if (delegateCache.length &gt; 0)\r\n            {\r\n                return delegateCache.pop()\r\n            }\r\n            else\r\n            {\r\n                return delegateComponent.createObject(elementCache)\r\n            }\r\n        }\r\n\r\n        function returnDelegate( item ) {\r\n            console.log(&quot;returnDelegate&quot;, item, &quot;size&quot;, delegateCache.length)\r\n            \r\n            item.parent = elementCache\r\n        \r\n            \/* \r\n                reset all properties of the delegate \r\n                this is important to get rid of bindings\r\n                if you dont do this, you may experience crashes\r\n                \r\n                i.e.\r\n                \r\n                item.myProperty = &quot;&quot;\r\n                item.myBindedProperty = false\r\n            *\/\r\n            item.anchors.fill = elementCache\r\n            item.name = &quot;&quot;\r\n            item.aStaticProperty = false\r\n\r\n            delegateCache.push( item )\r\n        }\r\n\r\n        Component.onCompleted: {\r\n            for (var i = 0; i &lt; 10; ++i)\r\n            {\r\n                var element = delegateComponent.createObject(elementCache)\r\n                delegateCache.push(element)\r\n            }\r\n        }\r\n\r\n        Component {\r\n            id: delegateComponent\r\n\r\n            MyComplexDelegate {}\r\n        }\r\n    } <\/code><\/pre><script>\nif (!document.getElementById('syntaxed-prism')) {\n\tvar my_awesome_script = document.createElement('script');\n\tmy_awesome_script.setAttribute('src','https:\/\/www.basyskom.de\/wp-content\/plugins\/syntax-highlighter-for-elementor\/assets\/prism2.js');\n\tmy_awesome_script.setAttribute('id','syntaxed-prism');\n\tdocument.body.appendChild(my_awesome_script);\n} else {\n\twindow.Prism && Prism.highlightAll();\n}\n<\/script>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t<\/section>\n\t\t\t\t<section class=\"elementor-section elementor-top-section elementor-element elementor-element-d94a571 elementor-section-boxed elementor-section-height-default elementor-section-height-default wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-equal-height-no\" data-id=\"d94a571\" data-element_type=\"section\" data-e-type=\"section\">\n\t\t\t\t\t\t<div class=\"elementor-container elementor-column-gap-default\">\n\t\t\t\t\t<div class=\"elementor-column elementor-col-100 elementor-top-column elementor-element elementor-element-9ecb8bc\" data-id=\"9ecb8bc\" data-element_type=\"column\" data-e-type=\"column\">\n\t\t\t<div class=\"elementor-widget-wrap elementor-element-populated\">\n\t\t\t\t\t\t<div class=\"elementor-element elementor-element-eb641f5 elementor-widget elementor-widget-text-editor\" data-id=\"eb641f5\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"text-editor.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t\t\t<p>Now let us build a <code data-no-translation=\"\">ListView<\/code>, that uses the cached delegates.<\/p>\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-9f18bbf elementor-widget elementor-widget-elementor-syntax-highlighter\" data-id=\"9f18bbf\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"elementor-syntax-highlighter.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t<pre data-no-translation=\"\"><code class='language-qml' data-no-translation=\"\">ListView {\r\n    id: listView\r\n\r\n    anchors.fill: parent\r\n    model: myModel\r\n\r\n    delegate: Item {\r\n        id: container\r\n\r\n        height: 40\r\n        width: parent.width\r\n\r\n        property Item item\r\n        \r\n        Connections {\r\n            target: item\r\n            ignoreUnknownSignals: true\r\n\r\n            onButtonClicked: {\r\n                console.log(&quot;HELLO WORLD&quot;)\r\n            }\r\n        }\r\n\r\n        Component.onCompleted:\r\n        {\r\n            item = elementCache.getDelegate()\r\n            item.parent = container\r\n            item.anchors.fill = Qt.binding(function (){ return container })\r\n            item.name = Qt.binding(function (){\r\n                return model.NameRole\r\n            })\r\n\r\n            item.aStaticProperty = model.constantBoolRole\r\n        }\r\n\r\n        Component.onDestruction:\r\n        {\r\n            elementCache.returnDelegate(item)\r\n        }\r\n    }\r\n} <\/code><\/pre><script>\nif (!document.getElementById('syntaxed-prism')) {\n\tvar my_awesome_script = document.createElement('script');\n\tmy_awesome_script.setAttribute('src','https:\/\/www.basyskom.de\/wp-content\/plugins\/syntax-highlighter-for-elementor\/assets\/prism2.js');\n\tmy_awesome_script.setAttribute('id','syntaxed-prism');\n\tdocument.body.appendChild(my_awesome_script);\n} else {\n\twindow.Prism && Prism.highlightAll();\n}\n<\/script>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t<\/section>\n\t\t\t\t<section class=\"elementor-section elementor-top-section elementor-element elementor-element-47bd0a6 elementor-section-boxed elementor-section-height-default elementor-section-height-default wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-equal-height-no\" data-id=\"47bd0a6\" data-element_type=\"section\" data-e-type=\"section\">\n\t\t\t\t\t\t<div class=\"elementor-container elementor-column-gap-default\">\n\t\t\t\t\t<div class=\"elementor-column elementor-col-100 elementor-top-column elementor-element elementor-element-84f7b74\" data-id=\"84f7b74\" data-element_type=\"column\" data-e-type=\"column\">\n\t\t\t<div class=\"elementor-widget-wrap elementor-element-populated\">\n\t\t\t\t\t\t<div class=\"elementor-element elementor-element-e7cfe44 elementor-widget elementor-widget-text-editor\" data-id=\"e7cfe44\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"text-editor.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t\t\t<p>In <code data-no-translation=\"\">Component.onCompleted<\/code> of the container delegate, the code gets one cached delegate from our <code data-no-translation=\"\">elementCache,<\/code>by calling <code data-no-translation=\"\">elementCache.getDelegate()<\/code>. Next, we simply change the parent of the delegate to the container.<br \/>In the next steps, the code creates bindings and sets static properties.<\/p><p>In <code data-no-translation=\"\">Component.onDestruction<\/code>, the code calls <code data-no-translation=\"\">elementCache.returnDelegate(item)<\/code>. This ensures that before the container is destroyed, the actual delegate will be pushed back into the cache (in <code data-no-translation=\"\">returnDelegate<\/code> the parent of the delegate will be set back to <code data-no-translation=\"\">elementCache<\/code>).<\/p>\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-b690dfe elementor-view-default elementor-widget elementor-widget-icon\" data-id=\"b690dfe\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"icon.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t<div class=\"elementor-icon-wrapper\">\n\t\t\t<div class=\"elementor-icon\">\n\t\t\t<i aria-hidden=\"true\" class=\"hm hm-centralize\"><\/i>\t\t\t<\/div>\n\t\t<\/div>\n\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-8ffeba1 elementor-widget elementor-widget-text-editor\" data-id=\"8ffeba1\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"text-editor.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t\t\t<h3>Working with Model Data<\/h3><p>Getting <strong>static data<\/strong> into the delegate can be done by setting a property.<br \/><code style=\"color: var( --e-global-color-text ); font-weight: var( --e-global-typography-text-font-weight );\" data-no-translation=\"\">item.aStaticProperty = model.constantBoolRole<\/code><\/p><p>Getting <strong>dynamic data<\/strong> into the delegate can be done by creating a <code data-no-translation=\"\">Qt.binding<\/code> object. <br \/><code data-no-translation=\"\">item.name = Qt.binding(function (){ return model.NameRole})<\/code><\/p><p>Last we need to <strong>connect signals<\/strong> 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.<\/p><p>This is done by adding a <code data-no-translation=\"\">Connections<\/code> object to the container. It is able to handle multiple signals.<\/p>\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-049a8fd elementor-widget elementor-widget-elementor-syntax-highlighter\" data-id=\"049a8fd\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"elementor-syntax-highlighter.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t<pre data-no-translation=\"\"><code class='language-qml' data-no-translation=\"\">Connections {\r\n    target: item\r\n    ignoreUnknownSignals: true\r\n\r\n    onButtonClicked: {\r\n        console.log(&quot;HELLO WORLD&quot;)\r\n    }\r\n} <\/code><\/pre><script>\nif (!document.getElementById('syntaxed-prism')) {\n\tvar my_awesome_script = document.createElement('script');\n\tmy_awesome_script.setAttribute('src','https:\/\/www.basyskom.de\/wp-content\/plugins\/syntax-highlighter-for-elementor\/assets\/prism2.js');\n\tmy_awesome_script.setAttribute('id','syntaxed-prism');\n\tdocument.body.appendChild(my_awesome_script);\n} else {\n\twindow.Prism && Prism.highlightAll();\n}\n<\/script>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t<\/section>\n\t\t\t\t<section class=\"elementor-section elementor-top-section elementor-element elementor-element-6d46355 elementor-section-boxed elementor-section-height-default elementor-section-height-default wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-equal-height-no\" data-id=\"6d46355\" data-element_type=\"section\" data-e-type=\"section\">\n\t\t\t\t\t\t<div class=\"elementor-container elementor-column-gap-default\">\n\t\t\t\t\t<div class=\"elementor-column elementor-col-100 elementor-top-column elementor-element elementor-element-9b8d4e6\" data-id=\"9b8d4e6\" data-element_type=\"column\" data-e-type=\"column\">\n\t\t\t<div class=\"elementor-widget-wrap elementor-element-populated\">\n\t\t\t\t\t\t<div class=\"elementor-element elementor-element-3ac5839 elementor-widget elementor-widget-text-editor\" data-id=\"3ac5839\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"text-editor.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t\t\t<p><b>That&#8217;s it!<\/b> A simple straightforward, JavaScript based, fast scrolling list caching mechanism. It works &#8211; no matter how complex your delegates are, even in Qt.5.2 \ud83d\ude42<\/p><p>If you have any questions or remarks, just drop a comment down below.<\/p>\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t<\/section>\n\t\t\t\t<\/div>","protected":false},"excerpt":{"rendered":"<p>Something that has traditionally been complicated to achieve in Qt\/QML, especially on low end hardware, is high performant list scrolling with complex delegates.<br \/>\nThis 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.<br \/>\nIn this blog post, I will explain how you can implement this feature in Qt Versions prior to 5.15.<\/p>","protected":false},"author":1,"featured_media":4011,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"inline_featured_image":false,"footnotes":""},"categories":[1,2,8],"tags":[126,15],"class_list":["post-3898","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-allgemein","category-blog","category-qt","tag-qml","tag-qt"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.basyskom.de\/en\/wp-json\/wp\/v2\/posts\/3898","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.basyskom.de\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.basyskom.de\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.basyskom.de\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.basyskom.de\/en\/wp-json\/wp\/v2\/comments?post=3898"}],"version-history":[{"count":81,"href":"https:\/\/www.basyskom.de\/en\/wp-json\/wp\/v2\/posts\/3898\/revisions"}],"predecessor-version":[{"id":7474,"href":"https:\/\/www.basyskom.de\/en\/wp-json\/wp\/v2\/posts\/3898\/revisions\/7474"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.basyskom.de\/en\/wp-json\/wp\/v2\/media\/4011"}],"wp:attachment":[{"href":"https:\/\/www.basyskom.de\/en\/wp-json\/wp\/v2\/media?parent=3898"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.basyskom.de\/en\/wp-json\/wp\/v2\/categories?post=3898"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.basyskom.de\/en\/wp-json\/wp\/v2\/tags?post=3898"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}