PCPP1 Block 3 (Teil b): tkinter, Events, Variablen und App-Klasse

Track Python · PCPP1 Block 3 (3.5 bis 3.8) · ca. 55 Min.

Worum es geht

GUI-Programmierung (graphical user interface) mit tkinter ist laut Quelle PCPP1 Block 3: 20 % der Prüfung, 8 Fragen (Stand der Quelle: 1. Oktober 2026, bitte vor der Buchung prüfen). Diese Lektion ist Teil b: bind und after (3.5), beobachtbare Variablen (3.6), Dialoge, Eingaben prüfen und Canvas (3.7) und die Anwendung als Klasse (3.8). Dazu baust du in zwei lokalen Aufgaben echte Fenster.

Was du aus Teil a brauchst: Du kennst Widgets, die drei Geometrie-Manager (pack, grid, place, nie gemischt im selben Container) und weißt, dass command=func eine Funktion ohne Klammern und ohne Argumente erwartet. Das steht in Teil a: Fenster, Layout und command.

Wichtig: tkinter läuft nicht im Browser. Die Codebeispiele zu echten Fenstern sind nicht ausführbar. Die Übung 1 ist eine Browser-Simulation, die Übungen 2 und 3 sind lokal (mit Skript-Selbsttest, der ohne Mausklicks prüft).

Nicht behandelt: Menüs und Scrollbars im Detail, filedialog, colorchooser, Canvas-Animationen.

Ablauf: JS/TS-Vergleich, vier kurze Schritte, Fallen, drei Übungen (eine im Browser, zwei lokal).

Von JS/TS her gedacht

Thema JS/TS (DOM, React) tkinter
Event-Handler mit Event el.addEventListener("click", (e) => ...) widget.bind("<Button-1>", handler), der Handler bekommt ein Event-Objekt
Zustand an UI binden useState und value={x} StringVar und textvariable=var
Timer setTimeout(fn, ms) root.after(ms, fn)
Anwendung als Komponente class App extends Component oder eine Funktionskomponente class App(tk.Tk)

Zustand an die Oberfläche binden, in beiden Welten:

// React: kontrollierte Eingabe
const [name, setName] = useState("");
<input value={name} onChange={(e) => setName(e.target.value)} />
# tkinter: die Variable ist der Zustand, das Widget hängt per textvariable daran
name = tk.StringVar()
tk.Entry(root, textvariable=name).pack()
name.trace_add("write", lambda *args: print("neu:", name.get()))

Konzept in kleinen Schritten

Schritt 1: bind, after und die Event-Loop (3.5, zweiter Teil)

command kennst du aus Teil a. Die zweite Möglichkeit, auf Ereignisse zu reagieren, ist bind. Der Handler bekommt dabei ein Event-Objekt:

def on_key(event):
    print(event.keysym, event.char)

def on_click(event):
    print(event.x, event.y)

root.bind("<Key>", on_key)
canvas = tk.Canvas(root, width=200, height=100, bg="white")
canvas.pack()
canvas.bind("<Button-1>", on_click)
canvas.create_rectangle(10, 10, 60, 40, fill="#ff8800")
  • command=func (Button, Scale u. a.): Der Callback bekommt keine Argumente.
  • widget.bind(sequenz, handler): Der Handler bekommt ein Event-Objekt mit x, y, widget, char, keysym.
  • Sequenzen: "<Button-1>" (linke Maustaste), "<Double-Button-1>", "<ButtonRelease-1>", "<Motion>", "<Key>", "<Return>", "<KeyPress-a>", "<Enter>", "<Leave>", "<FocusIn>", "<Configure>" (Größenänderung).
  • bind_all gilt für die ganze Anwendung, unbind entfernt eine Bindung.
  • after(ms, func) ruft func später auf, ohne die Event-Loop zu blockieren (Timer). protocol("WM_DELETE_WINDOW", handler) fängt das Schließen ab.
  • Blockierender Code im Handler friert die Oberfläche ein, weil die Event-Loop nicht weiterläuft. Lange Arbeit gehört in kleine Schritte mit after.

Schritt 2: Beobachtbare Variablen (3.6)

  • StringVar, IntVar, DoubleVar, BooleanVar: get() liest, set() schreibt. Verbundene Widgets (textvariable, variable) aktualisieren sich von selbst, ähnlich wie useState plus kontrollierte Eingabe in React.
  • Observer: var.trace_add("write", callback) ruft callback bei jeder Änderung auf. Die ältere Form var.trace("w", callback) ist veraltet. Der Callback bekommt drei Argumente (mit Python ausgeführt: Variablenname, Index, Modus), du schreibst ihn also als def cb(*args).
  • Radiobuttons teilen sich eine Variable, jeder hat einen eigenen value, der gewählte Wert landet in der Variablen.
modus = tk.StringVar(value="a")
tk.Radiobutton(root, text="A", variable=modus, value="a").pack()
tk.Radiobutton(root, text="B", variable=modus, value="b").pack()

def geaendert(*args):
    print("neu:", modus.get())

modus.trace_add("write", geaendert)

