Im mobilen Zeitalter führt an grafischen Applikationen kein Weg mehr vorbei. Allerdings wird in klassischen, statisch typisierten Programmiersprachen wie C++ relativ viel Code benötigt, um ein GUI zu implementieren. Nicht so mit Python und Qt, wie diese zweiteilige Artikelserie zeigt. Im ersten Teil wurde der Anwendungsfall vorgestellt und die Implementierung der einzelnen GUI-Komponenten erläutert. In Teil 2 beschäftigen wir uns nun mit der Datenhaltung und der Integration der einzelnen Komponenten zu einer lauffähigen Anwendung.
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.
Im ersten Teil der Artikelserie haben wir den Anwendungsfall vorgestellt und beschrieben, wie die einzelnen für den Anwender sichtbaren Elemente zusammenarbeiten; auch wurden entsprechende Komponenten prototypisch implementiert. Im Folgenden soll nun gezeigt werden, wie Daten gehalten werden und wie man die einzelnen Komponenten dazu bewegt, so zu interagieren, dass eine ausführbare Anwendung entsteht.
Die klassische Programmierung des letzten Jahrtausends basierte – und das war auch in jedem entsprechenden Lehrbuch zu finden – auf dem EVA-Prinzip: Eingabe-Verarbeitung-Ausgabe. Das heißt, es wurden bei Programmstart eine Menge von Eingabedaten (ursprünglich: Lochkarten) übergeben. Daraufhin erfolgte die Verarbeitung in Form eines beliebig komplizierten Algorithmus, der dann Ausgabedaten – als Bildschirmausgabe oder in einer Datei, oder ursprünglich eben auch in Form von Lochkarten – erzeugte. Die Benutzerschnittstelle trat hier nicht besonders prominent in den Vordergrund. Der wesentliche Teil des Programms war die Verarbeitung. Es gab letztendlich einen Weg durch das Programm, der vom Anwender (damals meistens = Programmierer) nicht verändert werden konnte.
In einer GUI-basierten Anwendung stellt sich die Situation anders dar. Es gibt im Allgemeinen keine klar getrennten Phasen von Eingabe, Verarbeitung und Ausgabe. Im Mittelpunkt steht die Benutzerschnittstelle, die dem Anwender erlaubt, beliebige Aktionen anzustoßen. Eine GUI-Anwendung entspricht eher einer Endlosschleife in vereinfachtem Python-Pseudocode:
while True:
user_input = get_user_input()
handle_user_input(user_input)
Trotzdem gibt es auch bei GUI-basierten Anwendungen natürlich Eingangsdaten und Ausgangsdaten. In unserem Fall werden entsprechend die Eingangsdaten den Quizfragen und den Eingaben des Teilnehmers, die Ausgangsdaten den Ergebnissen, wie sie einerseits in der Ergebnisanzeige ausgegeben und andererseits persistiert werden.
Eingangsdaten sind in diesem Zusammenhang zunächst die Quizfragen selbst. Diese könnten natürlich im Quellcode selbst abgelegt werden. Das wäre aber nicht sehr flexibel. Eine bessere Lösung besteht darin, die Quizfragen in Form einer Datei bereitzustellen, die bei Programmstart eingelesen werden kann. Für das Format der Datei wären verschiedene Möglichkeiten denkbar. In diesem Fall entscheiden wir uns für ein XML-Format, und zwar aus folgenden Gründen:
XML ist mithilfe der betreffenden Bibliotheken leicht zu parsen.
Die Behandlung von Encodings und Sonderzeichen stellt mit XML kaum ein Problem dar.
XML ist ein Standardformat, für dessen Verarbeitung eine gute Werkzeugunterstützung gegeben ist (z. B. Editoren mit Syntax-Highlighting und -prüfung).
Eine mögliche Alternative wäre in diesem Zusammenhang z. B. JSON, das ähnliche Vorteile bietet wie XML, dabei aber kompakter ist. XML wurde hier der Vorzug gegeben wegen der aus Sicht des Autors einfacheren Definition des Schemas über eine DTD. Natürlich sind ausdrucksstärkere Formate für die Schemaspezifikation verfügbar, wie z. B. XSD (XML-Schema) oder RNC (Relax NG). Für unser Beispiel beschränken wir uns aber auf eine DTD, weil diese für unsere Zwecke ausdrucksstark genug und weit verbreitet ist:
<!ELEMENT quiz_questions (question+)>
<!ELEMENT question (question_text, answers)>
<!ELEMENT question_text (#PCDATA)>
<!ELEMENT answers (answer+)>
<!ELEMENT answer (#PCDATA)>
<!ATTLIST question correct CDATA #REQUIRED>
Die DTD definiert den Aufbau der Datei mit der Definition des Schemas für die Quizfragen. Das root-Element quiz_questions enthält mindestens ein Element question. Dieses besteht aus dem Text der Frage (question_text) sowie einem Element answers. Dieses wiederum enthält einzelne Elemente für die jeweiligen Antworten (answer). Das Element question erhält ein Attribut correct, das die Nummer der jeweils richtigen Antwort enthält. Die DTD wird in einer Datei quizquestions.dtd gespeichert. Ein Beispiel für eine entsprechende Datei zeigt Listing 1 (gekürzt).
Listing 1
<?xml version="1.0" ?>
<quiz_questions>
...
<question correct="1">
<question_text>The first language supporting OOP was</question_text>
<answers>
<answer>C++</answer>
<answer>Smalltalk</answer>
<answer>Java</answer>
</answers>
</question>
...
</quiz_questions>
Es handelt sich hier um eine Frage mit drei möglichen Antworten. Die korrekte Antwort (Smalltalk) ist im Attribut correct des Tags question beschrieben (1). Zu beachten ist hier, dass die Nummerierung hier der üblichen Array-Indizierung entspricht. Ein Wert von 1 entspricht also der zweiten Antwort. Es folgt jetzt der Code zum Einlesen der Quizfragen:
from lxml import etree as ET
Zunächst importieren wir einen Teil der lxml-Bibliothek. Diese stellt eine zu der zum Standard von Python gehörenden ElementTree-Bibliothek weitgehend schnittstellenkompatible Bibliothek dar, die neben allgemein verbesserter Performance auch Unterstützung für die Validierung von DTDs bietet (Listing 2).
Listing 2
class QuizQuestion(object):
"""
Represent a question in a quiz with possible answers and the solution.
"""
def __init__(self,
question_text,
answers,
correct):
self.question_text = question_text
self.answers = answers
self.correct = correct
Wir definieren jetzt eine Klasse QuizQuestion für die Repräsentation einer einzelnen Quizfrage. Im Initializer der Klasse werden die Frage als Text (question_text), eine Liste von Strings (answers) sowie der Index der korrekten Antwort (correct) übergeben. Die Parameter werden gleichnamigen Attributen des Objekts zugewiesen:
assert self.correct >= 0
assert self.correct < len(self.answers)
Über die assert-Anweisungen wird sichergestellt, dass der Wert von correct einen gültigen Index für den Zugriff auf die Liste answers bildet:
def readXmlQuestions(fName):
tree = ET.ElementTree(file=fName)
dtd_file = open("quizquestions.dtd", "rb")
dtd = ET.DTD(dtd_file)
Die Funktion readXmlQuestions bekommt als Parameter den Dateinamen der Datei mit den Quizfragen. Die komplette Datei wird daraufhin als XML-Baum tree eingelesen:
dtd_file = open("quizquestions.dtd", "rb")
dtd = ET.DTD(dtd_file)
Ein Dateideskriptor für die Datei mit der DTD wird geöffnet und als DTD eingelesen:
root = tree.getroot()
if not dtd.validate(root):
raise Exception(dtd.error_log.filter_from_errors()[0])
Es wird dann das Wurzelelement (root) des XML-Baums geholt und anschließend mit dtd.validate(root) die Gültigkeit der XML-Struktur geprüft. Im Fehlerfall wird eine Exception mit dem Text des Fehlers geworfen:
questions = []
for child in root:
question_text = child.find("question_text")
correct = int(child.attrib["correct"])
answers_elem = child.find("answers")
Mit questions wird eine leere Liste als Container für die zurückzugebenden Objekte definiert. Anschließend wird über die Kind-Elemente des Wurzelelements der XML-Datei iteriert. Aus dem jeweiligen Kind-Objekt wird der Text der Frage (question_text) als Inhalt des gleichnamigen Elements, die korrekte Antwort als Wert des Attributs correct sowie das Element für die Liste der Antworten (answers_elem) als Inhalt des Elements answers ermittelt:
answers = []
for answer in answers_elem:
answers.append(answer.text)
Es wird dann eine leere Liste answers definiert. Anschließend wird über die enthaltenen answer-Elemente iteriert und...