Home | Lehre | Videos | Texte | Vorträge | Software | Person | Impressum, Datenschutzerklärung | ![]()
Stand: 2025-04-02
weitgehend formuliert von ChatGPT 4o, verbessert von Claude 3.7 Sonnet,
redigiert/korrigiert von Jörn Loviscach
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, um Anwendungen zu schreiben, die auf Windows, macOS, Linux und weiteren Plattformen laufen. PySide erlaubt, diese umfangreiche und vielseitige Bibliothek in Python zu nutzen. PySide6, die derzeit (April 2025) aktuelle 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 wird 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 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
(Sammelbegriff für Schaltflächen,
Eingabefelder, Menüs usw.) und automatisches Layout, sondern auch
erweiterte Funktionen wie Touch-Eingabe, Hardware-beschleunigte
Grafik-Ausgabe und Internationisierung. Dank solcher Bibliotheken kann
man sich auf die Implementierung der spezifischen Logik der Anwendung
konzentrieren, statt sich mit den Details der Oberflächenprogrammierung
und plattformspezifischen Fragen auseinandersetzen zu müssen. 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.
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())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:
translator = QTranslator()
locale = QLocale(QLocale.Language.German)
qt_translations_path = QLibraryInfo.path(QLibraryInfo.LibraryPath.TranslationsPath)
translator.load(locale, 'qtbase', '_', qt_translations_path)
app.installTranslator(translator)ToDoItemDie 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.DateFormat.ISODate)}: {self.title}')
def mark_as_done(self) -> None:
self.completed = True
font: QFont = self.font()
font.setStrikeOut(True)
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.
AddToDoDialogWeil 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')
layout = QVBoxLayout(self)
layout.addWidget(QLabel('Titel:'))
self.title_input = QLineEdit()
layout.addWidget(self.title_input)
layout.addStretch(1)
layout.addWidget(QLabel('Frist:'))
self.deadline_input = QDateEdit()
self.deadline_input.setCalendarPopup(True)
self.deadline_input.setDate(QDate.currentDate())
layout.addWidget(self.deadline_input)
layout.addStretch(1)
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Save | QDialogButtonBox.StandardButton.Cancel)
layout.addWidget(buttons)
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)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 und zwei dehnbare
Zwischenräume hinzugefügt:
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.
MainWindowDie 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')
central_widget = QWidget()
self.setCentralWidget(central_widget)
layout = QVBoxLayout(central_widget)
self._todo_list_widget = QListWidget()
layout.addWidget(self._todo_list_widget)
self._todo_list_widget.setSortingEnabled(True)
buttons_layout = QHBoxLayout()
layout.addLayout(buttons_layout)
add_button = QPushButton('Neues To-Do...')
buttons_layout.addWidget(add_button)
add_button.clicked.connect(self._open_add_todo_dialog)
mark_done_button = QPushButton('Als erledigt markieren')
buttons_layout.addWidget(mark_done_button)
mark_done_button.clicked.connect(self._mark_as_done)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:
current_item: ToDoItem | None = cast(ToDoItem | None, self._todo_list_widget.currentItem())
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:
dialog = AddToDoDialog(self)
if dialog.exec() == QDialog.Accepted:
title: str = dialog.title_input.text()
deadline: QDate = dialog.deadline_input.date()
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