Teil 1: Einfache GUIs mit Python und Qt

Außen GUI, innen hui!
Kommentare

Im Zeitalter der Smartphones und Tablets sind grafische Applikationen ein Muss geworden. Die Programmierung von GUIs auf der Ebene des Quelltextes ist allerdings nach wie vor aufwändig, da in klassischen statisch typisierten Programmiersprachen wie C++ relativ viel Code benötigt wird, um ein GUI zu implementieren. Doch es geht auch anders …

Ein gängiger Weg zur Implementierung ist der Einsatz von IDEs, die den benötigten Code automatisiert erzeugen. Bei diesem Vorgehen nimmt die IDE dem Entwickler viele Aufgaben ab, indem GUI-Elemente in GUI-Buildern entworfen und der entsprechende Code automatisch generiert wird. Eine Alternative kann hier die Verwendung einer Kombination einer modernen dynamisch typisierten Skriptsprache mit einem leistungsfähigen GUI-Toolkit sein. In dieser zweiteiligen Artikelserie soll gezeigt werden, wie mit Python und Qt mit wenig Code eine GUI-Anwendung realisiert werden kann. Der erste Teil der Artikelserie stellt den Anwendungsfall vor und erläutert die Implementierung der einzelnen GUI-Komponenten mit Python und Qt.

Bereitstellung von Qt-Funktionalität für Python

Um die Funktionalität der Qt-Bibliothek für die Programmierung mit Python zur Verfügung zu stellen, werden entsprechende Anbindungen (Bindings) benötigt. Es stehen hier zwei Alternativen für solche Bindings zur Verfügung:

  • PyQt: Dieses ist das zuerst entstandene Qt-Binding für Python, das von der britischen Firma Riverbank Computing entwickelt wurde.
  • PySide: Eine alternative Implementierung eines Qt-Binding für Python, das weitgehend identische Funktionalität bietet wie PyQt und dabei weitestgehende Kompatibilität zu PyQt zu wahren sucht. PySide entstand, nachdem es Nokia als damaliger Besitzer der Rechte an Qt nicht gelang, mit Riverbank zu einer Einigung zur Änderung der Lizenzierung von PyQt zu kommen.

PyQt gilt dabei im Allgemeinen als das ausgereiftere Binding, während an PySide die liberalere Lizenzierung geschätzt wird (Kasten: „Lizenzierung von PyQt und PySide“).

Für die Artikelserie benutzen wir an dieser Stelle PySide, eine Implementierung mit PyQt wäre aber genauso gut möglich und unter Umständen empfehlenswert.

The Aim of the Game

Die Programmierung eines GUI mit Python und Qt soll anhand eines einfachen Anwendungsfalls demonstriert werden. Es soll eine Quiz-Anwendung implementiert werden, wie sie z. B. bei Messen oder Konferenzen zum Einsatz kommen kann. Ein Teilnehmer muss zunächst seine Adressdaten eingeben. Anschließend werden ihm eine Reihe von Fragen in Form von Multiple-Choice-Formularen gestellt. Nach Beendigung des Quiz werden die Ergebnisse persistiert. Zum Abschluss wird dem Teilnehmer sein Ergebnis präsentiert.


Abb. 1: Interface-Flow-Diagramm der Anwendung

Abbildung 1 zeigt den Aufbau der Anwendung anhand eines Interface-Flow-Diagramms (zu Interface-Flow-Diagrammen [1]). Die Kästen stellen dabei die jeweils angezeigten Bildschirmelemente dar, die Pfeile repräsentieren die möglichen Übergänge. Einfacher Text an den Pfeilen dient zur Notation von Bedingungen, in eckige Klammern eingeschlossener Text steht für die Aktivierung des Übergangs über das Betätigen der betreffenden Schaltfläche. Entsprechend haben wir es mit folgenden Oberflächenelementen zu tun:

  • Start Screen: Startbildschirm
  • Registration Screen: Anmeldedialog, Formular für die Eingabe der persönlichen Daten
  • Question Screen: Dialog für die Darstellung einer Quizfrage als Multiple-Choice-Auswahl
  • Confirm Quit Dialog: Bestätigungsdialog für den Fall, dass „Abbrechen“ betätigt wird
  • Result Screen: Präsentierung des Ergebnisses des Quiz

Sollen die folgenden Beispiele am Rechner nachvollzogen werden, müssen ggf. vorher Python, Qt, PySide und lxml mit dem entsprechenden Python Binding installiert werden, näheres dazu im Kasten „Installation“.

Installation von Python, Qt, PyQt, PySide und lxml

