Rechenaufgaben lösen

Das Kind möchte ein Programm zum Üben von Rechenaufgaben sehen. Nun gut. Hier ist eine Version in PyQt5.
Unsere Oberfläche soll so aussehen.
Wir wollen ein kleines Fenster, in dem eine billig generierte Rechenaufgabe angezeigt wird. Der Schüler soll die Antwort eingeben und den Knopf “Antworten” drücken. Danach wird angesagt, ob die Antwort korrekt war, oder ob sie falsch war. Wenn sie falsch war, wird auch die korrekte Antwort angezeigt.
In der Statuszeile und in den Fortschrittbalken wird ein laufender Score mitgeführt. Nach 10 Aufgaben, oder wenn man “Quit” drückt, wird das Programm beendet.
Das Userinterface gestalten
Wir brauchen Qt Designer, und erzeugen dort ein MainWindow.
MainWindow erzeugen.
Mit einem Rechtsklick auf die Menubar und “Remove Menubar” können wir die nicht verwendete Menubar entfernen. Die kaum erkennbare Statusbar unten im Fenster lassen wir stehen.
Wir sehen rechts die im Programm vorhandene Bedienelemente-Hierarchie im Qt Designer. Das automatisch erzeugte “Centralwidget” ist leer, und hat einen Fehler (es ist mit einem roten Kuller markiert): Es hat noch kein Layout. Das Layout ordnet die im Centralwidget vorhandenen Unterelemente an.
Das Centralwidget ist mit einem roten Kuller als “ohne Layout” markiert.
Wir ziehen einen QPushbutton in das Fenster, und drücken im gepunkteten Hintergrund die rechte Maustaste, wählen dann ein VerticalLayout aus.
Nachdem mindestens ein Bedienelement im Centralwidget vorhanden ist, kann man mit der rechten Maustaste im Layout-Submenü ganz unten ein Layout auswählen.
Nach dem Zuweisen eines Layouts verschwindet der rote Kuller und die Elemente im Centralwidget ordnen sich an.
Wir setzen oberhalb des Buttons ein weiteres Element ein: Eine groupBox. Auch diese hat ein Layoutproblem: Wir setzen einen weiteren Button ein und wählen ebenso durch Rechtsklick ins Leere der Groupbox ein HorizontalLayout aus.
Wir haben oberhalb des ersten Buttons eine Groupbox positioniert, und sie passt sich automatisch an das Fenster an - das ist das VerticalLayout vom Centralwidget bei der Arbeit. Legen wir in die Groupbox einen weiteren QPushbutton, können wir der Groupbox ebenfalls ein Layout zuweisen. Hier wollen wir ein HorizontalLayout verwenden.
Nachdem wir diese Grundstruktur haben, können wir die anderen fehlenden Elemente schnell nachziehen. Dabei ist wichtig, daß wir für jedes Element den passenden Namen festlegen, unter dem es später im Programm aufgerufen werden soll.
Jedes Element hat einen Namen - rechts kann man die Widgethierarchie sehen, und die Namen, die wir den Elementen zugewiesen haben.
Nachdem wir Elemente korrekt benannt haben, müssen wir noch die Beschriftungen der Knöpfe und anderen Elemente anpassen. Dazu reicht es, den Knopf zu doppelklicken und den Text zu ersetzen.
Weiterhin kann man in den Widget-Eigenschaften bei einigen Elementen die Schriftgrößen anpassen. Ich habe a
, +
, b
und =
sowie den Richtig oder falsch?
-Text von 8 auf 12 Punkt Schriftgröße angepasst.
Am Ende wird die Datei als “aufgaben.ui” abgespeichert. Ich habe sie direkt dem PyCharm Projekt hinzugefügt.
Die Qt Designer Ausgabe “aufgaben.ui” wird Bestandteil des PyCharm Projektes.
PyCharm Basisprojekt
Wir setzen ein Basisprogram mit Python 3.8 in PyCharm auf, und fügen die “aufgaben.ui” unserem Projekt hinzu. Unser Programm soll “aufgaben.py” heißen. Es wird PyQt5 verwenden, also schreiben wir das in unsere “requirements.txt”. PyCharm installiert uns das dann in unser venv.
Wenn die Requirements korrekt installiert sind, funktioniert das Importieren der QtWidgets in der Python Console einwandfrei und ohne Fehler.
Wir können die UI-Datei jetzt laden und ausführen, aber unser Programm wird dann noch nichts tun:
from PyQt5 import QtWidgets, uic
import sys
class Ui(QtWidgets.QMainWindow):
button_quit: QtWidgets.QPushButton
group_aufgabe: QtWidgets.QGroupBox
label_a: QtWidgets.QLabel
label_b: QtWidgets.QLabel
label_op: QtWidgets.QLabel
button_loesen: QtWidgets.QPushButton
feld_antwort: QtWidgets.QLineEdit
label_richtig: QtWidgets.QLabel
fortschritt_richtig: QtWidgets.QProgressBar
fortschritt_total: QtWidgets.QProgressBar
statusbar: QtWidgets.QStatusBar
def load_ui(self) -> None:
uic.loadUi("aufgaben.ui", self)
self.button_quit = self.findChild(QtWidgets.QPushButton, "button_quit")
self.group_aufgabe = self.findChild(QtWidgets.QGroupBox, "group_aufgabe")
self.label_a = self.findChild(QtWidgets.QLabel, "label_a")
self.label_b = self.findChild(QtWidgets.QLabel, "label_b")
self.label_op = self.findChild(QtWidgets.QLabel, "label_op")
self.button_loesen = self.findChild(QtWidgets.QPushButton, "button_loesen")
self.label_richtig = self.findChild(QtWidgets.QLabel, "label_richtig")
self.fortschritt_richtig = self.findChild(QtWidgets.QProgressBar, "fortschritt_richtig")
self.fortschritt_total = self.findChild(QtWidgets.QProgressBar, "fortschritt_total")
self.statusbar = self.findChild(QtWidgets.QStatusBar, "statusbar")
self.feld_antwort = self.findChild(QtWidgets.QLineEdit, "feld_antwort")
def __init__(self):
super(Ui, self).__init__()
self.load_ui()
self.show()
app = QtWidgets.QApplication(sys.argv)
window = Ui()
app.exec()
Das Programm importiert QtWidgets
und uic
aus dem PtQt5
Package. Es definiert eine Klasse Ui
als Unterklasse von QtWidgets.QMainWindow
. Wir definieren eine Methode load_ui()
, die die aufgaben.ui
-Datei lädt. Im Konstruktor initialisieren wir die Superklasse (das QMainWindow) und rufen dann load_ui()
auf. Mit .show()
werden die Bedienlemente dann auch sichtbar.
Das Hauptprogramm hat die typische Minimalform für eine Qt-Anwendung: Erzeuge ein QApplication
-Objekt, erzeuge unser QMainWindow
(eigentlich eine Instanz unserer von QMainWindow
abgeleiteten Klasse Ui
) und starte dann die Event-Loop mit app.exec()
.
In unserer Ui
Klasse definieren wir einen Haufen Slots für alle diejenigen Bedienelemente der .ui
-Datei, die wir direkt ansprechen wollen. In der load_ui()
-Methode durchsuchen wir die .ui
-Datei mit .findChild()
nach diesen Elementen und merken sie uns in diesen Slots.
Wir können gleich noch den Quit-Button aktivieren: Aus
self.button_quit = self.findChild(QtWidgets.QPushButton, "button_quit")
wird
self.button_quit = self.findChild(QtWidgets.QPushButton, "button_quit")
self.button_quit.clicked.connect(self.quit_pressed)
und wir fügen der Ui
-Klasse eine Methode quit_pressed()
hinzu:
def quit_pressed(self) -> None:
msg: QtWidgets.QMessageBox = QtWidgets.QMessageBox()
msg.setText("Auf Wiedersehen.")
msg.setWindowTitle("Auf Wiedersehen.")
msg.setStandardButtons(QtWidgets.QMessageBox.Ok)
msg.setDefaultButton(QtWidgets.QMessageBox.Ok)
msg.setIcon(QtWidgets.QMessageBox.Information)
msg.exec()
sys.exit(0)
Dies ist zugleich ein Beispiel für das Erzeugen von PyQt5-Elementen ohne QtDesigner: Wir hätten diesen Dialog auch mit dem Designer in eine .ui
-Datei speichern können, um sie dann mit uic
zu laden. Stattdessen erzeugen wir hier den Abschiedsdialog manuell und rufen ihn dann auf, bevor wir das Programm beenden.
Dieses Teilprogramm ist nun schon lauffähig und zeigt das funktionsfähige Programm mit User-Interface an. Es tut jedoch noch nichts.
Aufgaben generieren
Um Rechenaufgaben zu erzeugen und dann feststellen zu können, ob jemand sie richtig ausgerechnet hat, brauchen wir eine Klasse Aufgabe
.
from random import randint, choice
from typing import List
class Aufgabe:
op: str = ""
a: int = 0
b: int = 0
loesung: int = 0
allops: List[str]
def ausdenken(self) -> None:
self.op = choice(self.allops)
self.a = randint(1, 10)
self.b = randint(1, 10)
if self.a < self.b:
self.a, self.b = self.b, self.a
if self.op == "+":
self.loesung = self.a + self.b
if self.op == "-":
self.loesung = self.a - self.b
def pruefen(self, loesung: int) -> bool:
return self.loesung == loesung
def __init__(self) -> None:
self.allops = ["+", "-"]
self.ausdenken()
Die Aufgabe hat die Variablen a
, b
und op
, wobei a
und b
zwei Zahlen zwischen 1 und 10 sein sollen, und op
entweder +
oder -
sein soll. Um die Subtraktion einfacher zu machen soll a
die Größere der beiden Zahlen sein. In der Variablen loesung
wollen wir uns die korrekte Lösung der Aufgabe merken.
Wir definieren uns einen Konstruktor, in dem wir auch die Liste der zugelassenen Operatoren in allops
vorbelegen: Es ist eine Liste von Strings mit zwei Einträgen: Plus und Minus. Wir rufen von dort auch schon einmal die Methode ausdenken()
auf, die sich eine neue Aufgabe ausdenkt.
In ausdenken
bestimmen wir eine Operation (Plus oder Minus) durch zufällige Auswahl eines Elementes aus der Liste mit choice()
und bestimmen zwei zufällige ganze Zahlen aus dem gewünschen Bereich mit randint()
. Falls a
die kleinere der beiden Zahlen ist, vertauschen wir sie.
Danach wird die Variable loesung
korrekt belegt, in Abhängigkeit vom Operator op
.
Das Prädikat pruefen(loesung)
teilt uns mit, ob die vom Benutzer eingegebene Lösung korrekt ist.
Den Spielstand mitführen
Der Benutzer kann nun Aufgaben bekommen und diese lösen. Wir können auch schon feststellen, ob diese Lösung korrekt ist. Wir brauchen aber noch Logik, die die Aufgaben zählt, die korrekten Aufgaben zählt und die uns sagt, ob wir fertig sind.
Wir brauchen Score
:
class Score:
score: int = 0
counter: int = 0
target: int = 10
def correct(self) -> None:
self.counter += 1
self.score += 1
def incorrect(self) -> None:
self.counter += 1
def done(self) -> bool:
return self.counter >= self.target
def __init__(self):
self.score = 0
self.counter = 0
self.target = 10
Die Klasse zählt in score
die korrekten Lösungen mit, in counter
werden alle gelösten Aufgaben mitgezählt und in target
speichern wir, wie viele Aufgabenrunden das Spiel insgesamt dauert.
Die Methode correct()
wird aufgerufen, wenn der Benutzer eine richtige Antwort eingegeben hat: Wir zählen dann counter
und score
gemeinsam hoch. Bei incorrect()
wird dagegen nur der counter
weiter gezählt. Das Prädikat done()
sagt uns, ob das Spiel beendet werden kann, weil genug Aufgaben gelöst worden sind.
Aufgaben und Score verkabeln
Wir verändern jetzt den Konstruktor unserer Hauptklasse Ui
: Dort wollen wir auch einen Score und eine Aufgabe erzeugen. Und statt show()
direkt aufzurufen haben wir jetzt eine Methode alles_updaten()
, die die Texte der verschiedenen Bedienelemente verändert und dann erst show()
aufruft.
class Ui(QtWidgets.QMainWindow):
aufgabe: Aufgabe
score: Score
...
def __init__(self):
super(Ui, self).__init__()
self.score = Score()
self.aufgabe = Aufgabe()
self.load_ui()
self.alles_updaten()
Und alles_updaten()
sieht nun so aus:
def set_aufgabe(self) -> None:
self.label_a.setText(str(self.aufgabe.a))
self.label_op.setText(str(self.aufgabe.op))
self.label_b.setText(str(self.aufgabe.b))
title = f"Aufgabe {self.score.counter+1}/{self.score.target}"
self.group_aufgabe.setTitle(title)
self.fortschritt_richtig.setValue(self.score.score)
self.fortschritt_total.setValue(self.score.counter)
def set_statusbar(self) -> None:
status = f"Aufgabe {self.score.counter}/{self.score.target}, {self.score.score} richtig."
self.statusbar.showMessage(status)
def alles_updaten(self) -> None:
self.set_statusbar()
self.set_aufgabe()
self.show()
Wir aktualisieren also das statusbar
Element in set_statusbar()
und die Aufgabenanzeige in der Groupbox in set_aufgabe()
.
In set_statusbar()
erzeugen wir den gewünschten Text durch Auslesen des Score und rufen dann showMessage()
für die Statusbar auf.
In set_aufgabe()
verändern wir die Labeltexte von label_a
, label_op
und label_b
durch Aufruf ihrer setText()
-Methoden. Wir aktualisieren den Titel der Groupbox durch den Aufruf von setTitle()
in der Groupbox, und wir setzen die beiden Fortschrittsbalken so, daß sie die Werte aus dem Score korrekt übernehmen. Das macht die Methode setValue
der ProgressBar.
Jetzt haben wir ein Programm, das nicht nur die UI lädt und anzeigt, sondern auch schon eine Aufgabe ausdenkt und korrekt anzeigt.
Benutzereingabe und Reaktion
Wir müssen jetzt auf Benutzereingaben reagieren. Das passiert, sobald der Benutzer den Knopf “Antworten” betätigt. Wir brauchen also eine Methode auswerten()
in Ui
und müssen diese mit dem Knopf verbinden.
Wir wollen ausdrücklich nicht, daß beim Verlassen der Editbox etwas passiert - nur der Knopf soll Funktionen auslösen. Darum verkabeln wir feld_antwort
nicht direkt (wir könnten editingFinished()
verkabeln, wenn wir das wollten).
Die Methode:
def auswerten(self) -> None:
loesung: int
text: str
text = self.feld_antwort.text()
if text is None or text == "":
return
try:
loesung = int(text)
except ValueError:
return
if self.aufgabe.pruefen(loesung):
self.score.correct()
ergebnis = f"Deine Lösung: {loesung}. Das war richtig."
else:
self.score.incorrect()
ergebnis = f"Deine Lösung: {loesung}. Richtig wäre: {self.aufgabe.loesung}."
self.label_richtig.setText(ergebnis)
self.feld_antwort.setText("")
self.alles_updaten()
if self.score.done():
self.quit_pressed()
else:
self.aufgabe.ausdenken()
self.alles_updaten()
}
Wir lesen die Benutzeringabe aus dem Antwortfeld mit text()
aus. Sie kann leer sein, dann ignorieren wir sie.
Danach versuchen wir diese Eingabe in eine ganze Zahl umzuwandeln. Wenn das nicht gelingt, ignorieren wir die Eingabe ebenfalls.
Schließlich rufen wir pruefung()
in unserer Aufgabe-Klasse auf. Wenn die Antwort richtig war, erzeugen wir einen passenden Ausgabestring und zählen den Score als correct()
. War sie falsch, erzeugen wir den anderen Ausgabestring und zählen den Score als incorrect()
.
In jedem Fall setzen wir den Ausgabestring in das Label label_richtig
und löschen das Eingabefeld. Danach aktualisieren wir das Userinterface durch Aufruf von alles_updaten()
.
Falls der Benutzer ausreichend viele Aufgaben gelöst hat, beenden wir das Programm als wäre Quit gedrückt worden. Andernfalls denken wir uns eine neue Aufgabe aus und zeigen sie an.
Der Knopf “Antworten” muß nun ebenfalls verkabelt werden: In load_ui()
passen wir die Zeile
self.button_loesen = self.findChild(QtWidgets.QPushButton, "button_loesen")
also zu
self.button_loesen = self.findChild(QtWidgets.QPushButton, "button_loesen")
self.button_loesen.clicked.connect(self.auswerten)
an.
Fertig!
Schließlich können wir in quit_pressed()
noch die Dialognachricht anpassen, um einen Endscore anzuzeigen:
def quit_pressed(self) -> None:
msg: QtWidgets.QMessageBox = QtWidgets.QMessageBox()
msg.setText(
f"Von {self.score.counter} Aufgaben waren {self.score.score} richtig."
)
msg.setWindowTitle("Endergebnis")
msg.setStandardButtons(QtWidgets.QMessageBox.Ok)
msg.setDefaultButton(QtWidgets.QMessageBox.Ok)
msg.setIcon(QtWidgets.QMessageBox.Information)
msg.exec()
sys.exit(0)
Nach dem Starten kann der Benutzer jetzt Aufgaben sehen, eine Lösung eingeben und den Knopf “Antworten” drücken. Das Programm wertet die Eingabe aus, zählt die Ergebnisse und bewegt die Fortschrittbalken. Nach 10 Eingaben gibt es ein Endergebnis aus und beendet sich.
Der vollständige Quelltext ist auf Github zu finden .