Home | Lehre | Videos | Texte | Vorträge | Software | Person | Impressum, Datenschutzerklärung |
Stand: 2024-06-15
weitgehend formuliert von ChatGPT-4o
PySide ist die offizielle Python-Bindung für das Qt-Framework, das eine der leistungsfähigsten und umfassendsten Klassenbibliotheken für die Entwicklung grafischer Benutzeroberflächen (Graphical User Interfaces: GUIs) darstellt. Ursprünglich von Nokia entwickelt und jetzt von der Qt Company gepflegt, bietet Qt eine plattformübergreifende Lösung, die es Entwicklern ermöglicht, Anwendungen zu schreiben, die auf Windows, macOS, Linux und weiteren Plattformen laufen. PySide ermöglicht es Python-Entwicklern, diese umfangreiche und vielseitige Bibliothek zu nutzen, um komplexe und interaktive GUIs zu erstellen, ohne sich tiefgehend mit den plattformabhängigen Details beschäftigen zu müssen. PySide6, die derzeit (Mai 2024) neueste Version, basiert auf Qt 6.
Wie schon von den anderen Bibliotheken bekannt, muss auch PySide erst
installiert werden (möglichst in das virtuelle Environment):
pip install pyside6
Neber der reinen Bibliothek werden dann
auch Hilfsprogramme installiert, insbesondere der Qt Designer, mit dem
man Fenster, Knöpfe usw. wie in einem Zeichenprogramm erzeugen und
anordnen kann. Er lässt sich im virtuellen Environment über die
Kommandozeile pyside6-designer
starten. Wir werden hier
aber nicht den Qt Designer verwenden, sondern, wie es üblich ist, die
Elemente der Oberfläche als Code schreiben, weil so viel klarer ist und
leichter zu bearbeiten ist, was wie zusammenhängt. Inzwischen kann man
einfach der KI eine Beschreibung der gewünschten Oberfläche geben und
erhält fast fehlerfrei diesen Code.
Qt und damit PySide stellt sich automatisch auf den Light/Dark-Modus
und die Bildschirmauflösung ein. Im Regelfall legt man obendrein die
grafische Oberfläche so an, dass die Elemente sich in ihrer Größe
automatisch anpassen, um den Platz optimal zu nutzen. Das ist ein erster
Schritt hin zum responsive design
, bei dem dann vielleicht sogar
Inhalte ein- oder ausgeblendet oder umpositioniert werden.
Klassenbibliotheken sind Sammlungen von vordefinierten Klassen und Funktionen, die Entwicklern helfen, häufige Aufgaben effizienter zu lösen. Sie stellen wiederverwendbare Komponenten bereit, die die Entwicklung beschleunigen und den Code konsistenter und leichter wartbar machen. In der GUI-Entwicklung bieten diese Bibliotheken nicht nur grundlegende Bausteine wie Widgets und Layout-Manager, sondern auch erweiterte Funktionen wie Event-Handling, Grafik-Rendering und Unterstützung für internationale Benutzeroberflächen. Durch die Nutzung solcher Bibliotheken kann man sich auf die Implementierung der spezifischen Logik ihrer Anwendung konzentrieren, anstatt sich mit den Details der Benutzeroberflächenprogrammierung und den Herausforderungen der plattformübergreifenden Kompatibilität auseinandersetzen zu müssen.
Klassenbibliotheken wie Qt und PySide bieten eine strukturierte und
effiziente Möglichkeit, grafische Benutzeroberflächen zu entwickeln. Sie
umfassen eine Vielzahl von vorgefertigten Widgets
(Sammelbegriff
für Schaltflächen, Eingabefelder, Menüs usw.) und Layouts. Diese
Bibliotheken abstrahieren viele der komplexen Details der
betriebssystemspezifischen GUI-Programmierung und stellen sicher, dass
Anwendungen auf verschiedenen Betriebssystemen gleichartig aussehen und
funktionieren. Neben PySide gibt es für Python andere beliebte
GUI-Bibliotheken wie Tkinter, wxPython und Kivy, die jeweils ihre
eigenen Vor- und Nachteile haben und für unterschiedliche
Anwendungsfälle geeignet sind.
Dies war der ursprüngliche Prompt:
Schreibe mir ein PySide-Progrämmchen zur Verwaltung einer To-Do-Liste. Jedes To-Do hat eine Bearbeitungsfrist, einen Titel und die Information, ob es erledigt ist. Im Hauptfenster des Programms sieht man alle To-Dos gemäß ihrer Frist aufgelistet, kann ein neues erzeugen (dazu öffnet sich ein Extra-Fenster, um Frist und Titel einzugeben), kann To-Dos als erledigt markieren.
Aus dem anfänglichen Prompt ist mit ein wenig Diskussion folgendes objektorientierte Design entstanden:
QMainWindow
. Sie verfügt nur über ein einziges Attribut,
das sie nicht geerbt hat:
_todo_list_widget
: Das Widget zum Speichern und zur
Darstellung der Liste im Fenster. Dies ist als (geschütztes) Attribut
angelegt, damit andere Methoden später diese Liste ändern können.__init__(self)
: Initialisiert das Hauptfenster und die
To-Do-Liste._open_add_todo_dialog(self)
: Öffnet einen Dialog zum
Hinzufügen eines neuen To-Do-Elements._mark_as_done(self)
: Markiert das ausgewählte
To-Do-Element als erledigt.QDialog
. Sie hat an eigenen, nicht geerbten
Attributen nur öffenliche Attribute; diese werden nach erfolgter Eingabe
zum Auslesen der Werte benutzt:
title_input
: Das Eingabefeld für den Titel des
To-Do-Elements.deadline_input
: Das Eingabefeld für das
Fälligkeitsdatum des To-Do-Elements.__init__(self, parent=None)
: Initialisiert den Dialog
und die Eingabefelder für Titel und Fälligkeitsdatum.QListWidgetItem
. Sie hat an
eigenen, nicht geerbten Attributen nur öffenliche Attribute:
title
: Der Titel des To-Do-Elements.deadline
: Das Fälligkeitsdatum des To-Do-Elements.completed
: Ein boolescher Wert, der anzeigt, ob das
To-Do-Element erledigt ist.__init__(self, title, deadline)
: Initialisiert das
To-Do-Element.mark_as_done(self)
: Markiert das To-Do-Element als
erledigt.Der Rahmen um die Klassen (die man später in eigene Dateien auslagern würde!) sieht so aus:
import sys
from PySide6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QHBoxLayout, QPushButton, QListWidget, QListWidgetItem, QWidget, QDialog, QLabel, QLineEdit, QDateEdit, QDialogButtonBox
from PySide6.QtGui import QFont
from PySide6.QtCore import Qt, QDate, QTranslator, QLocale, QLibraryInfo
from typing import cast
class MainWindow(QMainWindow):
# usw.
class AddToDoDialog(QDialog):
# usw.
class ToDoItem(QListWidgetItem):
# usw.
= QApplication(sys.argv)
app = MainWindow()
window
window.show()exec()) sys.exit(app.
Es wird also Qt gestartet, indem man ein Objekt der Klasse
QApplication
erzeugt. Dies erhält die Kommandozeile
sys.argv
, mit der man das Programm aufgerufen hat. (So kann
man beim Aufruf zum Beispiel noch Farben und Schritarten wählen.) Dann
wird eine Instanz des Hauptfensters erzeugt und angezeigt. Was
passiert, wenn man das mit mehreren Instanzen macht? Das Programm
befindet sich nach dem kurzen Start die gesamte (!) Zeit in der Funktion
app.exec()
, wo es in der Ereignisschleife
(event
loop) auf Eingaben wartet und darauf reagiert. Beim Klick auf den
Schließen-Knopf endet app.exec()
und gibt die Zahl 0 oder
aber einen Fehlercode zurück. Dieses Ergebnis wird an die Funktion
sys.exit(...)
gegeben, die das Programm dann mit diesem
Code beendet.
Zunächst ist die Oberfläche englisch mit Cancel
, Ctrl
usw. beschriftet. Um auf Deutsch mit Abbrechen
, Strg
usw.
umzustellen, sind folgende Zeilen (deren Inhalt Sie zunächst ignorieren
können) sofort nach dem Erzeugen der QApplication nötig:
= QTranslator()
translator = QLocale(QLocale.German)
locale = QLibraryInfo.location(QLibraryInfo.TranslationsPath)
qt_translations_path 'qtbase', '_', qt_translations_path)
translator.load(locale, app.installTranslator(translator)
ToDoItem
Die einfachste der drei Klassen ist ToDoItem
. Sie ist
zum einen ein simpler Datenspeicher für die öffentlichen Attribute
title
, deadline
und completed
;
zum anderen erbt sie von QListWidgetItem
und kann damit auf
dem Bildschirm als Listeneintrag in einem QListWidget
verwendet werden:
class ToDoItem(QListWidgetItem):
def __init__(self, title: str, deadline: QDate) -> None:
super().__init__()
self.title: str = title
self.deadline: QDate = deadline
self.completed = False
self.setText(f'{self.deadline.toString(Qt.ISODate)}: {self.title}')
def mark_as_done(self) -> None:
self.completed = True
= self.font()
font: QFont True)
font.setStrikeOut(self.setFont(font)
Die von QListWidgetItem
geerbten Methoden
setText
und setFont
stellen ein, welcher Text
in welcher Schriftart für diesen Listeneintrag in der Liste auf dem
Bildschirm erscheint.
AddToDoDialog
Weil die Klasse AddToDoDialog
den Großteil ihrer
Funktionalität von QDialog
aus der Klassenbibliothek erbt,
benötigt sie nichts anderes als einen recht kurzen Initialisierer:
class AddToDoDialog(QDialog):
def __init__(self, parent: QWidget) -> None:
super().__init__(parent)
self.setWindowTitle('Neues To-Do')
= QVBoxLayout(self)
layout
'Titel:'))
layout.addWidget(QLabel(self.title_input = QLineEdit()
self.title_input)
layout.addWidget(
'Frist:'))
layout.addWidget(QLabel(self.deadline_input = QDateEdit()
self.deadline_input.setCalendarPopup(True)
self.deadline_input.setDate(QDate.currentDate())
self.deadline_input)
layout.addWidget(
= QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
buttons
layout.addWidget(buttons)connect(self.accept)
buttons.accepted.connect(self.reject) buttons.rejected.
Der Initalisierer erhält eine Referenz parent
auf das
logisch übergeordnete Widget, bei uns dann das Hauptfenster der
Anwendung. Diese reicht sie an den Initialisierer der Elternklasse
QDialog
weiter. Diese Information wird dort je nach Typ des
Widgets zum Beispiel zur Speicherverwaltung verwendet.
Ein QVBoxLayout
wird erstellt und gleichzeitig im
Dialogfenster (self
) installiert. Das
QVBoxLayout
ordnet die enthaltenen Widgets vertikal von
oben nach unten an. Solche Layout-Manager in Qt verwalten die
Positionierung und Größenänderung der darin enthaltenen Widgets
automatisch, um den Platz auf dem Bildschirm optimal zu nutzen. Andere
Layout-Manager als QVBoxLayout
erzeugen zum Beispiel
Raster- oder Umblätter-Anordnungen.
Diesem layout
werden fünf Widgets hinzugefügt und damit
dann von oben nach unten angezeigt:
QLabel
mit dem Text Titel:als Beschriftung für das Eingabefeld.
QLineEdit
zur Eingabe des Titels des
To-Do-Elements.QLabel
mit dem Text Frist:als Beschriftung für das Datumseingabefeld.
QDateEdit
zur Eingabe des Fälligkeitsdatums, das
auf das aktuelle Datum gesetzt ist und bei Klick einen Kalender
anzeigt.QDialogButtonBox
mit OK- und
Abbrechen-Schaltflächen.
Die OK
- und Abbrechen
-Schaltflächen werden mit den (von
der Klasse QDialog
geerbten) Methoden accept
und reject
des Dialogs verdrahtet (connect
).
Sowohl accept
als auch reject
schließen das
Dialogfenster. Obendrein speichern diese beiden Methoden, ob das, was
man im Dialog eingestellt hat, gelten soll (accept
) oder
aber verworfen werden soll (reject
). Diese Information wird
dann in der Klasse MainWindow
weiterverwendet.
MainWindow
Die Klasse MainWindow
verrichtet die Hauptarbeit des
Programms. Sie erbt zwar viel Funktionalität von der Klasse
QMainWindow
aus der Klassenbibliothek, aber dennoch ist
hier einiges an Anpassung nötig. Beginnen wir mit dem
Initialisierer:
class MainWindow(QMainWindow):
def __init__(self) -> None:
super().__init__()
self.setWindowTitle('To-Do-Liste')
= QWidget()
central_widget self.setCentralWidget(central_widget)
= QVBoxLayout(central_widget)
layout
self._todo_list_widget = QListWidget()
self._todo_list_widget)
layout.addWidget(self._todo_list_widget.setSortingEnabled(True)
= QHBoxLayout()
buttons_layout
layout.addLayout(buttons_layout)
= QPushButton('Neues To-Do...')
add_button
buttons_layout.addWidget(add_button)connect(self._open_add_todo_dialog)
add_button.clicked.
= QPushButton('Als erledigt markieren')
mark_done_button
buttons_layout.addWidget(mark_done_button)connect(self._mark_as_done) mark_done_button.clicked.
Anders als der zuvor besprochene Dialog benötigt ein
QMainWindow
ein zentrales Widget. Typischerweise nimmt man
hier das allgemeine QWidget
, das keinen besonderen Inhalt
hat. Wie beim zuvor besprochenen Dialog ist ein Layout nötig, um die in
diesem zentralen Widget enthaltenen (Unter-)Widgets zu organisieren: ein
QListWidget
als Attribut
self._todo_list_widget
und ein Unter-Layout
buttons_layout
, das zwei Schaltflächen nebeneinander
(QHBoxLayout
statt QVBoxLayout
!) enthält.
Die beiden Schaltflächen werden mit den Methoden
open_add_todo_dialog
und mark_as_done
verdrahtet: Bei Klick auf eine davon ruft PySide die jeweilige Methode
auf.
Zwar hat schon der einzelne Listeneintrag in seiner Klasse
ToDoItem
eine Methode, um sich als erledigt zu markieren,
aber diese Methode muss nun noch aus dem Hauptfenster aufgerufen werden.
Der Verständlichkeit halber nennen wir die Methode von
MainWindow
, die das tut, ebenfalls
_mark_as_done
, allerdings mit Unterstrich, denn diese
Methode ist nicht zur Verwedung außerhalb von MainWindow
gedacht:
def _mark_as_done(self) -> None:
| None = cast(ToDoItem | None, self._todo_list_widget.currentItem())
current_item: ToDoItem if current_item is not None:
current_item.mark_as_done()
Diese Methode _mark_as_done
wurde schon im
Initialisierer von MainWindow
mit dem Klick auf die Als
erledigt markieren
-Schaltfläche verdrahtet. Auf diesen Klick hin
wird also eine Referenz auf den derzeit ausgewählten Listeneintrag
geholt. Ist nichts ausgewählt, erhält man die Nullreferenz
None
. Beachtet man letzteren Fall nicht, stürzt das
Programm dann ab.
Die zweite Zeile wäre ohne Type Hints deutlich einfacher:
current_item = self._todo_list_widget.currentItem()
. Aber
wenn man vergessen hat, das gefährliche None
zu behandeln,
liefern die Type Hints hier eine wichtige Warnung. Das
cast(X, a)
sagt dabei dem Typprüfer, dass man sich sicher
ist, dass in a
ein Objekt des Typs X
steckt.
(In anderen Sprachen wie C macht Casting tatsächlich etwas mit den
Daten, nicht so in Python.) Bei cast(ToDoItem | None, ...)
nutzen wir aus, dass in der Liste nur Objekte der Klasse
ToDoItem
enthalten sein können, denn andere fügen wir nicht
hinzu. Aber die Nullreferenz None
darf herauskommen, denn
es könnte in der Liste nichts ausgewählt sein.
Nun fehlt noch eine Methode in MainWindow
, um überhaupt
Einträge in der To-Do-Liste zu erzeugen:
def _open_add_todo_dialog(self) -> None:
= AddToDoDialog(self)
dialog if dialog.exec() == QDialog.Accepted:
str = dialog.title_input.text()
title: = dialog.deadline_input.date()
deadline: QDate self._todo_list_widget.addItem(ToDoItem(title, deadline))
Eine Instanz des AddToDoDialog
wird erzeugt und mit
exec()
ausgeführt
, das heißt, der Dialog erscheint
auf dem Bildschirm und wartet auf Eingaben; gleichzeitig ist das
Hauptfenster blockiert. Beendet man den Dialog mit OK
(oder
Return-Taste) oder Abbrechen
(oder Escape-Taste oder Klick auf
das X), endetet dialog.exec()
. (Vergleiche mit dem
Verhalten von exec()
im Hauptprogramm! Übrigens läuft das
dialog.exec()
hier als ein tief verschachtelter
Funktionsaufruf innerhalb des exec()
des Hauptprogramms.
Warum?)
Der Aufruf dialog.exec()
hat einen Rückgabewert. Dieser
Rückgabewert zeigt an, ob der Dialog mit OK
verlassen wurde oder
nicht. Falls ersteres, holen wir title
und
deadline
aus den Widgets des Dialogs. (Merke: Man kann auf
die Widgets des Dialogs auch dann zugreifen, bevor oder nachdem der auf
dem Bildschirm zu sehen ist/war!). Aus title
und
deadline
bauen wir eine Instanz von ToDoItem
und fügen jene der Liste hinzu. Warum erscheint der neue Eintrag in
der Liste nicht am Ende, sondern so weit vorne oder hinten, wie es
seinem Datum entspricht?
Ergänzen Sie diese Funktionalitäten:
Löschen des ausgewählten Eintrags
Export aller To-Do-Einträge in eine Textdatei
Ändern des ausgewählten Eintrags mit einem abgewandelten
AddToDoDialog