QML Tutorial

Better GUI with less effort

Adam Washington

2023-06-28

Created: 2023-06-28 Wed 12:35

1. Introduction

1.1. QML

  • A declarative method for defining GUIs
  • Based on the Qt library
  • Supports over a dozen languages.
  • Declarative GUI, not imperative
  • Divides code into a model and a view

1.2. Model

  • Performs detailed language and calculations
  • Written in your favourite programming language
  • Has no knowledge of the screen or window
  • Only acts via Properties, Signals, and Slots

1.3. Vocab

The Qt object model has a couple of pieces of vocab

Signal
A message which may trigger actions from multiple parts of the application
Slot
A routine that can be performed. Can be connected to a signal
Property
A value that can be read and optionally written. May emit a signal when changed.

1.4. View

  • Describes the on screen display of the model
  • Always written in QML
  • Used to connect the Properties, Signals, and Slots of the models

1.5. Why Bother?

  • Code reuse
    • A model can have multiple views
    • A view can apply to multiple models
  • Makes code easy to test
  • Less bugs
  • Shorter, more readable code
  • No name litter

2. Hello World

2.1. Full Text

We'll start with the simplest possible QML example.

import QtQuick 2.0

Rectangle {
  width: 600
  height: 600
  color: "green"
  Text {
    text: "Hello World!"
    anchors.centerIn: parent
  }
}

2.2. Imports

import QtQuick 2.0

Every QML file starts with a set of imports. You will always need to import QtQuick, as it is the default QML library. We'll encounter more libraries as we continue.

2.3. Widgets

Text {
  text: "Hello World!"
  anchors.centerIn: parent
}
  • Widget type followed by curly bracket
  • Attribute name, then colon, then value

2.4. Anchors

  • Describe constraints between edges of widgets
  • Full control of positioning
  • Lots of work
Button {
  anchors.left: parent.left
  anchors.right: closeButton.left
  anchors.top: parent.top
  height: parent.height/2
}

2.5. Layouts

  • ColumnLayout, RowLayout, GridLayout
  • Simpler to use
  • Handle arbitrary number of Widgets
import QtQuick.Layouts 1.2

ColumnLayout {
  Text {text: "Top"}
  Text {text: "Middle"}
  Text {text: "Bottom"}
}j

2.6. Containers

Rectangle {
  width: 600
  height: 600
  color: "green"
  Text {
    text: "Hello World!"
    anchors.centerIn: parent
  }
}

2.7. Python Loading

Now we need to write some python to actually display the QML file

from PySide2.QtQuick import QQuickView
from PySide2.QtWidgets import QApplication
import sys

app = QApplication(sys.argv)
view = QQuickView()
view.setSource("view.qml")
view.show()
app.exec_()

2.8. QApplication

  • Handles the busywork of running a GUI (e.g. managing event loop)
  • Takes a set of command line arguments
    • Better integration into user environment
    • Enable visually impaired users to customise display
from PySide2.QtWidgets import QApplication
import sys

app = QApplication(sys.argv)

Use the exec_ method to start the application

app.exec_()

2.9. QQuickView

  • Handles the display of the QML file
  • setSource picks the file
  • Must show the view for it to appear on screen
view = QQuickView()
view.setSource("view.qml")
view.show()

2.10. Exercises

  • Change the background to a less garish colour
  • Give a more personal greeting

3. Compute Stats

3.1. Model

We introduce our first data model class.

import numpy as np
from PySide2.QtCore import QObject, Property
from PySide2.QtQml import QQmlApplicationEngine, qmlRegisterType
class Stats(QObject):
  _data = None
  def __init__(self):
        QObject.__init__(self)
        self.data_ = np.loadtxt("data.txt")
  @Property(float)
  def x_mean(self):
        return np.mean(self.data_[:, 0])
  @Property(float)
  def y_mean(self):
        return np.mean(self.data_[:, 1])
qmlRegisterType(Stats, "Tutorial", 1, 0, "Stats")

3.2. qmlRegisterType

qmlRegisterType(Stats, "Tutorial", 1, 0, "Stats")
  • Make python data accessible in QML
  • Can only be performed on classes that inherit from QObject

3.3. Property

@Property(float)
  • Makes class data accessible from QML
  • Tells QML that the value is a float
  • Accessible by the function name

3.4. Data View

import QtQuick 2.0
import Tutorial 1.0

Rectangle {
  width: 600
  height: 600
  color: "#F0F0F0"
  Stats {id: myStats;}
  Text {
        text: myStats.x_mean + ", " + myStats.y_mean
        anchors.centerIn: parent
  }
}

3.5. Tutorial Import

import Tutorial 1.0

Import the QML type that we declared earlier

3.6. Load Stats

Create an instance of the stats object we defined earlier

Stats {id: myStats;}

Use the instance of the Stats class we created

text: myStats.x_mean + ", " + myStats.y_mean

3.7. Exercises

  • Create a new property to display the sum of the columns

4. Display

4.1. Create a Header

We're going to add some header text to our window

Text {
  id: titleText
  text: "Stat Calculator"
  anchors.top: parent.top
  anchors.horizontalCenter: parent.horizontalCenter
}

4.2. Data Display

We'll now display the x and y mean in a grid, instead of just a single label

GridLayout {
  anchors.top: titleText.bottom
  anchors.bottom: parent.bottom
  anchors.left: parent.left
  anchors.right: parent.right

  columns: 2

  Text {text: "X mean"}
  Text {text: myStats.x_mean}
  Text {text: "Y mean"}
  Text {text: myStats.y_mean}
}

