9. Graphische Benutzeroberfläche

In diesem Kapitel werden wir uns mit GUI-Programmen beschäftigen. GUI steht für Graphical User Interface. Es handelt sich also um Programme, welche nicht nur mit Texteingabe und -ausgabe arbeiten, sondern auch eine graphische Benutzeroberfläche haben. In anderen Worten einfach Programme, so wie wir uns das heutzutage gewohnt sind. Wir benutzen dazu das tkinter-Modul, welches in Python integriert ist.

Das Problem bei graphischen Oberflächen ist, dass wir das Programm nicht mehr einfach von oben nach unten durcharbeiten können. Wir haben ja keine Kontrolle darüber, auf welche Schaltfläche der Benutzer zu welchem Zeitpunkt gerade klickt. Darum benutzen wir für graphische Oberflächen sogenanntes ereignisbasiertes Programmieren. Dies bedeutet, dass das Programm nicht linear durchlaufen wird, sondern der Programmablauf durch bestimmte Ereignisse (bei uns werden das Klicks und Eingaben des Benutzers sein) beeinflusst wird.

Für unsere Programme heisst das, dass sie sich meistens in einer Endlosschleife befinden, welche nur dafür zuständig ist, darauf zu warten, dass der Benutzer etwas anklickt. Sobald dies geschieht (also ein Ereignis eintrifft) wird ein bestimmter Teil des Programms ausgeführt, welcher für die Aktion des Benutzers zuständig ist. Sobald dies geschehen ist, geht das Programm wieder in die Endlosschleife zurück und wartet auf weitere Aktionen. Diese Schleife heisst in tkinter mainloop(). Im Kapitel Einführung ins Programmieren bei der letzten Aufgabe haben wir bereits ein erstes GUI-Beispiel gesehen.

Im folgenden werden wir Stück für Stück ein GUI erstellen. Beginnen werden wir mit einem einfachen Fenster und dann kontinuierlich weitere Komponenten kennenlernen.

9.1. Fenster

Um eine graphische Benutzeroberfläche zu gestalten, brauchen wir als allererstes ein Fenster. Um ein solches zu erstellen benutzen wir, wie bereits erwähnt, dass tkinter-Modul von Python, welches uns das nötige Werkzeug zur Verfügung stellt. Dies geschieht wie gewohnt mit dem from-import Befehl:

from tkinter import *

Nun erstellen wir mit Tk() ein Fenster:

1
2
3
4
5
6
7
8
9
from tkinter import *

# Ein Fenster erstellen
fenster = Tk()
# Den Fenstertitle erstellen
fenster.title("Nur ein Fenster")

# In der Ereignisschleife auf Eingabe des Benutzers warten.
fenster.mainloop()

Lassen wir das Programm laufen, bekommen wir ein leeres Fenster wie z.B.

fenster

Das ist natürlich noch nicht so interessant, denn dieses Fenster wartet nur, bis es geschlossen wird. Aber nun können wir beginnen, dieses Fenster mit weiteren Komponenten zu bepacken.

Bemerkung

Je nach Betriebssystem, welches man benutzt, wird das Fenster anders aussehen. Das obige Programm wurde auf einem Rechner mit Ubuntu/Linux ausgeführt und hat somit ein Fenster mit obigem Design erstellt. Auf einem Windows oder OS X Rechner wird es anders aussehen. Dies wird auch bei den folgenden Themen so sein und bleiben.

9.2. Buttons und Labels

Nun fügen wir unserem Fenster einige neue Elemente hinzu. Wir beginnen hier mit zwei Buttons und einem Label. Ein Label bietet uns die Möglichkeit einen Text auf dem Fenster anzeigen zu lassen, welcher dem Benutzer z.B. Informationen oder Anweisungen mitteilen kann. Der erste Button soll beim Draufklicken den Infotext ändern und der zweite Button ist ein klassischer Beenden-Button, der das Programm beendet. Solche Elemente werden folgendermassen erstellt:

my_button = Button(fenster, option=value, ... )
my_label = Label(fenster, option=value, ... )

Das erste Argument definiert, welchem Fenster die jeweilige Komponente hinzugefügt wird. Mit den darauf folgenden Argumenten können wir unsere Komponente wie gewünscht einstellen, z.B. Grösse, Text, Farbe, Textfarbe. [1]

Achtung

Wir benutzen hier Python 3 und da gibt es manchmal kleine Unterschiede zu Python 2.7. Zum Beispiel:

# Python 3
from tkinter import *
# Python 2.7
from Tkinter import *