Installation unter Debian/Ubuntu

Unter Debian/Ubuntu kann die Installation relativ einfach über apt aus einer Shell vorgenommen werden:

sudo add-apt-repository ppa:pyside
sudo apt-get update
sudo apt-get install python-pyside

Da apt die Abhängigkeiten selbsttätig erkennt, wird damit ggf. auch Python und Qt mit installiert. lxml mit Python Binding erhält man dann durch sudo apt-get install python-lxml.

Installation unter Windows

Für die Installation von PyQt steht ein Standalone-Installer hier zur Verfügung. Enthalten ist hier sowohl PyQt als auch Qt selbst, bei Verwendung müssen ggf. nur noch Python und lxml nachinstalliert werden. Python selbst ist hier verfügbar. Für die Verwendung von PySide können entsprechende Binärpakete hier geladen werden. Die Installation von Qt unter Windows ist hier beschrieben. lxml kann als Binärpaket von hier bezogen werden.

Aufmacherbild: Python skin as background von Shutterstock / Urheberrecht: Teodora_D

[ header = Aller Anfang ist schwer ]

Aller Anfang ist schwer – Der Startbildschirm

Der Startbildschirm ist der Teil der Anwendung, mit dem der Teilnehmer als erstes in Berührung kommt. Bei einer Messe oder einer ähnlichen Veranstaltung müssen potenzielle Teilnehmer unter Umständen durch den Startbildschirm angelockt und dazu verführt werden, an dem Quiz teilzunehmen. Einerseits ist der Startbildschirm also über eine relativ lange Zeit zu sehen (nämlich in den Pausen zwischen den jeweiligen Quizläufen), andererseits ist die Gestaltung sehr wichtig, um das Quiz attraktiv zu machen. Natürlich könnte dieser Startbildschirm auch mit den „Bordmitteln“ von Qt gestaltet werden. Allerdings finden sich wahrscheinlich deutlich mehr Designer im Webbereich, die sind es gewohnt sind, ihre Oberflächen in HTML und CSS zu erzeugen. Es liegt also nahe, für die Darstellung des Startbildschirms ebenfalls diese Technologien zu nutzen, d. h. das entsprechende Bild mit den Techniken des Webdesigns entwickeln zu lassen. Eine entsprechende Implementierung hat darüber hinaus den Vorteil, dass Webdesigner meist seit Jahren darin geübt sind, Seiten so zu entwerfen, dass sie auch auf verschiedenen Ausgabegeräten vom Widescreen-Monitor bis zu Tablet und Smartphone benutzbar und ansehnlich sind. Der Startbildschirm soll daher wie folgt implementiert werden:

  • Es soll eine HTML-Seite angezeigt werden.
  • Er sollte im Vollbildmodus laufen, möglichst gut sichtbar sein und gleichzeitig den Desktop verdecken.
  • Da das grafische Design des Startbildschirms am aufwändigsten ist, sollte der Startbildschirm als Hintergrund der Applikation dienen und entsprechend dauerhaft sichtbar sein.
  • Es sollen Steuerelemente benutzt werden, mit denen das Quiz gestartet oder beendet werden kann.

Wir gehen die Implementierung für den Startbildschirm jetzt Schritt für Schritt durch. Der Inhalt wird in einer Datei startscreen.py abgelegt:

#!/usr/bin/env python
# -*- coding: latin-1 -*-
# file startscreen.py

from PySide import QtGui, QtWebKit

Die erste Zeile besteht aus dem so genannten Shebang. Es erlaubt unter unixoiden Betriebssystemen, dass das Skript direkt mit dem hier spezifizierten Interpreter (python) ausgeführt wird, wenn es direkt (also ohne, dass explizit der Interpreter aufgerufen wird) gestartet wird.

Die nächste Zeile legt die Kodierung der Quelltextdatei fest – in diesem Fall „latin-1“. Wichtig ist in diesem Zusammenhang, dass der Editor, mit dem die Datei erstellt wird, das hier angegebene Encoding benutzt. Es folgt eine informelle Zeile, die nur den Namen der Quelltextdatei enthält – sie kann ggf. weggelassen werden. Anschließend werden die notwendigen Bibliotheken QtGui (für die grafischen Elemente) und QWebKit (zur Bereitstellung von Browser-Funktionalität) importiert:

class QuizMain(QtGui.QMainWindow):
  def __init__(self, parent=None):
  super(QuizMain, self).__init__(parent)

