mayadev
mayadev

Reputation: 21

QML - Load heavy StackView Components using WorkerScript

I made a simple QML project using Python as backend. My goal is to display a BusyIndicator when my main StackView is busy loading a heavy component, but I have an issue. When I load this component, my entire application freezes until the loading is complete. I would like to use a WorkerScript to load my page in a thread separate from the main GUI thread, so the display of my BusyIndicator is not blocked. How could I achieve that ?

Here is my code:

main.qml :

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQml

ApplicationWindow {
    visible: true
    width: 640
    height: 480
    title: "StackView with WorkerScript"

    StackView {
        id: stackView
        anchors.fill: parent
        initialItem: component1

        Component {
            id: component1
            Loader {
                sourceComponent: Component1 {}
            }
        }

        Component {
            id: component2
            Loader {
                sourceComponent: Component2 {}
            }
        }
    }
}

Component1.qml :

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts

Item {
    Rectangle {
        anchors.fill: parent
        color: "lightblue"

        ColumnLayout {
            anchors.centerIn: parent

            Text {
                Layout.alignment: Qt.AlignCenter
                text: "Component 1"
            }

            Button {
                Layout.alignment: Qt.AlignCenter
                text: "Push Component 2"
                onClicked: {
                    stackView.push(component2)
                }
            }
        }
    }
}

Component2.qml :

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts

Item {
    Rectangle {
        anchors.fill: parent
        color: "lightgreen"

        Repeater {
            model: 30000

            Text {
                text: index
                font.pointSize: 12
            }
        }

        ColumnLayout {
            anchors.centerIn: parent

            Text {
                Layout.alignment: Qt.AlignCenter
                text: "Component 2"
            }

            Button {
                Layout.alignment: Qt.AlignCenter
                text: "Pop"
                onClicked: {
                    stackView.pop()
                }
            }
        }
    }
}

Upvotes: 0

Views: 67

Answers (1)

Stephen Quan
Stephen Quan

Reputation: 25871

There is only one QML engine thread for all UI/UX. So, to populate the Repeater and keep your BusyIndicator animations running and having responsive clickable Buttons you have to populate the Repeater with pauses.

Your demo, regardless of WorkerScript will block the UI/UX thread because, at the end of the day, the population of the Repeater must occur in the UI/UX thread.

The following is an example of how you use Timer to introduce pauses. The Timer implementation aims to keep UI/UX 50% busy and 50% idle by setting the Timer to run in 200ms intervals but we cap the population of records to 100ms.

    // Snippet from Component2A.qml

    Repeater {
        //model: 30000
        model: ListModel { id: listModel }
    }

    Timer {
        interval: 200
        running: listModel.count < 30000
        repeat: true
        triggeredOnStart: true
        onTriggered: {
            var start = Date.now();
            while (listModel.count < 30000 && Date.now() - start < 100)
                listModel.append( { dummy: 1 });
        }
    }

Alternatively, you can use Qt.callLater to recursively call a populate() function. In the following example, we have a populate() that inserts as many records it can capped within 100ms. Then we recursively call populate() with Qt.callLater(). This means the UI/UX will be running at 100% utilization but at every 100ms the UI/UX is unblocked to catch up on BusyIndicator animations and handling of Button events.

    // Snippet from Component2B.qml

    Repeater {
        // model: 30000
        model: ListModel {
            id: listModel
            function populate() {
                var start = Date.now();
                while (listModel.count < 30000 && Date.now() - start < 100)
                    listModel.append( { dummy: 1 });
                if (listModel.count < 30000)
                    Qt.callLater(populate)
            }
            Component.onCompleted: populate()
        }
    }

Sometime ago, I posted an AsyncPage.qml that allows one to approximate async-await syntax with generator function-yield syntax. The key here is now the Qt.callLater() appears in a yielded Promise. Like before, we schedule these calls every 100ms. The recursion appears to be removed, but, in fact, it is how function generator-yield syntax works (similar async-await) meaning that the recursive is there but now works behinds the scene.

You may think it's multi threaded now, but, as I mentioned at the start, all this occurs in the one QML engine thread, but, these scheduled pauses allows the QML engine thread to catch up on events and animations.

    // Snippet from Component2C.qml

    Repeater {
        // model: 30000
        model: ListModel {
            id: listModel
            Component.onCompleted: _asyncToGenerator(function *() {
                var start = Date.now();
                for (let i = 0; i < 30000; i++) {
                    listModel.append( { dummy: 1 });
                    if (Date.now() - start >= 100) {
                        yield new Promise(resolve => Qt.callLater(resolve));
                        start = Date.now();
                    }
                }
            } )();
        }
    }

You can try all 3 answers online!

For more information about AsyncPage refer to my original posting: https://stackoverflow.com/a/73470222/881441

Oh, and another change I made to your code is I made the parent item Page so that your business portion can start at 1 indent instead of 2 indent. Also the background Rectangle will be sized automatically for you so that you do not need anchor it. Similar to background there are other useful properties such as header and footer which can often reduce the need to host header and footer sections in your ColumnLayout.

Page {
    background: Rectangle { color: "lightblue" }
    // insert your code here
}

Upvotes: 0

Related Questions