-
Notifications
You must be signed in to change notification settings - Fork 1
12 Grafische Benutzeroberflächen
Wer bis hierhin durchgehalten und die vermittelten Inhalte mehr oder weniger begriffen hat, versteht nun die Grundlagen des Programmierens.
In diesem Kapitel geht es nun darum, den möglicherweise komplizierten Programmiercode hinter einer schönen Fassade (GUI = Graphical User interface) zu verbergen.
Zu Mensch-Maschinen-Interaktionen bestehen zahlreiche Philosophien, Konzepte, Frameworks (vorgefertigte Programmiermuster) etc., die von technischer Umsetzung über farbliche Gestaltung bis Ergonomie reichen.
Da wir hier in einem Programmier- und nicht in einem Designerkurs sind, werden wir uns eher in der Region der technischen Umsetzung bewegen. Eine ansprechende grafische Gestaltung kann danach immer noch erfolgen.
In diesem Kurs nutzen wird das Modul tkinter
, das bei jeder Pythoninstallation bereits standardmässig installiert ist.
Mit ein paar wenigen Zeilen Code steht dann auch schon das Grundgerüst für unser Programm.
# -*- coding: utf-8 -*-
# Modul importieren
from tkinter import *
# Bauplan definieren
class Gui:
def __init__(self):
# Hauptfenster
window = Tk()
# Titel
window.title("GUI")
# Fenster darstellen
window.mainloop()
# Programm ausführen
Gui()
Lasst euch durch die ominöse class
-Definition, das __init__
und dieses self
nicht beirren. Wir werden später sehen, wofür das gut sein kann.
So viel sei verraten, wir begeben uns jetzt allmählich in die Tiefen der Objektorientierten Programmierung.
Ins Hauptfenster können nun verschiedene so genannte Widgets (Buttons, Labels, Eingabefelder, Checkboxes etc.) gepackt werden.
# -*- coding: utf-8 -*-
from tkinter import *
class Widgets:
def __init__(self):
# Hauptfenster
window = Tk()
# Titel
window.title("Meine Widgets")
# Label
label = Label(master=window, width=50, text="Ich bin ein Label")
label.pack(padx=10, pady=10)
# Eingabe
eingabe = Entry(master=window, width=30)
eingabe.pack(padx=10, pady=10)
# Button
button = Button(master=window, width=10, text="Klick me!")
button.pack(padx=10, pady=10)
# Fenster fixieren
window.resizable(width=False, height=False)
# Fenster darstellen
window.mainloop()
# Programm ausführen
Widgets()
Im Beispiel oben haben wir die Widgets einfach nacheinander ins Hauptfenster gepackt (oder zuerst in ein frame
verschachtelt), dabei die Abstände zu umliegenden Widget angegeben (padx
, pady
) und die Ausrichtung über side
gesteuert.
Zudem haben wir die Grösse des Fensters fixiert.
Für einfache Anwendungen ist das ausreichend, doch sobald mehr Widgets in einem Fenster enthalten sind, stösst dieses dynamische Packing schnell an seine Grenzen.
Komplexere Layouts sind mit dem grid
-Pattern möglich, wir beschränken uns aber vorerst auf pack
.
Übung: Sehtest
sehtest.py
- Erstelle anhand des Beispiels oben das Gerüst für dein Hauptfenster
- Packe hintereinander vier Label-Widgets mit absteigender Schriftgrösse (z.B. 100, 80, 60, 40) hinein
- Setzte als Text für jedes der vier Labels drei zufällige Grossbuchstaben
Tipp:
- Das ganze Alphabet als Liste erhälst du z.B. so:
abc = string.ascii_uppercase
- Das Label-Widget eine Eigenschaft für die Schriftart, die über den Parameter
font
gesteuert werden kann:
font=("Arial", 100)
Beispiel:
Widgets dienen hauptsächlich der Benutzerinteraktion. Wir müssen also lernen, wie wir den Zustand und/oder die Eigenschaften eines Widgets je nach Interaktion abgreifen und/oder manipulieren können.
Wir beginnen also damit, Darstellung und Funktionalität zu kombinieren. Um die Übersicht zu behalten versuchen wir, im Code die Teile für Form von den Teilen für Funktionalität zu trennen.
Wie schon erwähnt, ist eine Klasse (class
) ein Bauplan, hier der Bauplan für unser Programm mit GUI.
Die erste Methode in einer Klasse heisst immer __init__
. Dies ist der so genannte Kontruktor, der die allerwichtigsten Angaben des Bauplans erhält.
Wenn eine neue Klasse initiiert wird (d.h. gemäss Bauplan ein neues Objekt erstellt wird), wird __init__
immer automatisch ausgeführt.
Für Programme mit GUI ist dies ziemlich praktisch, da einfach alles, was für die Darstellung (Hauptfenster, Widgets) nötig ist, in der __init__
-Methode untergebracht werden kann, und die eigentliche Programmlogik in separate Methoden ausgelagert werden kann.
# -*- coding: utf-8 -*-
import random
from tkinter import *
class FormVsFunctionality:
def __init__(self):
""" Hier steht nur Form.
"""
window = Tk()
window.title("Form vs. Funktionalität")
Button(master=window, text="Random", command=self.get_random_int).pack(padx=5, pady=5)
self.label = Label(master=window, width=40)
self.label.pack(padx=5, pady=5)
window.mainloop()
def get_random_int(self):
"""Hier steht Funktionalität.
"""
self.label.config(text=str(random.randint(1, 100)))
FormVsFunctionality()
Im Widget Button ist unter dem Parameter command
hinterlegt, welche Methode beim Klicken ausgeführt werden soll.
Der Button ist im Konstruktor __init__
definiert, die auszuführende Methode heisst get_random_int
.
Beide Methoden stehen innerhalb der Klasse FormVsFunctionality
auf derselben Hierarchiestufe (gleiche Einrückung).
Damit nun diese beiden Methoden miteinander kommunizieren können, müssen sie das via das gemeinsame übergeordnete Objekt machen. Also quasi über die Linie.
Das gemeinsame übergeordnete Objekt kann ganz einfach immer mit dem Schlüsselwort self
angesprochen werden.
Im Beispiel oben haben deshalb alle Elemente, die auch aus anderen Methoden angesprochen werden müssen, das Präfix self
.
Fieser kleiner Unterschied zwischen Funktion und Methode: Innerhalb einer Klasse spricht man von Methode, ausserhalb von Funktion.
Es ist aber niemand böse, wenn diese Begriffe gleichwertig verwendet werden.
Übung: Sehtest mit refresh
sehtest_refresh.py
- Kopiere dein Skript
sehtest.py
und speichere es untersehtest_refresh.py
.- Erweitere das neue Skript nun durch einen Button, bei dessen Betätigung neue Zufallsbuchstaben angezeigt werden.
- Versuche Darstellung (im Konstruktor
__init__
) und Logik (z.B. in der Methoderefresh
) zu trennen
Übung: Lichtschalter
lichtschalter.py
Programmiere eine kleine Applikation, die einen Lichtschalter simuliert, indem nach Betätigung eines Buttons z.B. Hintergrundfarbe oder Text hin- und her wechseln.
Tipp:
Den Zustand on/off wirst du vermutlich als Boolean in einer Variablen speichern wollen. Einen Boolean ins Gegenteil umwandeln geht so:
is_on = not is_on
Beispiel:
Ein Event wird üblicherweise durch den Benutzer ausgelöst, wenn er z.B. mit der Maus einen Button klickt, eine bestimmte Tastenkombination drückt o.ä.
Die einfachste Form solcher Events haben wir bereits kennengelernt, nämlich den Parameter command
bei den Buttons.
Events können aber noch viel mehr. Ein Event weiss z.B. von sich selbst, auf welchem Widget, zu welcher Zeit und über welches Eingabegerät (Taste, Tastenkombination, Links- oder Rechtsklick) er ausgelöst wurde.
Je nach dem kann das Programm darauf reagieren und eine entsprechende Aktion ausführen.
# -*- coding: utf-8 -*-
from tkinter import *
class Events:
def __init__(self):
window = Tk()
window.title("Events")
label = Label(master=window, text="Click me!", width=25)
label.pack(pady=20, padx=20)
label.bind(sequence="<Button-1>", func=self.leftclick)
label.bind(sequence="<Button-3>", func=self.rightclick)
window.mainloop()
def rightclick(self, event):
event.widget.config(text="Rechts")
def leftclick(self, event):
event.widget.config(text="Links")
Events()
Mit bind
wird festgelegt, auf welche Eingabe (sequence
) ein Widget überhaupt reagieren soll. Sobald diese Eingabe erfolgt (im Beispiel oben ein Mausklick), wird die unter func
angegebene Methode aufgerufen.
Der Methode wird dann der ganze Event event
mit all seinen Informationen als Parameter übergeben.
Der Parameter event
kann dann innerhalb der aufgerufenen Methode ausgewertet werden. Unter event.widget
findet sich z.B. das Widget, auf dem der Event ausgelöst wurde.
Nun kann die config
dieses Widgets (hier der Parameter text
) direkt manipuliert werden.
Über Kontrollvariablen können bestimmte Aktionen ausgelöst werden, ohne dass der Benutzer aktiv einen Event auslösen muss.
Verschiedene kompatible Widgets werden dazu über eine gemeinsame Kontrollvariable verknüpft und aktualisieren sich dynamisch gegenseitig.
# -*- coding: utf-8 -*-
from tkinter import *
class Controlvars:
def __init__(self):
# Hauptfenster
window = Tk()
window.title("Kontrollvariablen")
# Dynamische Variable im Hauptfenster registrieren
dynamischer_text = StringVar()
# Zwei Labels erstellen, deren Texte sich auf die dynamische Veriable beziehen
label1 = Label(master=window, width=20, textvariable=dynamischer_text)
label1.pack()
label2 = Label(master=window, width=20, textvariable=dynamischer_text)
label2.pack()
# Ein Eingabefeld erstellen, das den dynamischen Text ändert
eingabe = Entry(master=window, width=50, textvariable=dynamischer_text)
eingabe.pack(pady=10, padx=10)
# Hauptfenster anzeigen
window.mainloop()
# Programm ausführen
Controlvars()
Messageboxes sind kleine vordefinierte Fensterchen, die dem Benutzer ein Feedback auf seine getätigten Aktionen geben, und gegebenenfalls eine neue Aktion auslösen.
Es werden drei Arten unterschieden:
-
showinfo
: Enthält einen Titel, eine Message und den Button OK -
askyesno
: Enthält einen Titel, eine Message und die Buttons Ja und Nein -
askretrycancel
: Enthält einen Titel, eine Message und die Buttons Wiederholen und Abbrechen
askyesno
und askretrycancel
liefern einen Boolean zurück, der je nach geklicktem Button True
oder False
enthält.
Im Programm können wir nun entsprechend darauf reagieren.
import tkinter.messagebox
from tkinter import *
class Messages:
def __init__(self):
window = Tk()
window.title("Box")
btn_info = Button(master=window, text="Infobox", command=self.info)
btn_info.pack(pady=15, padx=15, side=LEFT)
btn_yesno = Button(master=window, text="Yes/No", command=self.yesno)
btn_yesno.pack(pady=15, padx=50, side=LEFT)
btn_retry = Button(master=window, text="Retry", command=self.retry)
btn_retry.pack(pady=15, padx=15, side=LEFT)
window.mainloop()
def info(self, message="Ich bin eine Infobox"):
tkinter.messagebox.showinfo("Info", message)
def yesno(self, message="Alles paletti?"):
result = tkinter.messagebox.askyesno("Ja - Nein", message)
if result:
self.info(message="Das freut micht!")
else:
self.info(message="Das ist aber schade")
def retry(self, message="Nochmals versuchen?"):
result = tkinter.messagebox.askretrycancel("Nochmals", message)
if result:
self.yesno(message="Besser jetzt?")
Messages()
Ein weiteres Widget, das wir uns bisher noch nicht angeschaut haben, ist das Canvas. Was das kann, sieht man z.B. hier: TkDocs. Man kann es verwenden, um z.B. darin Zeichnungen zu erstellen.
Übung: Simples Malprogramm
pyVinci.py
- Erstelle eine GUI mit einem Canvas, das als Zeichenfläche dient.
- Verwende Button-Widgets, um Dialog-Boxen zu öffnen, mit denen der Nutzer die Stiftfarbe wählen kann oder den Dateinamen festlegen kann, unter dem die Zeichnung gespeichert werden soll
- Verwende z.B. das Scale-Widget, um den Benutzer die Stiftgrösse wählen zu lassen.
Tipps:
- Mache Dich ein wenig mit der Dokumentation von Canvas widgets vertraut.
- Erinnerst Du Dich noch daran, wie wir bestimmte Events verwenden können, mit
bind()
, um damit bestimmte Funktionen auszuführen? Nützliche Events für ein Malprogramm sind beispielsweise<Button-1>
für die linken Maustaste, oder auch<B1-Motion>
. Letzteres ist sozusagen ein Dragging, also die Bewegung des Mauszeigers, während die linke Maustaste gedrückt ist.- Die Koordinaten von der Stelle, an dem ein Event (also z.B. ein Mausklick) ausgelöst wurde, erhält man mit
event.x
bzw.event.y
- Es gibt nützliche vordefinierte Dialoge, um beispielsweise eine Farbe auszuwählen (
from tkinter.colorchooser import askcolor
). Hier erhält man als Rückgabewert vom Dialog ein Tupel mit zwei Werten: Die gewählte Farbe als RGB oder in hexadezimaler Repräsentation. Wir können den zweiten Wert (Index=1!) verwenden, um die Linienfarbe zu ändern.- Auch um eine Datei zu speichern gibt es bereits einen fertigen Dialog (
from tkinter.filedialog import asksaveasfile
). Hier ist der Rückgabewert entweder der vom Nutzer gewählte Dateiname, oderNone
, falls auch "Abbrechen" geklickt wurde.- Ein weiteres nützliches widget ist vielleicht für dieses Programm
Scale()
. Das gibt einen Schieberegler, mit dem man beispielsweise die Stiftgrösse/Liniendicke wählen kann. Mit.get()
kann man den aktuell eingestellten Wert ablesen.
Finde mehr interaktive Beispiele zu den meisten Kapiteln als Python Notebook.