TUI-Anwendungen mit Delphi und DMVCFramework entwickeln
TUI: kein Schritt zurück
Wer die jüngste Entwicklung von DMVCFramework verfolgt hat, weiß bereits, dass Konsolenanwendungen eine zweite Blütezeit erleben. Im vorherigen Beitrag über Konsolenfarben habe ich die Low-Level-ANSI-API vorgestellt — die Records Fore, Back und Style — sowie den Gin-artigen HTTP-Log-Renderer. Das war die Grundlage.
Dieser Beitrag befasst sich mit dem, was darauf aufgebaut wurde: die vollständige TUI-Bibliothek in MVCFramework.Console.pas.
TUI steht für Text User Interface. Es ist die Welt von htop, lazygit, k9s und jedem CLI-Tool, das Pfeiltasten, farbige Boxen und Fortschrittsbalken verwendet. Diese Anwendungen sind keine „einfachen Konsolenprogramme" — es sind reichhaltige, interaktive Oberflächen, die zufällig in einem Terminal laufen.
Mit DMVCFramework 3.5.0 Silicon verfügen Delphi-Entwickler nun über ein vollständiges, plattformübergreifendes TUI-Toolkit:
- Farbige Textausgabe mit Ausrichtung
- Interaktive Menüs, die mit Pfeiltasten navigiert werden
- Animierte Spinner mit 10 Stilen
- Bestimmte und unbestimmte Fortschrittsbalken
- Tabellen mit statischer Anzeige und interaktiver Zeilenauswahl
- Umrandete Boxen mit optionalen Titeln
- Cursor-Steuerung: ausblenden, anzeigen, speichern, wiederherstellen, bewegen
- Tastatureingabe-Verarbeitung einschließlich Sondertasten
- Ein globales Theme-System
Alles in einer einzigen Unit — MVCFramework.Console — mit null Abhängigkeiten vom Rest des Frameworks. Sie können sie in jedes Delphi-Konsolenprojekt kopieren und sofort verwenden.
Einrichtung
Fügen Sie MVCFramework.Console zu Ihrer uses-Klausel hinzu. Für vollständige Unicode-Unterstützung (Spinner-Frames, Box-Zeichenzeichen, Emojis) rufen Sie EnableUTF8Console am Programmanfang auf:
program MyTUIApp;
{$APPTYPE CONSOLE}
uses
System.SysUtils,
MVCFramework.Console;
begin
EnableUTF8Console; // Erforderlich für Box-Zeichen, Spinner mit Emojis usw.
// ... Ihr Code hier
end.
Unter Windows ruft EnableUTF8Console SetConsoleOutputCP(CP_UTF8) auf. Unter Linux ist es ein No-Op, da UTF-8 der Standard ist. Wenn Sie auch planen, die ANSI-Farbprimitiven (Fore, Back, Style) direkt mit WriteLn zu verwenden, rufen Sie auch EnableANSIColorConsole auf. Alle High-Level-Funktionen dieser Bibliothek rufen es jedoch intern auf, sodass Sie sich in den meisten Fällen keine Gedanken darüber machen müssen.
ANSI-Farben: der Colorama-Ansatz (Fore / Back / Style)
MVCFramework.Console enthält zwei komplementäre Farbsysteme. Das erste — im vorherigen Beitrag behandelt — ist eine Low-Level-ANSI-API, die nach dem Python-colorama-Modell gestaltet ist. Es lohnt sich, es hier nochmals aufzugreifen, da es ein wesentliches Werkzeug für jede TUI-Anwendung ist und sich natürlich mit allem anderen in dieser Unit verbindet.
Die API besteht aus drei Records mit String-Konstanten:
| Record | Zweck | Beispiel |
|---|---|---|
Fore |
Vordergrund-(Text-)Farbe | Fore.Red, Fore.Green, Fore.Cyan |
Back |
Hintergrundfarbe | Back.DarkBlue, Back.DarkRed |
Style |
Textstil und Zurücksetzen | Style.Bright, Style.Dim, Style.ResetAll |
Jede Konstante ist ein roher ANSI-Escape-Sequenz-String. Farben werden mit dem +-Operator kombiniert, und Style.ResetAll beendet ein farbiges Segment. Rufen Sie EnableANSIColorConsole einmal beim Start auf (idempotent, No-Op unter Linux):
EnableANSIColorConsole;
// Grundlegende Vordergrundfarben
WriteLn(Fore.Red + 'Fehler: Verbindung abgelehnt' + Style.ResetAll);
WriteLn(Fore.Green + 'OK: Server gestartet auf :8080' + Style.ResetAll);
WriteLn(Fore.Yellow + 'Warnung: Cache-Miss-Rate 67%' + Style.ResetAll);
WriteLn(Fore.Cyan + 'Info: Verwende config.env' + Style.ResetAll);
WriteLn(Fore.DarkGray + '# Debug-Ausgabe' + Style.ResetAll);
Vordergrund, Hintergrund und Stil kombinieren
Die eigentliche Stärke liegt in der Kombination. Da Konstanten einfache Strings sind, ist jede Kombination eine einzelne Verkettung:
// Vordergrund + Hintergrund
WriteLn(Fore.White + Back.DarkBlue + ' INFO ' + Style.ResetAll + ' Server gestartet');
WriteLn(Fore.White + Back.DarkGreen + ' PASS ' + Style.ResetAll + ' TestUserAuth');
WriteLn(Fore.White + Back.DarkRed + ' FAIL ' + Style.ResetAll + ' TestPaymentTimeout');
WriteLn(Fore.White + Back.DarkYellow + ' WARN ' + Style.ResetAll + ' Hohe Speicherauslastung');
// Stil-Modifikatoren
WriteLn(Style.Bright + Fore.White + 'Fette weiße Überschrift' + Style.ResetAll);
WriteLn(Style.Dim + Fore.Gray + 'Gedämpfter sekundärer Text' + Style.ResetAll);
// Gemischtes Inline
WriteLn(
'Status: ' + Fore.Green + 'ONLINE' + Style.ResetAll +
' | Req/s: ' + Fore.Cyan + '142' + Style.ResetAll +
' | Fehler: ' + Fore.Red + '0' + Style.ResetAll
);
Wiederverwendbare Stil-Konstanten (wie CSS für die Konsole)
Anstatt Fore.White + Back.DarkGreen überall zu wiederholen, definieren Sie benannte Konstanten einmalig. Der Compiler löst sie zur Compile-Zeit auf — kein Runtime-Overhead:
const
BADGE_OK = Fore.White + Back.DarkGreen;
BADGE_FAIL = Fore.White + Back.DarkRed;
BADGE_WARN = Fore.White + Back.DarkYellow;
BADGE_INFO = Fore.White + Back.DarkBlue;
MUTED = Fore.DarkGray;
RESET = Style.ResetAll;
// Sauberer, lesbarer Code:
WriteLn(BADGE_OK + ' PASS ' + RESET + ' TestUserAuth');
WriteLn(BADGE_FAIL + ' FAIL ' + RESET + ' TestPaymentTimeout');
WriteLn(BADGE_WARN + ' SLOW ' + RESET + ' Abfrage dauerte 3.2s');
WriteLn(BADGE_INFO + ' NOTE ' + RESET + ' Verwende Fallback-Konfiguration');
WriteLn(MUTED + '--- Ende des Berichts ---' + RESET);
Wann Fore/Back/Style vs. TConsoleColor verwenden
Die beiden Systeme sind komplementär, nicht konkurrierend:
Verwende Fore / Back / Style wenn… |
Verwende TConsoleColor / WriteLine wenn… |
|---|---|
Inline-Färbung innerhalb eines WriteLn benötigt wird |
Vollzeilige farbige Ausgabe gewünscht ist |
| Badges oder Log-Präfixe erstellt werden | Box, Table, Menu (Theme-bewusst) verwendet wird |
| Compile-Zeit-Stil-Konstanten gewünscht sind | Das globale ConsoleTheme angewendet werden soll |
Style.Bright oder Style.Dim benötigt wird |
Hintergrundfarbe aus dem Theme benötigt wird |
Beide Systeme koexistieren. Eine typische TUI-Anwendung verwendet Fore/Back/Style für benutzerdefinierten Inline-Text und verlässt sich auf TConsoleColor-basierte Funktionen für strukturelle Widgets (Boxen, Tabellen, Menüs).
Textausgabe
Die Farbpalette
Die Bibliothek definiert TConsoleColor, eine Enumeration mit 17 Werten, die die klassischen 16 Terminal-Farben plus UseDefault (erbt vom aktiven Theme) abdeckt:
Black, DarkBlue, DarkGreen, DarkCyan, DarkRed, DarkMagenta,
DarkYellow, Gray, DarkGray, Blue, Green, Cyan, Red, Magenta,
Yellow, White, UseDefault
Diese entsprechen unter Windows der klassischen Windows Console API und unter Linux den ANSI-Codes. Die Namen sind auf beiden Plattformen identisch: Green ist immer grün, Red ist immer rot.
WriteLine und WriteColoredText
Der einfachste Weg zur farbigen Textausgabe:
// Einfacher Text (verwendet Terminal-Standards)
WriteLine('Serverinitialisierung wird gestartet...');
// Text mit Vordergrundfarbe
WriteLine('Server erfolgreich gestartet', Green);
// Text mit Vordergrund- und Hintergrundfarbe
WriteLine(' KRITISCH ', White, DarkRed);
// Farbiges Inline-Segment (ohne Zeilenumbruch)
WriteColoredText('[INFO] ', Cyan);
WriteLine('Lausche auf Port 8080', White);
WriteColoredText speichert und stellt die aktuellen Farben automatisch wieder her, sodass es sicher verschachtelt oder sequentiell aufgerufen werden kann, ohne sich um Farbübertragung sorgen zu müssen.
Ausgerichteter Text
WriteAlignedText füllt Text auf eine feste Breite mit konfigurierbarer Ausrichtung auf:
// Titel in einem 80 Zeichen breiten Bereich zentrieren
WriteAlignedText('DMVCFramework Server', 80, taCenter, Yellow);
// Wert rechtsbündig ausrichten
WriteAlignedText('v3.5.0', 80, taRight, DarkGray);
// Linksbündig mit Farbe
WriteAlignedText('Status: ONLINE', 80, taLeft, Green);
Die drei Ausrichtungsmodi — taLeft, taCenter, taRight — decken alle Layout-Anforderungen ab. Der Width-Parameter steuert die Ausgabespaltenbreite, nicht die String-Breite: kürzere Strings werden aufgefüllt, längere werden abgeschnitten.
CenterInScreen ist eine praktische Abkürzung, die einen String im physischen Konsolenfenster zentriert:
CenterInScreen('Drücken Sie eine Taste zum Fortfahren');
Es liest die aktuelle Konsolengröße mit GetConsoleSize und berechnet automatisch die korrekten GotoXY-Koordinaten.
Statusmeldungen
Vier vordefinierte Statusfunktionen erzeugen konsistent gestaltete Ausgaben:
WriteSuccess('Datenbankmigration in 1.2s abgeschlossen');
WriteWarning('Cache-Miss-Rate über 50% — TTL-Optimierung empfohlen');
WriteError('Verbindung auf Port 5432 abgelehnt');
WriteInfo('Verwende Fallback-Konfiguration aus Umgebungsvariablen');
Jede erzeugt ein farbiges Präfix-Badge gefolgt von weißem Meldungstext:
| Funktion | Badge | Badge-Farbe |
|---|---|---|
WriteSuccess |
[SUCCESS] |
Grün |
WriteWarning |
[WARNING] |
Gelb |
WriteError |
[ERROR] |
Rot |
WriteInfo |
[INFO] |
Cyan |
Überschriften und Trennlinien
WriteHeader erzeugt einen zentrierten Titel, der von horizontalen Linien gerahmt wird, unter Verwendung des aktiven Box-Zeichensatzes:
WriteHeader('Datenbank-Migrationstool');
WriteHeader('Konfigurationsübersicht', 60);
WriteHeader('Ergebnisse', 60, Green); // Benutzerdefinierte Farbe
Die Standardbreite beträgt 80 Spalten. Das horizontale Linienzeichen passt sich an ConsoleTheme.BoxStyle an (siehe Abschnitt Themes weiter unten).
WriteSeparator zeichnet eine einfache horizontale Linie — nützlich zum visuellen Trennen von Ausgabeabschnitten:
WriteSeparator; // 60 Bindestriche
WriteSeparator(80); // 80 Bindestriche
WriteSeparator(40, '='); // 40 Gleichheitszeichen
Formatierte Listen
WriteFormattedList rendert eine Titelliste mit konfigurierbarem Aufzählungsstil:
var Features: TStringArray;
SetLength(Features, 4);
Features[0] := 'Interaktive Menüs mit Pfeiltasten-Navigation';
Features[1] := 'Nicht-blockierende Spinner auf Hintergrund-Thread';
Features[2] := 'Tabellen mit statischer und interaktiver Anzeige';
Features[3] := 'Globales Theme-System';
WriteFormattedList('Neu in DMVCFramework 3.5.0:', Features, lsBullet);
WriteFormattedList('Schritte zum Upgrade:', Features, lsNumbered);
WriteFormattedList('Entfernte Funktionen:', Features, lsDash);
WriteFormattedList('Nächste Schritte:', Features, lsArrow);
Die vier Listenstile:
| Stil | Präfix |
|---|---|
lsBullet |
* |
lsNumbered |
1. |
lsDash |
- |
lsArrow |
> |
Boxen
Die Box-Funktion rendert einen umrandeten Inhaltsbereich, der standardmäßig automatisch auf die Inhaltsbreite skaliert:
// Einfache Box ohne Titel, Standardbreite (60)
Box(['Server: ONLINE', 'Datenbank: VERBUNDEN', 'Speicher: 65%']);
// Box mit Titel
Box('Systemstatus', ['Server: ONLINE', 'Datenbank: VERBUNDEN']);
// Box mit Titel und benutzerdefinierter Breite
Box('WARNUNG', ['Cache-Server antwortet nicht', 'Netzwerkverbindung prüfen'], 50);
Der Box-Inhalt wird automatisch links innerhalb des Rahmens aufgefüllt. Die Rahmenzeichen passen sich an ConsoleTheme.BoxStyle an.
Für detaillierte Zeichensteuerung verwenden Sie direkt das Low-Level-Primitiv DrawBox:
// Eine 40x10-Box ab Spalte 5, Zeile 3 zeichnen
DrawBox(5, 3, 40, 10, bsDouble, 'Debug-Panel');
Box-Stile
Vier Rahmenstile sind über TBoxStyle verfügbar:
DrawBox(0, 0, 30, 5, bsSingle, 'Einfach'); // ┌─┐ │ └─┘
DrawBox(0, 6, 30, 5, bsDouble, 'Doppelt'); // ╔═╗ ║ ╚═╝
DrawBox(0, 12, 30, 5, bsRounded, 'Abgerundet'); // ╭─╮ │ ╰─╯
DrawBox(0, 18, 30, 5, bsThick, 'Dick'); // ┏━┓ ┃ ┗━┛
Der Standard-Box-Stil für alle High-Level-Funktionen (Box, Menu, Table) wird durch ConsoleTheme.BoxStyle gesteuert. Eine einmalige Änderung wirkt sich auf alle Widgets aus:
ConsoleTheme.BoxStyle := bsDouble;
Box('Jetzt verwendet alles doppelte Rahmen', ['Element 1', 'Element 2']);
Tabellen
Statische Tabelle
Table rendert eine formatierte Tabelle mit automatisch angepassten Spalten:
var
Headers: TStringArray;
Data: TStringMatrix;
SetLength(Headers, 4);
Headers[0] := 'ID'; Headers[1] := 'Name';
Headers[2] := 'Framework'; Headers[3] := 'Sprache';
SetLength(Data, 3);
SetLength(Data[0], 4);
Data[0] := ['001', 'Mario Rossi', 'DMVCFramework', 'Delphi'];
SetLength(Data[1], 4);
Data[1] := ['002', 'Luigi Verdi', 'Spring Boot', 'Java'];
SetLength(Data[2], 4);
Data[2] := ['003', 'Anna Bianchi', 'ASP.NET Core', 'C#'];
// Ohne Titel
Table(Headers, Data);
// Mit Titel
Table(Headers, Data, 'Entwicklungsteam');
Spaltenbreiten werden automatisch berechnet: Jede Spalte ist so breit wie ihre breiteste Zelle (Kopfzeile oder Daten), plus 2 Zeichen innerer Auffüllung. Die Kopfzeile wird mit ConsoleTheme.TextHighlightColor zur visuellen Unterscheidung gerendert.
Interaktive Tabelle
TableMenu verwandelt eine Tabelle in einen interaktiven Selektor, der mit Pfeiltasten navigiert wird:
var SelectedRow: Integer;
SelectedRow := TableMenu('Entwickler auswählen', Headers, Data);
if SelectedRow >= 0 then
WriteSuccess('Sie haben ausgewählt: ' + Data[SelectedRow][1])
else
WriteWarning('Auswahl abgebrochen');
Die ausgewählte Zeile wird mit ConsoleTheme.BackgroundHighlightColor und ConsoleTheme.TextHighlightColor hervorgehoben. Navigationstasten:
| Taste | Aktion |
|---|---|
| ↑ / ↓ | Auswahl nach oben/unten bewegen |
| Enter | Auswahl bestätigen, Zeilenindex zurückgeben |
| Escape | Abbrechen, -1 zurückgeben |
Der Cursor wird während der Interaktion automatisch ausgeblendet und beim Beenden wiederhergestellt.
Sie können eine Zeile vorauswählen:
SelectedRow := TableMenu('Auswählen', Headers, Data, 2); // Beginnt mit Zeile 2 ausgewählt
Interaktive Menüs
Menu zeigt eine tastaturnavigierbare Liste in einer Box an. Es erscheint an der aktuellen Cursorposition und bereinigt sich, wenn der Benutzer eine Auswahl trifft oder Escape drückt.
var
Items: TStringArray;
Selected: Integer;
SetLength(Items, 5);
Items[0] := 'Server starten';
Items[1] := 'Server stoppen';
Items[2] := 'Protokolle anzeigen';
Items[3] := 'Einstellungen';
Items[4] := 'Beenden';
// Ohne Titel
Selected := Menu(Items);
// Mit Titel
Selected := Menu('Hauptmenü', Items);
// Mit Titel und vorausgewähltem Element
Selected := Menu('Dateimenü', Items, 2); // Beginnt mit 'Protokolle anzeigen' ausgewählt
Rückgabewert: Der Index des ausgewählten Elements (0-basiert) oder -1, wenn der Benutzer Escape gedrückt hat.
Navigation:
| Taste | Aktion |
|---|---|
| ↑ | Nach oben (mit Wrap) |
| ↓ | Nach unten (mit Wrap) |
| Enter | Bestätigen |
| Escape | Abbrechen |
Das ausgewählte Element wird mit invertierten Farben gerendert (Hintergrundhervorhebung). Das Menü passt seine Breite automatisch an das längste Element plus Auffüllung an. Wenn das Menü das untere Ende des Terminals überschreiten würde, wird es automatisch nach oben verschoben.
// Praktisches Beispiel: Haupt-Dispatch-Schleife
while True do
begin
Selected := Menu('Serversteuerung', ['Starten', 'Stoppen', 'Neu starten', 'Beenden']);
case Selected of
0: StartServer;
1: StopServer;
2: begin StopServer; StartServer; end;
3, -1: Break;
end;
end;
Fortschrittsbalken
Bestimmter Fortschritt
Progress mit MaxValue > 0 erzeugt einen bestimmten Fortschrittsbalken:
var
P: IProgress;
I: Integer;
P := Progress('Dateien herunterladen', 100);
for I := 1 to 100 do
begin
P.Update(I);
Sleep(20); // Arbeit simulieren
end;
P := nil; // Ruft Complete automatisch über den Destruktor auf
Der Balken wird als [==== ] 45% dargestellt. Der gefüllte Anteil wird als (Aktuell * Breite) div MaxValue berechnet. Die Standardbalkenbreite beträgt 50 Zeichen.
Das IProgress-Interface stellt bereit:
| Methode | Beschreibung |
|---|---|
Update(Value) |
Aktuellen Wert setzen (absolut) |
Increment(Amount) |
Um Amount erhöhen (Standard 1) |
SetMessage(Msg) |
Titelbeschriftung aktualisieren |
Complete |
Als erledigt markieren, “Fertig!” in grün ausgeben |
P := nil zu setzen aktiviert den Destruktor, der Complete aufruft, wenn es noch nicht geschehen ist. Das Einwickeln des Fortschritts in einen try/finally-Block ist daher optional — die Bereinigung ist garantiert.
// Inkrementierungsstil
P := Progress('Datensätze verarbeiten', 1000);
for I := 1 to 1000 do
begin
ProcessRecord(I);
P.Increment;
end;
Unbestimmter Fortschritt
Progress mit MaxValue = 0 erzeugt einen unbestimmten Spinner innerhalb eines Klammernpaars:
P := Progress('Konfiguration laden');
// Arbeit erledigen...
while StillLoading do
begin
P.Update(0); // Spinner vorwärtsbewegen
Sleep(50);
end;
P.Complete;
Der Spinner innerhalb der [|]-Klammern durchläuft die Zeichen |/-\. Rufen Sie Update regelmäßig auf, um die Animation vorwärtszubewegen. Dies ist ein blockierendes Muster — der Spinner bewegt sich nur vorwärts, wenn Sie Update aufrufen. Für einen vollständig nicht-blockierenden Spinner verwenden Sie stattdessen Spinner (siehe unten).
Spinner
Die Spinner-Funktion erstellt einen hintergrundthread-basierten, nicht-blockierenden animierten Spinner. Ihr Hauptthread arbeitet weiter, während der Spinner unabhängig animiert:
var S: ISpinner;
S := Spinner('Daten laden', ssLine, DarkGray);
// Hier Ihre Arbeit erledigen — Spinner animiert von selbst
FetchRemoteData;
S.Hide; // Oder: S := nil (ruft Destruktor auf → Hide)
Das Freigeben des Interface (auf nil setzen) ruft auch Hide auf, was den Thread stoppt und den Spinner vom Terminal löscht.
Die 10 Spinner-Stile
TSpinnerStyle bietet 10 Animationsstile:
| Stil | Zeichen | Intervall | Beschreibung |
|---|---|---|---|
ssLine |
-\│/ |
100ms | Klassischer Terminal-Spinner |
ssDots |
Braille-Muster | 80ms | Flüssige Punkt-Animation |
ssBounce |
Braille-Bounce | 80ms | Springender Punkt |
ssGrow |
Block-Elemente | 120ms | Wachsender Balken |
ssArrow |
Pfeilzeichen | 100ms | Rotierender Pfeil |
ssCircle |
Kreisviertel | 100ms | Rotierender Kreis |
ssClock |
🕐🕑🕒… | 200ms | 12 Uhrzeiger |
ssEarth |
🌍🌎🌏 | 200ms | Globus-Rotation |
ssMoon |
🌑🌒🌓… | 200ms | 8 Mondphasen |
ssWeather |
🌤🌧⛈… | 200ms | Wetter-Icons |
Jeder Stil verwendet automatisch das passende Animationsintervall. Sleep-Werte müssen nicht angepasst werden.
// Linien-Spinner (Standard)
S := Spinner(ssLine);
// Dots mit Meldung
S := Spinner('Mit Datenbank verbinden', ssDots, Cyan);
// Erd-Rotation, ohne Meldung
S := Spinner(ssEarth);
// Mondphasen mit benutzerdefinierter Farbe
S := Spinner('Synchronisieren', ssMoon, Blue);
Spinner in einer Arbeitsschleife
Ein häufiges Muster: Spinner während einer asynchronen Operation anzeigen, dann durch eine Statusmeldung ersetzen.
var S: ISpinner;
begin
S := Spinner('Mit Server verbinden', ssDots, Cyan);
try
ConnectToServer; // Blockiert, aber Spinner läuft auf separatem Thread
S.Hide;
WriteSuccess('Erfolgreich verbunden');
except
S.Hide;
WriteError('Verbindung fehlgeschlagen: ' + E.Message);
end;
end;
Confirm und Choose
Confirm
Confirm fragt den Benutzer nach einer Ja/Nein-Antwort mit optionalem Standard:
// Standard: Ja
if Confirm('Möchten Sie fortfahren?') then
StartOperation
else
Writeln('Abgebrochen.');
// Standard: Nein (sicherer für destruktive Aktionen)
if Confirm('Alle Datensätze löschen?', False) then
DeleteAllRecords;
Der Prompt zeigt [J/N] (J): oder [J/N] (N): je nach Standard. Enter ohne Eingabe akzeptiert den Standard.
Choose
Choose präsentiert eine nummerierte Liste und liest eine numerische Eingabe. Nützlich, wenn die Optionsanzahl klein ist und eine einfachere UX als ein vollständiges Pfeiltasten-Menü gewünscht wird:
var
Options: TStringArray;
Choice: Integer;
SetLength(Options, 3);
Options[0] := 'Schnellmodus';
Options[1] := 'Normalmodus';
Options[2] := 'Sicherheitsmodus';
Choice := Choose('Verarbeitungsmodus wählen:', Options);
if Choice >= 0 then
WriteSuccess('Sie haben gewählt: ' + Options[Choice]);
Ausgabe:
Verarbeitungsmodus wählen:
[1] Schnellmodus
[2] Normalmodus
[3] Sicherheitsmodus
Ihre Wahl: _
Gibt den 0-basierten Index der ausgewählten Option zurück, oder -1 bei ungültiger Eingabe.
Cursor-Steuerung
Ausblenden und Anzeigen
Beim Zeichnen interaktiver UI-Elemente (Menüs, Fortschrittsbalken, Spinner) beseitigt das Ausblenden des Cursors das visuelle Rauschen des blinkenden Carets, das über den Bildschirm springt:
HideCursor;
try
DrawDashboard;
WaitForKey;
finally
ShowCursor; // Immer in einem finally-Block wiederherstellen
end;
Die High-Level-Funktionen (Menu, TableMenu, Spinner) verwalten die Cursor-Sichtbarkeit intern. HideCursor muss nur manuell aufgerufen werden, wenn Low-Level-Zeichenprimitiven direkt verwendet werden.
GotoXY
GotoXY bewegt den Cursor zu einer absoluten Spalten-/Zeilenposition (0-basiert):
// Zur Spalte 10, Zeile 5 bewegen
GotoXY(10, 5);
Write('Wert: ');
GotoXY(18, 5);
Write(CurrentValue:6:2);
Dies ist die Grundlage aller ortsständigen Bildschirmaktualisierungen. Kombiniert mit GetCursorPosition kann eine Position gespeichert und später zurückgekehrt werden:
var Pos: TMVCConsolePoint;
Pos := GetCursorPosition;
Write('Verarbeite...');
DoSomeWork;
GotoXY(Pos.X, Pos.Y);
Write('Fertig! '); // Vorherigen Text überschreiben
Cursor-Position speichern und wiederherstellen
SaveCursorPosition und RestoreCursorPosition speichern/stellen Koordinaten in einer globalen Variable wieder her:
SaveCursorPosition;
// ... etwas anderswo zeichnen
RestoreCursorPosition;
Write('Zurück hier!');
Hinweis: Diese verwenden einen einzigen globalen Speicherslot und sind daher nicht re-entrant. Für komplexe Layouts mit mehreren gespeicherten Positionen verwenden Sie direkt GetCursorPosition / GotoXY.
Zeichenprimitiven
Für Layouts, die mehr als die High-Level-Widgets erfordern, können die rohen Zeichenfunktionen verwendet werden:
DrawBox
Zeichnet eine Box an absoluten Koordinaten ohne die Auffüllung und Farbverwaltung der High-Level-Box:
DrawBox(5, 2, 40, 10, bsSingle, 'Panel-Titel');
DrawBox(50, 2, 30, 10, bsDouble);
Parameter: X, Y (obere linke Ecke), Width, Height, Style (optional), Title (optional, im oberen Rahmen zentriert).
DrawHorizontalLine und DrawVerticalLine
// Einen 60-Zeichen-Trenner in Zeile 12 zeichnen
DrawHorizontalLine(0, 12, 60, bsSingle);
// Einen vertikalen Teiler in Spalte 40 ab Zeile 2, 20 Zeichen hoch zeichnen
DrawVerticalLine(40, 2, 20, bsSingle);
Verwenden Sie bsUseDefault, um den Stil von ConsoleTheme.BoxStyle zu erben.
ClearRegion
Löscht einen rechteckigen Bereich durch Überschreiben mit Leerzeichen:
// Einen 40x10-Bereich ab (5, 3) löschen
ClearRegion(5, 3, 40, 10);
Nützlich zum Aktualisieren von Bildschirmteilen ohne ClrScr aufzurufen (was alles löscht und den Cursor auf 0, 0 setzt).
Themes
Alle High-Level-Widgets lesen ihre Farben und den Box-Stil aus dem globalen ConsoleTheme-Record:
type
TConsoleColorStyle = record
TextColor: TConsoleColor; // Textkörper in Boxen und Tabellen
BackgroundColor: TConsoleColor; // Hintergrund (unter Linux verwendet)
DrawColor: TConsoleColor; // Box-Rahmen und Trennlinien
SymbolsColor: TConsoleColor; // Listen-Aufzählungszeichen, Präfixe
BackgroundHighlightColor: TConsoleColor; // Hintergrund des ausgewählten Elements
TextHighlightColor: TConsoleColor; // Text des ausgewählten Elements, Kopfzeilentext
BoxStyle: TBoxStyle; // Standard-Rahmenstil für alle Widgets
end;
Das Standard-Theme:
ConsoleTheme.TextColor := Cyan;
ConsoleTheme.BackgroundColor := Black;
ConsoleTheme.DrawColor := White;
ConsoleTheme.SymbolsColor := Gray;
ConsoleTheme.BackgroundHighlightColor := Cyan;
ConsoleTheme.TextHighlightColor := Blue;
ConsoleTheme.BoxStyle := bsRounded;
Eine Theme-Änderung wirkt sich auf alle nachfolgenden Aufrufe von Box, Table, Menu, WriteHeader, WriteFormattedList und ähnlichen Funktionen aus:
// Dunkles Terminal-Stil
ConsoleTheme.TextColor := White;
ConsoleTheme.DrawColor := DarkGray;
ConsoleTheme.BackgroundHighlightColor := DarkBlue;
ConsoleTheme.TextHighlightColor := White;
ConsoleTheme.BoxStyle := bsDouble;
// Ab hier verwenden alle Widgets das neue Theme
Box('Systeminfo', ['CPU: 12%', 'RAM: 4.2GB']);
Themes sind eine globale Variable. Wenn ein temporärer Theme-Wechsel benötigt wird, speichern und wiederherstellen:
var SavedTheme: TConsoleColorStyle;
SavedTheme := ConsoleTheme;
ConsoleTheme.BoxStyle := bsThick;
ConsoleTheme.DrawColor := Red;
Box('FEHLER', ['Kritischer Fehler erkannt']);
ConsoleTheme := SavedTheme; // Wiederherstellen
Tastatureingabe
GetKey und GetCh
GetKey blockiert, bis der Benutzer eine Taste drückt, und gibt einen Integer-Code zurück:
- Druckbare Zeichen:
Ord('A')= 65,Ord(' ')= 32 usw. - Sondertasten: Werte über 255, als benannte Konstanten definiert
var Key: Integer;
Key := GetKey;
if IsSpecialKey(Key) then
begin
case Key of
KEY_UP: MoveUp;
KEY_DOWN: MoveDown;
KEY_LEFT: MoveLeft;
KEY_RIGHT: MoveRight;
KEY_ENTER: Confirm;
KEY_ESCAPE: Cancel;
end;
end
else
begin
// Normales Zeichen
ProcessChar(Chr(Key));
end;
GetCh ist eine Abkürzung, die Char statt Integer zurückgibt. Bei Sondertasten gibt es #0 (Null-Zeichen) zurück — verwenden Sie GetKey, wenn Pfeiltasten unterschieden werden müssen.
Sondertasten-Konstanten
const
KEY_UP = 256 + 38; // VK_UP
KEY_DOWN = 256 + 40; // VK_DOWN
KEY_LEFT = 256 + 37; // VK_LEFT
KEY_RIGHT = 256 + 39; // VK_RIGHT
KEY_ESCAPE = 27;
KEY_ENTER = 13;
IsSpecialKey(KeyCode) gibt True zurück, wenn KeyCode > 255, d.h. wenn der Wert eine Pfeiltaste oder einen anderen virtuellen Schlüssel statt eines druckbaren Zeichens darstellt.
Nicht-blockierende Tastenprüfung
KeyPressed gibt True zurück, wenn eine Taste im Eingabepuffer wartet, ohne sie zu verbrauchen:
// Animationsschleife, die bei beliebigem Tastendruck endet
while not KeyPressed do
begin
UpdateAnimation;
Sleep(50);
end;
Key := GetKey; // Ausstehende Taste verbrauchen
WaitForReturn
WaitForReturn blockiert, bis der Benutzer Enter drückt — eine sauberere Alternative zu ReadLn für “Drücken Sie Enter zum Fortfahren”-Aufforderungen:
WriteInfo('Überprüfung abgeschlossen. Drücken Sie ENTER zum Fortfahren.');
WaitForReturn;
Terminal-Fähigkeiten
Konsolengröße
GetConsoleSize gibt die sichtbaren Terminal-Fensterabmessungen zurück:
var Size: TMVCConsoleSize;
Size := GetConsoleSize;
WriteLn(Format('Terminal: %d x %d', [Size.Columns, Size.Rows]));
GetConsoleBufferSize gibt die vollständige Puffergröße zurück (unter Windows kann der Scroll-Puffer größer als das Fenster sein). Für TUI-Layouts verwenden Sie immer GetConsoleSize — Sie wollen den sichtbaren Bereich.
Terminal-Erkennung
if IsTerminalCapable then
StartInteractiveMode
else
// Umgeleitet in Datei — interaktive Funktionen überspringen
WritePlainOutput;
WriteLn('Ausgeführt auf: ' + GetTerminalName);
// Windows → 'Windows Console'
// Linux → Wert der $TERM-Variable (z.B. 'xterm-256color')
Farben und ANSI
// ANSI-Farben aktivieren (erforderlich vor direkter Verwendung von Fore/Back/Style)
EnableANSIColorConsole;
// Prüfen ob ANSI aktiv ist
if IsANSIColorConsoleEnabled then
WriteLn(Fore.Green + 'Farben aktiv!' + Style.ResetAll);
Hilfsfunktionen
ClrScr
Löscht die gesamte Konsole und bewegt den Cursor auf 0, 0. Zu Beginn eines Bildschirms für einen sauberen Start verwenden:
ClrScr;
WriteHeader('Server-Steuerungspanel');
Beep
Gibt einen Systemton aus. Nützlich als akustisches Fehlersignal:
if CriticalError then
begin
Beep;
WriteError('Kritischer Fehler!');
end;
Unter Windows ruft es WinAPI.Windows.Beep(800, 200) auf. Unter Linux schreibt es #7 nach stdout.
FlashScreen
Kehrt kurz die Bildschirmfarben als visuellen Alarm um:
FlashScreen; // 100ms-Flash
WriteError('Ungültige Eingabe');
Unter Linux verwendet dies die ANSI-Reverse-Video-Escape-Sequenz. Unter Windows werden die Farben manuell mit FillConsoleOutputAttribute umgekehrt.
Farb-Hilfsfunktionen
// Den Namen einer Farbkonstante als String abrufen
WriteLn(ColorName(Green)); // 'Green'
WriteLn(ColorName(DarkGray)); // 'DarkGray'
// Farben manuell speichern/wiederherstellen (niedrigeres Niveau als TConsoleColorStyle)
SaveColors;
TextColor(Red);
TextBackground(DarkBlue);
Write('Hervorgehoben');
RestoreSavedColors;
SaveColors und RestoreSavedColors arbeiten auf einem einzigen globalen Slot (gleicher Vorbehalt wie SaveCursorPosition). Sie werden intern von WriteColoredText verwendet und werden selten direkt benötigt.
Alles zusammenführen: ein Server-Dashboard
Hier ist ein realistisches Beispiel, das Boxen, Statusmeldungen, einen Spinner, Fortschritt und ein Menü zu einem einfachen Server-Steuerungspanel kombiniert:
procedure RunServerPanel;
var
Menu: Integer;
S: ISpinner;
P: IProgress;
I: Integer;
Data: TStringMatrix;
Headers: TStringArray;
begin
EnableUTF8Console;
ClrScr;
// Kopfzeile
WriteHeader('DMVC Server-Steuerungspanel', 80, Cyan);
WriteLn;
// Status-Box
Box('Aktueller Status', [
'Webserver: ONLINE',
'Datenbank: VERBUNDEN',
'Cache: WARNUNG - 67% Miss-Rate',
'Backup: INAKTIV'
], 50);
WriteLn;
// Protokoll-Tabelle
SetLength(Headers, 3);
Headers[0] := 'Zeit'; Headers[1] := 'Level'; Headers[2] := 'Meldung';
SetLength(Data, 3);
Data[0] := ['14:32:01', 'INFO', 'Anfrage GET /api/users → 200 (12ms)'];
Data[1] := ['14:32:03', 'WARN', 'Cache-Miss: /api/products/42'];
Data[2] := ['14:32:07', 'ERROR', 'DB-Abfrage-Timeout nach 5000ms'];
Table(Headers, Data, 'Aktuelle Protokolleinträge');
WriteLn;
// Hauptmenü
Menu := Menu('Server-Aktionen', [
'Webserver neu starten',
'Cache leeren',
'Datenbankmigration ausführen',
'Vollständige Protokolle anzeigen',
'Beenden'
]);
case Menu of
0: begin
// Neustart mit Spinner
S := Spinner('Webserver wird neu gestartet', ssDots, Cyan);
Sleep(2000);
S.Hide;
WriteSuccess('Webserver erfolgreich neu gestartet');
end;
1: begin
// Cache leeren mit Fortschritt
P := Progress('Cache-Einträge werden geleert', 100);
for I := 1 to 100 do
begin
P.Update(I);
Sleep(15);
end;
P := nil;
WriteLn;
WriteSuccess('Cache geleert: 4.832 Einträge entfernt');
end;
2: begin
if Confirm('Datenbankmigration ausführen? Dies kann mehrere Minuten dauern.', False) then
begin
S := Spinner('Migration wird ausgeführt', ssGrow, Yellow);
Sleep(3000);
S.Hide;
WriteSuccess('Migration abgeschlossen: 12 Skripte angewendet');
end
else
WriteInfo('Migration abgebrochen');
end;
-1, 4:
WriteInfo('Auf Wiedersehen!');
end;
end;
Schnellreferenz
| Kategorie | Funktion / Typ | Beschreibung |
|---|---|---|
| Einrichtung | EnableUTF8Console |
UTF-8-Ausgabe aktivieren (Box-Zeichen, Emojis) |
| Einrichtung | EnableANSIColorConsole |
ANSI-Sequenzen unter Windows 10+ aktivieren |
| Low-Level-Farbe | Fore.Red, Back.Blue, Style.Bright |
Inline ANSI-Escape-Konstanten |
| Ausgabe | WriteLine(text, fg, bg) |
Vollzeilige farbige Ausgabe |
| Ausgabe | WriteColoredText(text, color) |
Farbiges Inline-Segment, kein Zeilenumbruch |
| Ausgabe | WriteAlignedText(text, width, align, color) |
Aufgefüllter/ausgerichteter Text |
| Status | WriteSuccess / WriteWarning / WriteError / WriteInfo |
Statusmeldungen mit Präfix |
| Layout | WriteHeader(title, width, color) |
Zentrierter Titel mit horizontalen Linien |
| Layout | WriteSeparator(width, char) |
Horizontale Trennlinie |
| Layout | WriteFormattedList(title, items, style) |
Aufzählung / nummeriert / Bindestrich / Pfeil |
| Widgets | Box(title, lines, width) |
Umrandeter Inhaltsbereich |
| Widgets | DrawBox(x, y, w, h, style, title) |
Rohe Box an absoluten Koordinaten |
| Widgets | Table(headers, data, title) |
Statische formatierte Tabelle |
| Widgets | TableMenu(title, headers, data) |
Tastaturnavigierbare Tabelle, gibt Zeilenindex zurück |
| Widgets | Menu(title, items, default) |
Tastaturnavigierbare Menü, gibt Element-Index zurück |
| Animation | Spinner(msg, style, color): ISpinner |
Hintergrundthread-Spinner (10 Stile) |
| Animation | Progress(msg, maxValue): IProgress |
Bestimmter / unbestimmter Fortschrittsbalken |
| Eingabe | GetKey: Integer |
Blockierendes Tastenlesen inkl. Sondertasten |
| Eingabe | KeyPressed: Boolean |
Nicht-blockierende Tastenverfügbarkeitsprüfung |
| Eingabe | WaitForReturn |
Blockiert bis Enter gedrückt wird |
| Eingabe | Confirm(msg, default): Boolean |
J/N-Aufforderung |
| Eingabe | Choose(title, options): Integer |
Nummerierte Auswahlaufgabe |
| Cursor | HideCursor / ShowCursor |
Cursor-Sichtbarkeit umschalten |
| Cursor | GotoXY(x, y) |
Zu absoluter Spalte/Zeile bewegen |
| Cursor | GetCursorPosition: TMVCConsolePoint |
Aktuelle Cursor-Koordinaten lesen |
| Bildschirm | ClrScr |
Bildschirm löschen, Cursor auf 0,0 |
| Bildschirm | ClearRegion(x, y, w, h) |
Rechteckigen Bereich löschen |
| Bildschirm | GetConsoleSize: TMVCConsoleSize |
Sichtbare Terminal-Abmessungen |
| Theme | ConsoleTheme: TConsoleColorStyle |
Globale Farben und Box-Stil für alle Widgets |
Häufig gestellte Fragen
Erfordert MVCFramework.Console eine vollständige DMVCFramework-Installation?
Nein. Die Unit ist eigenständig und hat keine Abhängigkeiten vom Rest des Frameworks. Kopieren Sie MVCFramework.Console.pas in jedes Delphi-Konsolenprojekt und fügen Sie es zur uses-Klausel hinzu. Keine Pakete, keine BPL-Dateien, kein IDE-Setup erforderlich.
Ist MVCFramework.Console plattformübergreifend?
Ja. Die Bibliothek kompiliert und läuft auf Windows 10+ und Linux mit derselben API. ANSI-Farbunterstützung ist unter Linux nativ; unter Windows wird sie automatisch über EnableANSIColorConsole aktiviert. Box-Zeichenzeichen und Emojis erfordern EnableUTF8Console beim Start.
Welche Delphi-Version wird benötigt?
Delphi 10 Seattle (D100) oder höher. Die Unit wird mit DMVCFramework 3.5.0 Silicon und späteren Versionen geliefert.
Wie lese ich Pfeiltasten in einer Delphi-Konsolenanwendung?
Verwenden Sie GetKey: Integer. Pfeiltasten geben Werte über 255 zurück: KEY_UP = 256+38, KEY_DOWN = 256+40, KEY_LEFT = 256+37, KEY_RIGHT = 256+39. Verwenden Sie IsSpecialKey(key), um Sondertasten von druckbaren Zeichen zu unterscheiden.
Wie erstelle ich einen nicht-blockierenden Spinner?
S := Spinner('Verbinde...', ssDots, Cyan);
DoWork; // Hauptthread läuft weiter; Spinner animiert unabhängig
S.Hide; // oder: S := nil
Der Spinner läuft auf einem Hintergrundthread. Zehn Stile sind verfügbar: ssLine, ssDots, ssBounce, ssGrow, ssArrow, ssCircle, ssClock, ssEarth, ssMoon, ssWeather.
Wie ändere ich die Farben und den Box-Stil aller Widgets auf einmal?
Ändern Sie den globalen ConsoleTheme-Record bevor Sie eine Widget-Funktion aufrufen. Alle nachfolgenden Aufrufe von Box, Menu, Table, WriteHeader und ähnlichen Funktionen verwenden die aktualisierten Einstellungen. Speichern und stellen Sie den Record wieder her, wenn ein temporärer Stilwechsel benötigt wird.
Wie erstelle ich ein interaktives, per Tastatur navigierbares Menü?
Selected := Menu('Optionen', ['Starten', 'Stoppen', 'Beenden']);
// Pfeiltasten navigieren, Enter bestätigt, Escape gibt -1 zurück
Für eine interaktive Tabelle mit Zeilenauswahl verwenden Sie TableMenu(Titel, Kopfzeilen, Daten).
Zusammenfassung
MVCFramework.Console bietet ein vollständiges Toolkit zum Entwickeln von TUI-Anwendungen in Delphi — von einfachen farbigen Statusmeldungen bis hin zu interaktiven Menüs mit Tastaturnavigation, animierten Spinnern, Fortschrittsbalken und strukturierten Tabellen.
Die wichtigsten Design-Punkte:
- Keine Framework-Abhängigkeit. Unit in jedes Konsolenprojekt einbinden.
- Plattformübergreifend. Windows (10+) und Linux mit derselben API.
- Interface-basierter Lebenszyklus.
ISpinnerundIProgressbereinigen automatisch beim Freigeben. - Theme-System. Ein einziger globaler Record steuert alle Widget-Farben und Rahmenstile.
- Zwei Ebenen. Low-Level (
TextColor,GotoXY,DrawBox) für benutzerdefinierte Layouts; High-Level (Menu,Table,Spinner) für Standard-Widgets.
Das vollständige Beispiel befindet sich in samples/console_sample/ConsoleDemo.dpr im DMVCFramework-Repository auf GitHub.
Dieser Beitrag ist Teil der DMVCFramework 3.5.0 “Silicon”-Serie. Für die Low-Level-ANSI-Farb-API (Fore, Back, Style) und den Gin-artigen HTTP-Log-Renderer, siehe den vorherigen Beitrag der Serie.
Comments
comments powered by Disqus