Easy and fast desktop GUIs with QML
Slides and examples at https://github.com/barche/juliacon2020-qml
Julia
using QML
using Observables
slidervalue = Observable(0.0)
load(
"slider.qml",
variables = JuliaPropertyMap(
"slidervalue" => slidervalue
)
)
exec_async()
QML
import QtQuick 2.6
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.0
ApplicationWindow {
visible: true
onClosing: Qt.quit()
ColumnLayout {
Slider {
value: variables.slidervalue
onValueChanged: {
variables.slidervalue = value
}
}
Text {
text: variables.slidervalue
}
}
}
Julia
using QML
using Observables
slidervalue = Observable(0.0)
load(
"slider.qml",
variables = JuliaPropertyMap(
"slidervalue" => slidervalue
)
)
exec_async()
QML
import QtQuick 2.6
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.0
ApplicationWindow {
visible: true
onClosing: Qt.quit()
ColumnLayout {
Slider {
value: variables.slidervalue
onValueChanged: {
variables.slidervalue = value
}
}
Text {
text: variables.slidervalue
}
}
}
import QtQuick 2.6
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.0
import org.julialang 1.0
ApplicationWindow {
title: "My Application"
width: 520
height: 570
visible: true
ColumnLayout {
spacing: 6
anchors.centerIn: parent
Button {
Layout.alignment: Qt.AlignCenter
text: "Push Me"
onClicked: Julia.showimage(juliaDisplay)
}
JuliaDisplay {
id: juliaDisplay
width: 512
height: 512
}
}
}
Defining functions for use in QML
using Test
using QML
using Images
using TestImages
qmlfunction("showimage",
d::JuliaDisplay -> display(d, testimage("mandrill"))
)
load("image.qml")
# Run the application
exec()
Defining functions for use in QML
using Test
using QML
using Images
using TestImages
showimage(d::JuliaDisplay) = display(d, testimage("mandrill"))
@qmlfunction showimage
load("image.qml")
# Run the application
exec()
using QML
# Julia Fruit model item. Each field is automatically a role, by default
mutable struct Fruit
name::String
cost::Float64
attributes::ListModel
end
# Attributes must have a description and are nested model items
mutable struct Attribute
description::String
end
# Construct using attributes from an array of QVariantMap, as in the append call in QML
function Fruit(name, cost, attributes::AbstractArray)
return Fruit(name, cost, ListModel([Attribute(QML.value(a)["description"]) for a in attributes]))
end
# Use a view, since no ApplicationWindow is provided in the QML
qview = init_qquickview()
# 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
fruitmodel = ListModel(fruitlist)
set_context_property(qmlcontext(), "fruitModel", fruitmodel)
# Load QML after setting context properties, to avoid errors on initialization
qml_file = joinpath(dirname(@__FILE__), "qml", "dynamiclist.qml")
set_source(qview, QUrl(qml_file))
QML.show(qview)
# Run the application
exec()
# Show that the Julia fruitlist was modified
println("Your fruits:")
for f in fruitlist
println(" $(f.name), \$$(f.cost)")
end
QML code is directly from the dynamic list Qt example, only the data model was moved from QML to Julia, as shown by the diff:
The model is supplied to an appropriate QML item:
ListView {
id: listView
anchors {
left: parent.left; top: parent.top;
right: parent.right; bottom: buttons.top;
margins: 20
}
model: fruitModel
delegate: listDelegate
}
Roles are available as variables inside the list delegates:
Text {
id: costText
anchors.verticalCenter: parent.verticalCenter
text: '$' + Number(cost).toFixed(2)
font.pixelSize: 15
color: "white"
font.bold: true
}
# Show that the Julia fruitlist was modified
println("Your fruits:")
for f in fruitlist
println(" $(f.name), \$$(f.cost)")
end
Your fruits:
Apple, $2.45
Banana, $1.95
Pizza Margarita, $7.7
Cumquat, $3.25
Durian, $9.95
import QtQuick 2.0
import QtQuick.Controls 1.0
import QtQuick.Layouts 1.0
import org.julialang 1.1
import QtQuick.Window 2.2
ApplicationWindow {
title: "My Application"
width: 800
height: 600
visible: true
ColumnLayout {
id: root
spacing: 6
anchors.fill: parent
RowLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignCenter
Text {
text: "Amplitude:"
}
Slider {
id: amplitudeSlider
width: 100
value: 1.0
minimumValue: 0.1
maximumValue: 5.0
onValueChanged: {
parameters.amplitude = value;
painter.update()
}
}
Text {
text: "Frequency:"
}
Slider {
id: frequencySlider
width: 100
value: 10.0
minimumValue: 1.0
maximumValue: 50.
onValueChanged: {
parameters.frequency = value;
painter.update()
}
}
}
JuliaPaintedItem {
id: painter
paintFunction : paint_cfunction
Layout.fillWidth: true
Layout.fillHeight: true
}
}
}
Use from Julia:
ENV["QSG_RENDER_LOOP"] = "basic" # multithreading in Qt must be off
using CxxWrap # for safe_cfunction
using QML
using Observables
# Set up plots with GR so QPainter can be used directly
using Plots
ENV["GKSwstype"] = "use_default"
gr(show=true)
const qmlfile = joinpath(dirname(Base.source_path()), "qml", "gr.qml")
f = Observable(1.0)
A = Observable(1.0)
# Arguments here need to be pointers
function paint(p::CxxPtr{QPainter}, item::CxxPtr{JuliaPaintedItem})
ENV["GKS_WSTYPE"] = 381
ENV["GKS_CONID"] = split(repr(p.cpp_object), "@")[2]
ENV["GKS_QT_VERSION"] = 5
dev = device(p[])[]
r = effectiveDevicePixelRatio(window(item[])[])
w, h = width(dev) / r, height(dev) / r
x = 0:π/100:π
y = A[]*sin.(f[]*x)
plot(x, y, ylims=(-5,5), size=(w, h))
return
end
load(qmlfile,
paint_cfunction = @safe_cfunction(paint, Cvoid, (CxxPtr{QPainter}, CxxPtr{JuliaPaintedItem})),
parameters = JuliaPropertyMap("frequency" => f, "amplitude" => A))
exec()
"""
Example of GR.jl integration
"""
JuliaCanvas element provides fast image drawing
JuliaCanvas was contributed by @treygreer
MakieViewport QML element renders Makie scenes using GLMakie:
QML Repeater with ListModel overlay on top of Makie:
Model definition:
# MUST disable threading in Qt
ENV["QSG_RENDER_LOOP"] = "basic"
using CxxWrap
using Observables
using QML
using Makie
const xpos = Node(collect(0.1:0.05:0.3))
const ypos = Node(rand(length(xpos[])))
plotscene = lines(xpos, ypos, color = :blue)
const needupdate = Observable(true)
on(plotscene.data_limits) do l
needupdate[] = true
end
positionmodel = ListModel(tuple.(xpos[], ypos[]), false)
# Convert model coordinates to screen (inverse of to_world)
function to_screen(scene, point)
cam = scene.camera
cam_res = widths(pixelarea(scene)[])
prj_view = cam.projection[] * cam.view[] * Makie.AbstractPlotting.transformationmatrix(scene)[]
pix_space = prj_view * Vec4f0(point[1], point[2], 0.0, 1.0)
clip_space = (pix_space[1], pix_space[2])
return ((clip_space .+ 1) ./ 2) .* cam_res
end
function update_scene(lm)
l = length(lm)
newx = zeros(l)
newy = zeros(l)
for i in 1:l
(newx[i], newy[i]) = lm[i]
end
xpos[] = newx
ypos[] = newy
return
end
getscreenpos(xy::Tuple, i::Integer) = to_screen(plotscene, xy)[i]
function setpos(lm, x_or_y, listidx, i)
newpos = [lm[listidx]...]
newpos[i] = x_or_y
lm[listidx] = (newpos...,)
update_scene(lm)
end
setscreenpos(lm, x_or_y, listidx, i) = setpos(lm, to_world(plotscene, Point2f0(x_or_y, x_or_y))[i], listidx, i)
addrole(positionmodel, "xpos", xy -> xy[1], (lm, x_or_y, i) -> setpos(lm, x_or_y, i, 1))
addrole(positionmodel, "ypos", xy -> xy[2], (lm, x_or_y, i) -> setpos(lm, x_or_y, i, 2))
addrole(positionmodel, "xposscreen", xy -> getscreenpos(xy,1), (lm, x_or_y, i) -> setscreenpos(lm, x_or_y, i, 1))
addrole(positionmodel, "yposscreen", xy -> getscreenpos(xy,2), (lm, x_or_y, i) -> setscreenpos(lm, x_or_y, i, 2))
# Render function that takes a parameter t from a QML slider
function render_function(screen)
display(screen, plotscene)
if needupdate[] # The screen positions change if a resize happens and are unknown before the first render
QML.force_model_update(positionmodel)
needupdate[] = false
end
return
end
load(joinpath("makie.qml"),
positionModel = positionmodel,
updates = JuliaPropertyMap("needupdate" => needupdate),
render_callback = @safe_cfunction(render_function, Cvoid, (Any,))
)
exec()
Model usage:
Repeater {
anchors.fill: parent
model: positionModel
Item {
id: pointDelegate
// ...
TextField {
id: xField
Layout.preferredWidth: 60
Layout.preferredHeight: 25
onAccepted: { xpos = parseFloat(text); updates.needupdate = true; viewport.update(); }
}
// Set initial point positions
Component.onCompleted: {
x = xposscreen/Screen.devicePixelRatio - pointMarker.width/2;
y = appRoot.height - yposscreen/Screen.devicePixelRatio - pointMarker.height/2;
}
}
}
| Style | Mac | Linux |
|---|---|---|
| QQuick 1 | ![]() |
![]() |
| Default | ![]() |
![]() |
| QQC2 | ![]() |
![]() |