Übungsblatt 1: GUIs und interaktive Grafik in PyQt5
Letzte Änderung : 15:50 Uhr, 18 April 2024 Abnahmetermin : 2. Mai 2024
Über dieses Übungsblatt Übersicht: In dieser Aufgabe geht es darum, dass Sie sich mit der Erstellung von GUIs in Python vertraut machen. Diese Praktikumsaufgabe ist anders als alle anderen — es kommt praktisch keine Mathematik vor. Statt dessen geht es darum, die Werkzeuge besser kennenzulernen, die wir im Verlauf des Praktikums verwenden werden: Die Sprache Python und einige wichtige Bibliotheken für numerische Mathematik (NumPy) und Grafikausgabe und graphische Benutzerschnittstellen (PyQt). Auch wenn es hier noch gar nicht um das Kernthema geht, ist dieses Aufgabenblatt im Gesamtkonzept des Praktikums trotzdem sehr wichtig. Im späteren Verlauf dieses Praktikums (in diesem und im nächsten Semester) werden wir immer wieder auf das hier gelernte zurückgreifen (die volle Stärke von Qt wird sich vor allem im zweiten Teil zeigen, wenn wir uns stärker mit Vektorgraphik beschäftigen). Übrigens: Python und dessen vielfältige Tools und Bibliotheken gut zu beherrschen, spart auch sonst im späteren Studium viel Arbeit (man holt die Zeit, dies zu lernen, sehr schnell wieder rein). Hintergrund: Kapitel 1 aus dem Skript zum Praktikum gibt eine Einführung in die hier behandelten Bibliotheken und bietet viele weitere Links zu Tutorials. Schauen Sie sich das alles am besten erstmal in Ruhe an, bevor Sie mit den Aufgaben loslegen. Bewertung: Wie auch in jedem weiteren Aufgabenblatt können Sie hier bis zu 100 Punkte erreichen. Am Ende der Veranstaltung „Angewandte Mathematik am Rechner 1“ müssen mindestens 50% der Punkte erreicht werden, um zu bestehen („aktive Teilnahme“). Der Abgabetermin dieses Aufgabenblattes ist der 1. Mai 2024, 15 Uhr (an dem Tag habe ich vorher sowieso keine Zeit). Der Abnahmetermin dieses Aufgabenblattes ist der 2. Mai 2024.
Aufgabe 1: „Snake“ mit Python und QT
In dieser Aufgabe wollen wir das GUI Toolkit Qt kennenlernen. Wir schauen uns hier an, wie wir interaktive Benutzerschnittstellen bauen können (immer gut, dies zu beherrschen — wir können dann später interaktive Experimente bauen). Außerdem, und noch wichtiger, sehen wir, wie wir einfache 2D Pixelbilder erzeugen und ausgeben können. Dies werden wir im weiteren immer wieder benutzen (im zweiten Teil auch mit „Vektorgraphik“ mehr dazu zu gegebener Zeit). Als Aufgabe haben wir uns folgendes vorgenommen: Wir möchten das bekannte Spiel Snake (auch bekannt als Nibbles) nachimplementieren.
Spielbeschreibung: Mithilfe der Pfeiltasten steuern Sie eine Schlange über den Bildschirm. Dabei bewegt sich die Schlange in die Richtung, die zuletzt durch die Pfeiltasten angegeben wurde. Ziel des Spieles ist es, zufällig erscheinende Früchte (dargestellt durch andersfarbige Punkte) auf dem Bildschirm zu fressen (indem die Schlange über/durch die Punkte bewegt wird). Was das Spiel schwerer macht, ist die Tatsache, dass nicht nur die Schlange mit jedem geschluckten Imbiss um eine Längeneinheit wächst und die Spielgeschwindigkeit mit der Länge der Schlange erhöht wird, sondern auch, dass die Schlange sich selbst nicht beißen darf! Varianten: die Schlange darf über den Rand hinaus und erscheint am gegenüberliegenden Ende des Spielfeldes, oder: sie darf den Spielfeldrand nicht berühren.
Aufgabe 1.1: Snake! (Arbeiten mit QImage/QPixmap)
[60 Punkte]
Im Folgenden implementieren wir das Spiel „Snake“ mithilfe von PyQt5. Als erstes programmieren wir das Spiel selbst. Hierfür brauchen wir einen minimalen Rahmen in QT. Tipp: Für Anfänger, die noch nie ein GUI programmiert haben, ist es vielleicht einfacher, mit Aufgabe 1.2 anzufangen und danach das eigentliche Spiel hinzuzufügen.
Lesen Sie Kapitel 1 im Skript über die Benutzung von PyQt5.
Erzeugen Sie ein leeres Fenster und fügen Sie ein Widget an, das Bilddaten anzeigen kann. Wir empfehlen, hier QLabel zu benutzen. Mit der Methode setPixmap() kann man diesem Widget ein Bild vom Typ QPixmap zuweisen, welches fortan angezeigt wird. Manchmal ist es einfacher, mit QImage zu arbeiten (QPixmap repräsentiert ein sogenanntes device-dependent bitmap, also ein Bild in genau dem Bitmapformat des benutzten Ausgabegerätes; bei QImage kann man sich das Bildformat frei aussuchen (unabhängig von Computer und Grafikkarte/Grafikmodus). Beide Formate lassen sich sehr einfach ineinander umwandeln.
Um das Spiel ablaufen zu lassen (animieren) müssen Sie einen QTimer erzeugen, der alle paar Millisekunden eine Aktualisierung auslöst: Das Spiel wird weitergeführt und ein neues Bild wird erzeugt und dem QLabel zugewiesen.
Nun geht es an das eigentliche Spiel. Es gibt hier viele Möglichkeiten; unsere Empfehlung ist, ein Bild vom Typ QImage/QPixmap mit der Auflösung (spielfeldhöhe,spielfeldbreite) zu erstellen, welches das Spielfeld darstellt und dieses pixelweise zu bemalen. Die Vorgehensweise dazu können Sie im PyQt-Kapitel des Skriptes nachschlagen. Jeder Pixel stellt dabei ein Feld des Spieles dar. Das Bild skalieren wir dann im Nachhinein (um nicht mit einer Lupe spielen zu müssen), z.B. mit der im Skript beschriebenen Methode scaledpixmap = pixmap.scaled(display.size(), Qt.KeepAspectRatio).
Es gibt auch eine ganze Reihe alternativer Lösungen1
Implementieren Sie nun das Spiel Snake. Auch hier gibt es viele Möglichkeiten. Weiter unten, am Ende dieses Aufgabenblattes, stehen einige nützliche Tipps.
Aufgabe 1.2: Das Konfigurationsmenü (Widget Tutorial)
[30 Punkte]
Nun fügen wir ein Menü hinzu, mit dem wir das Spiel konfigurieren und starten können. Falls Sie wenig Erfahrungen mit GUI Bibliotheken haben, mag es hilfreich sein, mit diesem Aufgabenteil anzufangen.
Implementieren Sie ein GUI-Hauptmenü in dem Sie alle spielinternen Einstellungen anpassen können wie etwa:
Spielername
Spielfeldgröße (z.B. \(16\times 16\) Felder)
Zoom Faktor (Wenn jedes Feld nur ein Pixel groß ist, macht das Spiel keinen Spaß.)
Spielgeschwindigkeit und Erhöhung derselben pro gegessener Frucht
Randbegrenzung [an/aus]
Erscheinungswahrscheinlichkeit für Früchte
... Sie müssen sich dabei nicht an die Vorlage halten: die Gestaltung der Benutzeroberfläche ist ganz überlassen (es geht ja darum, Qt zu lernen). Die Aufgabe gilt als bearbeitet, wenn es mindestens drei Optionen gibt, die man auch wirklich benutzen kann.
Tipp: Diese Internetseite bietet eine sehr schöne Übersicht häufig verwendeter Widgets mit Beispielcode, wie z.B. für die QSpinBox Widgets (für Eingabe von Zahlen), die in unserer Musterlösung oben für den Zoom Faktor benutzt wurde, QTab (Tab-Seiten), QList, QScrollBar, und viele mehr.
Aufgabe 1.3: Menüerweiterungen (mehr zu Qt Widgets)
[10 Punkte]
Erleichtern Sie die Verwendung Ihrer GUI, indem Sie uneindeutige Eingabefelder durch Tooltips mit kurzen Erklärungen versehen.
Fügen Sie einen Button hinzu, durch den das Spiel selbst gestartet wird. Beispielsweise kann sich dadurch ein neues Fenster öffnen. In diesem Fenster kann dann das Spiel angezeigt werden.
Erweitern Sie ihr Spielfeld-Fenster durch eine Menüleiste (Menubar) mit der Möglichkeit das Spiel zu pausieren und zu beenden, und einer StatusBar, die die aktuelle Punktzahl anzeigt.
Zeigen Sie zum Spielende (beispielsweise in einem QDialog) die erreichte Punktzahl an.
Erweitern Sie das Hauptfenster durch die Möglichkeit den Highscore der vergangenen Spiele anzuzeigen. Überlegen Sie sich, wie Sie dies implementieren können. Um zwei Möglichkeiten zu nennen: als separaten Tab im Hauptfenster, als Dialog beim Drücken eines zweiten Buttons.
Optionale Aufgaben: Menüerweiterungen (mehr zu Qt Widgets)
[keine Punkte, die Figuren visualisieren den Schwierigkeitsgrad]
Es erscheinen nicht nur Punkte zum essen, sondern auch Hindernisse.
Visualisieren Sie den Ort, an dem die Schlange die Frucht gegessen hat (z.B. durch eine andere Farbe, siehe Beispielbild).
Implementieren Sie Powerups/„extra Leben“.
Erweitern Sie Ihr Spiel zu einem zwei-Spieler Erlebnis. Zwei Möglichkeiten:
Zwei Schlangen (doppelt so viel Gewusel) wie im bekannten Spiel „Tron“.
ein Spieler übernimmt die Schlange, ein anderer darf per Maus Hindernisse in den Weg legen.
Tipps zum Programmieren des Spiels
Beginnen Sie damit, ein beliebiges Pixel bemalen zu können. Denken Sie daran, dass Sie, nachdem Sie alle Malvorgänge abgeschlossen haben, einmalig das QPixmap neu dem QLabel zuweisen müssen, damit die Änderungen sichtbar werden (label.setPixmap(pixmap)). Falls Sie QImages benutzen, müssen Sie diese erst in QPixmaps umwandeln und dann dem Label zuweisen.
Speichern Sie nun die aktuelle Position des Kopfes in einer Variablen. Beim Drücken der Pfeiltasten (siehe Skript unter KeyPressEvent) soll nun die Position des Kopfes um eine Pixel-Position verschoben werden. Färben Sie das Pixel der neuer Position (aufpassen - damit alles richtig funktioniert ist hier jetzt etwas extra Arbeit nötig...). Was Fällt Ihnen auf? Korrigieren Sie den Fehler!
Jetzt erst kommt die Zeit ins Spiel: Implementieren Sie nun das automatische Laufen des Punktes. Merken Sie sich nun in einer Variablen, in welche Richtung sich die Schlange aktuell bewegen soll. Und ändern Sie Ihren Code so ab, dass das Drücken der Pfeiltasten nur noch diese Variable ändert und selbst nicht mehr malt. Implementieren Sie nun einen Timer, der die aktuelle Richtung für einen Schritt anwendet. (Kopf verschieben + Pixel färben)
Bauen Sie nun die Früchte ein. Würfeln Sie zufällig eine ganze Zahl zwischen 0 und 10 bzw. Ihrer Wahrscheinlichkeitsvariablen (siehe Numpy-Abschnitt zu Zufallszahlen im Kapitel 1). Falls diese Zahl genau 0 ist, malen Sie eine Frucht an einer zufällig gezogenen Position (am besten mit einer anderen Farbe als die Schlange). Prüfen Sie vorher, ob die Position valide ist, d.h. die Frucht nicht auf die Schlange gesetzt wird.
Die Schlange soll nun länger werden dürfen. Wir merken uns die Positionen der Schlangensegmente in einer Liste. Nehmen wir an, die Schlange habe die Länge vier. Dann besteht die Liste aus vier zweielementigen Listen, die die aktuelle Position des jeweiligen Schlangenglieds angibt. Wie müssen Sie Ihren Code ändern, um die Schlange beim Überschreiten einer Frucht länger werden zu lassen und was muss angepasst werden, damit die Schlange sich insgesamt bewegt?}
Noch ein Tipp: Sie müssen nicht das ganze Spielfeld in jedem Animationsschritt neu malen. Für Snake reicht es aus, nur die geänderten Pixel zu überzeichnen.}
[1] Eine andere Möglichkeit ist z.B. einen eigenen Nachfahren von QWidget zu definieren, und diesen mit einem QPainter direkt bemalen zu lassen. Auch QPixmap lassen sich direkt mit einem QPainter bemalen (statt Pixel im QImage zu setzen). Je nach Art der Visualisierung ist der Unterschied zuerst einmal nur gering — in beiden Fällen können Sie die Pixel des Bildes manipulieren. Nun können Sie die Schlange zum Beispiel auch durch ein Polygon oder aneinanderliegende Rechtecke realisieren und so Ihrer Kreativität freien Lauf lassen. Für diese Aufgabe bedeutet dieser Ansatz jedoch mehr Arbeit.