Home > Blockchain >  In QML, the Loader freezes the UI when loading large/time-consuming objects
In QML, the Loader freezes the UI when loading large/time-consuming objects

Time:11-07

There are several questions on this subject that are unrelated to my question and They did not produce any results for me.

Imagine I have a splash screen with AnimatedImage in QML that I want to display when my heavy components are loading in the background, so I use a Loader to load assets in background, but when the loader starts loading my UI freezes(i.e. that AnimatedImage), I can see that BusyIndicator not freezes.

I have provided the full source code in the github repository so that you may test it more easily.

my questions are:

  1. Do Loaders really run in the background (for example, if I'm trying to connect to a server in my constructor, can Loader handle this situation or do I have to run it in another thread)?
  2. How should such scenarios be handled so that I do not see any glitches?

window.qml

import QtQuick 2.10
import QtQuick.Controls 2.3
import QtQuick.Layouts

Window {
    id:mainWindow
    y:100
    width: 640
    height: 480
    visible: true
    flags: Qt.FramelessWindowHint

    //splash screen
    Popup {
        id: popup
        width: mainWindow.width
        height: mainWindow.height
        modal: false
        visible: true

        Overlay.modeless: Rectangle {
            color: "#00000000"
        }

        //Splash loader
        Loader{
            id: splash
            anchors.fill: parent
            source: "qrc:/Splashscreen.qml"
        }
    }
    
    // Timer that will start the loading heavyObjects
    Timer {
        id: timer
        interval: 2000
        repeat: false
        running: true
        onTriggered: {
            loader.source = "qrc:/heavyObjects.qml"
            loader.active = true
        }
    }

    //write a loader to load main.qml
    Loader {
        id: loader
        anchors.fill: parent
        asynchronous: true
        active: false
        //when loader is ready, hide the splashscreen
        onl oaded: {
            popup.visible = false
        }

        visible: status == Loader.Ready
    }
}

SplashScreen.qml


import QtQuick 2.0
import QtQuick.Controls 2.0
import QtQuick.Window 2.2

Item {
    Rectangle {
        id: splashRect
        anchors.fill: parent
        color: "white"
        border.width: 0
        border.color: "black"

        AnimatedImage {
            id: splash
            source: "qrc:/images/Rotating_earth_(large).gif"
            anchors.fill: parent
        }
    }
}

heavyObject.qml

import QtQuick

Item {
    function cumsum() {
        for(var j=0;j<100;j  ){
            var p = 0
            for (var i = 0; i < 1000000; i  ) {
                p *= i
            }
        }
        return ""
    }

    // show dummy text that this is the main windows
    Text {
        text: "Main Window"   String(cumsum())
        anchors.centerIn: parent
    }
}

CodePudding user response:

Most things you do in QML are handled in the QML engine thread. If you do something heavy in that thread, it will block everything else. I haven't checked your source code, but, in terms of heavy initialization, we can break it up with Qt.callLater() or similar so that the QML engine thread can catch up on UI/UX events.

For example, in the following:

  • I changed cumsum from a function to a property
  • I introduced calcStep for do a calculation for one j iteration
  • I use Qt.callLater to instantiate the next iteration
  • I kick off the calculation during Component.onCompleted
    property string cumsum
    function calcStep(j) {
        if (j >= 100) {
            cumsum = new Date();
            return;
        }
        for (var i = 0; i < 1000000; i  ) {
             p *= i
        }
        Qt.callLater(calcStep, j 1);
    }
    Component.onCompleted: calcStep(0)
}

If your initialization is more sophisticated, you may want to give Promises a try. This allows you to write asynchronous routines in a synchronous type of way, e.g.

    property string cumsum
    function calc() {
        _asyncToGenerator(function*() {
            for(var j=0;j<100;j  ){
                var p = 0
                status = "j: "   j;
                yield pass();
                for (var i = 0; i < 1000000; i  ) {
                    p *= i
                }
            }
            cumsum = new Date();
        })();
    }
    function pass() {
        return new Promise(function (resolve, reject) {
            Qt.callLater(resolve);
        } );
    }
    Component.onCompleted: calc()

In the above, the cumsum calculation has been using a derivative of the async/await pattern. For this, to work I make use of _asyncToGenerator provided by a transpiler on babeljs.io. This is required since the QML/JS does not support async/await pattern until Qt6.6.

The pass() function operates similarly to Python pass but has my implementation of Qt.callLater wrapped in a Promise. Invoking it with yield pass(); does nothing but allows your function to momentarily release control so that the UI/UX events can catch up.

import QtQuick
import QtQuick.Controls
Page {
    property string cumsum
    property string status

    // show dummy text that this is the main windows
    Text {
        text: "Main Window: "   cumsum
        anchors.centerIn: parent
    }

    Text {
        text: status
        anchors.horizontalCenter: parent.horizontalCenter
        y: parent.height * 3 / 4
    }

    function calc() {
        _asyncToGenerator(function*() {
            for(var j=0;j<100;j  ){
                var p = 0
                status = "j: "   j;
                yield pass();
                for (var i = 0; i < 1000000; i  ) {
                    p *= i
                }
            }
            cumsum = new Date();
        })();
    }

    function pass() {
        return new Promise(function (resolve, reject) {
            Qt.callLater(resolve);
        } );
    }

    function _asyncToGenerator(fn) {
        return function() {
            var self = this,
            args = arguments
            return new Promise(function(resolve, reject) {
                var gen = fn.apply(self, args)
                function _next(value) {
                    _asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value)
                }
                function _throw(err) {
                    _asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err)
                }
                _next(undefined)
            })
        }
    }

    function _asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
        try {
            var info = gen[key](arg)
            var value = info.value
        } catch (error) {
            reject(error)
            return
        }
        if (info.done) {
            resolve(value)
        } else {
            Promise.resolve(value).then(_next, _throw)
        }
    }

    Component.onCompleted: calc()
}

You can Try it Online!

If you are interested in some of the work I've done with async and QML Promises refer to the following GitHub projects:

  • Related