QML.jl

Easy and fast desktop GUIs with QML

Slides and examples at https://github.com/barche/juliacon2020-qml

Quick example

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
                            }
                          }
                        }                
                      

Displays

QML code


            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
                }
              }
            }
          

Julia code

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

Julia code

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

Listmodels


          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
          

Faster displays

JuliaPaintedItem type


            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

Makie

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;
                }
              }
            }
          

Styling

Style Mac Linux
QQuick 1
Default
QQC2

Future work

  • Some easy templates for Makie integration
  • Cleaner GR integration
  • Native look for QtQuickControls 2 using QQC2 from KDE
  • Use of Observables in ListModel