Im nächsten Schritt wird eine Klasse QuizMain definiert, die von der Klasse QtGui.QMainWindow erbt. Letztere wird in Qt benutzt, um das Hauptfenster einer Anwendung zu implementieren. Es folgt mit __init__() der Initializer der Klasse (das entspricht in Python weitgehend, aber nicht vollständig, dem Konstruktor in Sprachen wie C++ oder Java – der Unterschied spielt für unser Beispiel aber keine Rolle). Er empfängt einen zusätzlichen Default-Parameter parent. Das entsprechend übergebene Argument wird dabei zum Parent-Objekt der Instanz gemacht. Die Parent-Child-Beziehung zwischen Qt-Objekten dient dabei zwei Zwecken: Zum einen kann so festgestellt werden, was das so genannte Top-Level-Window ist. Das ist dasjenige Widget, bei dem das parent-Attribut den Wert „None“ hat. Normalerweise ist es das Hauptfenster der Applikation (in diesem Fall QuizMain), prinzipiell kann aber jedes Qt-Widget als Top-Level-Window benutzt werden. Ein solches Top-Level-Window ist nicht Bestandteil eines anderen Fensters, und die Verantwortung für das Schließen des Fensters liegt beim Programmierer. Andererseits definiert die Beziehung Object Ownership, d. h. welche Widgets in welchen anderen Widgets enthalten sind. In einem Dialog sind z. B. die Buttons Child-Widgets des Dialog-Widgets. Bei Widgets, die ein Parent-Widget haben, sorgt Qt selbst dafür, dass beim Schließen des Parent-Widgets auch alle enthaltenen Child-Widgets geschlossen werden. Zur Initialisierung muss anschließend immer der Initializer der Superklasse aufgerufen werden.

Als Nächstes wird das grundlegende Layout festgelegt. Wir benutzen an dieser Stelle ein vertikales Layout, d. h. wir teilen die Darstellung in einen oberen Bereich, in dem die HTML-Seite dargestellt wird, und einen unteren, in dem die Steuerelemente (Buttons) positioniert werden.

Wir instanziieren dann zunächst ein Objekt der Klasse QWebKit.QWebView für die Aufnahme des HTML-Inhalts, das wir dann zu dem vorher angelegten vertikalen Layout hinzufügen:

layout = QtGui.QVBoxLayout()
content_html = QWebKit.QWebView()
layout.addWidget(content_html)

Jetzt fügen wir die Steuerelemente hinzu. Wir könnten an dieser Stelle ein horizontales Layout erstellen, diesem unsere Buttons (für Start des Quiz und Beendigung) hinzufügen und dieses Layout wiederum dem vertikalen Layout hinzufügen. Wir benutzen stattdessen hier ein vorgefertigtes Widget, indem wir ein Objekt der Klasse QButtonBox erstellen. Eine QButtonBox ist ein Layout-Container, der mehrere Buttons aufnehmen kann. Eine Besonderheit ist hier, dass eine QButtonBox plattformabhängig Standardbuttons anders darstellt; unter Windows wird z. B. ein OK-Button etwa immer links dargestellt, unter Gnome immer rechts: button_box = QtGui.QDialogButtonBox(). Wir erstellen zwei Buttons für die Buttonbox:

  • Einen Button zum Starten des Quiz; für die Beschriftung übergeben wir den String &Start quiz.
  • Einen Button, um die Anwendung zu beenden; hier übergeben wir den String &End quiz.

Das Kaufmannsund „&“ hat hier eine besondere Bedeutung: Der Buchstabe hinter diesem Zeichen wird von Qt als Tastatur-Shortcut benutzt. Das heißt die Schaltfläche „Start“ kann auch über den Tastatur-Shortcut Alt+s betätigt werden (Listing 1). Die so erzeugte Buttonbox wird dann dem Layout hinzugefügt.

start_button = button_box.addButton("&Start quiz",
               QtGui.QDialogButtonBox.ActionRole)
end_button = button_box.addButton("&End quiz",
             QtGui.QDialogButtonBox.ActionRole)
layout.addWidget(button_box)

Jede Instanz eines QMainWindow benötigt zumindest ein zentrales Widget. Zu diesem Zweck erzeugen wir eine Instanz der Klasse QWidget und benutzen das definierte Layout als Layout für das Widget.
Für unser vorher definiertes QWebView fehlt noch der anzuzeigende Inhalt. Dazu wird die Datei quiz.html gelesen und als Inhalt des QWebViews gesetzt. Anschließend wird das QWidget zum zentralen Widget der Anwendung gemacht und das Widget in den Vollbildmodus gestellt (Listing 2). Damit wäre der Aufbau des Hauptfensters der Anwendung abgeschlossen.