Schritt 3: Dialoge, Eingaben prüfen, Canvas (3.7)

  • messagebox: showinfo, showwarning, showerror, askyesno, askokcancel, askquestion. Die ask...-Funktionen liefern True/False (askquestion einen String). Weitere: filedialog, simpledialog, colorchooser.
  • Eingaben prüfen: entry.get() ist ein String. Mit int() in try/except ValueError umwandeln und den Fehler per Messagebox zeigen:
def lese_zahl():
    try:
        n = int(entry.get())
    except ValueError:
        messagebox.showerror("Fehler", "Bitte eine ganze Zahl eingeben")
        return None
    return n
  • Entry mit validate="key" und validatecommand prüft bei jeder Eingabe.
  • Canvas: Jede create_...-Methode liefert eine Element-ID. Mit ihr rufst du später move, coords, itemconfig oder delete auf.

Schritt 4: Anwendung als Klasse (3.8)

Für alles jenseits von Skripten bauen Profis die App als Klasse. Der Zustand liegt in self, Callbacks sind Methoden, und command=self.inc ist wieder eine Referenz ohne Klammern.

class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Counter")
        self.count = tk.IntVar(value=0)
        tk.Label(self, textvariable=self.count).pack()
        tk.Button(self, text="+1", command=self.inc).pack()

    def inc(self):
        self.count.set(self.count.get() + 1)

App().mainloop()

Falle: die Prüfungs- und Praxisklassiker

  1. Lange Berechnung im Handler friert das Fenster ein.
  2. trace mit alter Schreibweise oder falscher Signatur: Die Form var.trace("w", cb) ist veraltet, trace_add("write", cb) ist richtig, und der Callback bekommt drei Argumente (def cb(*args)).
  3. Radiobuttons ohne gemeinsame Variable: Jeder Knopf bekommt seine eigene Auswahl, sie schließen sich nicht gegenseitig aus. Alle teilen eine variable, jeder hat einen eigenen value.
  4. Eingabe ungeprüft umwandeln: int(entry.get()) wirft bei Text einen ValueError und der Handler bricht ab. Mit try/except ValueError fangen und per Messagebox melden.

Übungen

Übung 1: bind, unbind und command als Simulation

Du baust die Ereignislogik eines Widgets nach, ohne tkinter. Die Spielregeln für diese Übung:

  • MiniWidget.bind(sequenz, handler) merkt sich den Handler für die Sequenz (z. B. "<Button-1>"). Eine zweite Bindung derselben Sequenz ersetzt die erste (Vereinfachung für diese Übung).
  • MiniWidget.unbind(sequenz) entfernt die Bindung. Eine unbekannte Sequenz ist kein Fehler.
  • MiniWidget.ereignis(sequenz, **felder) löst ein Ereignis aus. Gibt es einen Handler, bekommt er ein Event-Objekt mit den felder als Attributen (zum Beispiel x, y) und zusätzlich widget (das Widget selbst). Rückgabe: True, wenn ein Handler lief, sonst False.
  • MiniButton(command=None) erbt von MiniWidget. klick(x=0, y=0) löst "<Button-1>" mit x und y aus und ruft danach command() auf, ohne Argumente, falls es eines gibt.
  • Zwei Widgets teilen nie ihre Bindungen.

Als Event-Objekt eignet sich SimpleNamespace:

Die Bindungen sind ein Dict von Sequenz auf Handler. Für das Event-Objekt: SimpleNamespace(**felder) plus ein weiteres Attribut. In MiniButton brauchst du einen eigenen __init__, und der darf den Konstruktor der Basisklasse nicht vergessen (sonst fehlt das Dict). Der Unterschied Event-Handler gegen command: Was bekommt welcher als Argument?

from types import SimpleNamespace

class MiniWidget:
    def __init__(self):
        self.bindungen = {}

    def bind(self, sequenz, handler):
        self.bindungen[sequenz] = handler

    def unbind(self, sequenz):
        self.bindungen.pop(sequenz, None)

    def ereignis(self, sequenz, **felder):
        handler = self.bindungen.get(sequenz)
        if handler is None:
            return False
        handler(SimpleNamespace(widget=self, **felder))
        return True

class MiniButton(MiniWidget):
    def __init__(self, command=None):
        super().__init__()
        self.command = command

    def klick(self, x=0, y=0):
        self.ereignis("<Button-1>", x=x, y=y)
        if self.command is not None:
            self.command()

MiniButton

Übung 2 (lokal): Zähler-App als Klasse mit echtem tkinter

Das läuft nicht im Browser, sondern auf deinem Rechner (Python 3.13 mit tkinter, das ist Teil der Standardinstallation). Die Aufgabe liegt in lernlabor/uebung/python/python_15_counter_app.py. Dort steht die Anleitung im Kopf der Datei. Du vervollständigst die Klasse App (3.8): IntVar, Label mit textvariable, drei Buttons, Untergrenze 0, bei 10 wird +1 mit state=tk.DISABLED gesperrt.

cd lernlabor && uv run python uebung/python/python_15_counter_app.py              # Fenster öffnen
cd lernlabor && uv run python uebung/python/python_15_counter_app.py --selbsttest # prüft per Skript, ohne Fenster

