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).

2 Likes