Kiste
EN

Grafische Oberflächen — `nutze gui`

Bis jetzt haben deine Programme im Terminal gelaufen. Mit dem gui-Modul baust du richtige Fenster mit Knöpfen, Eingabefeldern, Listen und Tabellen — eine eigene Desktop-App.

Wichtig: GUI-Programme laufen nur über den nativen Compiler (kiste build), nicht über kiste run. Eine GUI ist eine echte native App. Bau sie also so:

kiste build meine_app.ki -o meine_app.exe
./meine_app.exe

34.1 Dein erstes Fenster

nutze gui

nimm fenster = gui.fenster_öffne("Hallo", 320, 200)   // Titel, Breite, Höhe
nimm knopf = gui.knopf_neu("Klick mich")
gui.zeige(fenster, knopf)                              // Knopf ins Fenster
gui.starte()                                           // Fenster anzeigen + warten

gui.starte() öffnet das Fenster und läuft, bis du es schließt. Es muss die letzte Zeile sein.

34.2 Auf Klicks reagieren — Handler

Ein Handler ist eine Funktion, die aufgerufen wird, wenn etwas passiert. Sie bekommt das Widget, auf das sie reagiert:

nutze gui

funktion bei_klick(knopf) {
    gui.setze_text(knopf, "Geklickt! ✓")   // ändert die Knopf-Beschriftung
}

nimm fenster = gui.fenster_öffne("Klick-Demo", 320, 200)
nimm knopf = gui.knopf_neu("Drück mich")
gui.bei_klick(knopf, bei_klick)             // Handler an den Knopf binden
gui.zeige(fenster, knopf)
gui.starte()

34.3 Etiketten und Layout

Ein Etikett (etikett_neu) ist ein Text-Schild. Mehrere Widgets ordnest du mit vertikal (Spalte), horizontal (Reihe) oder raster (Gitter) an:

nutze gui

nimm titel = gui.etikett_neu("Meine App")
nimm knopf = gui.knopf_neu("OK")

nimm inhalt = gui.vertikal([titel, knopf])       // untereinander
// nimm inhalt = gui.horizontal([titel, knopf])  // nebeneinander
// nimm inhalt = gui.raster(2, [a, b, c, d])      // Gitter mit 2 Spalten

nimm fenster = gui.fenster_öffne("Layout", 320, 200)
gui.zeige(fenster, inhalt)                        // den Container anzeigen
gui.starte()

34.4 Farben und Aussehen anpassen

Kiste-Apps haben von Haus aus ein stimmiges dunkles Design. Du kannst es frei ändern:

gui.thema("dunkel")                    // oder "hell"
gui.setze_farbe("akzent", "#3b82f6")   // eigenes Blau für primäre Elemente
gui.setze_farbe("hintergrund", "#0f172a")

Farb-Rollen: akzent, hintergrund, flaeche, text, rahmen, gefahr (Hex-Werte "#rrggbb").

Live ändern: gui.setze_farbe(...) und gui.thema(...) wirken auch während die App läuft — rufst du sie aus einem Handler auf, wechselt die Oberfläche sofort die Farbe. So baust du z.B. einen Knopf, der das Theme umschaltet.

Knöpfe können verschiedene Farben tragen — je nach Wichtigkeit der Aktion:

gui.setze_wichtigkeit(knopf, "primär")    // akzent-farbig — die Haupt-Aktion
gui.setze_wichtigkeit(knopf, "gefahr")    // rot — Löschen, Abbrechen
gui.setze_wichtigkeit(knopf, "erfolg")    // grün — Bestätigen, Speichern
gui.setze_wichtigkeit(knopf, "warnung")   // gelb — Vorsicht
gui.setze_wichtigkeit(knopf, "normal")    // grau (Standard)

So zeigt ein Fenster mehrere unterschiedlich gefärbte Knöpfe nebeneinander. Beispiel: beispiele/gui_farbwechsel.ki — vier farbige Knöpfe, jeder wechselt live den Hintergrund.

34.5 Text-Eingabe

Ein Eingabefeld (eingabefeld) nimmt Text vom Nutzer entgegen:

nimm feld = gui.eingabefeld("Dein Name …")   // Platzhalter-Text
gui.setze_wert(feld, "Welt")                  // Startwert setzen
nimm name = gui.wert(feld)                    // aktuellen Text lesen

Auf jede Änderung reagieren — der Handler bekommt das Feld, den neuen Text holst du mit gui.wert:

funktion bei_eingabe(feld) {
    sag "Neuer Text: {gui.wert(feld)}"
}
gui.bei_änderung(feld, bei_eingabe)

Mehrzeilig — für Notizen, Editoren oder längere Texte nimmst du gui.textbereich:

nimm notiz = gui.textbereich("Schreib hier …")       // Standard-Höhe (8 Zeilen)
nimm gross = gui.textbereich("Langer Text …", 15)    // 15 Zeilen hoch

Ein Textbereich verhält sich wie ein Eingabefeld — gui.wert, gui.setze_wert und gui.bei_änderung funktionieren genauso. Mit der Enter-Taste machst du neue Zeilen; lange Zeilen brechen automatisch um, und über die Höhe hinaus wird gescrollt. Die Zeilenzahl ist optional (zweiter Parameter). Beispiele: beispiele/gui_editor.ki, beispiele/gui_notizen.ki.

34.6 Zustand teilen — globale Variablen in Handlern

Ein Handler bekommt nur das Widget, auf das er reagiert. Will er ein anderes Widget ändern (z.B. Eingabe ins Etikett spiegeln), legst du dieses als globale Variable an — Handler dürfen Top-Level-Variablen lesen und schreiben:

nutze gui

nimm anzeige = gui.etikett_neu("Hallo, Welt")   // global, oben deklariert
nimm feld = gui.eingabefeld("Dein Name …")

funktion bei_eingabe(f) {
    gui.setze_text(anzeige, "Hallo, {gui.wert(f)}")   // greift auf `anzeige` zu
}

gui.setze_wert(feld, "Welt")
gui.bei_änderung(feld, bei_eingabe)

nimm inhalt = gui.vertikal([anzeige, feld])
nimm fenster = gui.fenster_öffne("Live-Formular", 360, 160)
gui.zeige(fenster, inhalt)
gui.starte()

Tippst du ins Feld, aktualisiert sich das Etikett live. So baust du echte interaktive Apps — auch einen Zähler:

nimm stand = 0
funktion hoch(knopf) {
    stand = stand + 1
    gui.setze_text(knopf, "Geklickt: {stand}")
}

34.7 Auswahl, Listen und Tabellen

Auswahl-Box (Dropdown) — eine Option aus wenigen wählen:

nimm box = gui.auswahl_box(["Apfel", "Birne", "Kirsche"])
funktion bei_wahl(b) { sag "Gewählt: {gui.gewählt(b)}" }
gui.bei_wahl(box, bei_wahl)

gui.gewählt(box) gibt den aktuell gewählten Text.

Liste — einen Eintrag aus einer scrollbaren Liste wählen:

nimm liste = gui.liste_anzeige(["Eins", "Zwei", "Drei"])
funktion bei_auswahl(l) {
    sag "Eintrag {gui.gewählter_index(l)}: {gui.gewählter_wert(l)}"
}
gui.bei_auswahl(liste, bei_auswahl)

gui.gewählter_index(liste) → Position (0-basiert, -1 wenn keiner), gui.gewählter_wert(liste) → der Text.

Tabelle — 2D-Daten anzeigen und direkt bearbeiten. Du legst die Spalten an und hängst Zeilen an (jede Zeile eine liste<Text>):

nimm tab = gui.tabelle(["Artikel", "Menge"])    // Spalten-Überschriften
gui.zeile_anhängen(tab, ["Schrauben", "120"])    // Zeilen anhängen
gui.zeile_anhängen(tab, ["Muttern", "80"])

nimm m = gui.zell_wert(tab, 0, 1)                // Zelle lesen: Zeile 0, Spalte 1 → "120"
gui.setze_zell_wert(tab, 0, 1, "150")            // Zelle setzen

funktion bei_zelle(t) {
    nimm z = gui.gewählte_zeile(t)
    nimm s = gui.gewählte_spalte(t)
    sag "Zelle [{z},{s}] = {gui.zell_wert(t, z, s)}"
}
gui.bei_zell_auswahl(tab, bei_zelle)             // Klick auf eine Zelle
// gui.bei_zell_änderung(tab, ...)               // wenn der Nutzer eine Zelle editiert

Die Zellen sind editierbar — der Nutzer kann direkt hineintippen, gui.zell_wert liest dann den neuen Wert.

Zeilen zählen und entfernen — die Tabelle kann auch schrumpfen und ausgelesen werden:

nimm n = gui.zeilen_anzahl(tab)      // Anzahl der Datenzeilen
gui.zeile_entfernen(tab, 0)          // erste Zeile löschen (ungültiger Index = passiert nichts)

Damit kannst du eine Tabelle live neu befüllen (alle Zeilen weg, dann neue anhängen):

solange gui.zeilen_anzahl(tab) > 0 {
    gui.zeile_entfernen(tab, 0)
}
// … jetzt mit gui.zeile_anhängen neu füllen …