Wichtig für das Button-Objekt ist die command-Option. Ist diese nicht vorhanden, so passiert beim Draufklicken auf den Button gar nichts. Also definieren wir mit def eine Funktion, welche dem Button mit der command-Option als Funktion für das Klick-Ereignis übergeben wird. Mit der Funktion pack() können wir die einzelnen Komponenten dem Fenster übergeben. Unser Programm sieht dann folgendermassen aus:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from tkinter import *

# Die folgende Funktion soll ausgeführt werden, wenn
# der Benutzer den Button anklickt
def button_action():
    anweisungs_label.config(text="Ich wurde geändert!")


# Ein Fenster erstellen
fenster = Tk()
# Den Fenstertitle erstellen
fenster.title("Ich mache nun was.")

# Label und Buttons erstellen.
change_button = Button(fenster, text="Ändern", command=button_action)
exit_button = Button(fenster, text="Beenden", command=fenster.quit)

anweisungs_label = Label(fenster, text="Ich bin eine Anweisung:\n\
Klicke auf 'Ändern'.")

info_label = Label(fenster, text="Ich bin eine Info:\n\
Der Beenden Button schliesst das Programm.")

# Nun fügen wir die Komponenten unserem Fenster 
# in der gwünschten Reihenfolge hinzu.
anweisungs_label.pack()
change_button.pack()
info_label.pack()
exit_button.pack()

# In der Ereignisschleife auf Eingabe des Benutzers warten.
fenster.mainloop()

Beim Ausführen erhalten wir das erste Bild und nach dem Klick auf den Ändern-Button das zweite:

label1 label2

Bei einem Klick auf den Beenden-Button wird das Fenster und somit auch das Programm beendet.

9.3. Geometrie-Manager

Wir haben oben gesehen, dass mit der Funktion pack() die Komponenten dem Fenster hinzugefügt wurden. Doch wie sollen die einzelnen Komponenten auf dem Fenster angeordnet werden? Für solche Angelegenheiten bietet tkinter drei verschiedene Geometrie-Manager an:

  • pack
  • grid
  • place

Die drei Layout-Manager pack, grid und place ordnen die verschiedenen Komponenten auf dem Fenster an, jeder auf seine Weise. Sie sollten jedoch nie im gleichen Fenster gemischt werden. Wie sie genau funktionieren und was die Unterschiede zwischen diesen drei Layout-Managern sind, sehen wir gleich.

9.3.1. pack

Pack ist der am einfachsten zu benutzende der drei Geometrie-Manager. Statt dass man präzise erklären muss, wo eine Komponente auf dem Bildschirm erscheinen soll, werden sie relativ zueinander positioniert. Die Details werden von pack automatisch bestimmt. Man kann nur wenig selber bestimmen und ist deshalb in seinen Möglichkeiten im Vergleich zu den anderen Geometrie-Managern eingeschränkt. Hier eine Möglichkeit:

Option: Beschreibung:
side Bestimmt auf welcher Seite die Komponente gepackt werden soll: TOP (default), BOTTOM, LEFT, or RIGHT.

Auf das obige Beispiel angewandt, können wir nun unsere Buttons und Labels nebeneinander statt untereinander platzieren:

# Die Komponenten nebeneinander platzieren
anweisungs_label.pack(side=LEFT)
change_button.pack(side=LEFT)
info_label.pack(side=LEFT)
exit_button.pack(side=LEFT)

Die führt zu folgendem Layout:

pack

Möchte man die Komponenten aber nur etwas anders platzieren, wird es mit dem pack-Manager schon recht mühsam. Da ist der grid-Manager um einiges angenehmer.

9.3.2. grid

Der grid-Geometrie-Manager platziert die Komponenten in einer 2-dimensionalen Tabelle, die in Reihen und Spalten angeordnet ist. Die Position einer Komponente wird durch einen row und einen column-Wert bestimmt. Komponenten mit der selben column-Zahl und verschiedenen row-Zahlen werden übereinander angeordnet. Entsprechend werden Komponenten mit der selben row-Zahl und verschiedenen column-Zahlen in der selben Zeile platziert, d.h. sie stehen nebeneinander, also rechts und links voneinander.

Mit der grid-Methode übergibt man den row- und den column-Wert, wo die Komponente platziert werden soll. Die Grösse braucht nicht definiert zu werden, da der Grid-Manager automatisch die Ausdehnungen für die benutzten Komponenten berechnet.

Option: Beschreibung:
row Bestimmt in welcher Zeile man die Komponente setzen möchte.
column Bestimmt in welcher Spalte man die Komponente setzen möchte.
padx Diese Option kann man gebrauchen, wenn man in der Horizontalen noch zusätzlich etwas Abstand an die jeweilige Komponente einbauen möchte.
pady Analog wie padx, einfach in der Vertikalen

