PCPP1 Block 3 (Teil a): tkinter, Fenster, Layout und command
Track Python · PCPP1 Block 3 (3.1 bis 3.5, erster Teil) · ca. 50 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). Für dich als Frontend-Entwickler ist die Denkweise vertraut: Widgets sind Komponenten, Events sind Event-Handler, mainloop() ist die Event-Loop. Neu sind die Syntax, die drei Geometrie-Manager (geometry managers) und eine Handvoll Fallen, die in Prüfungsfragen immer wiederkehren.
Block 3 ist auf zwei Lektionen verteilt. Diese hier (Teil a) behandelt die Begriffe, das Fenster, die Widgets, die Geometrie-Manager und command mit seiner Klammer-Falle (3.1 bis 3.4 und den ersten Teil von 3.5). Teil b folgt mit bind, after, beobachtbaren Variablen, Dialogen und der App als Klasse, dazu den zwei lokalen Aufgaben mit echten Fenstern.
Wichtig: tkinter läuft nicht im Browser (Pyodide hat kein Fenstersystem). Darum sind die Codebeispiele zu echten Fenstern hier nicht ausführbar (einfache Codeblöcke ohne Ausführen-Knopf). Du übst die Denkweise trotzdem im Browser: Callback-Logik als reine Python-Simulation, Fallen per Code-Analyse mit dem Modul ast. Alle drei Übungen dieses Teils laufen im Browser.
Nicht behandelt: Menüs und Scrollbars im Detail, filedialog, colorchooser, PhotoImage-Details (nur die Falle), Canvas-Animationen.
Ablauf: JS/TS-Vergleich, fünf kurze Schritte, Fallen, drei Übungen.
Von JS/TS her gedacht
| Thema | JS/TS (DOM, React) | tkinter |
|---|---|---|
| Komponente | <button>, <input> |
Button, Entry (Widget) |
| Event-Handler | el.addEventListener("click", fn) |
command=fn (Button) oder widget.bind("<Button-1>", fn) |
| Handler-Argument | immer ein event |
command: keines. bind: ein Event-Objekt |
| Event-Loop | läuft im Browser von selbst | root.mainloop(), danach läuft Code erst nach dem Schließen |
| Layout | CSS Flexbox und Grid | pack() (ähnlich Flex-Reihe), grid() (Tabelle), place() (absolut) |
| Elternelement | parent.appendChild(el) |
tk.Label(parent, ...): erstes Argument ist der Parent |
Dieselbe Idee, ein Klick-Handler mit Argument, in beiden Sprachen:
// JS: addEventListener erwartet eine Funktion. Ein Aufruf liefert undefined.
btn.addEventListener("click", () => speichern(1)); // richtig
btn.addEventListener("click", speichern(1)); // Fehler: speichert sofort# Python/tkinter: genau dieselbe Falle
tk.Button(root, text="Speichern", command=lambda: speichern(1)) # richtig
tk.Button(root, text="Speichern", command=speichern(1)) # ruft sofort aufKonzept in kleinen Schritten
Schritt 1: Begriffe (3.1)
- Klassische Programmierung: Das Programm bestimmt den Ablauf. Ereignisgesteuerte Programmierung (event-driven programming): Das Programm wartet auf Ereignisse (events) und ruft passende Handler (callbacks) auf.
- Widget (auch Control): Bedienelement wie Fenster, Button, Label, Eingabefeld, Rahmen, Zeichenfläche. Toolkit: Bibliothek für GUIs, hier
tkinter. - Event Loop:
mainloop()wartet auf Ereignisse und verteilt sie. Alles danach im Code läuft erst, wenn das Fenster geschlossen ist.
Schritt 2: Ein Fenster aufbauen (3.2)
import tkinter as tk
from tkinter import messagebox
root = tk.Tk() # Hauptfenster, es darf nur eines geben
root.title("Demo")
root.geometry("300x150") # Breite x Höhe
name = tk.StringVar()
tk.Label(root, text="Name:").grid(row=0, column=0, padx=5, pady=5)
tk.Entry(root, textvariable=name).grid(row=0, column=1)
def greet():
value = name.get().strip()
if not value:
messagebox.showerror("Fehler", "Name fehlt")
return
messagebox.showinfo("Hallo", f"Hallo {value}")
tk.Button(root, text="OK", command=greet).grid(row=1, column=0, columnspan=2)
tk.Button(root, text="Ende", command=root.destroy).grid(row=2, column=0, columnspan=2)
root.mainloop()Tk()ist das Hauptfenster,Toplevel(root)erzeugt weitere Fenster. ZweiTk()-Instanzen sind eine Falle.destroy()schließt das Fenster und beendet die Anwendung.quit()beendet nur die Event-Loop.- Jedes Widget bekommt als erstes Argument seinen Parent (
rootoder einFrame).FrameundLabelFramegruppieren Widgets.
Schritt 3: Widgets und Optionen (3.3)
Wichtige Widgets laut Quelle: Label, Button, Entry (einzeilig), Text (mehrzeilig), Checkbutton, Radiobutton, Listbox, Scale, Spinbox, Canvas, Frame, LabelFrame, Menu, Scrollbar.
Zwei Dinge für die Prüfung:
- Optionen setzt du im Konstruktor (
Button(root, text="Ok")), nachträglich mitconfig(text="Neu")oderwidget["text"] = "Neu", und liest sie mitcget("text")oderwidget["text"]. Zustand:state=tk.NORMALodertk.DISABLED. Entry:get()liefert immer einen String, auch bei"42".insert(0, s)fügt ein,delete(0, tk.END)leert. BeimText-Widget heißen die Indizes"1.0"(Zeile.Spalte) statt0.
Farben als Name ("red") oder "#RRGGBB". Aus RGB-Werten wird der String mit f"#{r:02x}{g:02x}{b:02x}", zum Beispiel ergibt (255, 136, 0) den Wert #ff8800 (mit Python ausgeführt).
Bilder: PhotoImage braucht eine dauerhafte Referenz (Variable oder Attribut). Sonst räumt der Garbage Collector das Bild weg und es erscheint nicht.
Schritt 4: Geometrie-Manager (3.4)
Ein Widget wird erst sichtbar, wenn ein Geometrie-Manager es platziert. Es gibt drei:
| Manager | Prinzip | Wichtige Parameter |
|---|---|---|
pack() |
Widgets der Reihe nach in den Parent packen | side, fill, expand, padx, pady, anchor |
grid() |
Tabelle aus Zeilen und Spalten | row, column, rowspan, columnspan, sticky, padx, pady |
place() |
feste Pixel oder Anteile | x, y, relx, rely, width, height, anchor |
# pack: fill streckt, expand nutzt freien Platz
tk.Entry(root).pack(fill=tk.X)
# grid: sticky="nsew" füllt die Zelle; weight lässt Spalte/Zeile mitwachsen
tk.Label(root, text="A").grid(row=0, column=0, sticky="nsew")
root.columnconfigure(0, weight=1)
root.rowconfigure(0, weight=1)
# place: Pixel relativ zum Parent, Ursprung oben links
tk.Label(root, text="Mitte").place(relx=0.5, rely=0.5, anchor="center")Die wichtigste Regel: In einem Container nie pack und grid mischen. Das gibt einen Fehler (mit Python 3.13 und Tk 8.6 ausgeführt: TclError: cannot use geometry manager pack inside . which already has slaves managed by grid). In verschiedenen Containern ist es erlaubt, zum Beispiel pack für root und grid innerhalb eines Frame.
Ausblenden: pack_forget(), grid_forget(), place_forget(), und grid_remove() (merkt sich die Position).
Schritt 5: command und die Klammer-Falle (3.5, erster Teil)
Ein Button reagiert auf einen Klick über command=func. Zwei Regeln: Du übergibst die Funktion selbst (ohne Klammern), und der Callback bekommt keine Argumente. Die zweite Möglichkeit, auf Ereignisse zu reagieren, bind mit einem Event-Objekt, kommt in Teil b.
Die command-Falle kannst du im Browser nachbauen, ohne Fenster. Diese Mini-Klasse tut nur, was Button hier tut: Sie merkt sich eine Funktion und ruft sie beim Klick ohne Argumente auf.
Ausgabe: angelegt, hallo (beim Anlegen von falsch, nicht beim Klick), None, hallo (der echte Klick von richtig). Der Klick auf falsch bewirkt nichts.
Eine zweite Variante der Falle kennst du aus Lektion 01: Lambdas in einer Schleife teilen sich die Variable.
Falle: die Prüfungs- und Praxisklassiker
- Nie platziert: Widget erzeugt, aber kein
pack,gridoderplace. Es erscheint nichts. packundgridim selben Container gemischt: Fehler.command=func()stattcommand=func: Die Funktion läuft sofort, der Button bekommt deren Rückgabewert (meistNone).b = tk.Button(...).pack():pack()gibtNonezurück,bist danachNone. Erst erzeugen, dann platzieren.entry.get()ist ein String, auch bei Zahlen. Erst mitint()umwandeln.PhotoImageohne Referenz: Das Bild verschwindet (Garbage Collector).- Zwei
Tk()-Instanzen statt einerTk()plusToplevel. destroy()undquit()verwechselt:quit()beendet nur die Event-Loop, das Fenster bleibt als Objekt bestehen.
Übungen
Übung 1: Wann laufen die Callbacks wirklich?
Diese Simulation nutzt die Button-Klasse aus Schritt 5 (ohne tkinter). Der Code steht fest, du sagst voraus, was in log landet:
log = []
def speichern():
log.append("gespeichert")
return None
def melde(x):
log.append("melde " + x)
a = Button("A", command=speichern())
b = Button("B", command=speichern)
c = Button("C", command=lambda: melde("c"))
d = Button("D", command=melde("d"))
for knopf in (a, b, b, c, d):
knopf.invoke()Trage den Inhalt von log als Liste ein (fünf Einträge).
Trenne zwei Zeitpunkte: das Anlegen der vier Buttons und die fünf Klicks. Bei welchen Zeilen steht beim Anlegen ein Klammerpaar direkt hinter einem Funktionsnamen, und was bekommt der Button dann als command? Bei welchen Buttons passiert beim Klick überhaupt etwas?
antwort = ["gespeichert", "melde d", "gespeichert", "gespeichert", "melde c"]
antwortÜbung 2: Geometrie-Manager, was passiert?
Ein Kollege baut ein Einstellungsfenster:
root = tk.Tk()
rahmen = tk.Frame(root)
rahmen.pack(fill=tk.BOTH, expand=True)
tk.Label(rahmen, text="Port:").grid(row=0, column=0)
tk.Entry(rahmen).grid(row=0, column=1)
tk.Button(root, text="OK").pack(side=tk.BOTTOM)
root.mainloop()Welche Aussage stimmt? Trage den Buchstaben als String ein.
- A: Es gibt einen
TclError, weilpackundgridim selben Programm nie gemeinsam vorkommen dürfen. - B: Es gibt einen
TclError, weil einFramenur mitgridin ein Fenster gesetzt werden darf. - C: Es läuft, aber
LabelundEntryliegen übereinander in einer einzigen Zelle des Rahmens. - D: Es läuft, weil jeder Container nur einen Manager nutzt:
rootverwendetpack,rahmenverwendetgrid.
Die Regel in der Quelle spricht von einem Container, nicht vom ganzen Programm. Schreibe dir auf, welche Widgets direkt in root liegen und welche direkt in rahmen, und welcher Manager jeweils benutzt wird.
antwort = "D"
antwortÜbung 3: tkinter-Fallen per Code-Analyse finden
Ein Reviewer-Werkzeug soll tkinter-Quelltext lesen, ohne ihn auszuführen. Dafür gibt es das Modul ast (abstract syntax tree). Zum Aufwärmen: So sieht der Baum für eine Falle aus der Quelle aus.
Wichtig dabei: Bei tk.Button(...) ist func ein ast.Attribute (mit .attr), bei Button(...) (nach from tkinter import Button) ein ast.Name (mit .id). Ein Schlüsselwort-Argument ist ein ast.keyword mit .arg (Name) und .value (Ausdruck). Ein Lambda ist ein ast.Lambda, kein ast.Call. Eine Zuweisung ist ein ast.Assign, eine Zuweisung mit Typangabe (x: int = 1) ein ast.AnnAssign. Beide haben ein Feld .value für die rechte Seite. Hinweis: ast gehört nicht zum PCPP1-Stoff. Die Übung nutzt es nur, damit du tkinter-Fallen im Browser prüfen kannst. Rechne dafür mit etwas mehr Zeit.
Schreibe analysiere(quelltext). Sie gibt eine sortierte Liste ohne Duplikate mit diesen Befunden zurück:
"command-aufruf": ein Aufruf hat ein Schlüsselwort-Argumentcommand, dessen Wert direkt ein Aufruf ist (command=f()).command=f,command=obj.methodeundcommand=lambda: f()sind in Ordnung."pack-zuweisung": Das Ergebnis eines Aufrufs von.pack(...),.grid(...)oder.place(...)wird einer Variablen oder einem Attribut zugewiesen (b = tk.Button(...).pack()). Ein reiner Aufruf als eigene Anweisung (b.pack()) ist in Ordnung, und andere Methodenaufrufe in einer Zuweisung auch."doppeltes-tk":Tk()wird mehr als einmal aufgerufen (alstk.Tk()oder alsTk()).Toplevel(...)zählt nicht.
Gehe mit ast.walk über alle Knoten und prüfe pro Knotentyp mit isinstance. Für command brauchst du die Knoten, die Schlüsselwörter tragen. Für die Zuweisung brauchst du den Knotentyp der Zuweisung und den Typ ihres Werts. Für Tk musst du mitzählen und erst am Ende entscheiden, und es gibt zwei Schreibweisen des Namens.
import ast
def analysiere(quelltext):
baum = ast.parse(quelltext)
funde = set()
tk_aufrufe = 0
for knoten in ast.walk(baum):
if isinstance(knoten, ast.Call):
for kw in knoten.keywords:
if kw.arg == "command" and isinstance(kw.value, ast.Call):
funde.add("command-aufruf")
f = knoten.func
name = f.id if isinstance(f, ast.Name) else (f.attr if isinstance(f, ast.Attribute) else None)
if name == "Tk":
tk_aufrufe += 1
if isinstance(knoten, (ast.Assign, ast.AnnAssign)) and isinstance(knoten.value, ast.Call):
f = knoten.value.func
if isinstance(f, ast.Attribute) and f.attr in ("pack", "grid", "place"):
funde.add("pack-zuweisung")
if tk_aufrufe > 1:
funde.add("doppeltes-tk")
return sorted(funde)
analysiereMerksatz und Prüfstein
Merksatz: Ein Widget erscheint erst nach pack, grid oder place (nie zwei Manager im selben Container), und command bekommt eine Funktion ohne Klammern und ohne Argumente.
Prüfstein (offene Frage): Ein Button soll speichern(datei) aufrufen. Beim Start des Programms erscheint aber sofort die Speichern-Meldung, und der Klick bewirkt nichts. Erkläre die Ursache, gib zwei Wege zur Korrektur an und nenne eine Folgefalle, wenn du den Button in einer Schleife für mehrere Dateien erzeugst.
Weiter mit Teil b: Events, Variablen und die App als Klasse.
Quelle: quellen/python-glossar-pcap-pcpp1.md, Abschnitt “PCPP1 Block 3: GUI-Programmierung mit tkinter (20 %, 8 Fragen)”, 3.1 bis 3.5 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): Wortlaut des TclError beim Mischen von pack und grid, die Lambda-Schleifenfalle, ast als Werkzeug für Übung 3.