Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

Convenient way to execute C++ code after QML has rerendered



  • Hi all,

    In a QML application I have to update the view right after it has been rendered, and I'm looking for a simple snippet to achieve this.

    To be precise:

    1. I have a custom MyVTKModel {} QML element that uses QQuickFramebufferObject to render a 3D model.
    2. Another QML element Overlay {} is drawn on top of this, but it depends on how the 3D model is rendered -- for instance, it should know how many pixels one millimeter in the scene is.
    3. This means Overlay{} should update only after MyVTKModel{} has rendered.

    In a controller in C++ I'm changing the camera and moving the object, and it would now be very convenient if I could update the overlay in the same function. I was hoping I could just do QTimer::singleShot(0, update_overlay) where update_overlay would only be called after the QML had been rerendered.

    This is not the case, as the following minimal working example shows. It has a button, which calls a C++ function, which emits a change that updates the QML (a rectangle becomes wider) AND has this QTimer::singleShot(0, ...)

    // main.cpp
    int main(int argc, char *argv[])
    {
        qputenv("QSG_RENDER_LOOP", QByteArray("basic"));
        Controller controller;
        QGuiApplication app(argc, argv);
        QQmlApplicationEngine engine;
        engine.rootContext()->setContextProperty("Controller", &controller);
    
        const QUrl url(QStringLiteral("qrc:/main.qml"));
    
        QObject::connect(&engine, &QQmlApplicationEngine::objectCreated, &app, [url](QObject *obj, const QUrl &objUrl) {
            if (!obj && url == objUrl) QCoreApplication::exit(-1);
        }, Qt::QueuedConnection);
    
        engine.load(url);
        return app.exec();
    }
    
    // Controller.cpp -- it has the callback
    void Controller::click() {
        emit changeScene();
    
        QTimer::singleShot(0, [](){
            qDebug() << "Hello world";
        });
    }
    
    // main.qml
    import QtQuick 2.12
    import QtQuick.Controls 2.5
    import QtQuick.Window 2.12
    
    Window {
        visible: true
        width: 640
        height: 480
        title: "After render calback"
    
        property int swaps: 0;
    
        onBeforeSynchronizing: console.log("Before synchronizing")
        onBeforeRendering: console.log("Before rendering")
        onAfterRendering: console.log("After rendering")
        onFrameSwapped: console.log("Frame swapped: " + (++swaps))
    
        Rectangle {
            id: rect
            width: 20
            height: 20
            color: "blue"
            anchors.centerIn: parent
        }
    
        MouseArea {
            anchors.fill: parent
            onClicked: Controller.click()
        }
    
        Connections {
            target: Controller
            onChangeScene: {
                console.log("Changing the scene")
                rect.width += 1
            }
        }
    }
    

    When clicking the button, what I'm getting is:

    qml: Before synchronizing
    qml: Before rendering
    qml: After rendering
    qml: Frame swapped: 1
    
    qml: Before synchronizing
    qml: Before rendering
    qml: After rendering
    qml: Frame swapped: 2
    
    qml: Before synchronizing
    qml: Before rendering
    qml: After rendering
    qml: Frame swapped: 3
    
    qml: Changing the scene
    Hello world
    
    qml: Before synchronizing
    qml: Before rendering
    qml: After rendering
    qml: Frame swapped: 4
    

    What I was hoping for was that QTimer::singleShot would fire only after the next render / frame swap. So basically, what I wanted was:

    qml: Changing the scene
    
    qml: Before synchronizing
    qml: Before rendering
    qml: After rendering
    qml: Frame swapped: 4
    
    Hello world
    

    What would be a convenient way to execute that lambda in the callback only after QML has rerendered?


    EDIT: in my application I am using the single threaded render loop with qputenv("QSG_RENDER_LOOP", QByteArray("basic"));



  • What about using callLater()? It runs after the current function has exited if I remember right.



  • @fcarney thanks for your reply :) I tried it like this: adding a slot to the controller

    void Controller::click() {
        emit changeScene();
    }
    
    void Controller::later() {
        qDebug() << "Hello world";
    }
    

    and handling the onClicked as

    onClicked: {
        Qt.callLater(Controller.later)
        Controller.click()
    }
    

    but unfortunately Qt.callLater is handled before the scene is rerendered:

    ...
    qml: Frame swapped: 3
    qml: Changing the scene
    Hello world
    qml: Before synchronizing
    qml: Before rendering
    qml: After rendering
    qml: Frame swapped: 4
    


  • Okay, set a variable flag on your event. Then check that variable in the onFrameSwapped event. Then use callLater (if needed) or just do your thing at that point. Reset the variable flag.



  • Yes, exactly, something like this is what I wrote. I was just hoping there was a convenient way to keep the code for all this local to a single C++ function, but maybe that's too much to ask.


Log in to reply