Graphical user interfaces¶
Grafische interfaces met PySide¶
Als je een grafische applicatie schrijft roep je functies aan van het besturingssysteem om vensters, knoppen, menu's e.d. te laten tekenen en te reageren op muisklikken en het toetsenbord. Het lastige daaraan is dat een applicatie voor MacOS heel anders geschreven moet worden dan één voor Linux of Windows. Om die reden zijn er verschillende cross-platform bibliotheken ontwikkeld die als het ware tussen het besturingssysteem en je applicatie komen te staan. Je kunt dezelfde applicatie maken voor alle besturingssystemen en de bibliotheek kiest welke functies aangeroepen moeten worden om een venster te tekenen. Het voordeel is duidelijk: je hoeft maar één applicatie te schrijven die overal werkt. Het nadeel is dat je niet écht gebruik kunt maken van alle functies en opties die het besturingssysteem biedt. Hier kiezen we voor de voordelen en gaan we gebruik maken van misschien wel de meest populaire optie: Qt.1 De bibliotheek PySide6
is de officiële Pythonbibliotheek.
Info
Maak voor de oefeningen een nieuw conda environment test-qt
met:
conda create --name test-qt python=3.12
conda activate test-qt
pip install pyside6 pyqtgraph
test-qt
conda environment in Visual Studio Code en sluit alle oudeterminals met het -icoon.2
Een minimale Qt-applicatie ziet er als volgt uit:
UserInterface
class. De naam mag je zelf kiezen, zolang je maar aangeeft dat de class een afgeleide is van QtWidgets.QMainWindow
, het hoofdvenster van je applicatie. In het hoofdgedeelte van het programma (gedefinieerd in de functie main()
) maak je eerst een instance van QtWidgets.QApplication
.3 Ook maken we een instance van onze eigen class en we roepen de show()
method aan. Die hebben we niet zelf geprogrammeerd; die zit in de parent class QMainWindow
. Als laatste roepen we de exec()
method aan van onze QApplication
en de uitvoer daarvan (een exit code) geven we mee aan de functie sys.exit()
. Dat betekent dat als het programma afsluit met een foutmelding, dat een foutcode wordt meegegeven aan het besturingssysteem. Iemand anders die een script schrijft kan die code afvangen en daar iets mee doen.
Een aantal elementen uit dit programma (sys.argv
, sys.exit()
) zijn strikt genomen niet noodzakelijk, maar wel good practice. Ook het schrijven van een main()
functie is niet strikt noodzakelijk, maar het maakt het wel makkelijk om straks een zogeheten entry point te hebben als we weer een applicatie willen schrijven. In de pyproject.toml
geven we dan aan dat we de main()
functie willen aanroepen. Dat komt later.
Minimale GUI
ECPC
maak je een example-gui.py
aan en zet daarin de Python code. Je activeert de test-qt
conda environment en runt het bestand example-gui.py
. Er verschijnt een leeg venster in beeld met als venstertitel python
en drie knoppen. Een streepje (minimize), een vierkant (maximize) en een kruis (close). Je drukt op het kruisje en het venster sluit.
ECPC
├──
pythondaq
├──
oefenopdrachten
├──
example-gui.py
└── •••
└── •••
Pseudo-code
import sys
from PySide6 import QtWidgets
# create subclass of QtWidgets.QMainWindow
def main():
# create instance of QtWidgets.QApplication with arguments from sys.argv
# create instance of subclass
# call show method of subclass
# get exit code with exec method of QApplication instance and give exit code to sys.exit()
# when run this script:
# run main function
Checkpunten:
- Het juiste conda environment is geactiveerd
- De code is volledig overgenomen
- Er verschijnt een leeg venster
Projecttraject:
- Minimale GUI
- Parent class initialiseren
- Central widget toevoegen
- textbox toevoegen
- knoppen toevoegen
- Slots en signals toevoegen
- 'Hello world' en Quit knoppen toevoegen
Elke keer als je een nieuwe Qt applicatie gaat schrijven kun je bovenstaand stukje code copy/pasten. Als we dit programma draaien hebben we echter een klein leeg venster op het scherm, zonder elementen. Die elementen kunnen we op twee manieren toevoegen: door ze te programmeren of door het gebruik van een visueel ontwerp met Qt Designer. Beide zullen in de volgende secties toegelicht worden.
De interface programmeren¶
We gaan de eenvoudige interface programmeren die hieronder is weergegeven:
We doen dat door de class UserInterface
uit te breiden met widgets uit de QtWidgets
bibliotheek.
Het definiëren van layouts gebeurt in veruit de meeste opmaaksystemen met rechthoeken (Engels: boxes) die op verschillende manieren gestapeld worden — naast elkaar, boven elkaar, of op een rechthoekig grid bijvoorbeeld. Zulke systemen zijn ook hiërarchisch: je stopt boxes in andere boxes.
De layout van bovenstaande screenshot is als volgt opgebouwd. Het hoofdelement van de grafische interface is de central widget
:
De central widget
krijgt een verticale layout die we vbox
noemen:
In de verticale layout plaatsen we een textbox
en een horizontale layout die we hbox
noemen:
In de horizontale layout plaatsen we twee button
s:
Het stuk programma om bovenstaande layout op te bouwen geven we hieronder weer. We bespreken straks de code regel voor regel.
__init__()
. Helaas gaat dat niet zomaar. We schrijven namelijk niet helemaal zelf een nieuwe class (class UserInterface
), maar breiden de QMainWindow
-class uit (class UserInterface(QtWidgets.QMainWindow)
). Door dat te doen zijn er heel veel methods al voor ons gedefinieerd. Daar hoeven we verder niet over na te denken, onze interface werkt gewoon. Het gaat mis als wij zelf nieuwe methods gaan schrijven die dezelfde naam hebben. Stel dat de parent class
QMainWindow
een method click_this_button()
heeft. Als onze class ook een method click_this_button()
heeft, dan zal die worden aangeroepen in plaats van de method uit de parent class. Dat is handig als je de parent method wilt vervangen maar niet zo handig als je de parent method wilt aanvullen, zoals nodig is bij __init__()
. Immers, we willen onze eigen class initialiseren, maar we willen ook dat de parent class volledig wordt geïnitialiseerd.
De oplossing is gelukkig vrij eenvoudig: we kunnen de __init__()
van de parent class gewoon aanroepen en daarna ons eigen ding doen. De Pythonfunctie super()
verwijst altijd naar de parent class, dus met super().__init__()
wordt de parent class volledig geïnitialiseerd. Dat is dus het eerste dat we doen in regel 10. Kijk voor meer informatie over super().__init__()
in de paragraaf subclasses.
In de volgende opdrachten ga je zelf de hele applicatie opbouwen, zodat je precies weet wat in de code hierboven staat.
Parent class initialiseren
Je hebt geleerd hoe je widgets aan de applicatie kunt toevoegen. Omdat het veel stappen in een keer zijn ga je de instructies stap voor stap volgen en steeds tussendoor testen. Je begint met het maken van een __init__()
method voor de class UserInterface
en zorgt ervoor dat de parent class (QtWidgets.QMainWindow
) volledig wordt geïnitialiseerd. Je runt example-gui.py
en ziet dat er nog steeds een leeg venster wordt gestart. Je bent benieuwd of het initialiseren écht nodig is, daarom haal je de super()
-aanroep weg en kijkt wat er gebeurd als je example-gui.py
runt. Je zet super()
-aanroep heel gauw weer terug.
Pseudo-code
import sys
from PySide6 import QtWidgets
# create subclass of QtWidgets.QMainWindow
# def __init__()
# initialise the parent class Qtwidgets.QMainWindow
def main():
# create instance of QtWidgets.QApplication with arguments from sys.argv
# create instance of subclass
# call show method of subclass
# get exit code with exec method of QApplication instance and give exit code to sys.exit()
# when run this script:
# run main function
Checkpunten:
- Er is een
__init__()
method gemaakt voor de subclassUserInterface
. - In de
__init__()
method wordt de parent class geïnitialiseerd (regel 10). - Er verschijnt een leeg venster.
Projecttraject:
- Minimale GUI
- Parent class initialiseren
- Central widget toevoegen
- textbox toevoegen
- knoppen toevoegen
- Slots en signals toevoegen
- 'Hello world' en Quit knoppen toevoegen
Verder heeft iedere applicatie een centrale widget nodig. Niet-centrale widgets zijn bijvoorbeeld een menubalk, knoppenbalk of statusbalk.
Central widget toevoegen
Nu de parent class wordt geïnitialiseerd kan je een widget aanmaken met QtWidgets.QWidget()
, je noemt deze widget central_widget
. En stelt deze in als centrale widget met de method setCentralWidget()
van de class QtWidgets.QMainWindow
. Je runt example-gui.py
en ziet dat er nog steeds een leeg venster wordt gestart.
Pseudo-code
import sys
from PySide6 import QtWidgets
# create subclass of QtWidgets.QMainWindow
# def __init__()
# initialise the parent class Qtwidgets.QMainWindow
# create central widget with QtWidgets.QWidget()
# set central widget
def main():
# create instance of QtWidgets.QApplication with arguments from sys.argv
# create instance of subclass
# call show method of subclass
# get exit code with exec method of QApplication instance and give exit code to sys.exit()
# when run this script:
# run main function
Checkpunten:
- Er is een central widget gemaakt met
QtWidgets.QWidget()
(regel 14). - De widget wordt als centrale widget ingesteld met
setCentralWidget()
(regel 15). - De method
setCentralWidget()
is afkomstig van de classQtWidgets.QMainWindow
welke geïnitialiseerd is, de method wordt daarom metself.setCentralWidget()
aangeroepen. - Er verschijnt een leeg venster.
Projecttraject:
- Minimale GUI
- Parent class initialiseren
- Central widget toevoegen
- textbox toevoegen
- knoppen toevoegen
- Slots en signals toevoegen
- 'Hello world' en Quit knoppen toevoegen
Daarna gaan we layouts en widgets toevoegen. Layouts zorgen ervoor dat elementen netjes uitgelijnd worden. We willen het tekstvenster en de knoppen onder elkaar zetten en maken dus eerst een verticale layout. Aan die layout voegen we een textbox toe.
textbox toevoegen
Omdat je de textbox en de knoppen onder elkaar wilt uitlijnen voeg je een verticale layout toe. Door de central_widget
mee te geven tijdens het aanmaken van de verticale layout is de layout automatisch onderdeel van de central widget en zal deze in het venster verschijnen. Je maakt een textbox aan en voegt deze toe aan de verticale layout. Je runt example-gui.py
en ziet een venster met een textbox verschijnen, je typt een vrolijke tekst en sluit het venster.
Pseudo-code
import sys
from PySide6 import QtWidgets
# create subclass of QtWidgets.QMainWindow
# def __init__()
# initialise the parent class Qtwidgets.QMainWindow
# create central widget with QtWidgets.QWidget()
# set central widget
# create vertical layout as part of central widget
# create textbox
# add textbox to vertical layout
def main():
# create instance of QtWidgets.QApplication with arguments from sys.argv
# create instance of subclass
# call show method of subclass
# get exit code with exec method of QApplication instance and give exit code to sys.exit()
# when run this script:
# run main function
Checkpunten:
- Bij het aanmaken van de verticale layout is de
central_widget
als parameter meegegeven (regel 18). - Er is een tekstbox gemaakt (regel 19).
- De tekstbox (
QTextEdit
) is toegevoegd aan de verticale layout (regel 20). - Er verschijnt een venster met textbox waar je in kan typen .
Projecttraject:
- Minimale GUI
- Parent class initialiseren
- Central widget toevoegen
- textbox toevoegen
- knoppen toevoegen
- Slots en signals toevoegen
- 'Hello world' en Quit knoppen toevoegen
De knoppen zelf plaatsen we straks in een horizontale layout, dus die voegen we ook toe aan de vbox
. En we maken de layout compleet door knoppen toe te voegen aan de hbox
.
Knoppen toevoegen
Omdat de knoppen naast elkaar moeten komen te staan voeg je een horizontale layout toe aan de verticale layout. Je maakt een clear button
en een add button
en voegt deze toe aan de horizontale layout. Je runt example-gui.py
en ziet een venster met een textbox verschijnen met daaronder twee knoppen, je drukt verwoed op de knoppen maar er gebeurt niets4.
Pseudo-code
import sys
from PySide6 import QtWidgets
# create subclass of QtWidgets.QMainWindow
# def __init__()
# initialise the parent class Qtwidgets.QMainWindow
# create central widget with QtWidgets.QWidget()
# set central widget
# create vertical layout as part of central widget
# create textbox
# add textbox to vertical layout
# create horizontal layout
# add horizontal layout to vertical layout
# create clear_button
# add clear button to horizontal layout
# create add_text_button
# add add_text_button to horizontal layout
def main():
# create instance of QtWidgets.QApplication with arguments from sys.argv
# create instance of subclass
# call show method of subclass
# get exit code with exec method of QApplication instance and give exit code to sys.exit()
# when run this script:
# run main function
Checkpunten:
- Er is een horizontale layout aangemaakt (regel 21).
- De horizontale layout is toegevoegd aan de verticale layout (regel 22).
- Er is een
clear_button
enadd_text_button
aan gemaakt met daarop de tekst "Clear" en "Add text" respectievelijk (regels 24 en 26). - De buttons zijn toegevoegd aan de horizontale layout (regel 25 en 27).
- Als je op de knoppen drukt gebeurt er niets.
Projecttraject
- Minimale GUI
- Parent class initialiseren
- Central widget toevoegen
- textbox toevoegen
- knoppen toevoegen
- Slots en signals toevoegen
- 'Hello world' en Quit knoppen toevoegen
Info
Widgets zoals knoppen voeg je toe met addWidget()
. Layouts voeg je toe aan andere layouts met addLayout()
.
De horizontale layout (voor de knoppen) moeten we expliciet toevoegen aan de verticale layout zodat hij netjes verticaal onder het tekstvenster verschijnt. Merk op dat de verticale layout vbox
niet expliciet wordt toegevoegd (aan de centrale widget). De centrale widget (en alleen de centrale widget) krijgt een layout door bij het aanmaken van de layout de parent central_widget
op te geven, dus: QtWidgets.QVBoxLayout(central_widget)
. Alle andere widgets en layouts worden expliciet toegevoegd en daarvoor hoef je dus geen parent op te geven.
Als laatste verbinden we de knoppen aan functies. Zodra je op een knop drukt wordt er een zogeheten signal afgegeven. Die kun je verbinden met een slot. Er zijn ook verschillende soorten signalen. Het drukken op een knop zorgt voor een clicked signal, het veranderen van een getal in een keuzevenster geeft een changed signal. Wij verbinden één knop direct met een al bestaande method van het tekstvenster clear()
en de andere knop met een eigen method add_button_clicked()
. De naam is geheel vrij te kiezen, maar boven de functiedefinitie moet je wel de @Slot()
-decorator gebruiken (voor meer informatie over decorators zie paragraaf Decorators). PySide kan dan net wat efficiënter werken.
Slots en signals toevoegen
Je gaat functionaliteit aan de knoppen verbinden. Je verbint de clear_button
aan de clear()
method van textedit
. Je maakt een eigen Slot
met de naam add_text_button_clicked
die een tekst aan de textbox toegevoegd. Je vind de tekst "You clicked me." maar suf en bedenkt zelf een andere leuke tekst. Je runt example-gui.py
en ziet een venster met een textbox verschijnen met daaronder twee knoppen. Je drukt op "Add text" en er verschijnt tekst in de textbox, daarna druk je op "Clear" en de tekst verdwijnt.
() ontbreken bij clear
en add_text_button_clicked
Bij het verbinden van het clicked
-signaal met clicked.connect()
geef je aan connect de methods clear
en add_text_button_clicked
mee zonder deze aan te roepen (dat gebeurt later). Concreet betekent dit dat je de haakjes weglaat (regel 30 en 31).
Pseudo-code
import sys
from PySide6.QtCore import Slot
from PySide6 import QtWidgets
# create subclass of QtWidgets.QMainWindow
# def __init__()
# initialise the parent class Qtwidgets.QMainWindow
# create central widget with QtWidgets.QWidget()
# set central widget
# create vertical layout as part of central widget
# create textbox
# add textbox to vertical layout
# create horizontal layout
# add horizontal layout to vertical layout
# create clear_button
# add clear button to horizontal layout
# create add_text_button
# add add_text_button to horizontal layout
# connect clear_button to clear method of textedit
# connect add_text_button to add_text_button_clicked
# decorate method with Slot function
# def add_text_button_clicked
# add text to textedit
def main():
# create instance of QtWidgets.QApplication with arguments from sys.argv
# create instance of subclass
# call show method of subclass
# get exit code with exec method of QApplication instance and give exit code to sys.exit()
# when run this script:
# run main function
Checkpunten:
- Het
clicked
signaal vanclear_button
is metconnect
verbonden met declear()
method vantextedit
(regel 30). - Het clicked signaal van
add_text_button
is metconnect
verbonden met een eigen methodadd_text_button_clicked
(regel 31). - De method
add_text_button_clicked
is voorzien van een decorator@Slot()
met Slot met een hoofdletter en ronde haakjes erachter omdat Slot een functie is (regel 33). - De
Slot
functie is geïmporteerd vanuit dePySide6.QtCore
. - De method
add_text_button_clicked
voegt metappend
een tekst toe aantextedit
(regel 35). - Druk op de knop "Add text" zorgt voor het verschijnen van tekst in de textbox.
- Druk op de knop "Clear" zorgt ervoor dat alle tekst in de textbox verdwijnt.
Projecttraject
- Minimale GUI
- Parent class initialiseren
- Central widget toevoegen
- textbox toevoegen
- knoppen toevoegen
- Slots en signals toevoegen
- 'Hello world' en Quit knoppen toevoegen
Er zijn veel verschillende widgets met eigen methods en signals. Je vindt de lijst in de Qt for Python-documentatie. Qt zelf bestaat uit C++ code en PySide6 vertaalt alle methods e.d. letterlijk naar Python. Vandaar ook de methodnaam addWidget()
in plaats van add_widget()
. In C++ en Java is het wel gebruikelijk om functies CamelCase
namen te geven als kijkDitIsEenMooieFunctie()
, maar in Python zijn we snake_case
gewend, als in kijk_dit_is_een_mooie_functie()
.
Volgorde layout aanpassen
De volgorde waarin je layout en widgets toevoegt bepaalt het uiterlijk van de grafische interface. Verander de code om de layout aan te passen (zet bijvoorbeeld de knoppen boven de textbox of zet de knoppen onder elkaar en naast de textbox).
'Hello world' en Quit knoppen toevoegen
Nu de minimale GUI werkt wil je meer knoppen toevoegen. Je begint met een knop Hello, world
die de tekst "Hello, world" aan de textbox toevoegd. Je runt example-gui.py
en ziet dat de knop werkt. Daarna voeg je een Quit
-knop toe die onder de andere knoppen staat. Het signaal van deze knop verbind je met de method self.close()
zodat de applicatie wordt afgesloten. Je runt example-gui.py
drukt nog een paar keer op de Hello, world
-knop en daarna op de knop Quit
, het venster is gesloten de opdracht is voltooid .
Pseudo-code
Checkpunten:
- De 'Add Text' en 'Clear' knoppen werken nog zoals verwacht.
- Druk op de
Hello World
knop voegt de text "Hello World" toe aan de textbox. - De
Quit
knop staat _ onder_ de andere knoppen. - Druk op de
Quit
knop sluit het venster.
Projecttraject
- Minimale GUI
- Parent class initialiseren
- Central widget toevoegen
- textbox toevoegen
- knoppen toevoegen
- Slots en signals toevoegen
- 'Hello world' en Quit knoppen toevoegen
De interface ontwerpen met Qt Designer¶
Designer opstarten
Qt Designer wordt geïnstalleerd met het qt
package, dat standaard aanwezig is in Anaconda én geïnstalleerd wordt als je PySide6
installeert. Je start hem het makkelijkst op vanuit een terminal. Activeer je test-qt
conda environment als dat nog nodig is en type pyside6-designer
.
Zodra interfaces wat ingewikkelder worden is het een hoop werk om ze te programmeren. Daarom kun je met Qt Designer de interface ook visueel ontwerpen. Je bewaart dat als een .ui
-bestand. Vervolgens vertaal je het .ui
-bestand naar een Pythonbestand dat je importeert in je eigen programma. De volledige class van het vorige voorbeeld kan dan vervangen worden door:
from ui_simple_app import Ui_MainWindow
class UserInterface(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.ui.clear_button.clicked.connect(self.ui.textedit.clear)
self.ui.add_button.clicked.connect(self.add_button_clicked)
self.show()
@Slot()
def add_button_clicked(self):
self.ui.textedit.append("You clicked me.")
self.ui.clear_button
of self.ui.add_button
; die namen geven we aan de knoppen die we maken in Designer. De namen van alle objecten in Designer zijn daarna beschikbaar in onze code om bijvoorbeeld de signalen te koppelen. Merk op dat we nu niet meer self.clear_button
gebruiken maar self.ui.clear_button
. Alle widgets komen op deze manier onder een .ui
-object te hangen.
Designer gebruiken
- Open Designer en kies bij templates/forms voor
MainWindow
. Klik dan op Create. Ontwerp de user interface van het screenshot en gebruik dezelfde namen voor de widgets als het voorbeeld. Dus eenadd_button
knop, eenclear_button
knop en eentextedit
tekstveld. Het is niet erg als je venster niet dezelfde grootte heeft. Qt Designer kiest een andere standaardafmeting. - Bewaar het bestand als
simple_app.ui
.
ECPC
├──oefenopdrachten
├──simple_app.ui
├──example-gui.py
└── •••
└── ••• - In een terminal in Visual Studio Code, navigeer naar dezelfde map waarin je je script uit de vorige opdracht hebt staan5 en type in:
Deze stap moet je doen elke keer als je in Designer iets wijzigt. Gebruik de Up-toets om oude commando's terug te halen. Dat scheelt typewerk. Later, met Poetry, zullen we dit eenvoudiger maken.
ECPC
├──oefenopdrachten
├──simple_app.ui
├──simple_app.py
├──example-gui.py
└── •••
└── ••• - Copy/paste nu de voorbeeldcode in een nieuw script, fix eventuele importerrors en test de applicatie.
Functieplotter¶
Je hebt nu twee manieren gezien om een interface te bouwen: programmeren of Designer gebruiken. Let er wel op dat er dus een subtiel verschil is in het benaderen van de widgets. Je kunt bij zelf programmeren bijvoorbeeld self.add_button
gebruiken, maar als je Designer gebruikt moet dat self.ui.add_button
zijn.
In de eindopracht willen we data weergeven op een scherm. We zullen dus nog moeten plotten. In de volgende opdrachten gaan we daarmee aan de slag.
Je bent bekend met matplotlib en dat kan ook ingebouwd worden in Qt-applicaties. Helaas is matplotlib voor het gebruik in interactieve interfaces nogal traag zodra we te maken krijgen met meer data. We kiezen daarom voor een populair alternatief: PyQtGraph. Eén nadeel: de documentatie is niet fantastisch. Het geeft dus niets als je ergens niet uitkomt en je hulp nodig hebt van de assistent of een staflid.
De plotter als script¶
Om PyQtGraph te importeren en globale opties in te stellen moeten we bovenaan ons programma het volgende schrijven:
import pyqtgraph as pg
# PyQtGraph global options
pg.setConfigOption("background", "w")
pg.setConfigOption("foreground", "k")
Info
Als je je GUI het liefst programmeert, gebruik dan de volgende regel om een plot widget te krijgen in de __init__()
:
- Voeg aan je interface een Graphics View toe;
- Klik er op om hem te selecteren en klik daarna op de rechtermuistoets;
- Kies voor Promote To ...;
- Bij Promoted class name vul je in
PlotWidget
en bij Header file vul je inpyqtgraph
(zonder.h
aan het eind); - Dan klik je op Add en vervolgens op Promote.
De stappen zijn weergegeven in onderstaand screenshot. Bij de rode pijl vind je Graphics View en in het rode kader staat wat je moet invullen om te promoten:
Nu je dit een keer gedaan hebt kun je voortaan op een Graphics View meteen kiezen voor Promote to > PlotWidget en hoef je niets meer in te typen. Vergeet niet je widget nog even een handige naam te geven, bijvoorbeeld plot_widget
.
PySide6 documentatie
De documentatie van PySide6 is niet super-intuïtief. Daarom hebben we speciaal voor jullie een compacte documentatie📄 geschreven. Daarin kan je een lijst van widgets vinden met de meest handige methods en signals. De documentatie is dus niet compleet maar genoeg voor een simpele GUI. Een overzicht van alle classes gedocumenteerd in de compacte documentatie vind je hieronder.
Compacte PySide6 documentatie¶
Classes:
QApplication
: Beheert de controleflow en hoofdinstellingen van de GUI-applicatie.QLayout
: Basisclass van alle layout-objecten inQtWidgets
.-
QWidget
: Basisclass van alle widget-objecten inQtWidgets
. -
Subclasses van
QLayout
:QHBoxLayout
: Beheert een horizontale indeling van widgets.QVBoxLayout
: Beheert een verticale indeling van widgets.QGridLayout
: Beheert een roosterindeling waarbij de ruimte wordt verdeeld in rijen en kolommen.QFormLayout
: Beheert een indeling waarbij de ruimte wordt verdeeld in een linker kolom met labels en een rechter kolom met widgets.
-
Subclasses van
QWidget
:QMainWindow
: Biedt een framework voor het bouwen van de gebruikersinterface van een applicatie.QGroupBox
: Biedt een frame, een titel erboven, en kan verschillende andere widgets binnen zichzelf weergeven.QTextEdit
: Geeft tekst weer en stelt de gebruiker in staat om deze te bewerken.QCheckBox
: Schakelknop met een checkbox-indicator.QLabel
: Een widget die tekst weergeeft.QComboBox
: Een widget waarmee de gebruiker een keuze kan maken uit een lijst met opties.QSpinBox
: Een widget waarmee de gebruiker een geheel nummer kan kiezen uit een bereik.QDoubleSpinBox
: Een widget waarmee de gebruiker een komma getal kan kiezen uit een bereik.QPushButton
: Een knop die door de gebruiker kan worden ingedrukt.QLineEdit
: Een widget waarmee de gebruiker een enkele regel platte tekst kan invoeren en bewerken.QFileDialog
: Biedt een dialoogvenster waarmee de gebruiker bestanden of mappen kan selecteren.
Om daadwerkelijk een functie te plotten kun je deze code aanpassen:
import numpy as np
class UserInterface(QtWidgets.QMainWindow):
...
def plot(self):
x = np.linspace(-pi, pi, 100)
self.plot_widget.plot(x, np.sin(x), symbol=None, pen={"color": "m", "width": 5})
self.plot_widget.setLabel("left", "y-axis [units]")
self.plot_widget.setLabel("bottom", "x-axis [units]")
symbol
en pen
om te zien wat ze doen. Leeg maken kan met self.plot_widget.clear()
.
Functionplotter: plot
ECPC
map (zie hiernaast). Maak een Poetry project functionplotter
, voeg die toe aan GitHub Desktop en open hem in Visual Studio Code. Bekijk pyproject.toml
en zorg dat er een commando is aangemaakt om de applicatie te starten. Je maakt een nieuw conda environment aan met alleen Python daarin . Gebruik poetry install
om het project te installeren en voer het commando uit om de applicatie te starten. Als je applicatie af is verschijnt er een scherm met een plot waarin de functie $\sin(x)$ plot in het domein $(0, 2\pi)$ is weergegeven. Een golfje van trots gaat door je heen en je gaat door naar de volgende opdracht.
ECPC
├──
pythondaq
├──
functionplotter
├──
pyproject.toml
└── •••
└── •••
Pseudo-code
Checkpunten:
- Er is een repository
functionplotter
- Er is een commando om de applicatie te starten
- De applicatie laat een $\sin(x)$ plot zien in het domein $(0, 2\pi)$
- De applicatie werkt ook na
poetry install
in een nieuwe conda environment.
Projecttraject
- Functionplotter: plot
- Functionplotter: widgets
Functionplotter: widgets
Voer opnieuw het commando uit om de applicatie functionplotter
te starten. Dit keer zorg je dat de applicatie de mogelijkheid krijgt om het domein van de plot aan te passen. Je ziet dan de sinusplot veranderen wanneer je de startwaarde verhoogd. Je kunt de startwaarde ook naar beneden aanpassen. Hetzelfde geldt voor de stopwaarde. Dan maak je nog een widget om het aantal punten (num
) te kiezen waarmee de sinus wordt geplot. Speel eens met de widget en zie de sinus van hoekig naar mooi glad veranderen. Steeds als je een waarde aanpast moet de functie automatisch opnieuw geplot geworden.
Pseudo-code
Checkpunten:
- Het is mogelijk om de start waarde aan te passen.
- Het is mogelijk om de stop waarde aan te passen.
- Het is mogelijk om het aantal punten te kiezen waarmee de sinus functie wordt geplot.
- Na het aanpassen van een waarde wordt de plot automatisch opnieuw geplot.
Projecttraject
- Functionplotter: plot
- Functionplotter: widgets
Functieplotter: functie kiezen drop-down menu
Gebruik een QComboBox
om de functie te kunnen kiezen. Je moet hem leeg toevoegen aan je interface en vult hem vanuit je programma. Zoek de widget op in de documentatie om uit te zoeken welke functie je moet gebruiken om keuzemogelijkheden toe te voegen en welk signaal je moet koppelen om te zorgen dat de plot opnieuw wordt uitgevoerd als je de functie aanpast. Geef de gebruiker de keuzes $\sin(x)$, $\cos(x)$, $\tan(x)$ en $\exp(x)$.
Functieplotter: meer functies
Voeg aan de functiekiezer de functies $x$, $x^2$, $x^3$, en $\frac{1}{x}$ toe. Je kunt daarvoor lambda functions gebruiken, maar dat is niet per se nodig.
Functieplotter: functies typen
Vervang de functiekiezer door een tekstveld waarin de gebruiker zelf functies kan typen zoals x ** 2
, sin(x)
of 1 / sqrt(x + 1)
. Gebruik daarvoor het asteval
package.12 Documentatie vind je op https://newville.github.io/asteval/.
Waarschuwing
Gebruik nooit zomaar eval()
op een string die iemand anders aanlevert. Anders kan iemand met typen in een tekstveld of het inlezen van een tekstbestand je computer wissen bijvoorbeeld, of malware installeren. Als je eval()
wilt gebruiken, lees dan de sectie Minimizing the Security Issues of eval() in Python eval(): Evaluate Expressions Dynamically.13 Maar veel makkelijker is om asteval
te gebruiken.
Een grafische interface voor ons experiment¶
In het vorige hoofdstuk hebben we een tekst-interface geschreven voor ons experiment. We gaan nu een grafische interface schrijven voor hetzelfde experiment.
We hebben tot nu toe veel moeite gedaan om onze code te splitsen volgens het MVC-model: werken in laagjes, goed nadenken over wat waar hoort. Als dat netjes gelukt is kunnen we relatief makkelijk één van die laagjes vervangen. We kunnen de ArduinoVISADevice
vervangen door een RaspberryPiDevice
of een PicoScopeDevice
6. Ook kunnen we een nieuwe applicatie schrijven voor ons bestaande experiment. We hoeven dan alleen een extra view te schrijven (de interface met de gebruiker) en de rest kunnen we hergebruiken. Misschien dat we hier en daar iets willen aanpassen maar zorg er dan voor dat je oude applicatie nog steeds werkt!
We gaan nu — in stapjes — een grafische applicatie schrijven voor ons experiment.
Info
Je mag zelf kiezen of je de grafische interface gaat ontwerpen met Designer of dat je hem volledig programmeert.
Info
Als je Designer gaat gebruiken voor de grafische interface dan is het lastig dat je steeds pyside-uic
moet aanroepen en moet zorgen dat je in de goede directory staat. We kunnen met Poetry taken aanmaken die je met een eenvoudig commando kunt laten uitvoeren. Die taken zijn alleen beschikbaar tijdens het ontwikkelen van je applicatie. Doe dit als volgt:
- Installeer Poe the Poet — een zogeheten task runner — als development dependency met: We geven hiermee aan dat we dit package nodig hebben voor de ontwikkeling van onze applicatie, maar dat deze niet meegeleverd hoeft te worden als we de applicatie gaan delen met anderen.
- Voeg aan je
pyproject.toml
het volgende toe — uitgaande van de mappenstructuur in depythondaq
package enmainwindow.ui
als naam van je.ui
-bestand:Je kunt binnen de driedubbele aanhalingstekens meerdere regels toevoegen als je meerdere[tool.poe.tasks.compile] shell = """ pyside6-uic src/pythondaq/mainwindow.ui --output src/pythondaq/ui_mainwindow.py """ interpreter = ["posix", "powershell"]
.ui
-bestanden hebt — voor ieder bestand een regel. - In bovenstaande regels is de naam na
tool.poe.tasks
de naam van de taak — in dit geval duscompile
. Je kunt die naam zelf kiezen en vervolgens gebruiken om de taak uit te voeren in de terminal: En dat gaat een stuk sneller dan die langepyside-uic
-regel onthouden en intypen!
Pythondaq: leeg venster
pythondaq
applicatie. Je gaat dit in stapjes opbouwen zodat je tussendoor nog kunt testen of het werkt. Je maakt een gui.py
aan waarin een leeg venster wordt gemaakt. Het lege venster wordt getoond zodra je een commando in de terminal intypt. Je sluit het venster. Om te testen of dit bij andere mensen ook zou werken maak je een nieuwe conda environment aan met Python , installeer je de package met Poetry en test je opnieuw het commando, er verschijnt opnieuw een leeg venster.
ECPC
├──
pythondaq
├──
src/pythondaq
├──
gui.py
└── •••
└── •••
└── •••
Checkpunten:
- Het uitvoeren van een commando zorgt ervoor dat een leeg venster wordt getoond.
- Het commando werkt ook na het installeren van de package
pythondaq
met Poetry in een nieuwe conda environment met Python.
Projecttraject
- Pythondaq: leeg venster
- Pythondaq: plot scan
- Pythondaq: widgets
- Pythondaq: save
- Pythondaq: selecteer Arduino
Pythondaq: plot scan
Als het commando wordt uitgevoerd start de applicatie een scan en laat de metingen vervolgens zien in een plot binnen het venster. Voor het gemak heb je de poortnaam, start- en stopwaardes e.d. hard coded
in het script gezet. Later ga je er voor zorgen dat een gebruiker die kan instellen, maar dat komt straks wel.
Foutenvlaggen plotten
Foutenvlaggen toevoegen aan een pyqtgraph is helaas iets minder intuitief dan bij matplotlib. Met breedte en hoogte geef je aan hoe groot de vlaggen zijn, de vlag is 2 keer zo hoog of breed als de onzekerheid. Samen met de $x$ en $y$ data maak je dan een ErrorBarItem
aan die je expliciet toevoegt aan de plot. Let op: x
, y
, x_err
en y_err
moeten NumPy arrays zijn of, en dat geldt alleen voor de errors, een vast getal. Gewone lijsten werken helaas niet.
Checkpunten:
- Het uitvoeren van het commando zorgt ervoor dat een scan wordt gestart.
- Het LED lampje gaat branden.
- De resultaten van de meting worden geplot in het venster.
Projecttraject
- Pythondaq: leeg venster
- Pythondaq: plot scan
- Pythondaq: widgets
- Pythondaq: save
- Pythondaq: selecteer Arduino
Pythondaq: widgets
Na het uitvoeren van het commando start de pythondaq
applicatie waarin een aantal widgets zijn te zien waarmee de start- en stopwaardes, het aantal metingen kunnen worden ingesteld. Ook is er een startknop waarmee een nieuwe meting wordt uitgevoerd. Je vult verschillende (logische en niet logische) waardes in voor de start- en stopwaardes en het aantal metingen en ziet dat de applicatie naar verwachting werkt.
Pseudo-code
Checkpunten:
- In de applicatie kan de startwaarde worden aangepast.
- In de applicatie kan de stopwaarde worden aangepast.
- In de applicatie kan het aantal metingen worden aangepast.
- Druk op de startknop laat een meting starten.
- De applicatie werkt naar verwachting bij het invullen van logische en niet logische waardes voor start, stop en aantal metingen.
Projecttraject
- Pythondaq: leeg venster
- Pythondaq: plot scan
- Pythondaq: widgets
- Pythondaq: save
- Pythondaq: selecteer Arduino
Bewaren van meetgegevens¶
Je zou na iedere meting de gegevens automatisch kunnen wegschrijven naar bestanden zonder dat de gebruiker nog iets kan kiezen, maar je kunt ook gebruik maken van een Save
-knop en dialoogvensters. Je kunt de knop koppelen aan een method save_data()
en daarin de volgende regel opnemen:
De functie getSaveFileName()
opent een dialoogvenster om een bestand op te slaan. Vanwege het filter argument geeft het venster (op sommige besturingssystemen) alleen CSV-bestanden weer. In elk geval geldt op alle besturingssystemen dat als de gebruiker als naam metingen
intypt, dat het filterargument ervoor zorgt dat er automatisch .csv
achter geplakt wordt.7 De functie geeft twee variabelen terug: filename
en filter
, die je zelf hebt meegegeven in bovenstaande aanroep. Die laatste kenden we dus al en gooien we weg met behulp van de weggooivariabele _
.
Het enige dat het dialoogvenster doet is de gebruiker laten kiezen waar en onder welke naam het bestand moet worden opgeslagen. Je krijgt echt alleen een pad en bestandsnaam terug, de data is niet opgeslagen en het bestand is niet aangemaakt. De variabele filename
is echt niets anders dan een bestandsnaam, bijvoorbeeld: /Users/david/LED-rood.csv
. Nadat je die bestandsnaam gekregen hebt moet je dus zelf nog code schrijven zodat het CSV-bestand wordt opgeslagen onder die naam.
Pythondaq: save
Breid je code zodanig uit uit dat het volgende werkt: Je opent de applicatie en start een scan. Dan valt je oog op een Save
-knop, wanneer je op deze knop drukt wordt er een dialoogvenster geopent. Je kiest een locatie en typt een bestandsnaam, je klikt op Save
(of Opslaan
). Daarna ben je nieuwsgierig of het gelukt is. Via File Explorer
(of Verkenner
) navigeer je op de computer naar de locatie waar je het bestand hebt opgeslagen. Je opent het bestand en ziet de metingen staan. Tevreden sluit je het bestand af en ga je door naar de volgende opdracht.
Pseudo-code
Checkpunten:
- Druk op de knop
Save
opent een dialoogvenster. - De metingen worden opgeslagen als csv-bestand op de gegeven locatie en onder de gegeven bestandsnaam.
Projecttraject
- Pythondaq: leeg venster
- Pythondaq: plot scan
- Pythondaq: widgets
- Pythondaq: save
- Pythondaq: selecteer Arduino
Menu's, taak- en statusbalken
Menu's, taak- en statusbalken¶
Je kunt je grafische applicatie volledig optuigen met menu's of taakbalken. Ook kun je onderin je applicatie met een statusbalk weergeven wat de status is: gereed, aan het meten, foutcode, etc. Dat valt buiten het bestek van deze cursus, maar een mooie referentie is PySide6 Toolbars & Menus — QAction.14 Als je vaker grafische applicaties wilt gaan maken dan moet je dat zeker eens doornemen!
Pythondaq: statusbalk
Maak een statusbalk die aangeeft wat de identificatiestring is van het device dat geselecteerd is. Maak ook een menu waarmee je een CSV-bestand kunt opslaan en een nieuwe meting kunt starten. Let op: je hebt dan een menu-item én een knop die dezelfde method aanroepen. Je hoeft geen dubbele code te schrijven, maar moet de save_data()
-method wel twee keer verbinden.
Selecteer de Arduino¶
Je hebt nu waarschijnlijk nog de poortnaam van de Arduino in je code gedefinieerd als vaste waarde. Dat betekent dat als je de code deelt met iemand anders — bijvoorbeeld wanneer je de code inlevert op Canvas of wanneer je je experiment op een labcomputer wilt draaien — je het risico loopt dat je applicatie crasht omdat de Arduino aan een andere poort hangt. Zeker bij de overstap van Windows naar MacOS of Linux, of andersom! Je kunt dit op twee manieren oplossen:
- Je maakt een keuzemenu waarmee de gebruiker de Arduino kan selecteren;
- Je probeert de Arduino te detecteren op één van de poorten. De gebruiker hoeft dan niet te weten welke poort dat zou kunnen zijn. Het werkt dan vanzelf!
Je kunt je voorstellen dat mogelijkheid 2 de voorkeur heeft! Helaas is dit moeilijker dan gedacht. Zodra je andere devices gaat openen en commando's gaat sturen om te ontdekken wat voor apparaat het is kunnen er gekke dingen gebeuren. Onder MacOS bijvoorbeeld kunnen Bluetooth luidsprekers en koptelefoons opeens ontkoppelen. We gaan dus toch voor keuze 1. Bijkomend voordeel van deze keuze is dat je meerdere Arduino's aan je computer kunt hangen en kunt schakelen — vooral handig als je meerdere experimenten vanaf één computer wilt aansturen.
Pythondaq: selecteer Arduino
Je opent de applicatie en ziet een keuzemenu (QComboBox
) waarmee je de Arduino kunt selecteren. Je selecteert de juiste Arduino, start een meting en ziet het LED lampje branden. Je sluit de applicatie af en bent benieuwd wat er gebeurt als je meerdere Arduino's aansluit. Dus vraag je een (of twee, of drie) Arduino('s) van je buren, sluit deze aan op je computer en start opnieuw de applicatie. Je ziet dat er meerdere apparaten in het keuzemenu staan. Je kiest een Arduino, start een meting en ziet een lampje branden. Daarna selecteer je een andere Arduino, start een meting en ziet een ander lampje branden, hoe leuk .
Arduino afsluiten
Als je met meerdere Arduino's werkt kan het handig zijn om na afloop van de scan de communicatie met de Arduino weer te sluiten. In de opdracht Pyvisa in terminal heb je al eens gewerkt met het commando close
. Dit werkt ook voor pyvisa in een script. Je hebt in de controller de communicatie geopend met self.device = rm.open_resource(port, read_termination="\r\n", write_termination="\n")
, je kunt de communicatie met self.device
in de controller sluiten met self.device.close()
. Je kunt een method in de controller toevoegen die de communicatie sluit. Via het model kun je deze method aanroepen in de gui.
Pseudo-code
Checkpunten:
- In de applicatie kan een Arduino geselecteerd worden.
- De gekozen Arduino wordt gebruikt tijdens het uitvoeren van een scan
Projecttraject
- Pythondaq: leeg venster
- Pythondaq: plot scan
- Pythondaq: widgets
- Pythondaq: save
- Pythondaq: selecteer Arduino
Threads
Meerdere dingen tegelijkertijd: threads¶
Afhankelijk van de instellingen die we gekozen hebben kan een meting best lang duren. In ieder geval moeten we even wachten tot de meting afgelopen is en pas daarna krijgen we de resultaten te zien in een plot. Als een meting langer duurt dan een paar seconden kan het besturingssysteem zelfs aangeven dat onze applicatie niet meer reageert. En inderdaad, als we ondertussen op knoppen proberen te drukken dan reageert hij nergens op. Onze applicatie kan helaas niet twee dingen tegelijk. Kon hij dat wel, dan zouden we zien hoe de grafiek langzaam opbouwt tot het eindresultaat.
De manier waarop besturingssystemen meerdere dingen tegelijk doen is gebaseerd op processes en threads. Een process is, eenvoudig gezegd, een programma. Als je meerdere applicaties opstart zijn dat allemaal processen. Besturingssystemen regelen dat ieder proces een stuk geheugen krijgt en tijd van de processor krijgt toegewezen om zijn werk te doen. Processen zijn mooi gescheiden en kunnen dus eenvoudig naast elkaar draaien. Het wordt iets lastiger als een proces meerdere dingen tegelijk wil doen. Dat kan wel, met threads. Het besturingssysteem zorgt dat meerdere threads naast elkaar draaien.8
Threads geven vaak problemen omdat ze in zekere zin onvoorspelbaar zijn. Je weet niet precies hoe snel
een thread draait, dus je weet niet zeker wat er in welke volgorde gebeurt. Dit kan leiden tot problemen waarvan de oorzaak maar lastig te vinden is. Google maar eens op thread problems in programming
. We moeten dus voorzichtig zijn! Ook is het ombouwen van code zonder threads naar code met threads een klus waar makkelijk iets fout gaat. Het is dus belangrijk dat je in kleine stapjes je code aanpast en vaak test of het nog werkt.
Info
We gaan in het volgende stuk een kleine applicatie ombouwen van no-threads
naar threads
. We raden je ten zeerste aan om de code te copy/pasten en dan stapje voor stapje aan te passen zoals in de handleiding gebeurt. Probeer alle stappen dus zelf! Pas na stap 4 ga je aan de slag om je eigen code om te bouwen. Samenvattend: doorloop dit stuk handleiding twee keer. De eerste keer doe je de opdrachten met het demoscript, de tweede keer met je eigen code voor pythondaq
.
In regels 15--24 bouwen we een kleine user interface op met een plot widget en een startknop. We koppelen die knop aan de plot()
-method. In regel 27 maken we ons experiment (het model) aan en bewaren die. In regels 30--34 maken we de plot schoon, voeren we een scan uit en plotten het resultaat. model.py
vormt ons experiment. Eerst wordt een rij $x$-waardes klaargezet en dan, in een loop, wordt punt voor punt de sinus uitgerekend en toegevoegd aan een lijst met $y$-waardes. De time.sleep(.1)
wacht steeds 0.1 s en zorgt hiermee voor de simulatie van trage metingen. En inderdaad, als we deze code draaien dan moeten we zo'n vijf seconden wachten voordat de plot verschijnt.
In de volgende opdrachten gaan we de code stap voor stap ombouwen naar threads. Als we daarmee klaar zijn worden de metingen gedaan binnen de scan()
-method van de Experiment()
-class en verversen we ondertussen af en toe de plot. De plot()
-method van onze user interface wordt regelmatig aangeroepen terwijl de meting nog loopt en moet dus de hele tijd de huidige metingen uit kunnen lezen. Dat kan, als de metingen worden bewaard in instance attributes.9
Threads 0
Neem view.py
en model.py
over en test de applicatie.
Stap 1: de meetgegevens altijd beschikbaar maken¶
We maken in de scan()
-method lege lijsten self.x
en self.y
. Hier komen de meetgegevens in en die staan dus los van de lijst met $x$-waardes die je klaarzet. Met andere woorden: de variabele x
is niet hetzelfde als de variabele self.x
:
We zorgen er zo voor dat de lijst met meetgegevens voor zowel de $x$- als de $y$-waardes steeds even lang zijn. Dit is nodig voor het plotten: hij kan geen grafiek maken van 50 $x$-waardes en maar 10 $y$-waardes.10 Ook moeten we er voor zorgen dat er altijd (lege) meetgegevens beschikbaar zijn — ook als de meting nog niet gestart is. Anders krijgen we voordat we een meting hebben kunnen doen een foutmelding dat self.x
niet bestaat. We doen dat in de __init__()
:
We laten self.x = []
(en idem voor self.y
) ook staan in de scan()
-methode zodat bij iedere nieuwe scan de oude meetgegevens worden leeggemaakt.
Threads I
Pas de code aan zodat de meetgegevens altijd beschikbaar zijn. Test je code, de applicatie moet nog steeds werken.
Stap 2: plot de meetgegevens vanuit het experiment¶
Nu we de meetgegevens bewaren als instance attributes van de Experiment
-class kunnen we die ook plotten. We geven ze nog steeds terug als return value vanuit de scan()
-method voor ouderwetse
code,11 maar wij gaan nu de nieuwerwetse
instance attributes gebruiken:
De code wordt hier niet sneller van — hij maakt nog steeds pas een grafiek als de meting helemaal is afgelopen — maar we bereiden de code wel voor op het gebruik van de instance attributes.
Threads II
Pas de code aan zodat je instance attributes gebruikt voor het plotten. Test je code, het moet nog steeds werken als vanouds.
Stap 3: threads¶
We gaan nu met threads werken. Je importeert daarvoor de threading
module en maakt voor iedere thread een threading.Thread()
instance. Deze heeft twee belangrijke parameters: target
waarmee je de functie (of method) aangeeft die in de thread moet worden uitgevoerd, en args
waarmee je argumenten meegeeft voor die functie of method. We maken een nieuwe method start_scan()
waarmee we een nieuwe thread starten om een scan uit te voeren. We doen dit als volgt:
In plaats van dat onze plotfunctie de scan()
-method aanroept, moeten we nu de start_scan()
-method aanroepen. Maar: die method start een scan en sluit meteen af, terwijl de daadwerkelijke meting op de achtergrond wordt uitgevoerd. De plotfunctie moet — in deze stap nog even — wachten tot de scan klaar is. Er is een manier om op een thread te wachten. Je moet daartoe de join()
method van de thread aanroepen. In bovenstaande code hebben we de thread bewaard in de variabele _scan_thread
, dus hij is voor ons beschikbaar:
Threads III
- Pas de code aan zodat je een thread opstart om de scan op de achtergrond uit te voeren. Roep in je plotfunctie de goede method aan en wacht tot de thread klaar is. Test je code. Wederom moet het werken als vanouds.
- Kijk ook eens wat er gebeurt als je niet wacht tot de metingen klaar zijn door de regel
self.experiment._scan_thread.join()
uit te commentariëren (hekje ervoor). Niet vergeten het hekje weer weg te halen.
Stap 4: plotten op de achtergrond¶
We zijn er nu bijna. We gebruiken threads om de metingen op de achtergrond uit te voeren maar we wachten nog steeds tot de metingen klaar zijn voordat we — eenmalig — de grafiek plotten. In deze laatste stap doen we dat niet meer. Als je straks op de startknop drukt dan start de meting op de achtergrond. Ondertussen wordt er regelmatig geplot. Je ziet dan tijdens de metingen de plot opbouwen. We doen dat door het scannen en plotten van elkaar los te koppelen — niet meer samen in één functie — en door met een QTimer
de plotfunctie periodiek aan te roepen. Kijk de code goed door.
Hiermee zijn we klaar met de implementatie van threads. De gebruiker hoeft niet langer in spanning te wachten maar krijgt onmiddelijke feedback.
Threads IV
Pas de code op dezelfde manier aan zodat de metingen op de achergrond worden uitgevoerd terwijl je de plot ziet opbouwen. De code werkt nu niet als vanouds, en voelt veel sneller!
Pythondaq: threads in je eigen code
Doorloop nu opnieuw stappen 1 t/m 4 maar dan voor je eigen pythondaq
-applicatie.
Events
Stap 5: puntjes op de i
: events¶
Wanneer je op de startknop drukt, even wacht en dan wéér op de startknop drukt, dan kun je zien dat er twee metingen tegelijk worden uitgevoerd op de achtergrond. Dat wil je voorkomen. Ook is het wel aardig om metingen tussentijds te kunnen stoppen. Dat is vooral handig als je merkt dat een meting veel te lang gaat duren. Verder is het ook nog zo dat we er nu met onze timer voor gezorgd hebben dat de plotfunctie meerdere keren per seconde wordt uitgevoerd — of er nu een meting loopt of niet.
Je kunt dit oplossen met threading.Event()
objecten. Dit zijn objecten met set()
, clear()
en wait()
methods om gebeurtenissen aan te geven of er op te wachten. Zo kun je een event is_scanning
aanmaken die je set()
zodra een meting begint en clear()
zodra de meting is afgelopen. Je controleert bij de start van de meting dan bijvoorbeeld eerst of de meting al loopt met is_scanning.is_set()
en start alleen een meting als dat nog niet zo is.
Ook kun je in de grafische interface na het starten van een meting de startknop onbeschikbaar maken met start_button.setEnabled(False)
en weer beschikbaar maken met start_button.setEnabled(True)
. De knop wordt dan tussendoor grijs. Dat kan handig zijn om duidelijk te maken dat een meting al loopt en dat je niet nogmaals op de startknop kunt drukken.
Vergrendelen
Pas je code aan zodat je niet meerdere metingen tegelijk kunt starten. Zorg er ook voor dat de grafiek alleen geplot wordt tijdens de metingen (of tot kort daarna), maar niet de hele tijd.
-
Uitspraak: het Engelse cute. ↩
-
Of in één keer met View > Command Palette > Terminal: Kill All Terminals ↩
-
Die kun je eventuele command-line arguments meegeven die door Python in
sys.argv
bewaard worden. Meestal zijn die leeg, maar we geven ze gewoon door aan Qt. ↩ -
Waarom doen de knoppen niets als je er op klikt? ↩
-
Je moet dan wel eerst nieuwe controllers schrijven (of krijgen van een collega) om deze nieuwe instrumenten aan te sturen. Maar als je die hebt kun je vrij eenvoudig wisselen. ↩
-
Het eerste deel van het argument (
CSV files
) is vrij te kiezen en geeft alleen informatie aan de gebruiker. Het deel tussen haakjes (*.csv
) is het gedeelte dat echt van belang is. Het geeft de extensie die achter alle bestandsnamen geplakt wordt. ↩ -
Er is een subtiliteit. In Python draaien threads niet tegelijk, maar om de beurt. In de praktijk merk je daar niet veel van: threads worden zó vaak per seconde gewisseld dat het lijkt alsof ze tegelijk draaien. Terwijl de ene thread steeds even tijd krijgt voor een meting kan de andere thread steeds even de plot verversen. In het geval van zwaar rekenwerk schiet het alleen niet op. Er draait maar één berekening tegelijkertijd dus threads of niet, het is even snel. Wil je echt parallel rekenen, dan moet je kijken naar de
multiprocessing
module om meerdere processen te starten in plaats van threads. ↩ -
Variabelen die we in een class definiëren door ze aan te maken met
self.
ervoor zijn instance attributes. ↩ -
Hier zie je een probleem met threads. Het kán — in uitzonderlijke situaties — voorkomen dat de plot-functie nét wil gaan plotten als de $x$-waardes al langer gemaakt zijn, maar de $y$-waardes nog niet. Die kans is heel klein en wij accepteren het risico. Schrijf je software voor een complex experiment dat drie dagen draait, dan is dit iets waar je echt rekening mee moet houden. Je moet dan gebruik gaan maken van zogeheten locks of semaphores maar dat valt buiten het bestek van deze cursus. ↩
-
Door een beetje ons best te doen kunnen we ervoor zorgen dat zowel de command-line interface als de grafische interface allebei gebruikt kunnen worden. ↩
-
Matt Newville. Asteval: minimal python ast evaluator. URL: https://newville.github.io/asteval. ↩
-
Leodanis Pozo Ramos. Python eval(): evaluate expressions dynamically. 2020. URL: https://realpython.com/python-eval-function/. ↩
-
Martin Fitzpatrick. Pyside6 toolbars & menus — qaction. 2021. URL: https://www.pythonguis.com/tutorials/pyside6-actions-toolbars-menus/. ↩