Mit dem Grid-Manager lässt sich nun einfach definieren, wo man die einzelnen Komponenten platzieren möchte.

# Label und entsprechender Button nebeneinander
# mit etwas Abstand zur anderen Label-Button-Gruppe
anweisungs_label.grid(row=0, column=0, pady = 20)
change_button.grid(row=0, column=1, pady = 20)
info_label.grid(row=1, column=0)
exit_button.grid(row=1, column=1)

Und dies sieht dann so aus:

grid

9.3.3. place

Der Place-Geometrie-Manager erlaubt das explizite Setzen der Position und der Grösse eines Fenster, entweder in absoluten Werten oder relativ zu anderen Komponenten.

Option: Beschreibung:
x, y Absolute Positionierung: Horizontale und vertikale Koordinate (in Pixel), in welcher die Komponente gesetzt wird.
relx, rely Relative Positionierung: Horizontale und vertikale Platzierung bezüglich des Fensters, in welches die Komponente gepackt wird. Der Wert muss zwischen 0.0 und 1.0 liegen.
height Höhe der Komponente bestimmen (in Pixel)
width Breite der Komponente bestimmen (in Pixel)

Dieser Manager ist im Verlgeich zu den anderen beiden der aufwendigste. Man sollte ihn nur dann benutzen, wenn es nicht anders geht. Hier ein Beispiel:

# Zuerst definieren wir die Grösse des Fensters
fenster.geometry("450x400")
# Wir benutzen die absoluten Koordinaten um die Komponenten zu
# setzen und definieren deren Grösse
anweisungs_label.place(x = 0, y = 0, width=200, height=150)
change_button.place(x = 220, y = 0, width=200, height=150)
info_label.place(x = 100, y = 160, width=300, height=100)
exit_button.place(x = 100, y = 260, width=300, height=100)

Ausgeführt sieht es dann folgendermassen aus:

place

Natürlich kann man noch viel mehr machen, doch das würde den Rahmen dieses Tutorials sprengen. Im Internet finden sich zu allen drei Geometrie-Managern noch viele weitere interessante Beispiele und Einstellungsmöglichkeiten.

9.4. Eingabefeld

Bis jetzt hatte der Benutzer nur die Möglichkeit, über Button-Klicks mit dem Programm zu interagieren. Nun werden wir sehen, wie der Benutzer über ein Feld eine Eingabe machen kann, welche das Programm danach verarbeiten kann. Die Syntax, um ein solches Eingabefeld zu erstellen, sieht folgendermassen aus:

eingabefeld = Entry(fenster, option, ... )

Wir sehen nun gleich ein Programm, welches im Fenster auf die Eingabe des Benutzers wartet und danach eine entsprechende Ausgabe generiert:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
from tkinter import *

# Die folgende Funktion soll ausgeführt werden, wenn
# der Benutzer den Button Klick me anklickt
def button_action():
    entry_text = eingabefeld.get()
    if (entry_text == ""):
        welcome_label.config(text="Gib zuerst einen Namen ein.")
    else:
        entry_text = "Welcome " + entry_text + "!" 
        welcome_label.config(text=entry_text)

fenster = Tk()
fenster.title("Ich warte auf eine Eingabe von dir.")

# Anweisungs-Label
my_label = Label(fenster, text="Gib deinen Namen ein: ")

# In diesem Label wird nach dem Klick auf den Button der Benutzer
# mit seinem eingegebenen Namen begrüsst.
welcome_label = Label(fenster)

# Hier kann der Benutzer eine Eingabe machen
eingabefeld = Entry(fenster, bd=5, width=40)

welcom_button = Button(fenster, text="Klick me", command=button_action)
exit_button = Button(fenster, text="Beenden", command=fenster.quit)


# Nun fügen wir die Komponenten unserem Fenster hinzu
my_label.grid(row = 0, column = 0)
eingabefeld.grid(row = 0, column = 1)
welcom_button.grid(row = 1, column = 0)
exit_button.grid(row = 1, column = 1)
welcome_label.grid(row = 2, column = 0, columnspan = 2)

mainloop()

Wird das Programm ausgeführt, so bekommen wir folgendes Fenster:

entry

In Zeile 7 im obigen Programm wird überprüft, ob der Benutzer etwas in das Feld geschrieben hat oder nicht. Dementsprechend sieht dann die Ausgabe im Fenster nach dem Klick auf den Klick me-Button jeweils anders aus:

Ohne Eingabe: entry_leer
Mit Eingabe: entry_name