qtqml

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


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
        onLoaded: {
            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
    }
}

Solution

  • 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:

        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
    AsyncPage {
        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 * 6 / 10
        }
    
        Button {
            id: heavyCalcButton
            anchors.horizontalCenter: parent.horizontalCenter
            y: parent.height * 7 / 10
            text: "Calculate Now"
            onClicked: heavyCalc()
        }
    
        function heavyCalc() {
            _asyncToGenerator(function*() {
                cumsum = "";
                heavyCalcButton.enabled = false;
                yield pass();
                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();
                heavyCalcButton.enabled = true;
            })();
        }
    
        function pass() {
            return new Promise(function (resolve, reject) {
                Qt.callLater(resolve);
            } );
        }
    }
    
    // AsyncPage.qml
    import QtQuick
    import QtQuick.Controls
    Page {
        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)
            }
        }
    }
    
    

    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: