it-praxisportal/14_gui_mit_tkinter/index.html

356 lines
23 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Kapitel 14: Desktop-GUIs mit Tkinter & OOP</title>
<link rel="stylesheet" href="../assets/style.css">
</head>
<body>
<header>
<div class="header-container">
<nav class="nav-links">
<a href="../index.html">🏠 Zur Kursübersicht</a>
<span>&rsaquo;</span>
<a href="../13_persistenz_json_und_csv/index.html">Kapitel 13</a>
<span>&rsaquo;</span>
<span style="color: white; font-weight: 600;">Kapitel 14</span>
</nav>
<div class="header-badge">Kapitel 14 &bull; Schulabgleich 27.0 Tkinter</div>
<h1>Desktop-GUIs mit Tkinter &amp; OOP 🖥️✨</h1>
<p>Vom Text-Terminal zum echten Betriebssystem-Fenster mit Buttons, Eingabefeldern, Layout-Grids und ereignisgesteuerter OOP-Architektur.</p>
</div>
</header>
<main class="container">
<!-- 1. Das Konzept: Vom Terminal zum Touchscreen -->
<section class="card">
<h2>1. Vom Terminal zur grafischen Oberfläche (GUI)</h2>
<p>
Bisher liefen all deine Python-Programme linear im schwarzen Terminal ab: Text einlesen mit <code>input()</code>, Text ausgeben mit <code>print()</code>.
Echte Desktop-Software (wie Spotify, VS Code oder ein Taschenrechner) funktioniert jedoch völlig anders: Sie besitzt ein <strong>Fenster</strong>, das geduldig auf <strong>Mausklicks</strong> und <strong>Tastatureingaben</strong> wartet.
</p>
<div class="box box-analogie">
<div class="box-title">📺 Die Fernseh-Analogie: Vom Schwarz-Weiß-Fernseher zum Touchscreen</div>
<ul>
<li><strong>Terminal (Linear):</strong> Wie ein alter Filmstreifen. Bild für Bild läuft starr von oben nach unten ab. Am Ende ist der Film vorbei.</li>
<li><strong>GUI (Ereignisgesteuert / Event-Driven):</strong> Wie ein interaktiver Touchscreen. Das System schläft im Hintergrund (im sogenannten <strong>Event-Loop</strong>). Sobald du auf einen Knopf drückst, feuert ein Signal und deine Methode reagiert blitzschnell darauf!</li>
</ul>
</div>
<h3>Der Herzschlag einer GUI: Der Event-Loop</h3>
<p>
Ein GUI-Programm beendet sich nicht sofort, sondern übergibt die Kontrolle an <code>root.mainloop()</code>:
</p>
<div class="code-container">
<div class="code-header">
<div class="code-dots">
<span class="code-dot dot-red"></span>
<span class="code-dot dot-yellow"></span>
<span class="code-dot dot-green"></span>
</div>
<span>Python (Minimales Tkinter-Fenster)</span>
</div>
<pre><span class="kw">import</span> <span class="var">tkinter</span> <span class="kw">as</span> <span class="var">tk</span>
<span class="var">root</span> = <span class="var">tk</span>.<span class="fn">Tk</span>() <span class="cm"># 1. Hauptfenster erzeugen</span>
<span class="var">root</span>.<span class="fn">title</span>(<span class="str">"Mein erstes GUI-Fenster"</span>) <span class="cm"># 2. Fenstertitel festlegen</span>
<span class="var">root</span>.<span class="fn">geometry</span>(<span class="str">"400x300"</span>) <span class="cm"># 3. Fenstergröße (Breite x Höhe)</span>
<span class="var">label</span> = <span class="var">tk</span>.<span class="fn">Label</span>(<span class="var">root</span>, <span class="var">text</span>=<span class="str">"Hallo Welt! 👋"</span>, <span class="var">font</span>=(<span class="str">"Arial"</span>, <span class="num">16</span>))
<span class="var">label</span>.<span class="fn">pack</span>(<span class="var">pady</span>=<span class="num">20</span>) <span class="cm"># 4. Widget im Fenster platzieren</span>
<span class="var">root</span>.<span class="fn">mainloop</span>() <span class="cm"># 5. Ereignisschleife starten (wartet auf Events)</span></pre>
</div>
</section>
<!-- 2. Die wichtigsten Widgets im Überblick -->
<section class="card">
<h2>2. Bausteine einer GUI: Die wichtigsten Widgets</h2>
<p>
In Tkinter heißen alle visuellen Bedienelemente <strong>Widgets</strong> (Window Gadgets). Jedes Widget ist ein Python-Objekt, das wir in unserem Fenster platzieren können:
</p>
<table style="width: 100%; border-collapse: collapse; margin: 15px 0;">
<thead>
<tr style="background: var(--primary-light); text-align: left;">
<th style="padding: 10px; border: 1px solid var(--border-color);">Widget</th>
<th style="padding: 10px; border: 1px solid var(--border-color);">Klasse</th>
<th style="padding: 10px; border: 1px solid var(--border-color);">Zweck</th>
<th style="padding: 10px; border: 1px solid var(--border-color);">Wichtige Parameter &amp; Methoden</th>
</tr>
</thead>
<tbody>
<tr>
<td style="padding: 10px; border: 1px solid var(--border-color);"><strong>Hauptfenster</strong></td>
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>tk.Tk()</code></td>
<td style="padding: 10px; border: 1px solid var(--border-color);">Rahmen &amp; Titelleiste des Betriebssystems</td>
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>.title()</code>, <code>.geometry("BxH")</code>, <code>.mainloop()</code></td>
</tr>
<tr style="background: #fafafa;">
<td style="padding: 10px; border: 1px solid var(--border-color);"><strong>Textanzeige</strong></td>
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>tk.Label</code></td>
<td style="padding: 10px; border: 1px solid var(--border-color);">Zeigt Texte, Titel oder Zahlenwerte</td>
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>text="..."</code>, <code>font=("Arial", 14)</code>, <code>fg="color"</code>, <code>.config(text=...)</code></td>
</tr>
<tr>
<td style="padding: 10px; border: 1px solid var(--border-color);"><strong>Schaltfläche</strong></td>
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>tk.Button</code></td>
<td style="padding: 10px; border: 1px solid var(--border-color);">Knopf, der beim Anklicken eine Methode ausführt</td>
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>text="..."</code>, <code>command=self.klick_methode</code>, <code>bg="color"</code></td>
</tr>
<tr style="background: #fafafa;">
<td style="padding: 10px; border: 1px solid var(--border-color);"><strong>Eingabefeld</strong></td>
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>tk.Entry</code></td>
<td style="padding: 10px; border: 1px solid var(--border-color);">Einzeiliges Textfeld für Benutzereingaben</td>
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>.get()</code>, <code>.insert(0, text)</code>, <code>.delete(0, "end")</code></td>
</tr>
<tr>
<td style="padding: 10px; border: 1px solid var(--border-color);"><strong>Container</strong></td>
<td style="padding: 10px; border: 1px solid var(--border-color);"><code>tk.Frame</code></td>
<td style="padding: 10px; border: 1px solid var(--border-color);">Unsichtbare Box zum Gruppieren von Widgets</td>
<td style="padding: 10px; border: 1px solid var(--border-color);">Unterteilt komplexe Layouts in saubere Blöcke</td>
</tr>
</tbody>
</table>
</section>
<!-- 3. Layout Manager: Das Grid System -->
<section class="card">
<h2>3. Layout-Manager: Das 2D-Grid-System (Raster)</h2>
<p>
Widgets müssen auf dem Bildschirm positioniert werden. Der mächtigste und sauberste Layout-Manager in Tkinter ist <strong><code>grid()</code></strong>.
Er teilt den Raum wie eine Excel-Tabelle oder ein Schachbrett in <strong>Zeilen (<code>row</code>)</strong> und <strong>Spalten (<code>column</code>)</strong> ein:
</p>
<div style="background: #f1f5f9; border: 2px dashed #94a3b8; border-radius: var(--radius-md); padding: 20px; margin: 20px 0; font-family: var(--font-mono); font-size: 0.9rem;">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px; text-align: center;">
<div style="grid-column: 1 / span 2; background: #e0e7ff; border: 1px solid #6366f1; padding: 12px; border-radius: 6px; font-weight: bold; color: #3730a3;">
row=0, column=0, columnspan=2 &rarr; 🔢 Titel: "Python Klick-Zähler"
</div>
<div style="grid-column: 1 / span 2; background: #ffffff; border: 2px solid #6366f1; padding: 16px; border-radius: 6px; font-size: 1.4rem; font-weight: bold; color: #4f46e5;">
row=1, column=0, columnspan=2 &rarr; Zählerstand: "0"
</div>
<div style="grid-column: 1 / span 2; background: #f8fafc; border: 1px solid #cbd5e1; padding: 8px; border-radius: 6px; color: #64748b;">
row=2, column=0, columnspan=2 &rarr; Status-Info: "Klicks: 0 | Gerade Zahl"
</div>
<div style="background: #d1fae5; border: 1px solid #10b981; padding: 12px; border-radius: 6px; font-weight: bold; color: #065f46;">
row=3, column=0<br>➕ Erhöhen
</div>
<div style="background: #fee2e2; border: 1px solid #ef4444; padding: 12px; border-radius: 6px; font-weight: bold; color: #991b1b;">
row=3, column=1<br>➖ Verringern
</div>
<div style="grid-column: 1 / span 2; background: #f1f5f9; border: 1px solid #94a3b8; padding: 10px; border-radius: 6px; color: #1e293b;">
row=4, column=0, columnspan=2 &rarr; ↺ Zurücksetzen (Reset)
</div>
</div>
</div>
<div class="box box-warning">
<div class="box-title">⚠️ Häufigster Anfängerfehler: .pack() und .grid() niemals mischen!</div>
Verwende innerhalb eines Fensters (oder innerhalb eines Frames) <strong>entweder nur <code>grid()</code> oder nur <code>pack()</code></strong>.
Wenn du beide Layout-Manager im selben übergeordneten Fenster mischst, friert Python in einer Endlosschleife ein!
</div>
</section>
<!-- 4. Event-Driven & Methoden-Referenzen -->
<section class="card">
<h2>4. Event-Driven Programming: Was geschieht beim Klick?</h2>
<p>
Wenn der Benutzer auf einen Button klickt, soll ein Stück Code ausgeführt werden. In Tkinter übergeben wir dafür das <strong><code>command</code>-Attribut</strong>:
</p>
<div class="code-container">
<div class="code-header">
<span>Python: Button-Befehle richtig übergeben</span>
</div>
<pre><span class="cm"># ✅ RICHTIG: Nur den Methodennamen übergeben (OHNE Klammern!)</span>
<span class="var">self</span>.<span class="attr">btn_plus</span> = <span class="var">tk</span>.<span class="fn">Button</span>(<span class="var">self</span>.<span class="attr">root</span>, <span class="var">text</span>=<span class="str">"➕"</span>, <span class="var">command</span>=<span class="var">self</span>.<span class="fn">klick_plus</span>)
<span class="cm"># ❌ FALSCH: Mit Klammern wird die Methode SOFORT beim Erstellen aufgerufen!</span>
<span class="var">self</span>.<span class="attr">btn_plus</span> = <span class="var">tk</span>.<span class="fn">Button</span>(<span class="var">self</span>.<span class="attr">root</span>, <span class="var">text</span>=<span class="str">"➕"</span>, <span class="var">command</span>=<span class="var">self</span>.<span class="fn">klick_plus</span>())</pre>
</div>
<div class="box box-tipp">
<div class="box-title">💡 Warum ohne Klammern?</div>
Mit <code>self.klick_plus</code> übergibst du dem Button eine <strong>Visitenkarte (Referenz)</strong> der Methode. Der Button behält diese Visitenkarte und ruft die Methode erst dann an, wenn der Benutzer wirklich mit der Maus daraufklickt.
</div>
</section>
<!-- 5. Professionelle OOP-Architektur (MVC) -->
<section class="card">
<h2>5. Saubere Software-Architektur: Model-View-Trennung (MVC) 🏛️</h2>
<p>
In der professionellen Entwicklung schreibt man GUI-Apps niemals als wilden Spaghetticode, sondern trennt die <strong>Logik (Berechnung &amp; Daten)</strong> strikt von der <strong>Präsentation (Fenster &amp; Buttons)</strong>:
</p>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 15px; margin: 20px 0;">
<div style="background: #ffffff; border: 2px solid var(--primary); border-radius: var(--radius-sm); padding: 15px;">
<h4 style="color: var(--primary); margin-bottom: 8px;">1. Model: <code>ZaehlerLogik</code></h4>
<p style="font-size: 0.9rem; color: var(--text-muted);">
Reines Python! Verwaltet <code>wert</code>, <code>schrittweite</code>, Grenzwerte und Berechnungen.<br>
<strong>Vorteil:</strong> 100% unabhängig von Bildschirmen und in Sekundenbruchteilen per Unit Test prüfbar!
</p>
</div>
<div style="background: #ffffff; border: 2px solid var(--success); border-radius: var(--radius-sm); padding: 15px;">
<h4 style="color: var(--success); margin-bottom: 8px;">2. View / Controller: <code>ZaehlerApp</code></h4>
<p style="font-size: 0.9rem; color: var(--text-muted);">
Hält die Widgets (<code>Label</code>, <code>Button</code>, <code>Entry</code>).<br>
Bei einem Button-Klick ruft sie Methoden der <code>ZaehlerLogik</code> auf und spiegelt das Ergebnis im Label wider.
</p>
</div>
</div>
<div class="code-container">
<div class="code-header">
<span>Struktur einer OOP-GUI-Klasse</span>
</div>
<pre><span class="kw">class</span> <span class="fn">ZaehlerApp</span>:
<span class="kw">def</span> <span class="fn">__init__</span>(<span class="var">self</span>, <span class="var">root</span>: <span class="var">tk</span>.<span class="fn">Tk</span>):
<span class="var">self</span>.<span class="attr">root</span> = <span class="var">root</span>
<span class="var">self</span>.<span class="attr">root</span>.<span class="fn">title</span>(<span class="str">"Python Zähler"</span>)
<span class="cm"># 1. Datenmodell einbinden</span>
<span class="var">self</span>.<span class="attr">logik</span> = <span class="fn">ZaehlerLogik</span>()
<span class="cm"># 2. Widgets erstellen</span>
<span class="var">self</span>.<span class="attr">label_anzeige</span> = <span class="var">tk</span>.<span class="fn">Label</span>(<span class="var">self</span>.<span class="attr">root</span>, <span class="var">text</span>=<span class="str">"0"</span>, <span class="var">font</span>=(<span class="str">"Arial"</span>, <span class="num">32</span>))
<span class="var">self</span>.<span class="attr">label_anzeige</span>.<span class="fn">grid</span>(<span class="var">row</span>=<span class="num">0</span>, <span class="var">column</span>=<span class="num">0</span>)
<span class="var">self</span>.<span class="attr">btn_plus</span> = <span class="var">tk</span>.<span class="fn">Button</span>(<span class="var">self</span>.<span class="attr">root</span>, <span class="var">text</span>=<span class="str">"➕"</span>, <span class="var">command</span>=<span class="var">self</span>.<span class="fn">klick_plus</span>)
<span class="var">self</span>.<span class="attr">btn_plus</span>.<span class="fn">grid</span>(<span class="var">row</span>=<span class="num">1</span>, <span class="var">column</span>=<span class="num">0</span>)
<span class="kw">def</span> <span class="fn">klick_plus</span>(<span class="var">self</span>) -> <span class="kw">None</span>:
<span class="var">self</span>.<span class="attr">logik</span>.<span class="fn">erhoehen</span>()
<span class="var">self</span>.<span class="fn">aktualisiere_anzeige</span>()
<span class="kw">def</span> <span class="fn">aktualisiere_anzeige</span>(<span class="var">self</span>) -> <span class="kw">None</span>:
<span class="var">self</span>.<span class="attr">label_anzeige</span>.<span class="fn">config</span>(<span class="var">text</span>=<span class="fn">str</span>(<span class="var">self</span>.<span class="attr">logik</span>.<span class="attr">wert</span>))</pre>
</div>
</section>
<!-- 6. Deine Aufgaben in aufgabe.py -->
<section class="card">
<h2>6. 🎯 Deine Aufgaben in <code>aufgabe.py</code></h2>
<p>Öffne die Datei <code>14_gui_mit_tkinter/aufgabe.py</code> und implementiere die Klassen Schritt für Schritt:</p>
<ul class="task-list">
<li class="task-item">
<input type="checkbox" class="task-checkbox" id="t14_1">
<label for="t14_1" class="task-label">
<strong>TODO 1 (Logik-Init):</strong> <code>ZaehlerLogik.__init__</code> mit <code>startwert</code>, <code>min_wert</code>, <code>max_wert</code>, <code>schrittweite=1</code> und <code>klick_anzahl=0</code>.
</label>
</li>
<li class="task-item">
<input type="checkbox" class="task-checkbox" id="t14_2">
<label for="t14_2" class="task-label">
<strong>TODO 2 (Rechnen &amp; Grenzen):</strong> <code>erhoehen()</code>, <code>verringern()</code> (Grenzwerte mit <code>min()</code> / <code>max()</code> einhalten!) und <code>zuruecksetzen()</code>.
</label>
</li>
<li class="task-item">
<input type="checkbox" class="task-checkbox" id="t14_3">
<label for="t14_3" class="task-label">
<strong>TODO 3 (Zusatz-Logik):</strong> <code>setze_schrittweite(schritt)</code> (nur wenn <code>schritt &gt; 0</code>), <code>ist_gerade()</code> und Dunder <code>__str__()</code>.
</label>
</li>
<li class="task-item">
<input type="checkbox" class="task-checkbox" id="t14_4">
<label for="t14_4" class="task-label">
<strong>TODO 4 (GUI-Aufbau):</strong> <code>ZaehlerApp.__init__</code> (Fenstertitel, Geometrie, Instanziierung von <code>ZaehlerLogik</code>, Widgets erstellen und im Grid platzieren).
</label>
</li>
<li class="task-item">
<input type="checkbox" class="task-checkbox" id="t14_5">
<label for="t14_5" class="task-label">
<strong>TODO 5 (Event-Handler):</strong> Methoden <code>klick_plus</code>, <code>klick_minus</code>, <code>klick_reset</code> und <code>klick_schritt_setzen</code> mit Fehlerbehandlung für <code>Entry</code>.
</label>
</li>
<li class="task-item">
<input type="checkbox" class="task-checkbox" id="t14_6">
<label for="t14_6" class="task-label">
<strong>TODO 6 (Display-Sync):</strong> <code>aktualisiere_anzeige()</code> zur Aktualisierung der Labels <code>label_anzeige</code> und <code>label_info</code>.
</label>
</li>
</ul>
<h3>💡 Lösungshinweise (Aufklappen bei Bedarf)</h3>
<details>
<summary>🔍 Tipp zu TODO 2: Clamping an Grenzwerte (min/max)</summary>
<div class="details-content">
<p>Um einen Wert nach oben und unten sauber zu deckeln:</p>
<div class="code-container">
<pre><span class="cm"># Erhöhen mit oberer Grenze:</span>
<span class="var">self</span>.<span class="attr">wert</span> = <span class="fn">min</span>(<span class="var">self</span>.<span class="attr">max_wert</span>, <span class="var">self</span>.<span class="attr">wert</span> + <span class="var">self</span>.<span class="attr">schrittweite</span>)
<span class="cm"># Verringern mit unterer Grenze:</span>
<span class="var">self</span>.<span class="attr">wert</span> = <span class="fn">max</span>(<span class="var">self</span>.<span class="attr">min_wert</span>, <span class="var">self</span>.<span class="attr">wert</span> - <span class="var">self</span>.<span class="attr">schrittweite</span>)</pre>
</div>
</div>
</details>
<details>
<summary>🔍 Tipp zu TODO 5d: Fehler beim Entry-Auslesen abfangen</summary>
<div class="details-content">
<p>Benutzer können Tippfehler machen (z.B. Buchstaben eingeben). Fange das mit <code>try-except</code> ab:</p>
<div class="code-container">
<pre><span class="var">eingabe</span> = <span class="var">self</span>.<span class="attr">entry_schritt</span>.<span class="fn">get</span>().<span class="fn">strip</span>()
<span class="kw">try</span>:
<span class="var">neuer_schritt</span> = <span class="fn">int</span>(<span class="var">eingabe</span>)
<span class="kw">if</span> <span class="var">self</span>.<span class="attr">logik</span>.<span class="fn">setze_schrittweite</span>(<span class="var">neuer_schritt</span>):
<span class="var">self</span>.<span class="fn">aktualisiere_anzeige</span>()
<span class="kw">else</span>:
<span class="var">self</span>.<span class="attr">label_info</span>.<span class="fn">config</span>(<span class="var">text</span>=<span class="str">"⚠️ Schrittweite muss &gt; 0 sein!"</span>)
<span class="kw">except</span> <span class="fn">ValueError</span>:
<span class="var">self</span>.<span class="attr">label_info</span>.<span class="fn">config</span>(<span class="var">text</span>=<span class="str">"⚠️ Bitte eine ganze Zahl eingeben!"</span>)</pre>
</div>
</div>
</details>
</section>
<!-- 7. Testen & Navigation -->
<section class="card">
<h2>7. 🧪 Lösung überprüfen &amp; Starten</h2>
<h3>1. Automatische Tests ausführen:</h3>
<div class="code-container">
<div class="code-header">
<span>Terminal (im Ordner 14_gui_mit_tkinter)</span>
</div>
<pre><code>python3 test_aufgabe.py</code></pre>
</div>
<h3>2. Fertige grafische Applikation starten:</h3>
<div class="code-container">
<div class="code-header">
<span>Terminal</span>
</div>
<pre><code>python3 gui_app.py</code></pre>
</div>
<div class="footer-nav">
<a href="../13_persistenz_json_und_csv/index.html" class="btn btn-secondary">&larr; Zurück zu Kapitel 13</a>
<a href="../index.html" class="btn btn-secondary">🏠 Kursübersicht</a>
<a href="../15_parameter_und_container/index.html" class="btn">Weiter zu Kapitel 15 &rarr;</a>
</div>
</section>
</main>
<!-- Lokale Gamification-Scripts (funktionieren auch offline per Doppelklick via file://) -->
<script src="../assets/gamification_data.js"></script>
<script src="../assets/gamification.js"></script>
</body>
</html>