Better GUI with less effort
Created: 2023-06-28 Wed 12:35
model
and a view
Properties
, Signals
, and Slots
The Qt object model has a couple of pieces of vocab
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
}
}
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.
Text {
text: "Hello World!"
anchors.centerIn: parent
}
Button {
anchors.left: parent.left
anchors.right: closeButton.left
anchors.top: parent.top
height: parent.height/2
}
ColumnLayout
, RowLayout
, GridLayout
import QtQuick.Layouts 1.2
ColumnLayout {
Text {text: "Top"}
Text {text: "Middle"}
Text {text: "Bottom"}
}j
Rectangle {
width: 600
height: 600
color: "green"
Text {
text: "Hello World!"
anchors.centerIn: parent
}
}
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_()
from PySide2.QtWidgets import QApplication
import sys
app = QApplication(sys.argv)
Use the exec_
method to start the application
app.exec_()
setSource
picks the fileshow
the view for it to appear on screenview = QQuickView()
view.setSource("view.qml")
view.show()
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")
qmlRegisterType(Stats, "Tutorial", 1, 0, "Stats")
@Property(float)
float
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
}
}
import Tutorial 1.0
Import the QML type that we declared earlier
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
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
}
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
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()
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.
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()
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"
}
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
}
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()
}
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.For our example app, we will not need these.
@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]
@Slot()
def data(self, index, role):
if self._data is None:
return None
return float(self._data[index.row(),
index.column()])
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()
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
}
}
}
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.
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
Give ranges for our X and Y axes
ValueAxis {
id: axisX
min: -5
max: 5
}
ValueAxis {
id: axisY
min: -2
max: 2
}
Create a scatter plot
ScatterSeries {
id: series
name: myStats.filename
axisX: axisX
axisY: axisY
}
Connect the myStats
model to the series
scatter plot
VXYModelMapper {
model: myStats
series: series
xColumn:0
yColumn:1
}