Wählbare Tabelle (Master-Detail)gui.tabelle hat editierbare Zellen; ein Klick landet dann im Textfeld der Zelle, nicht in der Zeilen-Auswahl. Wenn du auf Zeilen-Klick reagieren willst (z. B. „Zeile wählen → oben bearbeiten"), nimm gui.tabelle_wählbar — eine read-only Tabelle, bei der ein Klick die ganze Zeile auswählt:

nimm tab = gui.tabelle_wählbar(["Name", "Betrag"])
gui.zeile_anhängen(tab, ["Buch", "25"])
funktion bei_zeile(t) {
    nimm z = gui.gewählte_zeile(t)        // -1 wenn keine
    wenn z >= 0 { sag "Zeile {z}: {gui.zell_wert(t, z, 0)}" }
}
gui.bei_zell_auswahl(tab, bei_zeile)

Faustregel: tabelle = direkt in Zellen tippen · tabelle_wählbar = Zeilen anklicken (Bearbeiten dann über separate Eingabefelder). Beide nutzen dieselben Funktionen (zeile_anhängen/zell_wert/zeilen_anzahl/zeile_entfernen).

Spaltenbreite selbst bestimmen — normalerweise wählt Kiste die Spaltenbreiten passend zum Inhalt (und deckelt sie, damit eine Tabelle nicht endlos breit wird). Bei längeren Texten (z. B. einer Referenz-/Cheat-Sheet-Tabelle) willst du eine Spalte vielleicht bewusst breiter machen, damit nichts abgeschnitten wird — dafür gibt es gui.spalten_breite:

nimm tab = gui.tabelle(["Taste", "Funktion"])
gui.zeile_anhängen(tab, ["Strg+S", "Speichern"])
gui.zeile_anhängen(tab, ["Strg+Z", "Rückgängig"])
gui.spalten_breite(tab, 0, 140)      // Spalte 0 (Taste) schmal
gui.spalten_breite(tab, 1, 420)      // Spalte 1 (Funktion) breit → langer Text bleibt lesbar

Die Spaltennummer ist 0-basiert, die Breite in Pixeln. Spalten ohne spalten_breite behalten die automatische, inhaltsabhängige Breite.

Links öffnen und Text kopieren — für ein „Über"-Fenster oder Hilfe-Seiten willst du oft einen anklickbaren Link (öffnet den Browser) und eine Mailadresse zum Kopieren anbieten:

nimm k = gui.knopf_neu("kiste-lang.org")
gui.bei_klick(k, funktion(x) { gui.öffne_url("https://kiste-lang.org") })

gui.öffne_url(url) öffnet die URL im Standard-Browser; beginnt sie mit mailto:, öffnet sich das Standard-Mailprogramm. Weil du aber nicht weisst, welches Mailprogramm deine Nutzer:innen verwenden, ist es oft freundlicher, die Adresse nur zum Kopieren anzubieten:

nimm m = gui.knopf_neu("hallo@example.com  ⧉")
nimm hinweis = gui.etikett_neu("")
gui.bei_klick(m, funktion(x) {
    gui.kopiere_text("hallo@example.com")       // in die Zwischenablage
    gui.setze_text(hinweis, "✓ kopiert")
})

gui.kopiere_text(text) legt beliebigen Text in die System-Zwischenablage — die Nutzer:in fügt ihn dann in ihrem eigenen Programm mit Strg+V ein.

Ein eigenes Programm-Icon — statt des neutralen Standard-Symbols in der Titelleiste und der Taskleiste kannst du dein eigenes Logo setzen:

gui.setze_app_icon("logo.png")     // VOR gui.starte() aufrufen

gui.setze_app_icon(pfad) lädt eine Bilddatei (PNG empfohlen) und macht sie zum Fenster- und Taskleisten-Icon. Ruf es vor gui.starte() auf; die Datei muss beim Programmstart auffindbar sein (am einfachsten neben der .exe).

Ein Fenster nicht maximierbar machen — ein kleines Dialog- oder Hilfefenster sieht maximiert oft schlecht aus (die Widgets kleben oben links, der Rest ist leer). Mit gui.fenster_fest fixierst du ein Fenster auf seine Inhaltsgröße: der Maximieren-Knopf wird deaktiviert und die Größe lässt sich nicht mehr ziehen.

nimm hilfe = gui.fenster_öffne("Hilfe", 600, 400)
gui.fenster_fest(hilfe, wahr)       // kein Maximieren; passt sich dem Inhalt an

gui.fenster_fest(fenster, fest) erwartet als zweites eine Wahrheit (wahr/falsch). Setz es am besten direkt nach gui.fenster_öffne (also vor dem ersten Anzeigen). Das Hauptfenster lässt man üblicherweise frei größenänderbar — fenster_fest ist für die kleinen Zweitfenster gedacht.

Doppelklick zum Öffnen — neben dem Einfachklick (Auswahl) kannst du auf eine Liste oder eine tabelle_wählbar einen Doppelklick-Handler legen — die gewohnte „Öffnen"-Geste aus Datei-Managern:

nimm liste = gui.liste_anzeige(["Ordner A", "Datei B"])
funktion auswahl(l) { sag "gewählt: {gui.gewählter_wert(l)}" }   // Einfachklick
funktion öffnen(l)  { sag "ÖFFNE: {gui.gewählter_wert(l)}" }     // Doppelklick
gui.bei_auswahl(liste, auswahl)
gui.bei_doppelklick(liste, öffnen)        // feuert NUR beim Doppelklick

Einfach- und Doppelklick koexistieren: ein einzelner Klick feuert bei_auswahl / bei_zell_auswahl, ein Doppelklick feuert bei_doppelklick. Im Doppelklick-Handler geben gui.gewählter_index / gui.gewählte_zeile schon die doppelt-geklickte Zeile zurück.

Tipp (Discoverability): Lass neben dem Doppelklick einen sichtbaren „Öffnen"-Knopf stehen — nicht jeder errät die Doppelklick-Geste. Beide dürfen denselben Handler nutzen. Hinweis: bei_doppelklick gilt für liste, tabelle_wählbar (read-only) und die Leinwand. Die editierbare tabelle hat keinen Doppelklick — dort gehört der Doppeltap dem Text-Cursor. Bei jeder anderen Bedienelement-Art meldet Kiste einen Fehler, statt den Handler stillschweigend zu ignorieren.

Doppelklick auf der Leinwand. Malst du deine Oberfläche selbst (Kapitel 34.10), bekommst du den Doppelklick genauso — gui.maus_x / gui.maus_y sagen dir, wo geklickt wurde. Der Einfachklick feuert weiter, und zwar sofort: erst wählt der Nutzer etwas aus, dann öffnet er es mit dem zweiten Klick.

funktion bei_klick(l) { sag "gewählt bei {gui.maus_x(l)}, {gui.maus_y(l)}" }
funktion bei_öffnen(l) { sag "geöffnet!" }

nimm lw = gui.leinwand(300, 200)
gui.bei_maus_klick(lw, bei_klick)
gui.bei_doppelklick(lw, bei_öffnen)

34.8 Dialoge, Menüs und Leisten

Eine echte App hat mehr als ein Fenster mit Widgets: kleine Dialoge zum Bestätigen oder Fragen, eine Menü-Leiste oben und eine Status-Leiste unten.

Dialoge — alle brauchen das Fenster, zu dem sie gehören:

funktion über_zeigen(id) {
    gui.dialog_info(fenster, "Über", "Mein Programm — Version 1.")
}

funktion beenden(id) {
    gui.dialog_frage(fenster, "Beenden", "Wirklich schließen?", antwort)
}
funktion antwort(ja) {
    wenn ja == 1 {
        gui.setze_text(status, "Bestätigt.")
    } sonst {
        gui.setze_text(status, "Abgebrochen.")
    }
}
  • gui.dialog_info(fenster, titel, text) zeigt eine Meldung mit OK-Knopf.
  • gui.dialog_frage(fenster, titel, text, handler) fragt Ja/Nein. Der Handler bekommt die Antwort als Zahl: 1 = ja, 0 = nein.

Unter Windows sind diese beiden Dialoge — wie die Datei-Dialoge weiter unten — echte Betriebssystem-Fenster. Das heißt: sie funktionieren auch, wenn dein Programm gerade im Vollbild läuft.

  • gui.dialog_datei(fenster, handler) öffnet einen Datei-Auswahl-Dialog. Der Handler bekommt das Fenster und liest den gewählten Pfad mit gui.gewählter_pfad(fenster) ("" wenn abgebrochen):
funktion datei_öffnen(id) {
    gui.dialog_datei(fenster, datei_gewählt)
}
funktion datei_gewählt(fenster) {
    nimm pfad = gui.gewählter_pfad(fenster)
    gui.setze_text(status, "Geöffnet: {pfad}")
}
  • gui.dialog_speichern(fenster, handler) öffnet einen Speichern-Dialog — genau wie dialog_datei, nur dass der Nutzer einen Ziel-Pfad wählt/eingibt. Den Pfad liest du wieder mit gui.gewählter_pfad(fenster).

Wirklich laden und speichern — kombiniere die Dialoge mit dem datei-Modul. Datei-Operationen können fehlschlagen (nicht gefunden, gesperrt) — pack sie in versuche/fange, dann landet ein Fehler in der Status-Leiste statt das Programm abzubrechen:

nutze gui
nutze datei

funktion speichern(id) {
    gui.dialog_speichern(fenster, speichern_gewählt)
}
funktion speichern_gewählt(fenster) {
    nimm pfad = gui.gewählter_pfad(fenster)
    wenn pfad != "" {
        versuche {
            datei.schreibe(pfad, gui.wert(feld))
            gui.setze_text(status, "Gespeichert: {pfad}")
        } fange e {
            gui.setze_text(status, "Konnte nicht speichern: {e}")
        }
    }
}

Laden geht spiegelbildlich mit gui.dialog_datei + datei.inhalt(pfad)gui.setze_wert(feld, …). Vollständig in beispiele/gui_editor.ki und beispiele/gui_notizen.ki.

Menü-Leiste — du baust sie Schritt für Schritt auf: Leiste → Menü → Einträge. Jeder Eintrag bekommt einen Handler (wie ein Knopf):

nimm leiste = gui.menü_leiste(fenster)
nimm m_datei = gui.menü_hinzufügen(leiste, "Datei")
gui.menü_eintrag(m_datei, "Öffnen …", datei_öffnen)
gui.menü_trenner(m_datei)
gui.menü_eintrag(m_datei, "Beenden", beenden)

Menü nachträglich umbeschriften — willst du einen Menütitel oder -Eintrag nach dem Start ändern (z. B. für eine Sprachumschaltung), merke dir sein Handle (nimm m_datei = … / nimm eintrag = gui.menü_eintrag(…)), ändere den Text mit gui.setze_text(handle, neuer_text) und wende die Leiste dann mit gui.menü_anwenden(fenster) neu an — erst dann zeigt das Menü den neuen Text.

Werkzeug-Leiste — eine Reihe von Knöpfen für schnelle Aktionen:

nimm knopf = gui.knopf_neu("Neu")
gui.bei_klick(knopf, neu_dokument)
nimm werkzeuge = gui.werkzeug_leiste([knopf])

Status-Leiste — eine Textzeile unten, die du laufend aktualisierst. Du legst sie selbst ans Ende deines Layouts und änderst sie mit gui.setze_text:

nimm status = gui.status_leiste("Bereit.")
// … gui.setze_text(status, "Gespeichert.")
nimm inhalt = gui.vertikal([werkzeuge, feld, status])

Ein vollständiger Mini-Editor, der alles zusammenbringt, steht in beispiele/gui_editor.ki.

34.8b Ein zweites Fenster öffnen

Die eingebauten Dialoge (dialog_info, dialog_frage, dialog_datei) decken die häufigsten Fälle ab. Manchmal brauchst du aber ein eigenes zweites Fenster mit deinen eigenen Widgets — ein Werkzeug-, Einstellungs- oder Farbwähler-Fenster, das neben dem Hauptfenster steht und das Hauptprogramm nicht blockiert.

Das geht mit gui.zeige — derselben Funktion, mit der du den Hauptfenster-Inhalt setzt. Der Trick: rufst du gui.zeige(fenster, inhalt) nach gui.starte() auf (also aus einem Handler, z. B. einem Knopf-bei_klick), dann öffnet Kiste dieses Fenster live als eigenes, zweites Fenster:

nutze gui

nimm haupt = gui.fenster_öffne("Haupt", 480, 300)
nimm dlg   = gui.fenster_öffne("Zweites Fenster", 320, 200)   // zweites Fenster anlegen …

// Inhalt des zweiten Fensters vorbereiten (tut noch nichts sichtbar):
nimm regler  = gui.schieberegler(0, 100)
nimm anzeige = gui.etikett_neu("Wert: 0")
nimm zu      = gui.knopf_neu("Schließen")

gui.bei_schieben(regler, funktion(r) {
    nimm w = gui.schieber_wert(r)
    gui.setze_text(anzeige, "Wert: {w}")
})
gui.bei_klick(zu, funktion(k) {
    gui.fenster_schließe(dlg)                 // nur dieses Fenster zu — App läuft weiter
})
nimm dlg_inhalt = gui.vertikal([anzeige, regler, zu])

// Ein Knopf im Hauptfenster öffnet das zweite Fenster LIVE:
nimm auf = gui.knopf_neu("Zweites Fenster öffnen")
gui.bei_klick(auf, funktion(k) {
    gui.zeige(dlg, dlg_inhalt)                // … erst hier geht es auf
})

gui.zeige(haupt, gui.vertikal([auf]))
gui.starte()
  • Vor gui.starte() setzt gui.zeige(fenster, inhalt) wie gewohnt den Inhalt des Hauptfensters. Nach gui.starte() öffnet derselbe Aufruf das Fenster als eigenständiges zweites Fenster (nicht-blockierend — das Hauptfenster bleibt bedienbar).
  • gui.fenster_schließe(fenster) schließt ein so geöffnetes Fenster wieder — ideal für „Übernehmen"/„Abbrechen"-Knöpfe. Danach kannst du es mit gui.zeige erneut öffnen. Das Hauptfenster zu schließen (dessen X oder fenster_schließe) beendet weiterhin die ganze App.
  • Das zweite Fenster hat eine eigene Titelleiste und ein eigenes X; alle Widgets darin (Knöpfe, Regler, Leinwand mit Maus, …) funktionieren genauso wie im Hauptfenster.

Merke: Lege beide Fenster mit gui.fenster_öffne an, bevor du gui.starte() aufrufst (das Hauptfenster ist einfach das zuerst angelegte). Das zweite Fenster bleibt unsichtbar, bis du es per gui.zeige aus einem Handler öffnest.

34.9 Weitere Widgets: Kästchen, Radio, Schieberegler, Fortschritt, Reiter

Fünf weitere Standard-Bausteine — jeder folgt demselben Muster wie bisher: Handler bekommt sein Widget, den Wert holst du mit einem Getter.

Kontrollkästchen (an/aus). gui.angehakt(k) liefert 1 (an) oder 0 (aus):

nimm haken = gui.kontrollkästchen("Benachrichtigungen aktivieren")
gui.bei_umschaltung(haken, bei_haken)
// gui.setze_angehakt(haken, 1)   // vorab anhaken

funktion bei_haken(k) {
    wenn gui.angehakt(k) == 1 {
        sag "an"
    } sonst {
        sag "aus"
    }
}

Radio-Knöpfe (eine Option aus mehreren). Sie benutzen dieselben Funktionen wie die Auswahl-Box: gui.gewählt und gui.bei_wahl:

nimm größe = gui.radio_knöpfe(["Klein", "Mittel", "Groß"])
gui.bei_wahl(größe, bei_größe)

funktion bei_größe(r) {
    sag "Schriftgröße: {gui.gewählt(r)}"
}

Schieberegler (eine Zahl in einem Bereich). gui.schieber_wert liefert die aktuelle Ganzzahl:

nimm laut = gui.schieberegler(0, 100)   // min, max
gui.bei_schieben(laut, bei_lautstärke)
// gui.setze_schieber_wert(laut, 42)     // vorab setzen

funktion bei_lautstärke(s) {
    sag "Lautstärke: {gui.schieber_wert(s)} %"
}

Fortschrittsbalken (reine Anzeige, 0–100 %). Du füllst ihn mit gui.setze_fortschritt — z. B. aus einem Schieberegler-Handler heraus:

nimm balken = gui.fortschritt_neu()
gui.setze_fortschritt(balken, 70)   // 70 %

Reiter (Tabs) — mehrere Seiten in einem Fenster. Du baust den Reiter- Container und hängst Seiten an (jede Seite ist ein Layout):

nimm seite1 = gui.vertikal([haken, größe])
nimm seite2 = gui.vertikal([laut, balken])
nimm reiter = gui.reiter_neu()
gui.reiter_hinzufügen(reiter, "Allgemein", seite1)
gui.reiter_hinzufügen(reiter, "Audio", seite2)

Ein vollständiges „Einstellungen"-Fenster mit allen fünf Widgets steht in beispiele/gui_widgets.ki.

34.10 Alles zusammen — ein kleines Formular

nutze gui

nimm info = gui.etikett_neu("Gib deinen Namen ein:")
nimm feld = gui.eingabefeld("Name …")

funktion bei_übernehmen(knopf) {
    gui.setze_text(info, "Hallo, {gui.wert(feld)}!")
}

gui.setze_farbe("akzent", "#22c55e")
nimm knopf = gui.knopf_neu("Übernehmen")
gui.setze_wichtigkeit(knopf, "primär")
gui.bei_klick(knopf, bei_übernehmen)

nimm inhalt = gui.vertikal([info, feld, knopf])
nimm fenster = gui.fenster_öffne("Mein Formular", 360, 200)
gui.zeige(fenster, inhalt)
gui.starte()

34.10a Formulare & Eingabe-Profi

Für echte Eingabe-Masken (Anmeldung, Einstellungen, Datenerfassung) gibt es eigene Bausteine — ein Formular richtet Etiketten und Felder sauber aus und sperrt den Absenden-Knopf automatisch, solange ein Feld ungültig ist.

Passwortfeld, Combobox, Datumsfeld — Entry-Verwandte, sie nutzen dieselben Getter wie das Eingabefeld (gui.wert, gui.setze_wert, gui.bei_änderung):

nimm pw   = gui.passwortfeld("Passwort")        // maskiert (•••)
nimm ort  = gui.combofeld(["Bern", "Zürich"])   // frei tippbar + Vorschläge
nimm tag  = gui.datumsfeld()                     // Kalender; Wert via gui.datum_wert(tag)

Validierung — prüfe eine Eingabe mit einem Muster (regulärer Ausdruck). Ist sie ungültig, erscheint dein Fehlertext am Feld und ein Formular sperrt sein Absenden:

nimm mail = gui.eingabefeld("E-Mail")
gui.setze_prüfung(mail, ".+@.+", "Bitte eine gültige E-Mail")
gui.setze_pflicht(pw, "Passwort darf nicht leer sein")   // Kurzform für „nicht leer"

Hinweis: In Kiste-Texten startet { … } eine Einsetzung. Enthält dein Prüf-Muster geschweifte Klammern (z. B. \d{4}), umgehe das, indem du das Muster ohne {} schreibst (etwa \d\d\d\d).

Komma-Schieberegler — ein Schieberegler mit Nachkommastellen. Komma-Zahlen schreibt man in Kiste mit dem Suffix f (z. B. 0.1f):

nimm regler = gui.schieberegler_komma(0.0f, 1.0f, 0.1f)   // min, max, Schrittweite
gui.setze_schieber_komma_wert(regler, 0.7f)
nimm laut = gui.schieber_komma_wert(regler)                // Komma-Wert lesen

Das Formular — Zeilen anhängen, Absenden/Abbrechen verdrahten:

nimm name = gui.eingabefeld("z. B. Anna")
gui.setze_pflicht(name, "Bitte Namen eingeben")

nimm f = gui.formular()
gui.formular_zeile(f, "Name", name)
gui.formular_zeile(f, "Passwort", pw)
gui.formular_zeile(f, "Ort", ort)

funktion abgesendet(formular) {
    gui.dialog_info(fenster, "Gespeichert", "Willkommen, {gui.wert(name)}!")
}
gui.formular_absenden(f, "Speichern", abgesendet)
gui.formular_abbrechen(f, "Abbrechen", abgesendet)

gui.zeige(fenster, f)

Der Speichern-Knopf bleibt ausgegraut, bis alle geprüften Felder gültig sind — so kann kein unvollständiges Formular abgeschickt werden.

34.10a-2 Dialoge & Kontextmenüs

Neben den einfachen Dialogen (dialog_info, dialog_frage, dialog_datei) gibt es flexible Dialoge für echte Programme. Ihr Ergebnis liest du wie beim Datei-Dialog über einen Getter am Fenster — der Handler bekommt das Fenster, du liest den Wert darin aus:

funktion bei_name(fenster) {
    nimm name = gui.eingabe_wert(fenster)          // eingegebener Text ("" = abgebrochen)
    gui.dialog_info(fenster, "Hallo", "Willkommen, {name}!")
}
gui.dialog_eingabe(fenster, "Name", "Wie heißt du?", bei_name)   // Eingabe-Dialog

Farbwählergui.dialog_farbe(fenster, titel, handler), Ergebnis als Hex über gui.farbe_wert(fenster):

funktion bei_farbe(fenster) {
    gui.setze_farbe("akzent", gui.farbe_wert(fenster))   // z. B. "#3b82f6"
}
gui.dialog_farbe(fenster, "Akzentfarbe wählen", bei_farbe)

Weitere Dialoge:

gui.dialog_inhalt(fenster, "Über", mein_widget, "Schließen")   // beliebiges Widget modal
gui.dialog_ordner(fenster, bei_ordner)                          // Ordner wählen (Pfad via gewählter_pfad)
gui.dialog_datei_gefiltert(fenster, [".png", ".jpg"], bei_bild) // nur Bilder zeigen

nimm warten = gui.dialog_warten(fenster, "Bitte warten", "lädt …")   // Spinner
// … Arbeit …
gui.dialog_schließe(warten)

Kontextmenü — ein Rechtsklick-Menü auf einem beliebigen Widget. Du wickelst das Widget mit gui.kontext_menü ein und hängst Einträge an; der Menü-Handle kommt statt des Widgets ins Layout:

nimm liste = gui.liste_anzeige(["Apfel", "Birne", "Kirsche"])
nimm menü = gui.kontext_menü(liste)
funktion bei_kopieren(m) {}
funktion bei_löschen(m)  {}
gui.kontext_eintrag(menü, "Kopieren", bei_kopieren)
gui.kontext_trenner(menü)
gui.kontext_eintrag(menü, "Löschen", bei_löschen)

gui.zeige(fenster, menü)   // der Wrapper (menü) kommt ins Layout, nicht die liste

Rechtsklickst du nun auf die Liste, erscheint das Menü an der Mausposition; der Handler eines Eintrags feuert bei dessen Auswahl.

34.10a-3 Inhalt & Branding: Markdown, Karten, Symbole, Accordion

Für formatierte Inhalte und ein eigenes Aussehen gibt es weitere Bausteine.

Markdowngui.markdown(text) zeigt formatierten Text: Überschriften mit #, fett, kursiv, Code, Aufzählungen und Links. Ideal für Hilfe- und Infotexte:

nimm hilfe = gui.markdown("# Hilfe\n\nDrücke **Speichern**, um zu sichern.\n\n- Punkt eins\n- Punkt zwei")

Hyperlinkgui.verweis(text, url) ist ein anklickbarer Link, der die Adresse im Browser öffnet:

nimm link = gui.verweis("Zur Webseite", "https://kiste-lang.org")

Titel-Kartegui.titelkarte(titel, untertitel, inhalt) ist eine Karte mit Titelzeile, Untertitel und einem Inhalts-Widget (die Fyne-„Card"). Anders als gui.karte (gemalter, farbiger Hintergrund) hat sie einen Textkopf:

nimm karte = gui.titelkarte("Einstellungen", "Anzeige", gui.vertikal([regler1, regler2]))

Symbolgui.symbol(alias) zeigt ein einzelnes Icon (dieselben Aliasse wie knopf_icon, z. B. "info", "warnung"); gui.symbol_bild(pfad) nimmt eine eigene Bilddatei.

Trennliniegui.trennlinie() ist ein dünner Strich, um in einer vertikal/horizontal Bereiche optisch zu trennen.

Accordiongui.akkordeon() mit gui.akkordeon_abschnitt(akk, titel, inhalt) baut aufklappbare Abschnitte (immer nur einer offen — spart Platz):

nimm akk = gui.akkordeon()
gui.akkordeon_abschnitt(akk, "Allgemein", allgemein_inhalt)
gui.akkordeon_abschnitt(akk, "Erweitert", erweitert_inhalt)

Eigenes Aussehen (Branding)gui.knopf_bild(text, bildpfad) gibt einem Knopf ein Icon aus einer eigenen Datei (statt der eingebauten Aliasse). In einer werkzeug_leiste gruppieren gui.werkzeug_trenner() und gui.werkzeug_abstand() die Werkzeuge (Trennstrich bzw. dehnbarer Abstand, der die folgenden Symbole nach rechts schiebt).

34.10a-4 Baum & sortierbare Tabellen

Ein Baum zeigt eine Hierarchie mit auf- und zuklappbaren Ästen — perfekt für einen Datei-Browser oder eine Gliederung. Du baust ihn Knoten für Knoten: jeder Knoten hat eine eindeutige ID, die ID seines Elternteils ("" = ganz oben) und eine Beschriftung. Ein Knoten mit Kindern wird automatisch zum aufklappbaren Ast.

nimm baum = gui.baum()
gui.baum_knoten(baum, "projekt", "", "Mein Projekt")
gui.baum_knoten(baum, "src", "projekt", "src")
gui.baum_knoten(baum, "src/main.ki", "src", "main.ki")
gui.baum_knoten(baum, "docs", "projekt", "docs")

funktion bei_wahl(b) {
    sag "Gewählt: {gui.baum_gewählt(b)}"     // liefert die ID des Knotens
}
gui.bei_baum_auswahl(baum, bei_wahl)

gui.baum_gewählt(baum) gibt die ID des angeklickten Knotens zurück; gui.baum_leeren(baum) entfernt alle Knoten (z. B. um den Baum neu aufzubauen).

Sortierbare Tabellegui.tabelle_sortierbar(tabelle) macht die Spaltenköpfe anklickbar: ein Klick sortiert die Zeilen nach dieser Spalte, ein zweiter Klick kehrt die Reihenfolge um (ein Pfeil ▲/▼ zeigt die aktive Spalte). Die Spaltenbreiten kannst du ohnehin mit der Maus ziehen.

nimm tab = gui.tabelle_wählbar(["Datei", "Größe"])
gui.zeile_anhängen(tab, ["zebra.txt", "300"])
gui.zeile_anhängen(tab, ["apfel.txt", "100"])
gui.tabelle_sortierbar(tab)     // Klick auf „Datei" sortiert alphabetisch

34.10a-5 App-Integration: Benachrichtigung, Zwischenablage, Schließen, Reiter

Damit sich ein Programm „fertig" anfühlt, gibt es ein paar Bausteine rund um das Betriebssystem und das Fenster.

Benachrichtigunggui.benachrichtige(titel, text) zeigt eine Desktop-Benachrichtigung des Betriebssystems (z. B. „Fertig gespeichert").

Zwischenablagegui.kopiere_text(text) schreibt, gui.hole_text() liest die Zwischenablage:

gui.kopiere_text("Hallo")
nimm zurück = gui.hole_text()     // "Hallo"

Schließen abfangen — mit gui.bei_fenster_schließen(fenster, handler) läuft dein Handler, wenn der Nutzer auf ✕ klickt — statt das Fenster zu schließen. So kannst du „Ungespeicherte Änderungen?" abfragen. Erst wenn du gui.fenster_schließe(fenster) aufrufst, schließt es wirklich:

funktion beim_schließen(fenster) {
    gui.dialog_frage(fenster, "Beenden?", "Wirklich schließen?", funktion(ja) {
        wenn ja == 1 { gui.fenster_schließe(fenster) }
    })
}
gui.bei_fenster_schließen(fenster, beim_schließen)

(gui.bei_fenster_geschlossen(fenster, handler) läuft dagegen NACH dem Schließen — zum Aufräumen.)

Reiter steuern — bei einem reiter liest gui.gewählter_reiter(reiter) den aktiven Tab (0 = erster), gui.setze_reiter(reiter, index) wählt einen, und gui.bei_reiter_wechsel(reiter, handler) feuert bei jedem Wechsel. Mit gui.reiter_hinzufügen_icon(reiter, titel, icon, inhalt) bekommt eine Lasche ein Icon:

nimm reiter = gui.reiter_neu()
gui.reiter_hinzufügen(reiter, "Allgemein", allgemein)
gui.reiter_hinzufügen_icon(reiter, "Info", "info", info_inhalt)
gui.bei_reiter_wechsel(reiter, funktion(r) { sag "Jetzt Tab {gui.gewählter_reiter(r)}" })

34.10b Politur: Icons, Teiler, Rand-Layout, Tastatur-Kürzel, Farb-Rahmen

Für „runde" Apps gibt es vier Bausteine:

Icons — Knöpfe und Werkzeug-Leisten mit Symbolen (deutsche Alias-Namen wie "speichern", "öffnen", "neu", "suchen", "einstellungen"):

nimm k = gui.knopf_icon("Speichern", "speichern")   // Icon + Text
// Echtes Icon-Toolbar:
nimm tb1 = gui.werkzeug_knopf("neu", funktion(i) {})
nimm tb2 = gui.werkzeug_knopf("speichern", funktion(i) {})
nimm leiste = gui.werkzeug_leiste([tb1, tb2])

Die verfügbaren Icon-Namen (deutsche Aliasse): "speichern", "öffnen", "neu", "löschen", "hinzufügen", "entfernen", "kopieren", "einfügen", "einstellungen", "suchen", "start", "stopp", "pause", "info", "warnung", "fehler", "frage", "zurück", "vor", "rückgängig", "wiederholen", "hoch", "runter", "home", "menü", "schließen", "bestätigen", "aktualisieren". Unbekannte Namen ergeben einfach kein Icon (kein Fehler). "rückgängig"/"wiederholen" sind die geschwungenen Undo/Redo- Pfeile (↶ ↷) — ideal für Knöpfe gui.knopf_icon("", "rückgängig").

Tooltips — ein nacktes Symbol ist ein Rätsel. gui.setze_tooltip(widget, text) hängt einem beliebigen Widget einen Hovertext an: bleibt die Maus kurz darüber, erscheint der Text. Ideal für Icon-Knöpfe (vor gui.zeige setzen) — es geht an jedem Widget, auch an einer gui.leinwand:

nimm k = gui.knopf_icon("", "speichern")
gui.setze_tooltip(k, "Speichern (Strg+S)")

Den Tooltip-Text darfst du jederzeit ändern — auch nach gui.zeige (z. B. wenn deine App die Sprache umschaltet): ein erneutes gui.setze_tooltip(k, "Save …") wirkt sofort beim nächsten Überfahren.

Teiler (ziehbar) und Rand-Layout:

nimm geteilt = gui.teiler_h(links, rechts, 30)   // 30 % links; 0 = mittig
nimm fenster_inhalt = gui.rand_layout(mitte, [leiste, fuss, 0, 0])
//                          mitte,   [oben, unten, links, rechts] (0 = leer)

Zentrieren und Verteilengui.zentriert und gui.dehnbar: In einer vertikal/horizontal-Reihe sitzen die Bausteine sonst links bzw. oben gepackt, der Rest bleibt als Leerraum. Zwei Helfer rücken das gerade:

// Ein Widget mittig im verfügbaren Platz:
gui.zeige(fenster, gui.zentriert(gui.etikett_neu("Titel")))

// Eine Gruppe zentrieren — dehnbare Platzhalter LINKS und RECHTS:
nimm leiste = gui.horizontal([gui.dehnbar(), knopf_a, knopf_b, gui.dehnbar()])

// Elemente an die beiden Enden schieben — ein Platzhalter in der Mitte:
nimm zeile = gui.horizontal([links, gui.dehnbar(), rechts])

gui.dehnbar() ist ein dehnbarer Platzhalter: er frisst den übrigen Platz. Ein Platzhalter zwischen zwei Widgets drückt sie auseinander; je einer außen zentriert die Gruppe; mehrere teilen sich den Rest gleichmäßig. So baust du zentrierte oder über die Breite verteilte Leisten ohne feste Pixelmaße.

Tastatur-KürzelStrg/Umschalt/Alt:

gui.taste(fenster, "Strg+S", funktion(f) {})

Auch die „Standard"-Kürzel Strg+Z, Strg+Y, Strg+C, Strg+V, Strg+X und Strg+A lassen sich so belegen und lösen zuverlässig aus (z. B. Rückgängig/Wiederholen oder Kopieren/Einfügen im eigenen Programm). Ein gerade fokussiertes Eingabefeld bekommt sein eigenes Kopieren/Einfügen — dein Kürzel greift, wenn kein Eingabefeld den Fokus hat.

Farb-Rahmen — ein Widget mit farbigem Hintergrund (Karte). Die Farbe lässt sich auch zur Laufzeit ändern (z. B. ein Saldo grün/rot je nach Vorzeichen):

nimm karte = gui.farb_rahmen(status, "#1e3a2f")
gui.setze_rahmen_farbe(karte, "#3a1e1e")   // live umfärben (rot)

Für einen weicheren Look gibt es gui.karte — wie farb_rahmen, aber die Hintergrund-Karte ist abgerundet und sanft berandet (schön zum Gruppieren, z. B. Panels/Werkzeug-Bereiche):

nimm gruppe = gui.karte(inhalt, "#141f33", "#334155", 12, 1)   // füllfarbe, randfarbe, radius, randdicke

Auch eine gui.karte färbst du live mit gui.setze_rahmen_farbe um.

Bildlauf (Scrollen) — wird ein Inhalt größer als der Platz im Fenster, wickelst du ihn in einen Scroll-Bereich. Dann erscheint eine Bildlaufleiste, statt dass das Fenster überläuft:

// Eine lange Spalte in einem begrenzten, scrollbaren Bereich:
nimm lang = gui.vertikal([ /* … viele Zeilen … */ ])
nimm bereich = gui.scroll_vertikal(lang)          // nur vertikal
gui.zeige(fenster, gui.feste_größe(bereich, 240, 300))

Drei Varianten: gui.scroll(widget) (beide Richtungen), gui.scroll_vertikal(widget) (nur hoch/ runter) und gui.scroll_horizontal(widget) (nur links/rechts). Wichtig: Ein Scroll-Bereich braucht einen begrenzenden Rahmen — meist gui.feste_größe(...) oder einen Teiler/rand_layout. Ohne Begrenzung wächst er einfach mit dem Inhalt und scrollt nie.

Kachel-Rastergui.kachel_raster(zellbreite, zellhöhe, [kinder]) ordnet gleich große Zellen an und bricht automatisch um, wenn die Reihe voll ist. Anders als raster (feste Spaltenzahl) passt sich die Spaltenzahl der Fensterbreite an — ideal für Galerien, Dashboards oder Werkzeug-Paletten:

nimm galerie = gui.kachel_raster(80, 44, [knopf_a, knopf_b, knopf_c, knopf_d, knopf_e])
gui.zeige(fenster, gui.scroll_vertikal(galerie))   // wird's zu viel, scrollt es

Teiler-Position merken — die Position eines teiler_h/teiler_v liest du mit gui.teiler_wert(teiler) (Prozent 1–99) und setzt sie mit gui.setze_teiler_wert(teiler, prozent). So kannst du z. B. die Breite einer Seitenleiste speichern und beim nächsten Start wiederherstellen:

nimm t = gui.teiler_h(seitenleiste, inhalt, 30)
gui.setze_teiler_wert(t, 25)              // Seitenleiste schmaler
nimm jetzt = gui.teiler_wert(t)           // aktuelle Position ablesen (z. B. nach dem Ziehen)

Tipp: Die Handler dürfen Closures sein, die lokale Variablen fangen (siehe Kap. 8.7) — kein Umweg über globale Variablen nötig. Beispiel: beispiele/gui_politur.ki.

Farben — was heute geht: die 5 Knopf-Rollen (gui.setze_wichtigkeit(knopf, "primär"/ "normal"/"gefahr"/"erfolg"/"warnung")), globale Farben (gui.setze_farbe(rolle, hex) + gui.thema(...)) und der Farb-Rahmen (farb_rahmen + setze_rahmen_farbe). Beliebige freie Hintergrund-/Textfarbe pro Standard-Widget ist eine Fyne-Grenze (kommt als eigene Phase).

34.10c Leinwand: frei zeichnen mit eigenen Farben

Bisher hast du fertige Widgets benutzt (Knöpfe, Listen, Tabellen). Manchmal willst du aber selbst zeichnen — ein Diagramm, ein Spielfeld, ein Muster — und dabei jede beliebige Farbe wählen, nicht nur die Theme-Rollen. Dafür gibt es die Leinwand:

nutze gui

nimm bild = gui.leinwand(400, 300)             // Zeichenfläche (Breite, Höhe in Pixel)

gui.rechteck(bild, 10, 10, 100, 50, "#3b82f6") // x, y (Ecke), breite, höhe, farbe
gui.kreis(bild, 200, 150, 30, "#dc2626")        // x, y (Mittelpunkt), radius, farbe
gui.linie(bild, 50, 50, 300, 250, "#16a34a", 2) // x1, y1, x2, y2, farbe, dicke
gui.text_an(bild, 20, 270, "Hallo", "#e4e4e7", 14) // x, y, text, farbe, größe

nimm f = gui.fenster_öffne("Leinwand", 420, 320)
gui.zeige(f, bild)                             // Leinwand wie jedes Widget einhängen
gui.starte()

Farben gibst du als Hex-Text an ("#rrggbb", z. B. "#3b82f6" = Blau). Das sind beliebige Farben — du bist nicht auf die fünf Theme-Rollen beschränkt.

Text zentrieren: gui.text_breite(text, größe) gibt die Pixelbreite eines Textes in der jeweiligen Schriftgröße zurück (dieselbe Schrift wie gui.text_an). Damit platzierst du Text exakt mittig statt an einer geschätzten Stelle — die halbe Breite links vom Mittelpunkt:

nimm txt = "Spiel vorbei"
gui.text_an(bild, 200 - gui.text_breite(txt, 24) div 2, 100, txt, "#e4e4e7", 24) // mittig um x=200

Abgerundete Rechtecke für einen weicheren Look zeichnest du mit gui.rechteck_rund:

gui.rechteck_rund(bild, 10, 10, 100, 60, 12, "#3b82f6", "", 0)      // rundes Feld, Radius 12
gui.rechteck_rund(bild, 130, 10, 100, 60, 12, "#3b82f6", "#0f172a", 2) // + dunkler Rand (2 px)
gui.rechteck_rund(bild, 250, 10, 100, 60, 12, "", "#0f172a", 2)     // NUR Rand (leere Füllung "")

Die Reihenfolge ist (leinwand, x, y, breite, höhe, radius, füllfarbe, randfarbe, randdicke). Eine leere Farbe "" (oder randdicke 0) lässt Füllung bzw. Rand weg — so malst du runde Farbflächen ebenso wie reine Umrisse.

Koordinaten zählen ab oben-links: x nach rechts, y nach unten, in Pixeln.

Neu zeichnen: Mit gui.leeren(bild) wischst du alles weg und zeichnest neu — auch aus einem Handler heraus, z. B. auf Knopfdruck:

nimm bild = gui.leinwand(300, 200)
gui.rechteck(bild, 20, 20, 120, 80, "#3b82f6")

nimm knopf = gui.knopf_neu("Umfärben")
gui.bei_klick(knopf, funktion(k) {
    gui.leeren(bild)                            // altes Bild weg
    gui.rechteck(bild, 20, 20, 120, 80, "#f59e0b") // neu in Bernstein
})

So baust du heute schon rundenbasierte Spiele (Brett neu zeichnen pro Zug) und freie Mal-Programme. (Animation pro Zeit-Takt kommt mit einem späteren Timer-Baustein.)

Vollständiges Beispiel: beispiele/gui_leinwand.ki.

34.10d Animation: die Leinwand bewegen mit bei_zeit

Mit der Leinwand kannst du zeichnen — mit einem Timer kannst du das Bild immer wieder neu zeichnen und so bewegte Grafik machen. gui.bei_zeit(ms, handler) ruft deinen Handler alle ms Millisekunden auf:

nutze gui

nimm bild = gui.leinwand(360, 200)
nimm x = 20
nimm dx = 5

funktion takt() {
    x = x + dx
    wenn x > 340 { dx = 0 - 5 }    // am rechten Rand umkehren
    wenn x < 20 { dx = 5 }         // am linken Rand umkehren
    gui.leeren(bild)
    gui.rechteck(bild, 0, 0, 360, 200, "#0f172a")  // Hintergrund
    gui.kreis(bild, x, 100, 16, "#38bdf8")          // wandernde Kugel
}

nimm uhr = gui.bei_zeit(16, funktion(t) {           // ~60-mal pro Sekunde
    takt()
})

nimm stop = gui.knopf_neu("Stop")
gui.bei_klick(stop, funktion(k) {
    gui.stoppe_zeit(uhr)                            // Animation anhalten
})

nimm f = gui.fenster_öffne("Animation", 380, 270)
gui.zeige(f, gui.vertikal([bild, stop]))
gui.starte()
  • gui.bei_zeit(ms, handler) gibt ein Handle zurück — damit kannst du den Timer später mit gui.stoppe_zeit(handle) anhalten.
  • 16 Millisekunden ≈ 60 Bilder pro Sekunde (flüssig). Größere Zahl = langsamer.
  • gui.nach_zeit(ms, handler) ruft den Handler nur einmal nach ms auf — praktisch für „nach 3 Sekunden einblenden" o. Ä.

So entstehen kleine Spiele und Animationen: jeder Takt bewegt etwas und zeichnet die Leinwand neu.

Vollständiges Beispiel: beispiele/gui_animation.ki.

34.10e Eingabe: Tastatur und Maus

Für Spiele willst du steuern — mit der Tastatur und der Maus. Beide folgen dem gleichen Muster wie die anderen Handler: dein Handler bekommt das Fenster bzw. die Leinwand, und du liest den Wert mit einem Getter.

Tastatur (auf dem Fenster):

gui.bei_taste(fenster, funktion(f) {
    nimm taste = gui.letzte_taste(f)
    wenn taste == "links" { x = x - 10 }
    wenn taste == "rechts" { x = x + 10 }
    wenn taste == "hoch" { y = y - 10 }
    wenn taste == "runter" { y = y + 10 }
    zeichne()
})

gui.letzte_taste(fenster) gibt einen deutschen Namen für die Spiel-Tasten zurück: "links", "rechts", "hoch", "runter", "leer" (Leertaste), "escape", "eingabe". Buchstaben kommen als Großbuchstabe ("W", "A", "S", "D", …).

Maus (auf der Leinwand):

gui.bei_maus_klick(bild, funktion(b) {
    nimm mx = gui.maus_x(b)
    nimm my = gui.maus_y(b)
    gui.kreis(bild, mx, my, 6, "#f59e0b")   // Punkt an die Klickstelle
})

gui.maus_x(leinwand) / gui.maus_y(leinwand) geben die Klickposition in Pixeln (Leinwand-lokal, oben-links = 0,0) — dieselbe Zählweise wie beim Zeichnen.

Neben dem Klick gibt es drei weitere Maus-Ereignisse auf der Leinwand:

  • gui.bei_maus_bewegung(leinwand, handler) — feuert, wenn die Maus sich über der Leinwand bewegt (ohne gedrückte Taste). Position wieder über gui.maus_x/gui.maus_y.

  • gui.bei_maus_ziehen(leinwand, handler) — feuert beim Ziehen (Bewegung mit gedrückter Taste). Zusätzlich liefern gui.zieh_dx(leinwand) / gui.zieh_dy(leinwand) die Verschiebung seit dem letzten Schritt (praktisch fürs Schieben).

  • gui.bei_maus_los(leinwand, handler) — feuert einmal beim Loslassen nach einem Ziehen (Drag-Ende); die Endposition steht in gui.maus_x/ gui.maus_y. Damit baust du Werkzeuge, die man aufzieht: drücken (bei_maus_klick), ziehen mit Live-Vorschau (bei_maus_ziehen), loslassen und festschreiben (bei_maus_los) — genau das Muster hinter Linie und Rechteck in Mal-Programmen.

  • gui.bei_maus_rechts(leinwand, handler) (seit 0.9.24) — feuert beim Rechtsklick auf die Leinwand; die Position steht wie immer in gui.maus_x/gui.maus_y.

  • gui.maus_taste(leinwand) (seit 0.9.24) — welche Taste die laufende Aktion ausgelöst hat: 1 = links, 2 = rechts, 0 = noch keine. Lesbar in allen Maus-Handlern. Auch Ziehen mit der rechten Taste funktioniert: es feuert bei_maus_ziehen (mit maus_taste = 2) und am Ende bei_maus_los. Nach so einem Rechts-Zug bleibt bei_maus_rechts bewusst aus — sonst setzte jeder Strich am Endpunkt noch einen Punkt.

    gui.bei_maus_ziehen(leinwand, funktion(l) {
        wenn gui.maus_taste(l) == 2 { radiere(l) } sonst { male(l) }
    })

    So bekommt ein Mal-Programm die vertraute Belegung: links malen, rechts radieren.

  • gui.bei_maus_rad(leinwand, handler) — feuert beim Drehen des Mausrads über der Leinwand. Wie weit gedreht wurde, liefern gui.rad_dy(leinwand) (senkrecht, grösser 0 = nach oben/vorwärts) und gui.rad_dx(leinwand) (waagerecht, meist 0). Praktisch zum Zoomen oder Scrollen: wenn gui.rad_dy(l) > 0 { hineinzoomen } sonst { herauszoomen }.

  • gui.leinwand_elastisch() — eine Leinwand ohne feste Größe: statt gui.leinwand(b, h) füllt sie ihren Platz und wächst mit dem Fenster/Vollbild mit. Platziere sie in einen füllenden Slot (z. B. die Mitte von gui.rand_layout). Ihre aktuelle Größe holst du mit gui.leinwand_breite(l) / gui.leinwand_höhe(l), und gui.bei_größe(l, handler) feuert bei jeder Größenänderung — dort rechnest du Zoom/Zentrierung neu und zeichnest passend. Mit gui.vollbild(fenster, an) (an = 1/0) schaltest du Vollbild ein/aus.

  • gui.bildschirm_breite() / gui.bildschirm_höhe() — die Größe des Bildschirms (genauer: die Arbeitsfläche des primären Monitors, also ohne Taskleiste), in denselben logischen Pixeln, die auch gui.fenster_öffne erwartet. Damit passt du die Fenstergröße an jeden Monitor an: auf kleinen Bildschirmen deckelst du das Fenster (es soll nicht über den Rand ragen), auf großen nutzt du den Platz. Beide sind schon vor gui.starte() aufrufbar — genau dann, wenn du die Startgröße festlegst. Ist keine Bildschirminfo verfügbar, liefern sie 0 (dann nimmst du eine feste Größe):

    nimm h = gui.bildschirm_höhe()
    nimm fensterhöhe = 1000                                  // Wunschgröße
    wenn h > 0 und fensterhöhe > h - 60 { fensterhöhe = h - 60 }   // auf kleinen Schirmen deckeln
    nimm fenster = gui.fenster_öffne("Meine App", gui.bildschirm_breite(), fensterhöhe)

    Beide liefern den primären Monitor. Bei mehreren, unterschiedlich skalierten Monitoren kann das Fenster auf einem anderen landen — willst du sicher den aktuellen Monitor füllen (egal welcher), nutze gui.maximiere oder gui.vollbild (siehe nächster Punkt).

  • gui.maximiere(fenster) — maximiert das Fenster wie der Maximieren-Knopf: es füllt die Arbeitsfläche des aktuellen Monitors (mit richtiger Skala), behält aber die Titelleiste mit Minimieren/Maximieren/Schließen und bleibt verschieb-, verkleiner- und wiederherstellbar. Das ist der Unterschied zu gui.vollbild(fenster, 1) (randlos, ohne Titelleiste/X — gut für ein spielfüllendes Fenster, aber der Nutzer kommt nur über einen Umschalter wieder heraus). Für „groß, aber ein normales Fenster" ist gui.maximiere die richtige Wahl. Rufe es auf, wenn das Fenster existiert (nach gui.starte()), am besten als Reaktion auf eine Nutzeraktion (Knopf/ Menü):

    nimm knopf = gui.knopf_neu("Fenster füllen")
    gui.bei_klick(knopf, funktion(k) { gui.maximiere(fenster) })

    Hinweis: Ruf gui.maximiere nicht im Wettlauf mit dem allerersten Fenster-Aufbau (unmittelbar beim Start) auf — auf Systemen mit hoher DPI-Skalierung kann das GUI-Backend die Größe während seines Init-Layouts sonst wieder zurücksetzen. Für ein großes Fenster gleich beim Start ist der Maximieren-Knopf der Titelleiste (den jedes normale Fenster hat) am zuverlässigsten.

  • gui.setze_cursor(leinwand, name) — welchen Mauszeiger die Leinwand zeigt. Praktisch für Mal-Programme, wo jedes Werkzeug einen eigenen Zeiger hat: "kreuz" (Fadenkreuz zum Zeichnen/ Auswählen), "hand" (Hand zum Schwenken), "pipette" (ein Pipetten-Symbol zum Farbe-Aufnehmen), "text" (Text-Cursor) und "standard" (der normale Pfeil). Der Zeiger wechselt, sobald die Maus die Leinwand berührt. Nur Leinwände mit einem Maus-Handler (bei_maus_klick & Co.) tragen einen eigenen Cursor. Ruf es beim Werkzeugwechsel auf:

    gui.bei_maus_klick(bild, funktion(b) {})   // macht die Leinwand cursor-fähig
    gui.setze_cursor(bild, "pipette")             // zeigt jetzt das Pipetten-Symbol
  • gui.setze_cursor_bild(leinwand, pfad, hotx, hoty) — der Cursor wird ein eigenes Bild: eine SVG- oder PNG-Datei (relativ neben dem Programm gesucht, genau wie bei gui.bild_an). So kann jedes Werkzeug sein eigenes Icon als Cursor zeigen — z. B. denselben Icon-Satz wie in der Werkzeug- leiste. hotx/hoty sagen, welcher Punkt im Bild genau unter der Mausspitze liegt (bei einem Stift z. B. die Spitze). SVGs werden scharf gerastert und bekommen einen feinen weißen Saum, damit sie auf hellem und dunklem Grund sichtbar sind:

    gui.setze_cursor_bild(bild, "icons/stift.svg", 3, 21)   // das Stift-Icon als Cursor, Spitze unten links

    Beide wirken sofort — auch wenn du den Cursor per Tastenkürzel wechselst, ohne die Maus zu bewegen.

Damit baust du echte steuerbare Spiele: die Tastatur bewegt die Spielfigur, die Maus klickt aufs Spielfeld. (Tipp: in zeichne() immer erst gui.leeren(bild), dann alles neu malen.)

Wichtig für flüssige Bewegung: Bewege die Figur nicht direkt im bei_taste-Handler (x = x - 10). Eine gehaltene Taste löst beim Betriebssystem erst einen Druck aus, dann eine Pause, dann die Tastenwiederholung — das ruckelt. Der Trick aus echten Spielen: der Timer (gui.bei_zeit, Abschnitt 34.10d) bewegt die Figur jeden Takt, und mit gui.taste_gedrückt(fenster, taste) (gibt 1 wenn die Taste gerade gehalten wird, sonst 0) bewegst du sie nur solange gedrückt — sie stoppt beim Loslassen:

gui.bei_zeit(16, funktion(t) {        // ~60×/Sekunde
    wenn gui.taste_gedrückt(fenster, "links") == 1 { x = x - 4 }
    wenn gui.taste_gedrückt(fenster, "rechts") == 1 { x = x + 4 }
    wenn gui.taste_gedrückt(fenster, "hoch") == 1 { y = y - 4 }
    wenn gui.taste_gedrückt(fenster, "runter") == 1 { y = y + 4 }
    zeichne()
})

So fühlt sich die Steuerung flüssig und natürlich an (bewegen beim Halten, stoppen beim Loslassen). Für Snake-artige Spiele (drücken → Richtung wechseln, Figur läuft selbst weiter) setzt du stattdessen im bei_taste-Handler nur eine Richtungs-Variable, die der Timer nutzt.

Vollständiges Beispiel: beispiele/gui_eingabe.ki.

34.10f Bilder und Sprites auf der Leinwand

Bis jetzt hast du mit Rechtecken, Kreisen, Linien und Text gemalt. Für echte Spiele willst du Bilder — eine Spielfigur, eine Münze, ein Hintergrund. Mit gui.bild_an lädst du eine Bilddatei (PNG, JPG oder GIF) und zeichnest sie wie jedes andere Primitiv auf die Leinwand:

gui.bild_an(bild, "bilder/münze.png", 100, 80, 40, 40)
//          Leinwand  Pfad             x    y  breite höhe
  • Pfad ist der Dateiname, relativ zu deiner .ki-Datei — leg die Bilder einfach neben (oder in einen Unterordner zu) dein Programm, z.B. bilder/münze.png. (Ein absoluter Pfad geht auch.) Das funktioniert egal, von wo aus du startest.
  • x, y ist die obere-linke Ecke (gleiche Zählweise wie beim Rechteck), in Pixeln.
  • breite, höhe ist die Zielgröße: das Bild wird in diese Box gezeichnet. So kannst du dasselbe kleine Sprite groß oder klein zeichnen. Beim Vergrößern bleibt Pixel-Grafik scharf (Nearest-Neighbor) — kleine Pixel werden zu klaren Kästchen statt verwaschen, genau richtig für Pixel-Kunst.

Ein bewegtes Sprite baust du genau wie eine bewegte Form — pro Timer-Takt leeren und an der neuen Position neu zeichnen:

nimm bild = gui.leinwand(400, 300)
nimm x = 40
nimm y = 40

funktion zeichne() {
    gui.leeren(bild)
    gui.rechteck(bild, 0, 0, 400, 300, "#0f172a")     // Hintergrund
    gui.bild_an(bild, "bilder/münze.png", x, y, 40, 40)  // Sprite (Pfad relativ zur .ki)
}

zeichne()
nimm f = gui.fenster_öffne("Sprites", 401, 301)
gui.bei_zeit(16, funktion(t) {
    x = x + 2
    wenn x > 360 { x = 0 }
    zeichne()
})
gui.zeige(f, bild)
gui.starte()

Schnell von Haus aus: Kiste lädt jede Bilddatei nur einmal und merkt sie sich. Auch wenn du dasselbe Bild 60-mal pro Sekunde neu zeichnest (wie oben im Timer), wird die Datei nur beim ersten Mal von der Festplatte gelesen — danach ist sie schnell im Speicher. Du musst dich um nichts kümmern.

Findet Kiste die Datei nicht (Tippfehler im Pfad, Datei fehlt), wird einfach nichts gezeichnet und eine kurze Warnung erscheint — dein Spiel stürzt nicht ab.

Vollständiges Beispiel: beispiele/gui_sprites.ki (hüpfende Münze + festes Sprite).

Ein selbst gebautes Bild zeichnen — gui.leinwand_bild. gui.bild_an lädt eine Datei. Manchmal hast du das Bild aber schon im Speicher — z.B. eines, das du mit dem bild-Modul (nutze bild) selbst zusammengesetzt hast. Dann zeichnest du es direkt aus seinem Handle, ohne den Umweg über die Festplatte:

nutze gui
nutze bild

nimm b = bild.neu(4, 4)                       // ein winziges 4×4-Bild im Speicher …
bild.setze_block(b, 1, 1, 2, 2, 40, 90, 200)  // … einen blauen 2×2-Block hinein

nimm lein = gui.leinwand(200, 200)
gui.leinwand_bild(lein, b, 0, 0, 200, 200)    // 50× vergrößert, scharf, EIN Objekt
//                Leinwand  Bild  x  y  breite höhe
  • Der zweite Wert ist ein bild-Handle (aus dem bild-Modul), kein Dateipfad.
  • x, y, breite, höhe sind wie bei gui.bild_an: obere-linke Ecke und Zielgröße, in die das Bild gestreckt wird.
  • Das Bild wird pixelgenau vergrößert (Nearest-Neighbor): kleine Pixel bleiben scharfe Kästchen statt verwaschen zu werden — genau richtig für Pixel-Grafik.

Warum das schnell ist. Eine ganze Pixel-Fläche (etwa ein Spielfeld oder eine Mal-Leinwand) könntest du auch Zelle für Zelle mit vielen gui.rechteck malen — aber das sind schnell Tausende Zeichenbefehle pro Bild, und ab ~64×64 Zellen ruckelt es bei jeder Mausbewegung. gui.leinwand_bild zeichnet den ganzen Inhalt als ein einziges Objekt: du baust dein bild einmal auf und zeigst es mit einem Aufruf — gleich schnell, egal ob 16×16 oder 256×256. (Genau so rendert der Pixel-Editor KistePix seine Leinwand.)

Ein stehendes Bild für sehr große Leinwände — gui.leinwand_bild_fest. Bei jedem Neuzeichnen einer Leinwand baut das Programm normalerweise alle Objekte neu auf. Für ein großes Leinwandbild (z. B. 1500×1000 Pixel) heißt das: das ganze Bild wird jedes Mal neu übernommen — auch wenn du nur einen Auswahlrahmen ziehst und der Bildinhalt sich gar nicht ändert. gui.leinwand_bild_fest macht das Bild zu einer stehenden Ebene: dasselbe Objekt bleibt erhalten, und neu übernommen wird nur, wenn du es sagst.

// neu = 1 -> Bild wurde geändert, neu übernehmen; neu = 0 -> unverändert lassen
gui.leinwand_bild_fest(lein, b, 0, 0, 200, 200, 1)
//                     Leinwand Bild x y breite höhe neu

Der letzte Wert neu steuert alles: setze ihn auf 1, wenn sich der Bildinhalt oder die Größe geändert hat (dann wird das Bild frisch übernommen), sonst auf 0 (dann bleibt das schon gezeigte Bild stehen). Vorschau, Gitter und Ähnliches malst du wie gewohnt darüber — die sind leicht und dürfen jedes Mal neu entstehen; nur das teure Leinwandbild bleibt liegen. So bleibt das Ziehen von Formen oder das Schwenken auch bei riesigen Leinwänden flüssig. (KistePix nutzt das für Aseprite-große Bilder.)

34.10g Klang und Musik — nutze klang

Ein Spiel ohne Ton ist halb so schön. Das klang-Modul spielt Klänge ab — kurze Effekte (Münze gefangen, Sprung) und Hintergrundmusik. Es ist ein eigenes Modul neben gui:

nutze gui
nutze klang

nimm ding = klang.lade("klaenge/muenze.wav")   // einmal laden → Handle
klang.spiele(ding)                              // Effekt abspielen
  • klang.lade(pfad) lädt eine Audiodatei (WAV, MP3 oder OGG) und gibt ein Handle zurück. Lade einen Klang einmal (z.B. am Programmstart) und spiele ihn dann beliebig oft — das ist schnell, weil die Datei nur einmal von der Festplatte gelesen wird. Der Pfad ist relativ zu deiner .ki-Datei (wie bei Bildern).
  • klang.spiele(handle) spielt den Klang ab. Es wartet nicht — dein Programm läuft sofort weiter, und mehrere Klänge dürfen sich überlagern (Effekt über Musik).
  • klang.spiele_schleife(handle) spielt in Endlosschleife — ideal für Hintergrundmusik. klang.stoppe(handle) hält die Schleife wieder an.
  • Kurzform klang.spiele_datei(pfad) lädt + spielt in einem Rutsch (ohne Handle) — praktisch für einen einmaligen Klang.

Im Spiel rufst du den Effekt einfach an der passenden Stelle auf, z.B. wenn die Münze gefangen wird:

wenn gefangen {
    punkte = punkte + 1
    klang.spiele(ding)
    naechste_muenze()
}

Klang braucht eine laufende App. Spiele Klänge in einem GUI-Programm (mit gui.starte()) — in einem Programm, das sofort endet, wird der Ton abgeschnitten. Findet Kiste die Datei nicht, bleibt es einfach still (kurze Warnung) — kein Absturz.

Audio ANALYSIEREN statt abspielen (seit 2026-06-12): klang.lese_samples(pfad) gibt die dekodierten Samples als liste<komma> heraus (Mono, Werte −1 bis +1) und klang.lese_abtastrate(pfad) die Abtastrate der Datei — damit kannst du z.B. Schläge in einem Lied erkennen (siehe beispiele/taktgeber/). Beide werfen einen fangbaren Fehler bei kaputten/fehlenden Dateien (anders als lade: ein Analyse-Werkzeug muss den Fehler zeigen können). Achtung Speicher: ein 3-Minuten-Song sind ~63 MB.

Vollständige Beispiele: beispiele/gui_klang.ki (Effekt + Musik-Schleife) und beispiele/gui_spiel.ki („Fang die Münze" mit Ding beim Fangen).

34.11 Spickzettel gui

Aufgabe Funktion
Fenster gui.fenster_öffne(titel, breite, höhe) · gui.zeige(fenster, inhalt) (vor starte() = Hauptfenster-Inhalt; nach starte() = öffnet ein eigenes zweites Fenster) · gui.fenster_schließe(fenster) (zweites Fenster wieder zu; erneut per zeige öffenbar) · gui.starte()
Knopf gui.knopf_neu(text) · gui.knopf_icon(text, icon) · gui.bei_klick(knopf, handler)
Etikett gui.etikett_neu(text) · gui.setze_text(widget, text)
Icons/Leiste gui.werkzeug_knopf(icon, handler) · gui.werkzeug_leiste([…]) · gui.setze_tooltip(widget, text) (Hovertext beim längeren Darüberfahren; vor gui.zeige setzen)
Layout-Politur gui.teiler_h/teiler_v(a, b, pos) · gui.rand_layout(mitte, [o,u,l,r]) · gui.farb_rahmen(widget, hex) · gui.karte(widget, füllfarbe, randfarbe, radius, randdicke) (abgerundete, sanft berandete Karte) · gui.setze_rahmen_farbe(rahmen, hex) (live)
Inhalt & Branding gui.markdown(text) (formatierter Text) · gui.verweis(text, url) (Link-Widget) · gui.titelkarte(titel, untertitel, inhalt) (Card mit Textkopf) · gui.symbol(alias) / gui.symbol_bild(pfad) · gui.trennlinie() · gui.akkordeon() + gui.akkordeon_abschnitt(akk, titel, inhalt) · gui.knopf_bild(text, bildpfad) · gui.werkzeug_trenner() / gui.werkzeug_abstand() (in werkzeug_leiste)
Tastatur gui.taste(fenster, "Strg+S", handler)
Layout gui.vertikal([…]) · gui.horizontal([…]) · gui.raster(spalten, […]) · gui.kachel_raster(zellbreite, zellhöhe, […]) (umbrechendes Gitter, Spaltenzahl passt sich der Breite an) · gui.zentriert(widget) (mittig im verfügbaren Platz) · gui.dehnbar() (dehnbarer Platzhalter: [dehnbar(), …, dehnbar()] zentriert eine Gruppe, [a, dehnbar(), b] schiebt auseinander) · gui.feste_größe(widget, breite, höhe) → Wrapper-Handle: das Widget bekommt EXAKT diese Größe (zentriert); der Wrapper kommt ins Layout, Events/Setter bleiben am Original
Bildlauf & Teiler gui.scroll(widget) (beide Richtungen) · gui.scroll_vertikal(widget) · gui.scroll_horizontal(widget) — bildlaufbarer Bereich; braucht einen begrenzenden Rahmen (feste_größe/Teiler/rand_layout), sonst wächst er mit dem Inhalt · gui.teiler_h/teiler_v(a, b, pos) · gui.teiler_wert(teiler) (Position 1–99 % lesen) · gui.setze_teiler_wert(teiler, prozent) (live setzen)
Farben gui.thema("dunkel"/"hell") · gui.setze_farbe(rolle, hex) (live aus Handlern) · gui.setze_wichtigkeit(knopf, "primär"/"gefahr"/"erfolg"/"warnung"/"normal") · gui.setze_knopf_farbe(knopf, hex) (eigene Hintergrundfarbe für EINEN Knopf, live umfärbbar; ersetzt optisch die Wichtigkeit)
Eingabe gui.eingabefeld(platzhalter) · gui.textbereich(platzhalter[, zeilen]) (mehrzeilig) · gui.passwortfeld(platzhalter) (maskiert) · gui.wert(feld) · gui.setze_wert(feld, text) · gui.bei_änderung(feld, handler)
Validierung gui.setze_prüfung(feld, muster, fehlertext) (Regexp; ungültig → Fehler am Feld + Formular-Absenden gesperrt) · gui.setze_pflicht(feld, fehlertext) (Feld darf nicht leer sein). Muster mit {} meiden (Interpolation) — z. B. \d\d\d\d statt \d{4}
Combobox & Datum gui.combofeld([…]) (frei tippbar + Vorschläge) · gui.datumsfeld() · gui.datum_wert(feld) (Text „TT.MM.JJJJ") — beide mit wert/setze_wert/bei_änderung
Formular gui.formular() · gui.formular_zeile(f, "Etikett", widget) · gui.formular_absenden(f, "Text", handler) (Absenden erst aktiv, wenn alle geprüften Felder gültig) · gui.formular_abbrechen(f, "Text", handler)
Komma-Schieberegler gui.schieberegler_komma(min, max, schritt) (Komma-Literale mit f: 0.1f) · gui.schieber_komma_wert(s) · gui.setze_schieber_komma_wert(s, wert) · gui.bei_schieben(s, handler)
Auswahl-Box gui.auswahl_box([…]) · gui.gewählt(box) · gui.bei_wahl(box, handler)
Liste gui.liste_anzeige([…]) · gui.gewählter_index(l) · gui.gewählter_wert(l) · gui.bei_auswahl(l, handler) · gui.bei_doppelklick(l, handler) · gui.setze_einträge(l, […]) (ersetzt alle, hebt Auswahl auf) · gui.eintrag_anhängen(l, text) · gui.eintrag_entfernen(l, index) · gui.einträge_anzahl(l) — Listen wachsen jetzt zur Laufzeit wie Tabellen
Tabelle gui.tabelle([spalten]) (editierbar) · gui.tabelle_wählbar([spalten]) (Zeilen-Auswahl) · gui.zeile_anhängen(t, [zellen]) · gui.zeile_entfernen(t, index) · gui.zeilen_anzahl(t) · gui.zell_wert(t,z,s) · gui.setze_zell_wert(t,z,s,text) · gui.gewählte_zeile/_spalte(t) · gui.bei_zell_auswahl/_änderung(t, handler) · gui.bei_doppelklick(t, handler) (tabelle_wählbar) · gui.tabelle_sortierbar(t) (Klick auf Spaltenkopf sortiert, ▲/▼)
Baum gui.baum() · gui.baum_knoten(baum, id, eltern_id, titel) (eltern_id "" = Wurzel) · gui.baum_gewählt(baum) (ID des gewählten Knotens) · gui.bei_baum_auswahl(baum, handler) · gui.baum_leeren(baum)
Dialoge gui.dialog_info(fenster, titel, text) · gui.dialog_frage(fenster, titel, text, handler) · gui.dialog_datei(fenster, handler) · gui.dialog_speichern(fenster, handler) · gui.gewählter_pfad(fenster)
Dialoge (Profi) gui.dialog_eingabe(fenster, titel, text, handler) + gui.eingabe_wert(fenster) · gui.dialog_farbe(fenster, titel, handler) + gui.farbe_wert(fenster) · gui.dialog_inhalt(fenster, titel, widget, schließen_text) · gui.dialog_ordner(fenster, handler) · gui.dialog_datei_gefiltert/speichern_gefiltert(fenster, [endungen], handler) · gui.dialog_warten(fenster, titel, text) / gui.dialog_fortschritt(...) + gui.setze_dialog_fortschritt(d, prozent) + gui.dialog_schließe(d)
Kontextmenü gui.kontext_menü(widget) (Rechtsklick-Wrapper; der Menü-Handle kommt ins Layout) · gui.kontext_eintrag(menü, "Titel", handler) · gui.kontext_trenner(menü)
Menü gui.menü_leiste(fenster) · gui.menü_hinzufügen(leiste, titel) · gui.menü_eintrag(menü, titel, handler) · gui.menü_trenner(menü) · gui.menü_anwenden(fenster) (Leiste neu anwenden nach Umbeschriften)
Leisten gui.werkzeug_leiste([knöpfe]) · gui.status_leiste(text)
Kontrollkästchen gui.kontrollkästchen(text) · gui.angehakt(k) (1/0) · gui.setze_angehakt(k, an) · gui.bei_umschaltung(k, handler)
Radio-Knöpfe gui.radio_knöpfe([…]) · gui.gewählt(r) · gui.bei_wahl(r, handler)
Schieberegler gui.schieberegler(min, max) · gui.schieber_wert(s) · gui.setze_schieber_wert(s, wert) · gui.bei_schieben(s, handler)
Fortschritt gui.fortschritt_neu() · gui.setze_fortschritt(balken, prozent) (0–100)
Reiter gui.reiter_neu() · gui.reiter_hinzufügen(reiter, titel, inhalt) · gui.reiter_hinzufügen_icon(reiter, titel, icon, inhalt) · gui.gewählter_reiter(r) · gui.setze_reiter(r, index) · gui.bei_reiter_wechsel(r, handler)
App-Integration gui.benachrichtige(titel, text) (Desktop-Benachrichtigung) · gui.kopiere_text(text) / gui.hole_text() (Zwischenablage schreiben/lesen) · gui.öffne_url(url) (Standard-Browser/Mailprogramm) · gui.zeige_im_ordner(pfad) (Datei-Manager öffnen, Datei markieren — z. B. nach einem Export) · gui.bei_fenster_schließen(fenster, handler) (SetCloseIntercept — Handler schließt via gui.fenster_schließe) · gui.bei_fenster_geschlossen(fenster, handler) (nach dem Schließen)
Leinwand gui.leinwand(breite, höhe) · gui.rechteck(b, x, y, breite, höhe, farbe) · gui.kreis(b, x, y, radius, farbe) · gui.linie(b, x1, y1, x2, y2, farbe, dicke) · gui.text_an(b, x, y, text, farbe, größe) · gui.bild_an(b, pfad, x, y, breite, höhe) (Bild/Sprite aus Datei, scharf/Nearest-Neighbor) · gui.leinwand_bild(b, bild, x, y, breite, höhe) (In-Memory-bild-Handle, scharf/Nearest-Neighbor, EIN Objekt) · gui.rechteck_rund(b, x, y, breite, höhe, radius, füllfarbe, randfarbe, randdicke) (abgerundet/berandet; leere Farbe "" = weglassen) · gui.leeren(b) — freie Hex-Farben
Leinwand (responsive) gui.leinwand_elastisch() (füllt ihren Platz, wächst mit Fenster/Vollbild) · gui.leinwand_breite(l) / gui.leinwand_höhe(l) (aktuelle Pixelgröße) · gui.bei_größe(l, handler) (feuert bei Größenänderung) · gui.vollbild(fenster, an) (an = 1/0)
Bildschirm gui.bildschirm_breite() / gui.bildschirm_höhe() (logische Größe der Arbeitsfläche des primären Monitors, ohne Taskleiste; schon vor gui.starte() aufrufbar; 0 = keine Info → feste Größe nehmen) — Fenster-Startgröße an jeden Monitor anpassen
Fenster füllen gui.maximiere(fenster) (maximiert im Fenstermodus: füllt den aktuellen Monitor, behält Titelleiste/X, bleibt verschieb-/verkleinerbar; nach gui.starte()) · gui.vollbild(fenster, 1) (randloses Vollbild, ohne Titelleiste)
Timer/Animation gui.bei_zeit(ms, handler) (periodisch, gibt Handle) · gui.nach_zeit(ms, handler) (einmalig) · gui.stoppe_zeit(handle)
Eingabe (Spiele) gui.bei_taste(fenster, handler) + gui.letzte_taste(fenster) (Text: "links"/"rechts"/"hoch"/"runter"/"leer"/Buchstabe) · gui.taste_gedrückt(fenster, taste) (Ganz 1/0, gehalten — für flüssige Bewegung im Game-Loop) · gui.bei_maus_klick(leinwand, handler) + gui.maus_x(b) / gui.maus_y(b) (Ganz) · gui.bei_maus_bewegung(leinwand, handler) (ohne Taste) · gui.bei_maus_ziehen(leinwand, handler) + gui.zieh_dx(b) / gui.zieh_dy(b) (Verschiebung) · gui.bei_maus_los(leinwand, handler) (Loslassen nach Ziehen → Gummiband-Werkzeuge) · gui.bei_maus_rad(leinwand, handler) + gui.rad_dy(b) / gui.rad_dx(b) (Mausrad → Zoom/Scroll) · gui.setze_cursor(leinwand, name) (Mauszeiger je Werkzeug: "standard"/"kreuz"/"hand"/"text"/"pipette"; nur Leinwände mit Maus-Handler) · gui.setze_cursor_bild(leinwand, pfad, hotx, hoty) (Cursor aus eigener SVG/PNG-Datei; hotx/hoty = Punkt unter der Mausspitze)
Klang/Sound (nutze klang) klang.lade(pfad) → Handle (WAV/MP3/OGG, Pfad relativ zur .ki) · klang.spiele(handle) (nicht-blockierend, überlappend) · klang.spiele_schleife(handle) (Musik) · klang.stoppe(handle) · klang.spiele_datei(pfad) (Kurzform) · klang.lese_samples(pfad)liste<komma> (Analyse, wirft) · klang.lese_abtastrate(pfad) → ganz (wirft)

Merke: Widget-Handler sehen nur ihr eigenes Widget als Parameter. Für alles andere (andere Widgets, Zähler, Zustand) nutzt du globale Variablen (Abschnitt 34.6).

Vollständige Beispiele findest du in beispiele/gui_hallo.ki, beispiele/gui_farben.ki, beispiele/gui_farbwechsel.ki (bunte Knöpfe + Live-Farbwechsel), beispiele/gui_formular.ki, beispiele/gui_auswahl.ki, beispiele/gui_editor.ki, beispiele/gui_notizen.ki (Notiz-App), beispiele/gui_quiz.ki, beispiele/gui_leinwand.ki (frei zeichnen mit eigenen Farben) beispiele/gui_animation.ki (bewegte Kugel mit Timer), beispiele/gui_eingabe.ki (Tastatur + Maus steuern die Leinwand) und beispiele/gui_sprites.ki (hüpfendes Bild-Sprite).

34.12 Zwei vollständige Beispiel-Apps

Diese zwei größeren Apps führen viele Bausteine zusammen — gute Vorlagen für eigene Programme:

Haushaltsbuch (beispiele/gui_buchhaltung.ki) — eine echte kleine Buchhaltung:

  • Einnahmen/Ausgaben (Kontrollkästchen), Saldo = Einnahmen − Ausgaben (kann negativ werden);
  • wählbare Tabelle im Master-Detail: Zeile anklicken → oben in Feldern bearbeiten → „Aktualisieren"/„Löschen";
  • exakte Geldbeträge (geld + Dezimal, 5-Rappen-Rundung), getippter Betrag via als_dezimal;
  • CSV-Persistenz: automatisch in den Profil-Ordner speichern + beim Start laden, plus „Speichern unter…"/„Öffnen…";
  • der Saldo-Rahmen färbt sich live grün/rot (setze_rahmen_farbe).

Datei-Browser (beispiele/gui_datei_browser.ki) — durch das Datei-System navigieren:

  • Split-Ansicht (teiler_h): links eine wählbare Tabelle (Name/Typ/Größe), rechts Text-Vorschau;
  • Live-Navigation durch Leeren + Neufüllen der Tabelle (zeilen_anzahl/zeile_entfernen/ zeile_anhängen) aus datei.einträge;
  • Vorschau via datei.inhalt (Binärdateien/zu große werden sauber abgefangen);
  • read-only — nur ansehen und navigieren.

Bauen + starten (Beispiel):

kiste build beispiele/gui_buchhaltung.ki -o gui_buchhaltung.exe && ./gui_buchhaltung.exe

34.13 Dein erstes Spiel — „Fang die Münze"

Jetzt führen wir alles aus den Spiele-Abschnitten zusammen: Leinwand (zeichnen, 34.10c), Timer (Game-Loop, 34.10d) und Eingabe (gehaltene Tasten, 34.10e). Das Spiel: Du steuerst eine blaue Kugel mit den Pfeiltasten und fängst eine goldene Münze. Jede gefangene Münze gibt einen Punkt, dann springt sie zur nächsten Ecke.

Schritt 1 — der Zustand. Ein Spiel ist im Kern ein Häufchen globaler Variablen, die sich über die Zeit ändern: die Spielerposition, die Münzposition, der Punktestand.

nutze gui

nimm bild = gui.leinwand(400, 300)
nimm x = 200          // Spieler
nimm y = 150
nimm muenze_x = 60    // Münze
nimm muenze_y = 60
nimm phase = 0        // welche Ecke als Nächstes
nimm punkte = 0

Schritt 2 — das Bild zeichnen. Eine Funktion malt den aktuellen Zustand. Immer erst leeren, dann alles neu — Hintergrund, Münze, Spieler, Punktestand:

funktion zeichne() {
    gui.leeren(bild)
    gui.rechteck(bild, 0, 0, 400, 300, "#0f172a")
    gui.kreis(bild, muenze_x, muenze_y, 12, "#fbbf24")   // Münze (gold)
    gui.kreis(bild, x, y, 18, "#38bdf8")                 // Spieler (blau)
    gui.text_an(bild, 12, 12, "Punkte: {punkte}", "#e4e4e7", 18)
}

Schritt 3 — die Münze versetzen. Wenn die Münze gefangen wird, soll sie woanders auftauchen. Wir wechseln einfach zwischen vier festen Ecken (kein Zufall nötig):

funktion naechste_muenze() {
    phase = phase + 1
    wenn phase > 3 { phase = 0 }
    wenn phase == 0 { muenze_x = 60
        muenze_y = 60 }
    wenn phase == 1 { muenze_x = 340
        muenze_y = 60 }
    wenn phase == 2 { muenze_x = 340
        muenze_y = 240 }
    wenn phase == 3 { muenze_x = 60
        muenze_y = 240 }
}

Schritt 4 — der Game-Loop. Das Herz des Spiels: ~60-mal pro Sekunde Eingabe lesen, Spielfigur bewegen, auf Treffer prüfen, neu zeichnen.

zeichne()
nimm fenster = gui.fenster_öffne("Kiste — Fang die Münze", 420, 360)

gui.bei_zeit(16, funktion(t) {
    // bewegen — nur solange gedrückt
    wenn gui.taste_gedrückt(fenster, "links") == 1 { x = x - 4 }
    wenn gui.taste_gedrückt(fenster, "rechts") == 1 { x = x + 4 }
    wenn gui.taste_gedrückt(fenster, "hoch") == 1 { y = y - 4 }
    wenn gui.taste_gedrückt(fenster, "runter") == 1 { y = y + 4 }
    // im Spielfeld halten
    wenn x < 18 { x = 18 }
    wenn x > 382 { x = 382 }
    wenn y < 18 { y = 18 }
    wenn y > 282 { y = 282 }
    // Münze gefangen? (ein Kasten rund um die Münze)
    wenn x > muenze_x - 26 und x < muenze_x + 26 und y > muenze_y - 26 und y < muenze_y + 26 {
        punkte = punkte + 1
        naechste_muenze()
    }
    zeichne()
})

gui.zeige(fenster, bild)
gui.starte()

Das ist ein vollständiges Spiel in ~40 Zeilen. Das Muster gilt für fast jedes Spiel: Zustand in globalen Variablen, eine zeichne()-Funktion, und ein bei_zeit-Game-Loop, der Eingabe lesen → Zustand ändern → neu zeichnen macht.

Ideen zum Weiterbauen: eine Zeitbegrenzung (gui.nach_zeit), mehrere Münzen, ein Gegner, der dir folgt, oder die Maus statt der Tastatur (gui.bei_maus_klick).

Vollständiges Beispiel: beispiele/gui_spiel.ki.

34.14 Ein richtiges Spiel — „Schlange" (Snake)

„Fang die Münze" hat einen festen Zustand: zwei Positionen, ein Punktestand. Bei Snake wächst der Zustand mit — die Schlange wird mit jedem Futter länger. Dafür brauchen wir eine Liste, die während des Spiels wächst und schrumpft. Damit sind alle Spiele-Bausteine versammelt: Leinwand, Timer, Tasten, Listen, Zufall und Klang.

Das Spielfeld ist ein Gitter. 20 × 15 Zellen, jede Zelle 20 Pixel → Leinwand 400 × 300. Die Schlange bewegt sich nicht pixelweise, sondern zellenweise — pro Game-Loop-Tick einen Schritt. Deshalb passt hier die diskrete Tastenbehandlung (gui.bei_taste, 34.10e): Ein Tastendruck ändert die Richtung, die Bewegung selbst macht der Timer.

Die Schlange lebt in zwei parallelen Listen. schlange_x[i]/schlange_y[i] sind die Gitter-Koordinaten des i-ten Glieds; der Kopf ist das letzte Element:

nutze gui
nutze klang
nutze liste
nutze zufall
nutze wert

nimm schlange_x = [5, 6, 7]   // drei Glieder, Kopf rechts
nimm schlange_y = [7, 7, 7]
nimm dx = 1                   // Richtung: nach rechts
nimm dy = 0

Bewegen = vorn anfügen, hinten abschneiden. Das ist der Kern von Snake — kein Glied „bewegt sich", die Liste verschiebt sich:

liste.hänge_an(schlange_x, kopf_x)   // neuer Kopf wächst voran …
liste.hänge_an(schlange_y, kopf_y)
wenn kopf_x == futter_x und kopf_y == futter_y {
    punkte = punkte + 1              // gefressen: Schwanz BLEIBT → Schlange wächst
    klang.spiele(ding)
    neues_futter()
} sonst {
    liste.entferne(schlange_x, 0)    // sonst rückt der Schwanz nach
    liste.entferne(schlange_y, 0)
}

Listen, die wachsen, speichern beliebig. Sobald du eine Liste mit liste.hänge_an/liste.entferne veränderst, speichert sie im Build-Modus Werte vom Typ beliebig. Beim Herauslesen wandelst du eine Koordinate mit wert.als_ganz(...) wieder in eine Zahl — sonst sagt dir der Compiler beim Rechnen oder Vergleichen genau das. Im Spiel ist das in zwei kleine Helfer gekapselt, danach bleibt der Code sauber:

funktion glied_x(i) { gib wert.als_ganz(schlange_x[i]) }
funktion glied_y(i) { gib wert.als_ganz(schlange_y[i]) }

Kollision = Liste absuchen. Wand ist ein Bereichs-Check; für „beißt sich selbst" und „Futter liegt frei" reicht eine Funktion, die das Gitter gegen die Schlange prüft:

funktion auf_schlange(zx, zy) {
    für i = 0 bis liste.länge(schlange_x) - 1 {
        wenn glied_x(i) == zx und glied_y(i) == zy { gib 1 }
    }
    gib 0
}

Futter per Zufall (zufall.ganz(0, 19) / zufall.ganz(0, 14)) — und falls die Zelle besetzt ist, einfach neu würfeln (solange auf_schlange(...) == 1).

Richtungswechsel mit Regel. Die 180°-Wende ist verboten — sonst beißt sich die Schlange sofort selbst:

gui.bei_taste(fenster, funktion(f) {
    nimm taste = gui.letzte_taste(f)
    wenn taste == "links" und dx != 1 { dx = -1
        dy = 0 }
    // … rechts/hoch/runter analog …
    wenn taste == "leer" und lebt == 0 { neustart() }
})

Spiel vorbei + Neustart. Statt das Fenster zu schließen, setzen wir lebt = 0 — der Tick tut dann nichts mehr, zeichne() blendet „Spiel vorbei!" ein, und die Leertaste ruft neustart(): Zustand zurücksetzen (schlange_x = [5, 6, 7] — einer Liste darf eine frische Liste zugewiesen werden), neues Futter, weiter geht's.

Das Muster zum Mitnehmen: Fester Zustand (Richtung, Punkte) lebt in Zahlen- Variablen, wachsender Zustand (die Schlange) in Listen. Der Game-Loop liest die Richtung, verschiebt die Liste, prüft Kollisionen, zeichnet neu — genau das gleiche Eingabe → Zustand → Bild-Muster wie in 34.13, nur mit einer Liste im Kern.

Vollständiger Spielkern (~130 Zeilen): beispiele/gui_schlange.ki.

Spiel das ruhig ein paar Runden — und achte dabei auf das, was stört: Das Spiel rast sofort los, wenn du das Programm startest. Die Punkte stehen mitten im Spielfeld, die Schlange läuft drunter durch. Es gibt keine Pause, und der Rekord ist nach dem Schließen weg. Genau diese vier Ärgernisse beheben wir im nächsten Abschnitt — Schritt für Schritt.

34.15 Snake ausbauen — vom Spielkern zum richtigen Spiel

Der Spielkern aus 34.14 funktioniert, fühlt sich aber noch nicht wie ein Spiel an. Mit fünf Umbauten wird er zu einem: Titelbild, Punkte außerhalb des Felds, Pause, eine Rangliste mit Namen und Hintergrundmusik. Jeder Schritt nutzt nur Bausteine, die du aus diesem Kapitel schon kennst. Das fertige Ergebnis steht in beispiele/gui_schlange_deluxe.ki (auf doppelt so großem Feld: 40 × 30 Zellen, Leinwand 800 × 600).

Schritt 1 — Titelbild statt Sofort-Start. Das Grundproblem: gui.bei_zeit tickt ab Programmstart, also rennt die Schlange sofort los. Die Lösung ist ein modus-Global, das sagt, in welchem Zustand das Spiel ist:

nimm modus = 0   // 0 = Titelbild · 1 = Spiel läuft · 2 = Spiel vorbei · 3 = Pause

Der Game-Loop-Tick prüft als Allererstes wenn modus == 1 { … } — auf dem Titelbild tut er nichts. zeichne() verzweigt nach Modus: Titel (großer Schriftzug, Rangliste), Spielfeld, oder Spielfeld + „Spiel vorbei"-Panel. Gestartet wird per Klick aufs Spielfeld (gui.bei_maus_klick, 34.10e):

funktion bei_feld_klick(b) {
    wenn modus == 0 oder modus == 2 { starte_spiel() }
}
gui.bei_maus_klick(bild, bei_feld_klick)

Während des Spiels (und in der Pause) tut der Klick bewusst nichts — sonst würde ein versehentlicher Klick die laufende Runde wegwerfen.

Schritt 2 — Punkte raus aus dem Spielfeld. Statt gui.text_an auf die Leinwand zu malen, bekommt der Punktestand ein Etikett in einer Kopfzeile über dem Spielfeld — echte Widgets und die Leinwand, gemischt in einem Layout:

nimm punkte_etikett = gui.etikett_neu("Punkte: 0")
// … beim Fressen:
gui.setze_text(punkte_etikett, "Punkte: {punkte}")
// … beim Fenster-Aufbau — Rand-Layout: die MITTE (der Lautstärke-Regler) bekommt
// die restliche Breite, links bleiben Punktestand + Musik-Haken kompakt:
nimm links = gui.horizontal([punkte_etikett, musik_haken])
nimm kopf = gui.rand_layout(musik_regler, [0, 0, links, 0])
gui.zeige(fenster, gui.vertikal([kopf, bild]))

In einem schlichten gui.horizontal würde der Regler auf seine Minimalbreite zusammenfallen — für „ein Element soll sich dehnen" ist gui.rand_layout das richtige Werkzeug.

Schritt 3 — Pause. Mit dem modus-Muster fast geschenkt: Die Taste P schaltet zwischen 1 (läuft) und 3 (Pause) um; der Tick läuft nur bei modus == 1, und zeichne() legt bei Pause ein kleines Panel übers Feld. Buchstaben-Tasten kommen in gui.letzte_taste großgeschrieben an:

wenn taste == "P" {
    wenn modus == 1 { modus = 3
        zeichne() } sonstwenn modus == 3 { modus = 1 }
}

Schritt 4 — Rangliste mit Namen. Drei Zutaten: der Name, den der Spieler nach dem Spiel direkt auf den Game-Over-Schirm tippt (kein Eingabefeld, das mitten im Spiel die Tasten wegschnappen könnte — die Buchstaben liest der Tasten-Handler selbst, so wie in 34.10e), zwei parallele Listen rang_punkte/rang_namen (dasselbe Muster wie die Schlange!), und eine Datei, damit der Rekord den Neustart überlebt.

Neue Ergebnisse werden an der richtigen Stelle eingefügt — die Listen bleiben dadurch immer aufsteigend sortiert, ganz ohne Sortier-Funktion:

funktion trage_ein(p, name) {
    nimm pos = 0
    für i = 0 bis liste.länge(rang_punkte) - 1 {
        wenn rang(i) < p { pos = i + 1 }     // hinter alle kleineren Ergebnisse
    }
    liste.füge_ein(rang_punkte, pos, p)
    liste.füge_ein(rang_namen, pos, name)
    solange liste.länge(rang_punkte) > 6 {   // nur die besten 5 behalten
        liste.entferne(rang_punkte, 0)       // kleinster fliegt vorne raus
        liste.entferne(rang_namen, 0)
    }
    // … dann mit datei.schreibe sichern (zwei Zeilen pro Eintrag: Punkte, Name)
}

Die Anzeige läuft rückwärts durch die Liste (größte zuerst). Beim Programmstart liest datei.zeilen die Einträge paarweise wieder ein — über dieselbe Einfüge-Funktion, damit die Listen garantiert sortiert bleiben, egal in welcher Reihenfolge die Datei daherkommt. Die erste Zeile der Datei ist ein Format-Marker (schlange-v2): Steht da etwas anderes (etwa eine Datei von einer älteren Spielversion), wird sie schlicht ignoriert statt Unsinn anzuzeigen. Tippt niemand einen Namen, trägt das Spiel „Anonym" ein.

Schritt 5 — Hintergrundmusik. Ein Kontrollkästchen in der Kopfzeile schaltet eine Musik-Endlosschleife (34.10g) an und aus, ein Schieberegler daneben regelt die Lautstärke — live, also auch während die Schleife schon läuft. klang.lautstärke wirkt pro Klang-Handle; der Regler setzt einfach beide Handles und wird so zur Master-Lautstärke des Spiels (0 = alles stumm):

funktion bei_musik(k) {
    wenn gui.angehakt(k) == 1 { klang.spiele_schleife(musik) }
    sonst { klang.stoppe(musik) }
}
funktion bei_lautstärke(s) {
    nimm w = gui.schieber_wert(s)   // 0–100
    klang.lautstärke(musik, w)      // Musik (auch die laufende Schleife)
    klang.lautstärke(ding, w)       // Fress-Ton
}

Das Muster zum Mitnehmen: Vom Spielkern zum richtigen Spiel führt kein neuer Baustein — nur das modus-Global als Zustands-Weiche, Widgets + Leinwand in einem Layout, und Listen + Datei für alles, was bleiben soll.

Vollständiges Spiel (~250 Zeilen): beispiele/gui_schlange_deluxe.ki.

34.16 Der GUI-Designer der Kiste-IDE — klicken statt tippen

Die Kiste-IDE hat einen GUI-Designer: Du klickst deine Oberfläche zusammen, und der Designer erzeugt daraus den Kiste-Code aus diesem Kapitel. Du musst also nichts Neues lernen — der Designer schreibt genau die gui.-Aufrufe, die du schon kennst, nur eben für dich.

So startest du: In der IDE-Seitenleiste das Raster-Symbol (vier Quadrate) klicken → ein neuer Entwurf öffnet sich (Datei-Endung .kid). Links die Palette (alle Widgets und Container), in der Mitte der Aufbau-Baum und die Fenster-/Thema-Einstellungen, rechts die Eigenschaften des gewählten Elements.

Die wichtigste Regel: Der Baum IST dein Layout. Die Einrückung im Aufbau-Baum zeigt die Verschachtelung — was eingerückt unter einer horizontal-Reihe steht, liegt nebeneinander; was unter einer vertikal-Spalte steht, liegt untereinander. Die Wurzel ist immer eine vertikal-Spalte namens inhalt.

Klick-Logik der Palette: Ein Klick auf ein Palette-Element fügt es in den gerade ausgewählten Container ein (bzw. hinter die Auswahl, wenn ein Widget ausgewählt ist). Mit ▲/▼ sortierst du innerhalb des Containers, mit „▶ rein" schiebst du die Auswahl in den Container, der im Baum direkt darüber steht, mit „◀ raus" wieder heraus.

Rezept: Zwei Knöpfe nebeneinander mit 50 Pixel Abstand

  1. Wurzel vertikal »inhalt« im Baum anklicken.
  2. Palette (Gruppe CONTAINER): „Reihe (horizontal)" klicken.
  3. Die neue Reihe ist ausgewählt — jetzt aus der Palette nacheinander: KnopfAbstand (Platzhalter)Knopf.
  4. F5 (oder „▶ Vorschau") → das echte Fenster zeigt beide Knöpfe nebeneinander.

Der Baum sieht dann so aus — und erzeugt diesen Code:

vertikal »inhalt«                       nimm knopf_1 = gui.knopf_neu("Knopf")
    horizontal »horizontal_1«           nimm abstand_1 = gui.etikett_neu("")
        knopf »knopf_1«                 nimm abstand_1_größe = gui.feste_größe(abstand_1, 50, 1)
        abstand »abstand_1« ⌗50×1       nimm knopf_2 = gui.knopf_neu("Knopf")
        knopf »knopf_2«                 nimm horizontal_1 = gui.horizontal([knopf_1, abstand_1_größe, knopf_2])

Sollen die Knöpfe die Breite gleichmäßig teilen (je 50 %), nimm statt der Reihe ein Raster mit 2 Spalten — Raster-Zellen teilen den Platz gleich auf.

Feste Größe

Jedes Widget hat im Eigenschaften-Panel den Abschnitt „Feste Größe" (Breite/Höhe). Leer = das Layout entscheidet (wie bisher). Beide gesetzt = das Widget bekommt exakt diese Größe und sitzt zentriert im übrigen Platz. Im Code steckt dahinter gui.feste_größe(widget, breite, höhe) — der Designer erledigt das Wrapping automatisch.

Knöpfe färben — Wichtigkeit

Knöpfe färbst du über die Eigenschaft Wichtigkeit: normal, primär (Akzentfarbe — die wählst du frei unter Thema!), gefahr (rot), erfolg (grün), warnung (gelb). Soll ein Knopf eine ganz eigene Farbe bekommen, setz den Haken bei „Eigene Hintergrundfarbe" und wähl sie im Picker — im Code steckt dahinter gui.setze_knopf_farbe(knopf, "#rrggbb"), das auch live aus Handlern umfärben kann (die eigene Farbe ersetzt die Wichtigkeit). Dazu Farb-Rahmen aus der Palette: ein Container, der seinem Inhalt einen frei wählbaren Hintergrund gibt. Die sechs Thema-Rollen (akzent, hintergrund, fläche, text, rahmen, gefahr) stellst du im Thema-Abschnitt mit Farb-Pickern ein — siehe 34.4. Merke: Thema-Rollen färben global (alle Knöpfe, das ganze Fenster), setze_knopf_farbe färbt einen Knopf.

Events und eigener Code — die Marker-Regel

  1. Widget anwählen → rechts unter Events beim passenden Event (z. B. bei_klick) auf klicken — der Standard-Handlername wird eingetragen.
  2. „Generieren" klicken → neben der .kid entsteht name.ki mit deinem Layout und einem leeren Funktions-Rumpf pro Handler.
  3. In der .ki deine Logik in die Handler schreiben — alles zwischen den beiden Marker-Blöcken gehört dir und bleibt beim nächsten Generieren unangetastet. Die Marker-Blöcke selbst (VOM DESIGNER GENERIERT …) nie von Hand ändern — sie werden bei jedem Generieren ersetzt.
  4. F5 im Designer = Vorschau: speichert, generiert, baut und startet das echte Fenster in einem Schritt.

Rezept: Liste, die zur Laufzeit wächst

Eingabefeld + Knopf „Hinzufügen" + Liste in den Baum, beim Knopf bei_klick mit ✚ anlegen, Generieren — und im Handler:

funktion bei_klick_knopf_1(knopf) {
    gui.eintrag_anhängen(liste_1, gui.wert(eingabefeld_1))
    gui.setze_wert(eingabefeld_1, "")
}

Mehr Listen-Werkzeug: gui.setze_einträge(l, […]) (alles ersetzen), gui.eintrag_entfernen(l, index), gui.einträge_anzahl(l) — siehe Spickzettel 34.11.

Menü und Tastatur-Kürzel

Beides sind eigene Abschnitte unter dem Aufbau (Titel anklicken zum Auf-/Zuklappen):

  • Menü: „+ Menü" → Titel Datei → „+ Eintrag" → Titel Neu vergibt den Handler-Namen (neu_gewählt); „+ Trenner" für den Strich, ▲/▼ sortiert. Generiert wird die komplette Leiste (gui.menü_leistemenü_hinzufügenmenü_eintrag).
  • Tastatur-Kürzel: „+ Kürzel" → Taste (Strg+S) + Handler (✚ macht daraus strg_s_gedrückt). Generiert gui.taste(fenster, …).
  • Der -Knopf neben jedem Handler (Events, Kürzel, Menü-Einträge) öffnet die generierte .ki direkt an der Funktion — der kürzeste Weg vom Designer zur Logik.

Die Schema-Vorschau und Platz auf kleinen Bildschirmen

Die Vorschau (Schema) über dem Baum zeigt dein Fenster als Anordnung von Kästen — mit echten Layout-Regeln (Spalte dehnt auf volle Breite, Reihe nicht, Teiler teilt im eingestellten Verhältnis, Reiter-Köpfe sind klickbar). Klicken wählt aus, Ziehen verschiebt Widgets zwischen Containern. Es bleibt ein Schema — die Pixel-Wahrheit zeigt F5.

Platz-Tipps: jeden Abschnitts-Titel anklicken = einklappen (wird gemerkt) · Strg+B klappt den Explorer ein · ⛶ Fokus im Designer-Kopf räumt Explorer + Ausgabe mit einem Klick weg · Speichern unter (Diskette mit Stift, Strg+Umschalt+S) legt eine Ausprobier-Kopie an — bei Entwürfen wandert die generierte .ki mit deiner Handler-Logik automatisch mit.

Der Designer kann (noch) nicht alles, was das gui-Modul kann — Dialoge, Leinwand-Zeichnen, Timer und Sound schreibst du wie gewohnt in die Handler. Genau dafür ist die Marker-Regel da: Designer fürs Gerüst, du für das Verhalten.