qw = QtGui.QWidget()
qw.setLayout(layout)
f = open("quiz.html", "r") # fixme: use contextmanager here?
txt = f.read()
f.close()
content_html.setHtml(txt)
self.setCentralWidget(qw)
self.showFullScreen()

Damit die Applikation testweise schon einmal gestartet werden kann, wurden noch einige Zeilen hinzugefügt, die ausgeführt werden, wenn das Skript mit python startscreen.py gestartet wird. Dazu wird zunächst ein Objekt der Klasse QApplication erstellt, dem die beim Aufruf übergebenen Parameter (sys.argv) übergeben werden. Dann wird eine Instanz von QuizMain erzeugt, auf dieser show() aufgerufen, um das Hauptfenster anzuzeigen, und schließlich mit dem Aufruf der Methode exec_() auf der Applikationsinstanz die Kontrolle an die Eventverarbeitung von Qt übergeben (Listing 3).
Ein Beispiel für eine entsprechende HTML-Datei findet sich unter dem Namen quiz.html. Abbildung 2 zeigt den Startbildschirm.


Abb. 2: Der Startbildschirm

if "__main__" == __name__:
  import sys
  app = QtGui.QApplication(sys.argv)
  qm = QuizMain()
  qm.show()
    app.exec_()

[ header = Der Anmeldedialog ]

Ihr Name bitte! Der Anmeldedialog

Wenn ein neuer Teilnehmer am Quiz teilnehmen will, soll dieser sich zunächst anmelden. Wir gehen hier davon aus, dass die Teilnehmer Preise gewinnen können, die ihnen nachträglich zugeschickt werden sollen. Daher ist die Eingabe der Adressdaten hier zwingend erforderlich. Benötigt werden Eingabefelder für:

  • Name
  • Vorname
  • Straße und Hausnummer
  • Postleitzahl
  • Wohnort

Wir gehen auch hier wieder den Code zum Erzeugen des Dialogs im Einzelnen durch. Der Code wird in der Datei registerdialog.py abgelegt:

#!/usr/bin/env python
# -*- coding: latin-1 -*-
# file registerdialog.py

from PySide import QtGui, QtCore

Der Beginn der Datei ist bis auf den geänderten Dateinamen identisch zum Hauptfenster; es werden dieselben Bibliotheken importiert.
Es wird dann eine Klasse RegisterDialog definiert, die von QtGui.QDialog erbt. QDialog ist in Qt die Basisklasse zur Implementierung von Dialogen. Danach wird wieder wie bei der Implementierung des Hauptfensters der Initializer der Superklasse aufgerufen:

="brush: python">class RegisterDialog(QtGui.QDialog):
  def __init__(self, parent=None):
  super(RegisterDialog, self).__init__(parent)

Für das Layout der Elemente des Dialogs wird ein QGridLayout benutzt. Ein QGridLayout bietet eine komfortable Möglichkeit, Elemente in einem tabellenartigen Layout anzuordnen. Für unseren Anmeldedialog benutzen wir ein zweispaltiges Layout, bei dem in der jeweils linken Spalte eine Beschreibung und in der rechten Spalte ein Eingabefeld positioniert wird: grid = QtGui.QGridLayout().

Wir definieren jetzt zunächst die Beschriftung (über ein QLabel); das Kaufmannsund wird wieder benutzt, um ein Tastaturkürzel zu definieren. Das Textfeld für die Eingabe des Namens wird mit einem QLineEdit implementiert. Die entsprechende Variable wird als Member-Variable der Instanz definiert, um auf den Inhalt auch nach dem Durchlauf des Initializers zugreifen zu können. Über die Methode setBuddy() von QLabel wird festgelegt, dass bei Benutzung des für das QLabel definierten Tastaturkürzels die Eingabe in das angegebene Feld (QLineEdit) springt:

christian_name_label = QtGui.QLabel("&Christian name:")
self.christian_name_edit = QtGui.QLineEdit()
christian_name_label.setBuddy(self.christian_name_edit)

Für den Nachnamen, Straße, Postleitzahl und Stadt gehen wir analog vor und definieren jeweils eine Beschriftung (QLabel) und ein dazugehöriges Eingabefeld (QLineEdit) und verknüpfen die beiden über die setBuddy()-Methode.

Jetzt werden die definierten Elemente dem Layout hinzugefügt. Die Beschriftung wird dabei an Position (0, 0) platziert (Zeile, Spalte), das Eingabefeld an Position (0, 1). Dasselbe geschieht mit den restlichen definierten Dialogelementen (Listing 4).

