This package provides an interface to Qt5 QML. It uses the CxxWrap
package to expose C++ classes. Current functionality allows interaction between QML and Julia using Observables, ListModels and function calling. There is also a generic Julia display, as well as specialized integration for image drawing, GR plots and Makie.
Installation on Linux, Mac and Windows should be as easy as: (in pkg mode, hit ]
in the Julia REPL):
add QML
See https://barche.github.io/QML.jl/dev
A set of examples is available in the repository at https://github.com/barche/QmlJuliaExamples
To run the examples, execute the following code block in the Julia REPL.
# Alternatively, execute the git command directly in the shell or download the zip file
isdir("QmlJuliaExamples") && run(`git clone --depth 1 https://github.com/barche/QmlJuliaExamples`)
cd("QmlJuliaExamples")
# As an alternative to next three lines,
# 1) Start Julia with `julia --project`
# 2) Run `instantiate` from the pkg shell.
using Pkg
Pkg.activate(".")
Pkg.instantiate()
using QML
include(joinpath(dirname(pathof(QML)), "runexamples.jl"))
runexamples()
The above will run all the examples in series. To run a single example, include one of the files in the QmlJuliaExamples directory. For example, include("board.jl")
.
We support three methods of loading a QML file: QQmlApplicationEngine
, QQuickView
and QQmlComponent
. These behave equivalently to the corresponding Qt classes.
The easiest way to run the QML file main.qml
from the current directory is using the load
function, which will create and return a QQmlApplicationEngine
and load the supplied QML file:
using QML
load("main.qml")
exec()
The lifetime of the QQmlApplicationEngine
is managed from C++ and it gets cleaned up when the application quits. This means it is not necessary to keep a reference to the engine to prevent it from being garbage collected prematurely.
The QQuickView
creates a window, so it's not necessary to wrap the QML in ApplicationWindow
. A QML file is loaded as follows:
qview = init_qquickview()
set_source(qview, "main.qml")
QML.show(qview)
exec()
Using QQmlComponent
the QML code can be set from a Julia string wrapped in QByteArray
:
qml_data = QByteArray("""
import ...
ApplicationWindow {
...
}
""")
qengine = init_qmlengine()
qcomp = QQmlComponent(qengine)
set_data(qcomp, qml_data, "")
create(qcomp, qmlcontext());
# Run the application
exec()
Interaction with Julia happens through the following mechanisms:
- Call Julia functions from QML
- Read and set context properties from Julia and QML
- Emit signals from Julia to QML
- Use data models
Note that Julia slots appear missing, but they are not needed since it is possible to directly connect a Julia function to a QML signal in the QML code (see the QTimer example below).
In Julia, functions are registered using the qmlfunction
function:
my_function() = "Hello from Julia"
my_other_function(a, b) = "Hi from Julia"
qmlfunction("my_function", my_function)
qmlfunction("my_other_function", my_other_function)
For convenience, there is also a macro that registers any number of functions that are in scope and will have the same name in QML as in Julia:
@qmlfunction my_function my_other_function
However, the macro cannot be used in the case of non-exported functions from a different module or in case the function contains a !
character.
In QML, include the Julia API:
import org.julialang
Then call a Julia function in QML using:
Julia.my_function()
Julia.my_other_function(arg1, arg2)
Context properties are set using the context object method. To dynamically add properties from Julia, a QQmlPropertyMap
is used, setting e.g. a property named a
:
propmap = QML.QQmlPropertyMap()
propmap["a"] = 1
This sets the QML context property named property_name
to value julia_value
.
The value of a property can be queried from Julia like this:
@test propmap["a"] == 1
To pass these properties to the QML side, the property map can be the second argument to load
:
load(qml_file, propmap)
There is also a shorthand notation using keywords:
load(qml_file, a=1, b=2)
This will create context properties a
and b
, initialized to 1
and 2
.
When an Observable
is set in a QQmlPropertyMap
, bi-directional change notification is enabled. For example, using the Julia code:
using QML
using Observables
const qml_file = "observable.qml"
const input = Observable(1.0)
const output = Observable(0.0)
on(output) do x
println("Output changed to ", x)
end
load(qml_file, input=input, output=output)
exec_async() # run from REPL for async execution
In QML we add a slider for the input and display the output, which is twice the input (computed in QML here):
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
ApplicationWindow {
id: root
title: "Observables"
width: 512
height: 200
visible: true
ColumnLayout {
spacing: 6
anchors.fill: parent
Slider {
value: input
Layout.alignment: Qt.AlignCenter
Layout.fillWidth: true
minimumValue: 0.0
maximumValue: 100.0
stepSize: 1.0
tickmarksEnabled: true
onValueChanged: {
input = value;
output = 2*input;
}
}
Text {
Layout.alignment: Qt.AlignCenter
text: output
font.pixelSize: 0.1*root.height
}
}
}
Moving the slider will print the output on Julia. The input can also be set from the REPL using e.g. input[] = 3.0
, and the slider will move accordingly and call QML to compute the output, which can be queried using output[]
.
Most fundamental types are converted implicitly. Mind that the default integer type in QML corresponds to Int32
in Julia.
We also convert QVariantMap
, exposing the indexing operator []
to access element by a string key. This mostly to deal with arguments passed to the QML append
function in list models.
Defining signals must be done in QML in the JuliaSignals block, following the instructions from the QML manual. Example signal with connection:
JuliaSignals {
signal fizzBuzzFound(int fizzbuzzvalue)
onFizzBuzzFound: lastFizzBuzz.text = fizzbuzzvalue
}
The above signal is emitted from Julia using simply:
@emit fizzBuzzFound(i)
There must never be more than one JuliaSignals block in QML
The ListModel
type allows using data in QML views such as ListView
and Repeater
, providing a two-way synchronization of the data. The dynamiclist example from Qt has been translated to Julia in the dynamiclist.jl
example. As can be seen from this commit, the only required change was moving the model data from QML to Julia, otherwise the Qt-provided QML file is left unchanged.
A ListModel is constructed from a 1D Julia array. In Qt, each of the elements of a model has a series of roles, available as properties in the delegate that is used to display each item. The roles can be added using the addrole
function, for example:
julia_array = ["A", 1, 2.2]
myrole(x::AbstractString) = lowercase(x)
myrole(x::Number) = Int(round(x))
array_model = ListModel(julia_array)
addrole(array_model, "myrole", myrole, setindex!)
adds the role named myrole
to array_model
, using the function myrole
to access the value. The setindex!
argument is a function used to set the value for that role from QML. This argument is optional, if it is not provided the role will be read-only. The arguments of this setter are collection, new_value, key
as in the standard setindex!
function.
To use the model from QML, it can be exposed as a context attribute, e.g:
load(qml_file, array_model=array_model)
And then in QML:
ListView {
width: 200
height: 125
model: array_model
delegate: Text { text: myrole }
}
If no roles are added, one default role named string
is exposed, calling the Julia function string
to convert whatever value in the array to a string.
If new elements need to be constructed from QML, a constructor can also be provided, using the setconstructor
method, taking a ListModel
and a Julia function as arguments, e.g. just setting identity to return the constructor argument:
setconstructor(array_model, identity)
In the dynamiclist example, the entries in the model are all "fruits", having the roles name, cost and attributes. In Julia, this can be encapsulated in a composite type:
mutable struct Fruit
name::String
cost::Float64
attributes::ListModel
end
When an array composed only of Fruit
elements is passed to a listmodel, setters and getters for the roles and the constructor are all passed to QML automatically, i.e. this will automatically expose the roles name
, cost
and attributes
:
# Our initial data
fruitlist = [
Fruit("Apple", 2.45, ListModel([Attribute("Core"), Attribute("Deciduous")])),
Fruit("Banana", 1.95, ListModel([Attribute("Tropical"), Attribute("Seedless")])),
Fruit("Cumquat", 3.25, ListModel([Attribute("Citrus")])),
Fruit("Durian", 9.95, ListModel([Attribute("Tropical"), Attribute("Smelly")]))]
# Set a context property with our listmodel
propmap["fruitModel"] = ListModel(fruitlist)
See the full example for more details, including the addition of an extra constructor to deal with the nested ListModel
for the attributes.
QTimer
can be used to simulate running Julia code in the background. Excerpts from test/gui.jl
:
const bg_counter = Observable(0)
function counter_slot()
global bg_counter
bg_counter[] += 1
end
@qmlfunction counter_slot
load(qml_file, timer=QTimer(), bg_counter=bg_counter)
Use in QML like this:
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import org.julialang
ApplicationWindow {
title: "My Application"
width: 480
height: 640
visible: true
Connections {
target: timer
onTimeout: Julia.counter_slot()
}
ColumnLayout {
spacing: 6
anchors.centerIn: parent
Button {
Layout.alignment: Qt.AlignCenter
text: "Start counting"
onClicked: timer.start()
}
Text {
Layout.alignment: Qt.AlignCenter
text: bg_counter.toString()
}
Button {
Layout.alignment: Qt.AlignCenter
text: "Stop counting"
onClicked: timer.stop()
}
}
}
Note that QML provides the infrastructure to connect to the QTimer
signal through the Connections
item.
QML.jl provides a custom QML type named JuliaDisplay
that acts as a standard Julia multimedia Display
. Currently, only the image/png
mime type is supported. Example use in QML from the plot
example:
JuliaDisplay {
id: jdisp
Layout.fillWidth: true
Layout.fillHeight: true
onHeightChanged: root.do_plot()
onWidthChanged: root.do_plot()
}
The function do_plot
is defined in the parent QML component and calls the Julia plotting routine, passing the display as an argument:
function do_plot()
{
if(jdisp === null)
return;
Julia.plotsin(jdisp, jdisp.width, jdisp.height, amplitude.value, frequency.value);
}
Of course the display can also be added using pushdisplay!
, but passing by value can be more convenient when defining multiple displays in QML.
QML.jl provides a custom QML type named JuliaCanvas
which presents a canvas to be painted via a julia callback function. This approach avoids the MIME content encoding overhead of the JuliaDisplay approach.
Example use in QML from the canvas
example:
JuliaCanvas {
id: circle_canvas
paintFunction: paint_cfunction
Layout.fillWidth: true
Layout.fillHeight: true
Layout.minimumWidth: 100
Layout.minimumHeight: 100
}
The callback function paint_cfunction
is defined in julia:
# fix callback arguments (TODO: macro this?)
function paint_circle(buffer::Array{UInt32, 1},
width32::Int32,
height32::Int32)
width::Int = width32
height::Int = height32
buffer = reshape(buffer, width, height)
buffer = reinterpret(ARGB32, buffer)
paint_circle(buffer)
end
# callback to paint circle
function paint_circle(buffer)
width, height = size(buffer)
for x in 1:width
for y in 1:height
# paint here..., e.g.
buffer[x,y] = ARGB32(1, 0, 0, 1) #red
end
end
end
load(qmlfile,
#...
paint_cfunction = CxxWrap.@safe_cfunction(paint_circle, Cvoid, (Array{UInt32,1}, Int32, Int32))
)
Note that the canvas buffer is allocated (and freed) in the C++ code. A new unitialized buffer is allocated for each frame (this could change).
At the moment, only the 32-bit QImage::Format_RGB32 (alpha, red, green, blue) image format is supported.
See the example for details on emitting an update signal from julia to force redrawing the JuliaCanvas.
NOTE
Set
ENV["QSG_RENDER_LOOP"] = "basic"
at the top of your Julia file to avoid crashes or infinite loops when using JuliaCanvas.
When launching the application using exec
, execution in the REPL will block until the GUI is closed. If you want to continue using the REPL with an active QML gui, exec_async
provides an alternative. This method keeps the REPL active and polls the QML interface periodically for events, using a timer in the Julia event loop. An example (requiring packages Plots.jl and PyPlot.jl) can be found in repl-background.jl
, to be used as:
include("repl-background.jl")
plot([1,2],[3,4])
This should display the result of the plotting command in the QML window.
For further examples, see the documentation
.
- Julia 1.6 minimal requirement
- Signals in
JuliaSignals
must have arguments of typevar
- Role indices are 1-based now on the Julia side
- The interface of some functions has changed because of the way CxxWrap handles references and pointers more strictly now
- No more automatic conversion from
String
toQUrl
, use theQUrl("mystring")
constructor - Setting a
QQmlPropertyMap
as context object is not supported as of Qt 5.12