How to update QML properties from Julia in QML.jl (Signal-like pattern?)

I am currently learning QML.jl, IMO matured GUI framework for Julia. It’s documentation is quite limited so I decided that first prototype in PySide6(which has lot of resources) then port the code to Julia.

I am currently stuck on porting code with Signal based communiction from Python to Julia.

PySide6 example

Below is a minimal example of a live digital clock.
Python emits the current time every second to QML using a Qt signal.

# basicclock.py

import sys
from datetime import datetime

from PySide6.QtCore import Property, QObject, QTimer, QUrl, Signal
from PySide6.QtQml import QQmlApplicationEngine
from PySide6.QtWidgets import QApplication


class Clock(QObject):
    timeChanged = Signal(str)

    def __init__(self):
        super().__init__()
        self._time = ""
        self.timer = QTimer(self)
        self.timer.timeout.connect(self.update_time)
        self.timer.start(1000)
        self.update_time()

    def update_time(self):
        self._time = datetime.now().strftime("%H:%M:%S")
        self.timeChanged.emit(self._time)

    def get_time(self):
        return self._time

    time = Property(str, get_time, notify=timeChanged)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    engine = QQmlApplicationEngine()

    clock = Clock()
    engine.rootContext().setContextProperty("clock", clock)

    engine.load(QUrl("basicclock.qml"))
    if not engine.rootObjects():
        sys.exit(-1)
    sys.exit(app.exec())

Corresponding QML

// basicclock.qml

import QtQuick
import QtQuick.Controls

ApplicationWindow {
    visible: true
    width: 300
    height: 150
    title: "Digital Clock"

    Rectangle {
        anchors.fill: parent
        color: "#2E3440"

        Text {
            id: timeText
            anchors.centerIn: parent
            text: clock.time
            font.pixelSize: 48
            color: "#D8DEE9"
        }
    }
}


QML.jl version (current workaround)

I can recreate the same UI in QML.jl, but only by moving the timer logic into QML instead of keeping it in Julia.

# basicclock.jl

using QML
using Dates

clock = JuliaPropertyMap("time"=>Dates.format(now(), "HH:MM:SS"))

function update_clock()
    new_time = Dates.format(now(), "HH:MM:SS")
    clock["time"] = new_time
end

@qmlfunction update_clock

function @main(ARGS)
    loadqml("basicclock.qml", clock=clock)
    exec()
end

Corresponding QML

// basicclock.qml

import QtQuick
import QtQuick.Controls
import jlqml

ApplicationWindow {
    visible: true
    width: 300
    height: 150
    title: "Digital Clock"

    Rectangle {
        anchors.fill: parent
        color: "#2E3440"

        Text {
            id: timeText
            anchors.centerIn: parent
            text: clock.time
            font.pixelSize: 48
            color: "#D8DEE9"
        }
    }

    Timer {
        interval: 1000
        running: true
        repeat: true
        onTriggered: Julia.update_clock()
    }
}

While this approach works, I’m unsure whether moving application logic into QML solely to update state is the intended or recommended design.

Questions

  1. How to emit data from background running Julia task to Qml component?
  2. Is there any equivalent to PySide6 Signal class and it’s methods in QML.jl or does QML.jl provides different paradigm for this?
  3. Is it possible to achieve the same behavior as the PySide6 example using a Julia Timer or QTimer, without moving the timer logic into QML?

Any guidance and recommended practices would be greatly appreciated.

3 Likes

With the following Julia code the unmodified QML from the Python example should work:

using QML
using Observables
using Dates

gettime() = Dates.format(now(), "HH:MM:SS")
const time = Observable(gettime())
qml_file = joinpath(dirname(@__FILE__), "qml", "basicclock.qml")

# Load the QML file, setting a context property named clock containing the time
engine = loadqml(qml_file, clock = JuliaPropertyMap("time" => time))

timer_task = @async begin
  while true
      time[] = gettime()
      sleep(1)
  end
end

# Run the application in the background
exec_async()

To answer the questions:

  1. The QML program has to be executed with exec_async to allow modifications from other Julia tasks. This method of running integrates the Julia and Qt event loops so events from both are processed. The regular exec blocks the calling Julia thread until the program exits.
  2. In Julia we use Observable for this.
  3. I used sleep here, but a Julia timer would work also.

The choice between QML or Julia to put the timer logic depends really on if exec_async is acceptable for your application. It might have an impact on QML performance since it is not using the native Qt event loop, and it also leaves a Julia REPL open (which you can then use to modify the state, try for example time[] = "hello" while this is running).

3 Likes

@barche Thanks for the solution. However, the QML application window closes instantly without showing any UI.

1 Like

Does it work when you include the .jl file from a running Julia REPL?

No, Same behavior occurs when running from the REPL as well. There’s no UI involved, but the time variable does change according to the code. When I print time[], it updates to the current time as expected.

Seems there is a problem with exec_async in the current QML release, I’ll have to investigate.

3 Likes

Thanks for your effort. I’m having trouble finding a few QML-related APIs:

  1. The QML binding for QUrl::toLocalFile seems to be missing. I found QUrlFromLocalFile , but I couldn’t find the reverse binding.
  2. I’m unable to find support for QImage or QQuickImageProvider. Is there a way to expose a Julia image to QML?

Did you try QmlJuliaExamples/images at master · barche/QmlJuliaExamples · GitHub ?

1 Like

I initially overlooked these examples. The images are rendering correctly in QML.

Have you found any workaround for QUrl::toLocalFile?

At the moment, this is what I’m using:

function to_local_file(file_uri::AbstractString)
    prefix = @static Sys.iswindows() ? "file:///" : "file://"
    path = replace(file_uri, prefix => "")
    return path
end

on QML side

const fileUrl = fileDialog.selectedFile
const cleanFileUrl = decodeURIComponent(fileUrl)
Julia.to_local_file(cleanFileUrl)

This should now be fixed in QML 0.12.

3 Likes

@barche It works correctly with QML 0.12. However, when I run it using
julia --project=. basicclock.jl, Julia opens the REPL.
Is this the intended behavior?

Yes, when using exec_async there is no Qt event loop and the main Julia thread instead processes events in a loop that runs until a REPL that is started in a separate task exits. This allows manipulating data that is used in the GUI directly from the REPL, but it also allows other tasks like the timer here to run. With the normal exec, the Qt event loops blocks the Julia main thread until the GUI exits, so then events can only come from interaction with the GUI or by using Qt’s own timers.

1 Like