grid.addWidget(christian_name_label, 0, 0)
grid.addWidget(self.christian_name_edit, 0, 1)
grid.addWidget(name_label, 1, 0)
grid.addWidget(self.name_edit, 1, 1)
grid.addWidget(street_label, 2, 0)
grid.addWidget(self.street_edit, 2, 1)
grid.addWidget(zip_label, 3, 0)
grid.addWidget(self.zip_edit, 3, 1)
grid.addWidget(town_label, 4, 0)
grid.addWidget(self.town_edit, 4, 1)

An dieser Stelle wird wieder eine Buttonbox erstellt, die in diesem Fall nur zwei Standardbuttons ( OK und Cancel) enthält:

button_box = QtGui.QDialogButtonBox(
  QtGui.QDialogButtonBox.Ok|
  QtGui.QDialogButtonBox.Cancel
  )

Wir fügen die ButtonBox jetzt dem GridLayout hinzu. Die ersten beiden Parameter (Zeile, Spalte) sind bekannt. Über die beiden zusätzlichen optionalen Parameter kann die Ausdehnung des Elements festgelegt werden, nämlich in vertikaler (Anzahl Zeilen) und horizontaler (Anzahl Spalten) Richtung. Hier wird also festgelegt, dass die ButtonBox nur eine Zeile belegt, sich aber über zwei Spalten ausdehnt, also über die gesamte Tabellenbreite. Das Layout des Dialogs wird jetzt auf das definierte GridLayout gesetzt, abschließend wird der Titel des Dialogfensters gesetzt:

grid.addWidget(button_box, 5, 0, 1, 2)
self.setLayout(grid)
self.setWindowTitle("The great computer language quiz - Register")

Um den Dialog testweise starten zu können, wird wieder, wie beim Startbildschirm, Code hinzugefügt. Es muss zunächst eine Applikationsinstanz erstellt werden, dann wird der Dialog instanziiert. Anschließend wird das Ergebnis der Dialogausführung über print auf der Standardausgabe ausgegeben (Abb. 3):

if "__main__" == __name__:
  import sys
  app = QtGui.QApplication(sys.argv)
  rd = RegisterDialog()
  print rd.exec_()

Abb. 3: Der Anmeldedialog

[ header = Zum Eingemachten ]

Zum Eingemachten – die Quizfragen

Der Anmeldedialog hätte sich problemlos auch mit einem GUI-Designer wie Qt-Designer erstellen lassen, da wir es hier mit einem statischen Layout zu tun haben – die Anzahl und Position der Elemente liegt von vornherein fest und ändert sich nicht. Anders ist es bei den Dialogen für die einzelnen Quizfragen, da es für verschiedene Fragen verschiedene Anzahlen von Antworten geben kann (Listing 5).
Der Beginn ist weitgehend identisch zum vorherigen Dialog, nur heißt unsere Dialogklasse jetzt QuestionDialog, und wir schreiben den Code in die Datei questiondialog.py. Dem Initializer wird lediglich ein weiterer Parameter question übergeben. Dabei handelt es sich um ein Objekt, das den Fragentext, eine Liste von Fragen sowie die Information über die richtige Antwort enthält.

#!/usr/bin/env python
# -*- coding: latin-1 -*-
# file questiondialog.py

from PySide import QtGui, QtCore

class QuestionDialog(QtGui.QDialog):
  def __init__(self, question, parent=None):
    super(QuestionDialog, self).__init__(parent)

Wir erzeugen zunächst ein vertikales Layout. Dann definieren wir ein QLabel, dem wir das Attribut question_text des question-Objekts übergeben:

layout = QtGui.QVBoxLayout()
question_label = QtGui.QLabel(question.question_text)
layout.addWidget(question_label)

Wir erstellen nun zunächst eine Instanz von QGroupBox und initialisieren sie mit dem Text „Answers“. Eine QGroupBox kann benutzt werden, um mehrere Radiobuttons so zu gruppieren, dass jeweils nur ein Radiobutton zur selben Zeit aktiviert werden kann. Der übergebene Text dient als Überschrift für die darunter positionierten Radiobuttons.
Wir erzeugen zunächst mit vbox ein weiteres vertikales Layout und erstellen ein Attribut self.radio_buttons als leere Liste, die die erstellten Radiobuttons aufnimmt. Dann iterieren wir über die in der Liste question.answers enthaltenen Antworttexte, erzeugen für jede Antwort eine Instanz der Klasse QRadioButton, der wir den Antworttext übergeben, und fügen diese Instanz dem Layout vbox hinzu; schließlich fügen wir den erzeugten Radiobutton zur Liste self.radio_buttons hinzu.
Nach Beendigung der Schleife benutzen wir die Methode setChecked von QRadioButton, um jeweils den ersten Radiobutton zu aktivieren. Dann setzen wir vbox als Layout der QGroupBox und fügen dem Layout für den Gesamtdialog die QGroupBox hinzu (Listing 6).