Der Selbsttest klickt mit invoke() auf die Buttons, ohne mainloop(). Er meldet, was fehlt (nie platziert, falscher Startwert, Obergrenze, Reset). Das Ergebnis Selbsttest bestanden heißt: Aufgabe erfüllt. Falls import tkinter bei dir fehlschlägt (Python ohne tkinter), installiere nichts auf Verdacht, sondern prüfe zuerst, welches Python uv benutzt.

Der Grenzwert gehört in die Methoden, die den Wert ändern, und danach muss der Zustand des +1-Buttons neu gesetzt werden. Überlege, ob du den Button dafür als Attribut speichern musst, und was command=self.inc von command=self.inc() unterscheidet.

class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Counter")
        self.count = tk.IntVar(value=0)
        tk.Label(self, textvariable=self.count).pack()
        self.plus = tk.Button(self, text="+1", command=self.inc)
        self.plus.pack()
        tk.Button(self, text="-1", command=self.dec).pack()
        tk.Button(self, text="Reset", command=self.reset).pack()

    def _sync(self):
        self.plus.config(state=tk.DISABLED if self.count.get() >= 10 else tk.NORMAL)

    def inc(self):
        if self.count.get() < 10:
            self.count.set(self.count.get() + 1)
        self._sync()

    def dec(self):
        self.count.set(max(0, self.count.get() - 1))
        self._sync()

    def reset(self):
        self.count.set(0)
        self._sync()

Übung 3 (lokal): Formular mit grid, Gewichten und trace_add

Auch lokal: lernlabor/uebung/python/python_15_formular_trace.py. Die genaue Aufgabe steht im Kopf der Datei. Du schreibst die Funktion baue_formular(root). Sie soll:

  • Label, Entry und Gruß-Label im Raster platzieren (grid mit sticky und columnspan, dazu columnconfigure(1, weight=1)),
  • einen Button anlegen, der gesperrt ist, solange das Namensfeld leer ist,
  • per trace_add("write", ...) beim Tippen den Gruß aktualisieren und den Button sperren oder freigeben.
cd lernlabor && uv run python uebung/python/python_15_formular_trace.py              # Formular ausprobieren
cd lernlabor && uv run python uebung/python/python_15_formular_trace.py --selbsttest # prüft Gitterpositionen, Gewichte, Trace

Der Callback bekommt von trace_add Argumente, die du nicht brauchst. Der Anfangszustand entsteht nicht von selbst, weil noch nichts geschrieben wurde. Und grid liefert None: Erst die Entry-Variable erzeugen, dann platzieren.

def baue_formular(root):
    name = tk.StringVar()
    gruss = tk.StringVar()
    tk.Label(root, text="Name:").grid(row=0, column=0, sticky="e", padx=5, pady=5)
    entry = tk.Entry(root, textvariable=name)
    entry.grid(row=0, column=1, sticky="ew", padx=5)
    tk.Label(root, textvariable=gruss).grid(row=1, column=0, columnspan=2)
    button = tk.Button(root, text="Senden", state=tk.DISABLED)
    button.grid(row=2, column=0, columnspan=2, pady=5)
    root.columnconfigure(1, weight=1)

    def geaendert(*args):
        wert = name.get().strip()
        gruss.set(f"Hallo {wert}" if wert else "")
        button.config(state=tk.NORMAL if wert else tk.DISABLED)

    name.trace_add("write", geaendert)
    return {"name": name, "gruss": gruss, "button": button, "entry": entry}

Merksatz und Prüfstein

Merksatz: bind ruft einen Handler mit einem Event-Objekt, command ohne Argumente. mainloop() ist die Event-Loop und blockiert, Zustand liegt in StringVar und Co. (trace_add("write", ...) meldet jede Änderung), und eine App gehört als Klasse mit Callbacks als Methoden (command=self.inc, ohne Klammern).

Prüfstein (offene Frage): Ein Eingabefeld soll bei jeder Änderung ein Label aktualisieren, und der Speichern-Button soll nur aktiv sein, wenn das Feld nicht leer ist. Welche zwei Mechanismen kommen dafür in Frage (bind oder trace_add), worin unterscheiden sich die Argumente ihrer Callbacks, und warum friert die Oberfläche ein, wenn dein Handler zehn Sekunden rechnet?

Zurück zu Teil a: Fenster, Layout und command.

Quelle: quellen/python-glossar-pcap-pcpp1.md, Abschnitt “PCPP1 Block 3: GUI-Programmierung mit tkinter (20 %, 8 Fragen)”, 3.5 bis 3.8 und “Typische Fallen in Block 3”. Blockgewicht und Fragenzahl laut Quelle (Stand 1. Oktober 2026, bitte prüfen). Nicht in der Quelle, aber mit Python 3.13 und Tk 8.6 ausgeführt (bitte prüfen, ob die PCPP1 sie fragt): die drei Argumente des trace_add-Callbacks, invoke(), winfo_manager(), grid_info() und columnconfigure(...)["weight"] in den lokalen Selbsttests.