2

I have a qml application which performs a rather long action upon a users request. During the time, I want to display an overlay over the whole screen, so the user is aware that the application is working, basically a busy indicator.

My Problem is, that the application starts with the task, before updating the UI component. Here's a minimal example to demonstrate the problem:

import QtQuick 2.9
import QtQuick.Window 2.3

Window {
    visible: true
    width: 640
    height: 480
    title: qsTr("Ui Demo")

    Rectangle {
        id: rectangle
        anchors.fill: parent
        color: "green"    
    }


    MouseArea {
        id: action

        anchors.fill: parent
        onClicked: {
            rectangle.color = "red"
            for(var i = 0; i < 10000; i++)
                console.log(i)
        }
    }
}

What I want is, that the Rectangles color turns red while the for loop is running, but the behavior I see is that the color changes only after the loop has finished.

I also tried the following with no difference:

Rectangle {
    id: rectangle
    anchors.fill: parent
    color: "green"

    onColorChanged: {
        for(var i = 0; i < 10000; i++)
            console.log(i)
    }
}

I know, that the cleanest solution would be to perform the heavy work on a different thread to not block the UI thread. But I do not wish to do this, because in my actual application the blocking work is updating a ListModel, which (as noted here for example)

Qt views unfortunately don't know how to deal with [when they are] in foreign threads.

So I would need to implement a new, asynchronous Model class, which is effort and time my customer is currently not willing to pay for.

Therefor my question is: How can I make sure, that the UI is redrawn/updated as soon as I set the property?

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
T3 H40
  • 2,326
  • 8
  • 32
  • 43

2 Answers2

2

A possible approach is to use transform the sequential logic of the "for" to an asynchronous logic through a Timer:

import QtQuick 2.9
import QtQuick.Window 2.3

Window {
    visible: true
    width: 640
    height: 480
    title: qsTr("Ui Demo")

    Rectangle {
        id: rectangle
        anchors.fill: parent
        color: "green"
    }


    MouseArea {
        id: action

        anchors.fill: parent
        onClicked: {
            rectangle.color = "red"
            timer.start()
        }
    }
    Timer{
        id: timer
        interval: 1
        property int counter: 0
        repeat: true
        onTriggered: {
            counter += 1
            console.log(counter)
            if(counter > 100000)
                timer.stop()
        }
    }
}
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • I thought about using a Timer, but scratched the idea as I did not want to delay execution. For some reason though I did not think about an interval of 1... `>.<` I slightly [adapted](https://stackoverflow.com/a/50945027/2865814) your solution to match my real code a little better and it works fine. Thanks! – T3 H40 Jun 20 '18 at 09:43
1

Reliable Workaround

Thanks to eyllanesc's answer I figured out a possible solution. I use a single shot timer, to start my work, because in the actual code I cannot call different steps with a repeating timer - but I do not need to anyway, as I don't want to display any animated UI elements. This code works for my purposes:

import QtQuick 2.9
import QtQuick.Window 2.3

Window {
    visible: true
    width: 640
    height: 480
    title: qsTr("Ui Demo")

    Rectangle {
        id: rectangle
        anchors.fill: parent
        color: "green"
    }


    MouseArea {
        id: action

        anchors.fill: parent
        onClicked: {
            rectangle.color = "red"
            timer.start()
        }
    }

    Timer {
        id: timer

        interval: 1
        repeat: false
        onTriggered: {
            for(var i = 0; i < 10000; i++)
                console.log(i)
            rectangle.color = "green"
        }
    }
}

Adding a Timer - even with only a 1 msec interval - grants the logic with the processing time to change the color, before starting with the actual work. While this looks like a slightly hacky workaround it works just fine.

More elegant though less reliable approach

There is a cleaner, though less reliable solution: Qts callLater() function seems to be sort of what I was looking for. Even though the official documentation seems incomplete, I found the function documentation in its source code:

Use this function to eliminate redundant calls to a function or signal.

The function passed as the first argument to Qt.callLater() will be called later, once the QML engine returns to the event loop.

When this function is called multiple times in quick succession with the same function as its first argument, that function will be called only once.

For example: \snippet qml/qtLater.qml 0

Any additional arguments passed to Qt.callLater() will be passed on to the function invoked. Note that if redundant calls are eliminated, then only the last set of arguments will be passed to the function.

Using the call later function delayed the call to the working code most of the time for long enough so that the UI would get updated. However about a third of the times, this would fail and show the same behavior as described in the question.

This approach can be implemented like the following:

import QtQuick 2.9
import QtQuick.Window 2.3

Window {
    visible: true
    width: 640
    height: 480
    title: qsTr("Ui Demo")

    Rectangle {
        id: rectangle
        anchors.fill: parent
        color: "green"
    }


    MouseArea {
        id: action

        anchors.fill: parent
        onClicked: {
            rectangle.color = "red"
            Qt.callLater(action.doWork)
        }

        function doWork() {
            for(var i = 0; i < 10000; i++)
                console.log(i)
            rectangle.color = "green"
        }
    }
}
T3 H40
  • 2,326
  • 8
  • 32
  • 43
  • 1
    using [`Qt.callLater()`](http://doc.qt.io/qt-5/qml-qtqml-qt.html#callLater-method) seems more appropriate. – GrecKo Jun 20 '18 at 10:10
  • 1
    @GrecKo That's pretty much exactly what I was hoping for, but a quick test makes it look like occasionally "later" isn't late enough, and the Ui again is not updated early enough. – T3 H40 Jun 20 '18 at 11:03
  • I also found Qt.callLater() works most of the time, but, not all the time. – Stephen Quan Sep 15 '22 at 00:23