group_box = QtGui.QGroupBox("Answers")
vbox = QtGui.QVBoxLayout()
self.radio_buttons = []
for a in question.answers:
  rb = QtGui.QRadioButton(a)
  vbox.addWidget(rb)
  self.radio_buttons.append(rb)
self.radio_buttons[0].setChecked(True)
group_box.setLayout(vbox)
layout.addWidget(group_box)

Der restliche Code des Initializers ist analog zum Ende des Startbildschirms, nur dass hier für die Beschriftungen der beiden Buttons die Strings „&Next question“ und  „&End quiz“ benutzt werden (Listing 7).

button_box = QtGui.QDialogButtonBox(
         )
start_button = button_box.addButton("&Next question",
               QtGui.QDialogButtonBox.ActionRole)
end_button = button_box.addButton("&End quiz",
             QtGui.QDialogButtonBox.ActionRole)
layout.addWidget(button_box)
self.setLayout(layout)
self.setWindowTitle("The great programming language quiz")

Wir fügen auch hier wieder Code hinzu, mit dem wir unseren Dialog testen können. Dazu erstellen wir wieder eine Instanz von QApplication:

if "__main__" == __name__:
  import sys
  app = QtGui.QApplication(sys.argv)

Um den Dialog testen zu können, muss der Parameter question übergeben werden. Das entsprechende Objekt muss die Attribute question_text (den Text der Quizfrage) und answers (eine Liste möglicher Antworten) enthalten. Eine Möglichkeit an dieser Stelle wäre, eine entsprechende Klasse zu erstellen und eine Instanz dieser Klasse zu erzeugen. Stattdessen benutzen wir hier eine in dynamischen Sprachen oft benutzte Technik namens Duck Typing. Für den Test ist ausreichend, ein Objekt mit den benötigten Attributen zu erstellen.
Wir definieren an dieser Stelle daher nur eine Klasse Duck. Sie hat keine eigene Funktionalität und Attribute, sondern dient an dieser Stelle nur dem Zweck, anschließend question als Instanz dieser Klasse erzeugen zu können. Anschließend setzen wir die Attribute question_text, answers und correct (Listing 8).

class Duck(object):
  pass
question = Duck()
question.question_text = "This is the text of the question"
question.answers = [
  "The first answer",
  "The second answer",
  "The third and correct answer"
  ]
question.correct = 2

[ header = Einschub ]

Einschub: Duck Typing

In objektorientierten Sprachen mit statischer Typisierung, wie C++, wird die Semantik eines Objekts (d. h., was es darstellt und wie es verwendet werden kann), im Wesentlichen aus dem Typ des Objektes ermittelt. Das ist insofern sinnvoll, als dass bei statischer Typisierung Methoden und Attribute des Objekts festgelegt sind. Eine C++-Funktion func(), die als Argument eine Instanz einer bestimmten Klasse erwartet, kann nur verwendet werden, wenn arg eine Instanz von SomeClass ist – unabhängig davon, ob arg die Attribute name und desc bereitstellt:

int func(SomeClass arg) {
  cout << arg.name << endl;
  cout << arg.desc & << endl;
  ...
}

In diesem Beispiel ist der Typ des übergebenen Objektes aber eigentlich irrelevant – entscheidend ist hier nur, ob das Objekt über die entsprechenden Attribute verfügt, Voraussetzung dafür ist aber nicht unbedingt, dass arg eine Instanz von SomeClass ist.
Duck Typing bedeutet nun, dass die Semantik eines Objekts anhand seiner Methoden und Attribute ermittelt wird. In Python würde folgende Funktion vollkommen unabhängig davon funktionieren, von welchem Typ arg ist, sofern es nur die betreffenden Attribute bereitstellt:

def func(arg):
  print(arg.name)
  print(arg.desc)

Der Name „Duck Typing“ leitet sich dabei ab von dem Satz „If it walks like a duck and quaks like a duck, it is a duck“. Um entsprechend zu ermitteln, ob ein Objekt eine Ente repräsentieren soll, würde man in Python hier also keine Typprüfung durchführen:

if isinstance(arg, Duck):
  print("It is a duck!")

Stattdessen würde man auf Attribute/Methoden prüfen:

