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 mitx,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_allgilt für die ganze Anwendung,unbindentfernt eine Bindung.after(ms, func)ruftfuncspä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 wieuseStateplus kontrollierte Eingabe in React.- Observer:
var.trace_add("write", callback)ruftcallbackbei jeder Änderung auf. Die ältere Formvar.trace("w", callback)ist veraltet. Der Callback bekommt drei Argumente (mit Python ausgeführt: Variablenname, Index, Modus), du schreibst ihn also alsdef 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. Dieask...-Funktionen liefernTrue/False(askquestioneinen String). Weitere:filedialog,simpledialog,colorchooser.- Eingaben prüfen:
entry.get()ist ein String. Mitint()intry/except ValueErrorumwandeln 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 nEntrymitvalidate="key"undvalidatecommandprüft bei jeder Eingabe.- Canvas: Jede
create_...-Methode liefert eine Element-ID. Mit ihr rufst du spätermove,coords,itemconfigoderdeleteauf.
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
- Lange Berechnung im Handler friert das Fenster ein.
tracemit alter Schreibweise oder falscher Signatur: Die Formvar.trace("w", cb)ist veraltet,trace_add("write", cb)ist richtig, und der Callback bekommt drei Argumente (def cb(*args)).- Radiobuttons ohne gemeinsame Variable: Jeder Knopf bekommt seine eigene Auswahl, sie schließen sich nicht gegenseitig aus. Alle teilen eine
variable, jeder hat einen eigenenvalue. - Eingabe ungeprüft umwandeln:
int(entry.get())wirft bei Text einenValueErrorund der Handler bricht ab. Mittry/except ValueErrorfangen 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 denfelderals Attributen (zum Beispielx,y) und zusätzlichwidget(das Widget selbst). Rückgabe:True, wenn ein Handler lief, sonstFalse.MiniButton(command=None)erbt vonMiniWidget.klick(x=0, y=0)löst"<Button-1>"mitxundyaus und ruft danachcommand()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 FensterDer 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 (
gridmitstickyundcolumnspan, dazucolumnconfigure(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, TraceDer 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.