40

I would like to create a delay function in javascript that takes a parameter of amount of time to delay, so that I could use it do introduce delay between execution of JavaScript lines in my QML application. It would perhaps look like this:

function delay(delayTime) {
  // code to create delay
}

I need the body of the function delay(). Note that setTimeout() of JavaScript doesn't work in QML.

Sнаđошƒаӽ
  • 16,753
  • 12
  • 73
  • 90

7 Answers7

48

As suggested in the comments to your question, the Timer component is a good solution to this.

function Timer() {
    return Qt.createQmlObject("import QtQuick 2.0; Timer {}", root);
}

timer = new Timer();
timer.interval = 1000;
timer.repeat = true;
timer.triggered.connect(function () {
    print("I'm triggered once every second");
})

timer.start();

The above would be how I'm currently using it, and here's how I might have implemented the example in your question.

function delay(delayTime) {
    timer = new Timer();
    timer.interval = delayTime;
    timer.repeat = false;
    timer.start();
}

(Which doesn't do anything; read on)

Though the exact way you are looking for it to be implemented suggests that you are looking for it to block until the next line of your program executes. But this isn't a very good way to go about it as it would also block everything else in your program as JavaScript only runs in a single thread of execution.

An alternative is to pass a callback.

function delay(delayTime, cb) {
    timer = new Timer();
    timer.interval = delayTime;
    timer.repeat = false;
    timer.triggered.connect(cb);
    timer.start();
}

Which would allow you to use it as such.

delay(1000, function() {
    print("I am called one second after I was started.");
});

Hope it helps!

Edit: The above assumes you're working in a separate JavaScript file that you later import into your QML file. To do the equivalent in a QML file directly, you can do this.

import QtQuick 2.0

Rectangle {
    width: 800
    height: 600

    color: "brown"

    Timer {
        id: timer
    }

    function delay(delayTime, cb) {
        timer.interval = delayTime;
        timer.repeat = false;
        timer.triggered.connect(cb);
        timer.start();
    }

    Rectangle {
        id: rectangle
        color: "yellow"
        anchors.fill: parent
        anchors.margins: 100
        opacity: 0

        Behavior on opacity {
            NumberAnimation {
                duration: 500
            }
        }
    }

    Component.onCompleted: {
        print("I'm printed right away..")
        delay(1000, function() {
            print("And I'm printed after 1 second!")
            rectangle.opacity = 1
        })
    }
}

I'm not convinced that this is the solution to your actual problem however; to delay an animation, you could use PauseAnimation.

Sнаđошƒаӽ
  • 16,753
  • 12
  • 73
  • 90
Marcus Ottosson
  • 3,241
  • 4
  • 28
  • 34
  • A also think that `Timer` is nice solution – folibis Feb 14 '15 at 10:47
  • 1
    Be careful if you use this more than once. Please see the subsequent answer from Bumsik Kim below. It took me a long time to realize I had multiple functions still connected to my timer without realizing it. – DaveK Mar 29 '20 at 20:20
  • I thing you need to call timer.destroy(); ``` ... timer.triggered.connect(cb); timer.triggered.connect(function () { timer.destroy(); }); ... ``` – Israel Lins Albuquerque Jul 18 '20 at 00:42
  • The delay is freezing the qml 2.15 app now. – Ingo Mi Nov 15 '20 at 00:12
  • I'm just starting with JavaScript, because there's some QML work to do. Should you not use `timer = Timer();` instead of `timer = new Timer();`? `new` will create a new object and inject it to `Timer` as `this`, but you don't use it. So the `new` keyword here seems superfluous and confusing to me. – Johannes Schaub - litb Feb 05 '21 at 18:33
19

Marcus' answer does the job, but there is one big problem.

The problem is that the callback keeps connected to triggered signal even after triggered once. This means that if you use that delay function again, the timer will triggers all callbacks connected before again. So you should disconnect the callback after triggered.

This is my enhanced version of the delay function:

Timer {
    id: timer
    function setTimeout(cb, delayTime) {
        timer.interval = delayTime;
        timer.repeat = false;
        timer.triggered.connect(cb);
        timer.triggered.connect(function release () {
            timer.triggered.disconnect(cb); // This is important
            timer.triggered.disconnect(release); // This is important as well
        });
        timer.start();
    }
}

...

timer.setTimeout(function(){ console.log("triggered"); }, 1000);
manicaesar
  • 5,024
  • 3
  • 26
  • 29
Bumsik Kim
  • 5,853
  • 3
  • 23
  • 39
4

Here's another variation which utilizes the Component object to house the Timer object.

Then we implement a setTimeout look-a-like function to dynamically create and invoke this Timer object.

N.B. The answer assumes Qt5.12.x which includes ECMAScript 7 (and therefore ECMAScript 6) to utilize parameter shortcuts, rest parameters and spread syntax:

    function setTimeout(func, interval, ...params) {
        return setTimeoutComponent.createObject(app, { func, interval, params} );
    }

    function clearTimeout(timerObj) {
        timerObj.stop();
        timerObj.destroy();
    }

    Component {
        id: setTimeoutComponent
        Timer {
            property var func
            property var params
            running: true
            repeat: false
            onTriggered: {
                func(...params);
                destroy();
            }
        }
    }

In the following snippet, we will invoke console.log(31), console.log(32), console.log(33) in a random time delay between 0-1000ms from now.

console.log("Started");
setTimeout(console.log, Math.floor(1000 * Math.random()), 31);
setTimeout(console.log, Math.floor(1000 * Math.random()), 32);
setTimeout(console.log, Math.floor(1000 * Math.random()), 33);

See also: https://community.esri.com/groups/appstudio/blog/2019/05/22/ecmascript-7-settimeout-and-arrow-functions

Stephen Quan
  • 21,481
  • 4
  • 88
  • 75
  • 1
    Nicely capsuled except for the reference to `app` in `setTimeoutComponent.createObject(app, …)`. It is safe to pass `null` here instead ([see](https://doc.qt.io/qt-5/qtqml-javascript-dynamicobjectcreation.html#creating-a-component-dynamically)). No matter for garbage collection either as the object is self-`destroy()`ing. – tanius Jun 27 '20 at 13:51
1

The answer from Bumsik Kim is great, this answer changes it slightly so that the timer can be used on a repeating basis and then stopped and reused when desired.

The QML for the timer to add where required.

// Allow outside access (optional)
property alias timer: timer

Timer {
    id: timer

    // Start the timer and execute the provided callback on every X milliseconds
    function startTimer(callback, milliseconds) {
        timer.interval = milliseconds;
        timer.repeat = true;
        timer.triggered.connect(callback);
        timer.start();
    }

    // Stop the timer and unregister the callback
    function stopTimer(callback) {
        timer.stop();
        timer.triggered.disconnect(callback);
    }
}

This can be used as follows.

timer.startTimer(Foo, 1000); // Run Foo every 1 second
timer.stopTimer(Foo); // Stop running Foo

timer.startTimer(Bar, 2000); // Run Bar every 2 seconds
timer.stopTimer(Bar); // Stop running Bar

function Foo() {
    console.log('Executed Foo');
}

function Bar() {
    console.log('Executed Bar');
}
The Thirsty Ape
  • 983
  • 3
  • 16
  • 31
1

Here's my continued evolution of the prior answers https://stackoverflow.com/a/62051450/3220983 and https://stackoverflow.com/a/50224584/3220983...

Add this file / component to your project:

Scheduler.qml

import QtQuick 2.0

Timer {
    id: timer

    property var _cbFunc: null
    property int _asyncTimeout: 250

    // Execute the callback asynchonously (ommiting a specific delay time)
    function async( cbFunc )
    { delay( cbFunc, _asyncTimeout ) }

    // Start the timer and execute the provided callback ONCE after X ms
    function delay( cbFunc, milliseconds )
    { _start( cbFunc, milliseconds, false ) }

    // Start the timer and execute the provided callback repeatedly every X ms
    function periodic( cbFunc, milliseconds )
    { _start( cbFunc, milliseconds, true ) }

    function _start( cbFunc, milliseconds, isRepeat ) {
        if( cbFunc === null ) return
        cancel()
        _cbFunc        = cbFunc
        timer.interval = milliseconds
        timer.repeat   = isRepeat
        timer.triggered.connect( cbFunc )
        timer.start()
    }

    // Stop the timer and unregister the cbFunc
    function cancel() {
        if( _cbFunc === null ) return
        timer.stop()
        timer.triggered.disconnect( _cbFunc )
        _cbFunc = null
    }
}

Then, implement in another component like:

...
Scheduler { id: scheduler; }
scheduler.delay( function(){ console.log('Delayed'); }, 3000 );

You can use anonymous functions like shown here, or else callback into named functions. Use the simple async to fire off code in a non-blocking manner, if you aren't too concerned about the exact timing. Note that while it's tempting to use a 0 ms timeout for an "asynchronous" callback (as one would with a C++ QTimer), that is not the right approach with a QML Timer! These timers don't appear to queue events on an event loop where screen redraws are given priority. So, if your goal is to defer a given operation to achieve "instant" UI changes first, you need to dial up the delay interval as shown here. Using 0ms will often cause the code to fire prior to redraws.

Note that one of these "Scheduler" instances can only be bound to one callback function, on one given interval, at a time. Multiple instances are required if you need to "overlap" delayed events.

BuvinJ
  • 10,221
  • 5
  • 83
  • 96
0

you can use QtTest

import QtTest 1.0
import QtQuick 2.9

ApplicationWindow{
    id: window

    TestEvent {
        id: test
    }

    function delay_ms(delay_time) {
        test.mouseClick(window, 0, 0, Qt.NoButton, Qt.NoModifier, delay_time)
    }
}
fcying
  • 39
  • 7
0

This should be sufficient:

void QmlUtils::singleShot(int msec, QJSValue callback)
{
    QTimer::singleShot(msec, this, [callback] () mutable {
        if (callback.isCallable())
            callback.call();
    });
}

then call it in QML, wherever you are:

qmlUtils.singleShot(5000, () => console.log("Hello!"))

Done.

If you want, you can use this without even writing it. Simply expose it to QML with:

ctx->setContextProperty("lqtUtils", new lqt::QmlUtils(qApp));
Luca Carlon
  • 9,546
  • 13
  • 59
  • 91