if hasattr(arg, "walk") and hasattr(arg, "quak"):
  print("It is a duck!")

Der entscheidende Punkt beim Duck Typing ist also, dass nicht der Typ, sondern die Schnittstelle eines Objektes definiert, was mit dem Objekt gemacht werden kann. Zu weiteren Details zu Duck Typing sei auch auf den entsprechenden Artikel der englischen Wikipedia verwiesen.
Zum Schluss erstellen wir eine Instanz der Dialogklasse, der wir das Objekt question übergeben, und führen den Dialog aus. Eine mögliche Ausprägung des Dialogs ist in Abbildung 4 zu sehen:

sd = QuestionDialog(question)
  sd.exec_()


Abb. 4: Der Quizfragendialog

[ header = Der Abbruchdialog ]

Wollen Sie wirklich schon gehen? Der Abbruchdialog

Wenn der Anwender im vorigen Dialog den Button mit der Aufschrift „End quiz“ betätigt, sollte das Quiz nicht sofort beendet werden – der Anwender könnte diesen Button ja irrtümlich betätigen, und alle schon eingegebenen Antworten wären in diesem Fall verloren. Vielmehr soll hier nochmal eine Nachfrage erfolgen, ob das Quiz wirklich beendet werden soll. Ist dieses nicht der Fall, soll der Anwender einfach wieder zum Fragedialog zurückkehren. Es handelt sich hier also lediglich um eine Nachfrage, bestehend aus einem Text mit zwei möglichen Antworten.
Listing 9 beginnt wieder mit dem inzwischen bekannten, weitgehend identischen Header, den wir an dieser Stelle hier auch nicht weiter diskutieren.
Es folgt die Definition einer Funktion runCancelDialog. Sie soll dazu dienen, einen Dialog mit der Nachfrage für den Abbruch anzuzeigen und die Auswahl des Anwenders zurückzugeben, wie aus dem Docstring der Funktion hervorgeht (Listing 9).

def runCancelDialog(parent=None):
  """
  Show cancel message box.
  Return True if button "Yes" is pressed, False otherwise
  """
  mb = QtGui.QMessageBox.warning(parent,
    "End quiz?",
    "If you quit the quiz now, all your answers "
    "will be lost. Do you really want to quit now?",
    QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
    )
  return True if mb == QtGui.QMessageBox.Yes else False

Letztlich handelt es sich bei dieser Abfrage auch um einen modalen Dialog. Es wäre also möglich gewesen, wie schon bei dem Anmelde- und Quizfragendialog, eine eigene Klasse zu definieren, die von QDialog erbt. In diesem Fall würde das aber bedeuten, mit Kanonen auf Spatzen zu schießen – das Mittel in Qt, um einen Dialog mit einer Abfrage und 1-n Buttons für die Antwort zu implementieren, ist eine QMessageBox. Wir benutzen hier die statische Methode warning von QMessageBox, um mit minimalem Aufwand einen Dialog anzuzeigen. Folgendes wird der Methode warning bei der Erstellung übergeben:

  • Parent: Ein QWidget oder None (in diesem Fall ist die MessageBox das Top-Level-Window der Hierarchie)
  • Fensterüberschrift
  • Text der Message
  • Ein Integer-Wert, der die zu benutzenden Buttons festlegt – die Werte der benötigten Buttons werden binär oder-verknüpft, um die anzuzeigenden Buttons zu definieren. An dieser Stelle werden die Buttons „Yes“ und „No“ angezeigt.
  • Der Rückgabewert der Methode entspricht dem jeweils gedrückten Button. Damit der aufrufende Code sich nicht mit der Umwandlung der Buttons in benutzbare Werte beschäftigen muss, geben wir hier einfach True zurück, wenn der Button „Yes“ gedrückt wurde, und in jedem anderen Fall (auch, wenn die Messagebox etwa mit Escape beendet wird) False.

Unser Testcode unterscheidet sich von dem bisher gezeigten insofern, als wir hier kein QWidget instanziieren, sondern lediglich die Funktion runCancelDialog aufrufen und ihren Rückgabewert ausgeben. Abbildung 5 zeigt den Abbruchdialog:

if "__main__" == __name__:
  import sys
  app = QtGui.QApplication(sys.argv)
  result = runCancelDialog(parent=None)
  print result


Abb. 5: Der Abbruchdialog

[ header = Die Ergebnisanzeige ]

Schatz, wie war ich? Die Ergebnisanzeige