This also requires that we import

import QtQuick.Layouts 1.2

4.3. Exercises

  • Put the X and Y means on the same row
  • Move the title to the bottom of the window
  • Move the title to the left side of the window
  • Remove the GridLayout and layout the entire window with anchors (this will be tedious).

5. Filename

5.1. Signal

Allow the model to load any data file we want. First, we'll need to add a signal that will emit when the file name is changed. (This will be more important later).

dataChanged = Signal()

5.2. Filename Property

We now need to add a property for the file name. The expanded Property tag tells the model to update the property's value every time the dataChanged signal is emitted.

@Property(QUrl, notify=dataChanged)
def filename(self):
        """ The name of the data file """
        return self._filename

We'll also want to update the Property decorator for the other properties to update them when the data is changed.

5.3. Filename Setter

We're going to add our first property setter. It's like the getter, but takes an extra parameter for the new value. We emit the dataChanged signal at the end to indicate that the data has changed.

@filename.setter
def filename(self, name):
        """ Load a new data file """
        self._filename = name
        self._data = np.loadtxt(name.toLocalFile())
        self.dataChanged.emit()

5.4. Update View

Since the model now picks the file through the filename property, we need to update the view to pick a file.

Stats {
  id: myStats
  filename: "file:data.txt"
}

5.5. Exercises

  • Set title text to the name of the data file

6. Interactive

6.1. Interactivity

After all this time, we'll finally add a truly interactive component. Specifically, we'll add a FileDialog to the view to set the data file.

FileDialog {
  id: fileDialog
  title: "Choose a data file"
  onAccepted: myStats.filename = fileDialog.fileUrl
}

6.2. Button

We have the file dialog, but we need a button to trigger the dialog

Button {
        id: loadButton
        anchors.top: titleText.bottom
        anchors.left: parent.left
        anchors.right: parent.right
        text: "Load file"
        onClicked: fileDialog.open()
}

6.3. Exercises

  • Using the console.log slot, bind to the dataChanged signal on the Stats model and print the file name to the console every time the data changes.
  • Find a better position for the file dialog button.
  • Create a bool property on the Stats model that explains whether a file has been loaded.

7. Display Tables

7.1. QAbstractTableModel

  • Thus far, every model that we've build has been based on QObject
  • Special Subclasses for advanced data
    QAbstractListModel
    Used for 1D data
    QAbstractTableModel
    Used for 2D data
    QAbstractItemModel
    Used for Tree Data

7.2. Mandataory Overloads

rowCount()
Number of data elements
columnCount()
Number of properties on elements
data(index, role)
Access an element by index

7.3. Recommended Overloads

headerData(section, orientation, role)
Column headers
setData(index, value, role)
Update an element by index
flags(index)
Metadata for an element

For our example app, we will not need these.

7.4. Add Count Overloads

@Slot()
def rowCount(self, parent):
    if self._data is None:
        return 0
    return self._data.shape[0]

@Slot()
def columnCount(self, parent):
    if self._data is None:
        return 0
    return self._data.shape[1]

7.5. Add Data Overloads

@Slot()
def data(self, index, role):
    if self._data is None:
        return None
    return float(self._data[index.row(),
                            index.column()])

7.6. Signals

Just like other properties, the abstract models need signals to inform the view of changes. Thankfully, the class provides a series of helper functions.

@filename.setter
def filename(self, name):
    """ Load a new data file """
    self._filename = name
    self.beginResetModel()
    self._data = np.loadtxt(name.toLocalFile())
    self.endResetModel()
    self.dataChanged.emit()

7.7. Table View

TableView {
        anchors.top: loadButton.bottom
        anchors.bottom: parent.bottom
        anchors.left: parent.left
        width: parent.width
        model: myStats
        columnSpacing:10

        delegate: Rectangle {
                color: "#F0F0F0"
                implicitWidth: 280
                implicitHeight: realText.height + 5
                Text {
                        id: realText
                        text: display
                }
        }
}

7.8. Exercises

  • The current code will produce glitches if the data file has more than two columns. Either ignore the other columns (easy) or allow an arbitrary column number.

8. Charting

8.1. One Model, Many Views

Using the QAbstractTableModel allows us to take advantage of multiple views that can use this model.

Change how we view the data without changing a single line of Python.

8.2. ChartView

ChartView {
        title: "Scatters"
        anchors.top: loadButton.bottom
        anchors.bottom: parent.bottom
        anchors.left: parent.left
        anchors.right: parent.right
        antialiasing: true
        <<Axes>>
        <<ScatterPlot>>
        <<ModelMapper>>
}

Requires a new import

import QtCharts 2.5

8.3. Axes

Give ranges for our X and Y axes

ValueAxis {
        id: axisX
        min: -5
        max: 5
}

ValueAxis {
        id: axisY
        min: -2
        max: 2
}

8.4. Series

Create a scatter plot

ScatterSeries {
        id: series
        name: myStats.filename
        axisX: axisX
        axisY: axisY
}

8.5. Data

Connect the myStats model to the series scatter plot

VXYModelMapper {
        model: myStats
        series: series
        xColumn:0
        yColumn:1
}

8.6. Exercises

  • The current chart range is hard coded. Add the necessary data properties to the Stats model to allow the axes to scale with the data.

9. Discussion

9.1. Opinions on this training

  • Would it be useful for Mantid developers?
  • Would it be useful for Instrument Scientists?