Skip to content

12 Grafische Benutzeroberflächen

Juergen Hansmann edited this page Apr 1, 2022 · 10 revisions

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.


Das Hauptfenster

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.

Widgets

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()

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

  1. Erstelle anhand des Beispiels oben das Gerüst für dein Hauptfenster
  2. Packe hintereinander vier Label-Widgets mit absteigender Schriftgrösse (z.B. 100, 80, 60, 40) hinein
  3. 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:
sehtest

Form vs. Funktionalität

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.

Objektorientierte Programmierung

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

  1. Kopiere dein Skript sehtest.py und speichere es unter sehtest_refresh.py.
  2. Erweitere das neue Skript nun durch einen Button, bei dessen Betätigung neue Zufallsbuchstaben angezeigt werden.
  3. Versuche Darstellung (im Konstruktor __init__) und Logik (z.B. in der Methode refresh) 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:
off

on

Events

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.

Sequenzen

# -*- 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.

Kontrollvariablen

Ü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

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 einfaches Zeichenprogramm mit GUI

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

  1. Erstelle eine GUI mit einem Canvas, das als Zeichenfläche dient.
  2. 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
  3. 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, oder None, 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.