Hat der Teilnehmer alle Quizfragen beantwortet, soll ihm ein Feedback zu seinen Antworten gegeben werden. Es könnte ihm einfach angezeigt werden wie viele Fragen er richtig beantwortet hat. Vermieden werden soll aber, dass Antworten weitergegeben werden können. Bekommt jemand als Ergebnis, dass er alle Antworten richtig beantwortet hat, kann er sein Wissen über die richtigen Antworten weitergeben. Deswegen soll lediglich eine Einstufung angezeigt werden und nicht das exakte Ergebnis. Wir legen dabei folgende Einstufung fest:

  • 0-24 % richtige Antworten: „Rookie“
  • 25-50 % richtige Antworten: „Average“
  • 50-75 % richtige Antworten: „Expert“
  • 75-100 % richtige Antworten: „Guru“

Wie im vorigen Dialog handelt es sich um einen sehr einfache Komponente: Es wird lediglich ein Ergebnis angezeigt. Sie ist insofern noch einfacher, als dass der Anwender hier auch keine Wahl zwischen verschiedenen Aktionen hat – er kann lediglich den Dialog mit Ok beenden (Listing 10).

from PySide import QtGui

def runResultDialog(percent, parent=None):
  """
  Show result depending on passed percentage
  of correct answers.
  """
assert (0 <= percent) and (percent <= 100)
percentInt = int(percent)

Wir beginnen wieder mit dem Import von QtGui und definieren eine Funktion runResultDialog, die das Ergebnis anzeigt und die als Parameter den Anteil der richtig beantworteten Fragen in Prozent (percent) sowie einen optionalen Parameter parent erhält.
Wir stellen durch die assert-Anweisung dann sicher, dass sich percent in einem gültigen Bereich befindet. Wir wandeln den übergebenen Wert in einen int-Typen um (percentInt). Was, wenn percent bereits eine Ganzzahl ist? Das ist kein Problem, da die Funktion int() bei Übergabe eines Integer-Wertes denselben unverändert zurückgibt.
Wir definieren eine Abbildung, die entsprechend der oben definierten Einstufung der erreichten Prozentzahl jeweils ein Paar (<Benennung>, <Beschreibung>) zuordnet. Anschließend bringen wir die Schlüssel des Dictionaries mit sorted() in eine aufsteigende Reihenfolge und iterieren in einer for-Schleife darüber.
Im Schleifenkörper prüfen wir jeweils, ob percentInt kleiner als der gerade aktuelle Schlüssel ist; ist das der Fall, „entpacken“ wir den Inhalt des Dictionary-Werts in die Variablen name und desc und beenden die Schleife. Da wir sichergestellt haben, dass der übergebene Wert zwischen 0 und 100 inklusive liegt, wird der Schleifenkörper in jedem Fall auf diese Weise verlassen. Wir bauen jetzt einen String, der die Ranking-Information darstellt, in den wir über printf-Ersetzung die Variablen name und desc einfügen. Eine Besonderheit ist hier, dass wir Rich-Text-Formatierung benutzen, indem wir den Wert von name fett darstellen, indem wir ihn mit dem HTML-Tag <b> (bold) einbetten. Anschließend zeigen wir die Messagebox mit der statischen Methode information an (Listing 11).

ranking2percent = {
  25: ("Rookie", "0-24 %"),
  50: ("Average", "25-49 %"),
  75: ("Expert", "50-74 %"),
  101: ("Guru", "75-100 %")
  }

for k in sorted(ranking2percent):
  if percentInt < k:
    name, desc = ranking2percent[k]
    break

rankStr = "You were ranked %s (%s)" % (name, desc)
  QtGui.QMessageBox.information(
    parent,
    "Thank you for partaking in this quiz!",
    rankStr
    )

Zum Abschluss gibt es dann wieder den obligatorischen Testcode. Der Tester kann einen Prozentwert über die Tastatur eingeben, woraufhin der Ergebnisdialog entsprechend parametrisiert gestartet wird. Ein mögliches Ergebnis ist in Abbildung 6 zu sehen:

if "__main__" == __name__:
  import sys
  app = QtGui.QApplication(sys.argv)
  percent = int(raw_input("Enter result (number)n"))
  runResultDialog(percent)


Abb. 6: Der Ergebnisdialog

Damit wollen wir den ersten Teil der Artikelserie abschließen. Sie kennen nun den Anwendungsfall und die Implementierung der einzelnen GUI-Komponenten mit Python und Qt. Im zweiten Teil werde ich Ihnen die Datenhaltung sowie die Integration der einzelnen Komponenten zu einer lauffähigen Anwendung zeigen.

Aufmacherbild: Python skin as background von Shutterstock / Urheberrecht: Teodora_D

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -