Costruire applicazioni TUI con Delphi e DMVCFramework
TUI: non un passo indietro
Se hai seguito il recente sviluppo di DMVCFramework, sai già che le applicazioni console stanno vivendo una seconda vita. Nel post precedente sui colori console, ho introdotto le API ANSI di basso livello — i record Fore, Back e Style — e il renderer di log HTTP in stile Gin. Quella era la fondazione.
Questo post riguarda ciò che ci è stato costruito sopra: la libreria TUI completa integrata in MVCFramework.Console.pas.
TUI sta per Text User Interface. È il mondo di htop, lazygit, k9s e di ogni strumento CLI che usa tasti freccia, box colorati e barre di avanzamento. Queste applicazioni non sono “semplici programmi console” — sono interfacce ricche e interattive che si eseguono in un terminale.
Con DMVCFramework 3.5.0 Silicon, gli sviluppatori Delphi dispongono ora di un toolkit TUI completo e cross-platform:
- Output di testo colorato con allineamento
- Menu interattivi navigati con i tasti freccia
- Spinner animati con 10 stili
- Barre di avanzamento determinate e indeterminate
- Tabelle con visualizzazione statica e selezione interattiva delle righe
- Box con bordi e titoli opzionali
- Controllo del cursore: nasconde, mostra, salva, ripristina, sposta
- Gestione dell’input da tastiera inclusi i tasti speciali
- Un sistema di temi globale
Tutto in una singola unit — MVCFramework.Console — con zero dipendenze dal resto del framework. Puoi copiarla in qualsiasi progetto console Delphi e usarla immediatamente.
Setup
Aggiungi MVCFramework.Console alla clausola uses. Per il supporto Unicode completo (frame degli spinner, caratteri box-drawing, emoji), chiama EnableUTF8Console all’inizio del programma:
program MyTUIApp;
{$APPTYPE CONSOLE}
uses
System.SysUtils,
MVCFramework.Console;
begin
EnableUTF8Console; // Necessario per caratteri box, spinner con emoji, ecc.
// ... il tuo codice qui
end.
Su Windows, EnableUTF8Console chiama SetConsoleOutputCP(CP_UTF8). Su Linux è un no-op perché UTF-8 è il default. Se hai intenzione di usare direttamente le primitive ANSI (Fore, Back, Style) con WriteLn, chiama anche EnableANSIColorConsole. Tuttavia, tutte le funzioni di alto livello di questa libreria la chiamano internamente, quindi nella maggior parte dei casi non è necessario preoccuparsene.
Colori ANSI: l’approccio colorama (Fore / Back / Style)
MVCFramework.Console include due sistemi di colore complementari. Il primo — trattato nel post precedente — è una API ANSI di basso livello modellata sulla libreria colorama di Python. Vale la pena riprenderlo qui perché è uno strumento essenziale per qualsiasi applicazione TUI, e si compone naturalmente con tutto il resto di questa unit.
L’API consiste di tre record con costanti stringa:
| Record | Scopo | Esempio |
|---|---|---|
Fore |
Colore del testo (primo piano) | Fore.Red, Fore.Green, Fore.Cyan |
Back |
Colore di sfondo | Back.DarkBlue, Back.DarkRed |
Style |
Stile testo e reset | Style.Bright, Style.Dim, Style.ResetAll |
Ogni costante è una stringa di sequenza di escape ANSI grezza. I colori si compongono con l’operatore +, e Style.ResetAll termina un segmento colorato. Chiama EnableANSIColorConsole una volta all’avvio (idempotente, no-op su Linux):
EnableANSIColorConsole;
// Colori del testo base
WriteLn(Fore.Red + 'Errore: connessione rifiutata' + Style.ResetAll);
WriteLn(Fore.Green + 'OK: server avviato su :8080' + Style.ResetAll);
WriteLn(Fore.Yellow + 'Avviso: cache miss rate 67%' + Style.ResetAll);
WriteLn(Fore.Cyan + 'Info: uso config.env' + Style.ResetAll);
WriteLn(Fore.DarkGray + '# output di debug' + Style.ResetAll);
Combinare colore testo, sfondo e stile
La vera potenza sta nella composizione. Poiché le costanti sono semplici stringhe, qualsiasi combinazione è una singola concatenazione:
// Testo + sfondo
WriteLn(Fore.White + Back.DarkBlue + ' INFO ' + Style.ResetAll + ' Server avviato');
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 + ' Uso memoria elevato');
// Modificatori di stile
WriteLn(Style.Bright + Fore.White + 'Intestazione bianca grassetto' + Style.ResetAll);
WriteLn(Style.Dim + Fore.Gray + 'Testo secondario attenuato' + Style.ResetAll);
// Misto inline
WriteLn(
'Stato: ' + Fore.Green + 'ONLINE' + Style.ResetAll +
' | Req/s: ' + Fore.Cyan + '142' + Style.ResetAll +
' | Errori: ' + Fore.Red + '0' + Style.ResetAll
);
Costanti di stile riutilizzabili (come il CSS per la console)
Invece di ripetere Fore.White + Back.DarkGreen dappertutto, definisci costanti con nomi significativi una sola volta. Il compilatore le risolve a compile-time — zero overhead a runtime:
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;
// Codice pulito e leggibile:
WriteLn(BADGE_OK + ' PASS ' + RESET + ' TestUserAuth');
WriteLn(BADGE_FAIL + ' FAIL ' + RESET + ' TestPaymentTimeout');
WriteLn(BADGE_WARN + ' SLOW ' + RESET + ' Query ha impiegato 3.2s');
WriteLn(BADGE_INFO + ' NOTE ' + RESET + ' Uso configurazione di fallback');
WriteLn(MUTED + '--- fine del report ---' + RESET);
Quando usare Fore/Back/Style rispetto a TConsoleColor
I due sistemi sono complementari, non in competizione:
Usa Fore / Back / Style quando… |
Usa TConsoleColor / WriteLine quando… |
|---|---|
Hai bisogno di colorazione inline in un WriteLn |
Vuoi output colorato su tutta la riga |
| Stai costruendo badge o prefissi di log | Stai usando Box, Table, Menu (con tema) |
| Vuoi costanti di stile a compile-time | Vuoi che si applichi il ConsoleTheme globale |
Hai bisogno di Style.Bright o Style.Dim |
Hai bisogno del colore di sfondo dal tema |
Entrambi i sistemi coesistono. Una tipica applicazione TUI usa Fore/Back/Style per testo inline personalizzato e si affida alle funzioni basate su TConsoleColor per i widget strutturali (box, tabelle, menu).
Output di testo
La palette dei colori
La libreria definisce TConsoleColor, un’enumerazione a 17 valori che copre i classici 16 colori del terminale più UseDefault (eredita dal tema attivo):
Black, DarkBlue, DarkGreen, DarkCyan, DarkRed, DarkMagenta,
DarkYellow, Gray, DarkGray, Blue, Green, Cyan, Red, Magenta,
Yellow, White, UseDefault
Su Windows corrispondono alla classica Windows Console API, su Linux ai codici ANSI. I nomi sono identici su entrambe le piattaforme: Green è sempre verde, Red è sempre rosso.
WriteLine e WriteColoredText
Il modo più semplice per emettere testo colorato:
// Testo semplice (usa i default del terminale)
WriteLine('Avvio inizializzazione server...');
// Testo con colore del testo
WriteLine('Server avviato con successo', Green);
// Testo con colore del testo e di sfondo
WriteLine(' CRITICO ', White, DarkRed);
// Segmento colorato inline (senza a capo)
WriteColoredText('[INFO] ', Cyan);
WriteLine('In ascolto sulla porta 8080', White);
WriteColoredText salva e ripristina automaticamente i colori correnti, quindi è sicuro anidarle o chiamarle in sequenza senza preoccuparsi della propagazione dei colori.
Testo allineato
WriteAlignedText aggiunge padding al testo fino a una larghezza fissa con allineamento configurabile:
// Centra un titolo in un'area larga 80 caratteri
WriteAlignedText('DMVCFramework Server', 80, taCenter, Yellow);
// Allinea a destra un valore
WriteAlignedText('v3.5.0', 80, taRight, DarkGray);
// Allinea a sinistra con colore
WriteAlignedText('Stato: ONLINE', 80, taLeft, Green);
I tre modi di allineamento — taLeft, taCenter, taRight — coprono tutte le esigenze di layout. Il parametro Width controlla la larghezza della colonna di output, non la larghezza della stringa: le stringhe più corte vengono imbottite, quelle più lunghe troncate.
CenterInScreen è una scorciatoia di convenienza che centra una stringa nella finestra fisica della console:
CenterInScreen('Premi un tasto per continuare');
Legge la dimensione corrente della console con GetConsoleSize e calcola automaticamente le coordinate GotoXY corrette.
Messaggi di stato
Quattro funzioni di stato predefinite producono output con stile coerente:
WriteSuccess('Migrazione database completata in 1.2s');
WriteWarning('Cache miss rate superiore al 50% — considera di ottimizzare il TTL');
WriteError('Connessione rifiutata sulla porta 5432');
WriteInfo('Uso configurazione di fallback dall''ambiente');
Ognuna produce un badge prefisso colorato seguito da testo bianco del messaggio:
| Funzione | Badge | Colore badge |
|---|---|---|
WriteSuccess |
[SUCCESS] |
Verde |
WriteWarning |
[WARNING] |
Giallo |
WriteError |
[ERROR] |
Rosso |
WriteInfo |
[INFO] |
Ciano |
Intestazioni e separatori
WriteHeader produce un titolo centrato racchiuso da linee orizzontali, usando il set di caratteri box-drawing attivo:
WriteHeader('Strumento di migrazione database');
WriteHeader('Riepilogo configurazione', 60);
WriteHeader('Risultati', 60, Green); // Colore personalizzato
La larghezza predefinita è 80 colonne. Il carattere della linea orizzontale si adatta a ConsoleTheme.BoxStyle (vedi la sezione Temi più avanti).
WriteSeparator disegna una semplice linea orizzontale — utile per dividere visivamente le sezioni di output:
WriteSeparator; // 60 trattini
WriteSeparator(80); // 80 trattini
WriteSeparator(40, '='); // 40 segni di uguale
Liste formattate
WriteFormattedList renderizza una lista con titolo e stile bullet configurabile:
var Features: TStringArray;
SetLength(Features, 4);
Features[0] := 'Menu interattivi con navigazione a frecce';
Features[1] := 'Spinner non bloccanti su thread in background';
Features[2] := 'Tabelle con visualizzazione statica e interattiva';
Features[3] := 'Sistema di temi globale';
WriteFormattedList('Novità in DMVCFramework 3.5.0:', Features, lsBullet);
WriteFormattedList('Passi per l''aggiornamento:', Features, lsNumbered);
WriteFormattedList('Funzionalità rimosse:', Features, lsDash);
WriteFormattedList('Prossimi passi:', Features, lsArrow);
I quattro stili di lista:
| Stile | Prefisso |
|---|---|
lsBullet |
* |
lsNumbered |
1. |
lsDash |
- |
lsArrow |
> |
Box
La funzione Box renderizza un’area di contenuto con bordi, auto-dimensionandosi alla larghezza del contenuto di default:
// Box semplice, senza titolo, larghezza default (60)
Box(['Server: ONLINE', 'Database: CONNESSO', 'Memoria: 65%']);
// Box con titolo
Box('Stato del sistema', ['Server: ONLINE', 'Database: CONNESSO']);
// Box con titolo e larghezza personalizzata
Box('AVVISO', ['Server cache non risponde', 'Controlla la connessione di rete'], 50);
Il contenuto del box viene automaticamente imbottito a sinistra all’interno del bordo. I caratteri del bordo si adattano a ConsoleTheme.BoxStyle.
Per un controllo preciso del disegno, usa direttamente la primitiva di basso livello DrawBox:
// Disegna un box 40x10 partendo dalla colonna 5, riga 3
DrawBox(5, 3, 40, 10, bsDouble, 'Pannello Debug');
Stili box
Sono disponibili quattro stili di bordo tramite TBoxStyle:
DrawBox(0, 0, 30, 5, bsSingle, 'Singolo'); // ┌─┐ │ └─┘
DrawBox(0, 6, 30, 5, bsDouble, 'Doppio'); // ╔═╗ ║ ╚═╝
DrawBox(0, 12, 30, 5, bsRounded, 'Arrotondato'); // ╭─╮ │ ╰─╯
DrawBox(0, 18, 30, 5, bsThick, 'Spesso'); // ┏━┓ ┃ ┗━┛
Lo stile box predefinito per tutte le funzioni di alto livello (Box, Menu, Table) è controllato da ConsoleTheme.BoxStyle. Cambiarlo una volta influisce su tutti i widget:
ConsoleTheme.BoxStyle := bsDouble;
Box('Ora tutto usa bordi doppi', ['Voce 1', 'Voce 2']);
Tabelle
Tabella statica
Table renderizza una tabella formattata con colonne auto-dimensionate:
var
Headers: TStringArray;
Data: TStringMatrix;
SetLength(Headers, 4);
Headers[0] := 'ID'; Headers[1] := 'Nome';
Headers[2] := 'Framework'; Headers[3] := 'Linguaggio';
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#'];
// Senza titolo
Table(Headers, Data);
// Con titolo
Table(Headers, Data, 'Team di sviluppo');
Le larghezze delle colonne vengono calcolate automaticamente: ogni colonna è larga quanto la sua cella più larga (intestazione o dati), più 2 caratteri di padding interno. La riga di intestazione viene renderizzata con ConsoleTheme.TextHighlightColor per distinzione visiva.
Tabella interattiva
TableMenu trasforma una tabella in un selettore interattivo navigato con i tasti freccia:
var SelectedRow: Integer;
SelectedRow := TableMenu('Seleziona uno sviluppatore', Headers, Data);
if SelectedRow >= 0 then
WriteSuccess('Hai selezionato: ' + Data[SelectedRow][1])
else
WriteWarning('Selezione annullata');
La riga selezionata viene evidenziata usando ConsoleTheme.BackgroundHighlightColor e ConsoleTheme.TextHighlightColor. Tasti di navigazione:
| Tasto | Azione |
|---|---|
| ↑ / ↓ | Sposta la selezione su/giù |
| Invio | Conferma la selezione, restituisce l’indice della riga |
| Escape | Annulla, restituisce -1 |
Il cursore viene nascosto automaticamente durante l’interazione e ripristinato all’uscita.
Puoi pre-selezionare una riga:
SelectedRow := TableMenu('Seleziona', Headers, Data, 2); // Inizia con la riga 2 selezionata
Menu interattivi
Menu mostra una lista navigabile da tastiera racchiusa in un box. Appare alla posizione corrente del cursore e si pulisce da solo quando l’utente effettua una selezione o preme Escape.
var
Items: TStringArray;
Selected: Integer;
SetLength(Items, 5);
Items[0] := 'Avvia server';
Items[1] := 'Ferma server';
Items[2] := 'Visualizza log';
Items[3] := 'Impostazioni';
Items[4] := 'Esci';
// Senza titolo
Selected := Menu(Items);
// Con titolo
Selected := Menu('Menu principale', Items);
// Con titolo e voce pre-selezionata
Selected := Menu('Menu file', Items, 2); // Inizia con 'Visualizza log' selezionato
Valore restituito: l’indice della voce selezionata (0-based), o -1 se l’utente ha premuto Escape.
Navigazione:
| Tasto | Azione |
|---|---|
| ↑ | Sposta su (con wrap) |
| ↓ | Sposta giù (con wrap) |
| Invio | Conferma |
| Escape | Annulla |
La voce selezionata viene renderizzata con colori invertiti (sfondo evidenziato). Il menu si adatta automaticamente in larghezza alla voce più lunga più padding. Se il menu supererebbe il fondo del terminale, viene riposizionato automaticamente verso l’alto.
// Esempio pratico: loop di dispatch principale
while True do
begin
Selected := Menu('Controllo server', ['Avvia', 'Ferma', 'Riavvia', 'Esci']);
case Selected of
0: StartServer;
1: StopServer;
2: begin StopServer; StartServer; end;
3, -1: Break;
end;
end;
Barre di avanzamento
Avanzamento determinato
Progress con MaxValue > 0 produce una barra di avanzamento determinata:
var
P: IProgress;
I: Integer;
P := Progress('Download file', 100);
for I := 1 to 100 do
begin
P.Update(I);
Sleep(20); // Simula lavoro
end;
P := nil; // Chiama Complete automaticamente tramite il distruttore
La barra viene renderizzata come [==== ] 45%. La parte piena viene calcolata come (Corrente * Larghezza) div MaxValue. La larghezza predefinita della barra è 50 caratteri.
L’interfaccia IProgress espone:
| Metodo | Descrizione |
|---|---|
Update(Value) |
Imposta il valore corrente (assoluto) |
Increment(Amount) |
Incrementa di Amount (default 1) |
SetMessage(Msg) |
Aggiorna l’etichetta del titolo |
Complete |
Segna come fatto, stampa “Fatto!” in verde |
Impostare P := nil attiva il distruttore, che chiama Complete se non è già stato fatto. Questo significa che wrappare l’avanzamento in un blocco try/finally è opzionale — la pulizia è garantita.
// Stile incrementale
P := Progress('Elaborazione record', 1000);
for I := 1 to 1000 do
begin
ProcessRecord(I);
P.Increment;
end;
Avanzamento indeterminato
Progress con MaxValue = 0 produce uno spinner indeterminato all’interno di una coppia di parentesi:
P := Progress('Caricamento configurazione');
// Esegui il lavoro...
while StillLoading do
begin
P.Update(0); // Avanza lo spinner
Sleep(50);
end;
P.Complete;
Lo spinner all’interno delle parentesi [|] cicla attraverso i caratteri |/-\. Chiama Update periodicamente per avanzare l’animazione. Questo è un pattern bloccante — lo spinner avanza solo quando chiami Update. Per uno spinner completamente non bloccante, usa Spinner (vedi sotto).
Spinner
La funzione Spinner crea uno spinner animato non bloccante eseguito su un thread in background. Il thread principale continua a lavorare mentre lo spinner anima in modo indipendente:
var S: ISpinner;
S := Spinner('Caricamento dati', ssLine, DarkGray);
// Fai il tuo lavoro qui — lo spinner anima da solo
FetchRemoteData;
S.Hide; // Oppure: S := nil (chiama il distruttore → Hide)
Rilasciare l’interfaccia (impostando a nil) chiama anche Hide, che ferma il thread e cancella lo spinner dal terminale.
I 10 stili spinner
TSpinnerStyle offre 10 stili di animazione:
| Stile | Caratteri | Intervallo | Descrizione |
|---|---|---|---|
ssLine |
-\│/ |
100ms | Spinner terminale classico |
ssDots |
Pattern Braille | 80ms | Animazione a punti fluida |
ssBounce |
Rimbalzo Braille | 80ms | Punto rimbalzante |
ssGrow |
Elementi blocco | 120ms | Barra crescente |
ssArrow |
Caratteri freccia | 100ms | Freccia rotante |
ssCircle |
Quarti di cerchio | 100ms | Cerchio rotante |
ssClock |
🕐🕑🕒… | 200ms | 12 facce orologio |
ssEarth |
🌍🌎🌏 | 200ms | Rotazione globo |
ssMoon |
🌑🌒🌓… | 200ms | 8 fasi lunari |
ssWeather |
🌤🌧⛈… | 200ms | Icone meteo |
Ogni stile usa automaticamente l’intervallo di animazione appropriato. Non è necessario regolare i valori di Sleep.
// Spinner a linea (default)
S := Spinner(ssLine);
// Dots con messaggio
S := Spinner('Connessione al database', ssDots, Cyan);
// Rotazione terra, senza messaggio
S := Spinner(ssEarth);
// Fasi lunari con colore personalizzato
S := Spinner('Sincronizzazione', ssMoon, Blue);
Spinner in un ciclo di lavoro
Un pattern comune: mostra uno spinner mentre un’operazione asincrona è in esecuzione, poi sostituiscilo con un messaggio di stato.
var S: ISpinner;
begin
S := Spinner('Connessione al server', ssDots, Cyan);
try
ConnectToServer; // Blocca, ma lo spinner gira su un thread separato
S.Hide;
WriteSuccess('Connesso con successo');
except
S.Hide;
WriteError('Connessione fallita: ' + E.Message);
end;
end;
Confirm e Choose
Confirm
Confirm chiede all’utente una risposta sì/no con un default opzionale:
// Default: Sì
if Confirm('Vuoi continuare?') then
StartOperation
else
Writeln('Annullato.');
// Default: No (più sicuro per azioni distruttive)
if Confirm('Eliminare tutti i record?', False) then
DeleteAllRecords;
Il prompt mostra [S/N] (S): o [S/N] (N): a seconda del default. Premere Invio senza digitare nulla accetta il default.
Choose
Choose presenta una lista numerata e legge un input numerico. Utile quando il numero di opzioni è piccolo e si vuole un’UX più semplice rispetto a un menu completo con tasti freccia:
var
Options: TStringArray;
Choice: Integer;
SetLength(Options, 3);
Options[0] := 'Modalità rapida';
Options[1] := 'Modalità normale';
Options[2] := 'Modalità sicura';
Choice := Choose('Seleziona modalità di elaborazione:', Options);
if Choice >= 0 then
WriteSuccess('Hai scelto: ' + Options[Choice]);
Output:
Seleziona modalità di elaborazione:
[1] Modalità rapida
[2] Modalità normale
[3] Modalità sicura
La tua scelta: _
Restituisce l’indice 0-based dell’opzione selezionata, o -1 se l’input non è valido.
Controllo del cursore
Nascondere e mostrare
Quando si disegnano elementi UI interattivi (menu, barre di avanzamento, spinner), nascondere il cursore elimina il rumore visivo del cursore lampeggiante che salta per lo schermo:
HideCursor;
try
DrawDashboard;
WaitForKey;
finally
ShowCursor; // Ripristina sempre in un blocco finally
end;
Le funzioni di alto livello (Menu, TableMenu, Spinner) gestiscono internamente la visibilità del cursore. È necessario chiamare HideCursor manualmente solo quando si usano direttamente le primitive di disegno di basso livello.
GotoXY
GotoXY sposta il cursore a una posizione assoluta colonna/riga (0-based):
// Sposta alla colonna 10, riga 5
GotoXY(10, 5);
Write('Valore: ');
GotoXY(18, 5);
Write(CurrentValue:6:2);
Questa è la base di tutti gli aggiornamenti dello schermo in-place. Combinato con GetCursorPosition, puoi registrare una posizione e tornarci in seguito:
var Pos: TMVCConsolePoint;
Pos := GetCursorPosition;
Write('Elaborazione...');
DoSomeWork;
GotoXY(Pos.X, Pos.Y);
Write('Fatto! '); // Sovrascrive il testo precedente
Salvare e ripristinare la posizione del cursore
SaveCursorPosition e RestoreCursorPosition memorizzano/ripristinano le coordinate in una variabile globale:
SaveCursorPosition;
// ... disegna qualcosa altrove
RestoreCursorPosition;
Write('Di ritorno qui!');
Nota: queste usano un unico slot di memorizzazione globale, quindi non sono rientranti. Per layout complessi con più posizioni salvate, usa direttamente GetCursorPosition / GotoXY.
Primitive di disegno
Per i layout che richiedono più dei widget di alto livello, puoi usare le funzioni di disegno grezze:
DrawBox
Disegna un box a coordinate assolute senza il padding e la gestione del colore dell’Box di alto livello:
DrawBox(5, 2, 40, 10, bsSingle, 'Titolo pannello');
DrawBox(50, 2, 30, 10, bsDouble);
Parametri: X, Y (angolo in alto a sinistra), Width, Height, Style (opzionale), Title (opzionale, centrato nel bordo superiore).
DrawHorizontalLine e DrawVerticalLine
// Disegna un separatore di 60 caratteri alla riga 12
DrawHorizontalLine(0, 12, 60, bsSingle);
// Disegna un divisore verticale alla colonna 40, dalla riga 2, alto 20 caratteri
DrawVerticalLine(40, 2, 20, bsSingle);
Usa bsUseDefault per ereditare lo stile da ConsoleTheme.BoxStyle.
ClearRegion
Cancella un’area rettangolare sovrascrivendola con spazi:
// Cancella una regione 40x10 a partire da (5, 3)
ClearRegion(5, 3, 40, 10);
Utile per aggiornare parti dello schermo senza chiamare ClrScr (che cancella tutto e riposiziona il cursore a 0, 0).
Temi
Tutti i widget di alto livello leggono i loro colori e lo stile box dal record globale ConsoleTheme:
type
TConsoleColorStyle = record
TextColor: TConsoleColor; // Testo del corpo in box e tabelle
BackgroundColor: TConsoleColor; // Sfondo (usato su Linux)
DrawColor: TConsoleColor; // Bordi box e separatori
SymbolsColor: TConsoleColor; // Bullet delle liste, prefissi
BackgroundHighlightColor: TConsoleColor; // Sfondo della voce selezionata
TextHighlightColor: TConsoleColor; // Testo della voce selezionata, testo intestazione
BoxStyle: TBoxStyle; // Stile bordo default per tutti i widget
end;
Il tema predefinito:
ConsoleTheme.TextColor := Cyan;
ConsoleTheme.BackgroundColor := Black;
ConsoleTheme.DrawColor := White;
ConsoleTheme.SymbolsColor := Gray;
ConsoleTheme.BackgroundHighlightColor := Cyan;
ConsoleTheme.TextHighlightColor := Blue;
ConsoleTheme.BoxStyle := bsRounded;
Cambiare il tema influisce su tutte le chiamate successive a Box, Table, Menu, WriteHeader, WriteFormattedList e funzioni simili:
// Stile terminale scuro
ConsoleTheme.TextColor := White;
ConsoleTheme.DrawColor := DarkGray;
ConsoleTheme.BackgroundHighlightColor := DarkBlue;
ConsoleTheme.TextHighlightColor := White;
ConsoleTheme.BoxStyle := bsDouble;
// Da questo punto, tutti i widget usano il nuovo tema
Box('Info sistema', ['CPU: 12%', 'RAM: 4.2GB']);
I temi sono una variabile globale. Se hai bisogno di cambiare tema temporaneamente, salva e ripristina:
var SavedTheme: TConsoleColorStyle;
SavedTheme := ConsoleTheme;
ConsoleTheme.BoxStyle := bsThick;
ConsoleTheme.DrawColor := Red;
Box('ERRORE', ['Rilevato guasto critico']);
ConsoleTheme := SavedTheme; // Ripristina
Input da tastiera
GetKey e GetCh
GetKey blocca finché l’utente non preme un tasto e restituisce un codice intero:
- Caratteri stampabili:
Ord('A')= 65,Ord(' ')= 32, ecc. - Tasti speciali: valori superiori a 255, definiti come costanti con nome
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
// Carattere normale
ProcessChar(Chr(Key));
end;
GetCh è una scorciatoia che restituisce Char invece di Integer. Per i tasti speciali restituisce #0 (carattere nullo) — usa GetKey se devi distinguere i tasti freccia.
Costanti tasti speciali
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) restituisce True quando KeyCode > 255, ovvero quando il valore rappresenta un tasto freccia o altro tasto virtuale piuttosto che un carattere stampabile.
Controllo tasto non bloccante
KeyPressed restituisce True se un tasto è in attesa nel buffer di input, senza consumarlo:
// Loop animato che esce alla pressione di qualsiasi tasto
while not KeyPressed do
begin
UpdateAnimation;
Sleep(50);
end;
Key := GetKey; // Consuma il tasto in attesa
WaitForReturn
WaitForReturn blocca finché l’utente non preme Invio — un’alternativa più pulita a ReadLn per i prompt “premi invio per continuare”:
WriteInfo('Revisione completata. Premi INVIO per continuare.');
WaitForReturn;
Capacità del terminale
Dimensione della console
GetConsoleSize restituisce le dimensioni visibili della finestra del terminale:
var Size: TMVCConsoleSize;
Size := GetConsoleSize;
WriteLn(Format('Terminale: %d x %d', [Size.Columns, Size.Rows]));
GetConsoleBufferSize restituisce la dimensione completa del buffer (su Windows, il buffer di scorrimento può essere più grande della finestra). Per i layout TUI, usa sempre GetConsoleSize — vuoi l’area visibile.
Rilevamento del terminale
if IsTerminalCapable then
StartInteractiveMode
else
// Reindirizzato a file — salta le funzioni interattive
WritePlainOutput;
WriteLn('In esecuzione su: ' + GetTerminalName);
// Windows → 'Windows Console'
// Linux → valore della variabile $TERM (es. 'xterm-256color')
Colori e ANSI
// Abilita i colori ANSI (necessario prima di usare Fore/Back/Style direttamente)
EnableANSIColorConsole;
// Controlla se ANSI è attivo
if IsANSIColorConsoleEnabled then
WriteLn(Fore.Green + 'Colori attivi!' + Style.ResetAll);
Funzioni di utilità
ClrScr
Cancella l’intera console e sposta il cursore a 0, 0. Usa all’inizio di uno schermo per dare uno slate pulito:
ClrScr;
WriteHeader('Pannello di controllo server');
Beep
Emette un segnale acustico di sistema. Utile come segnale audio di errore:
if CriticalError then
begin
Beep;
WriteError('Guasto critico!');
end;
Su Windows chiama WinAPI.Windows.Beep(800, 200). Su Linux scrive #7 su stdout.
FlashScreen
Inverte brevemente i colori dello schermo come avviso visivo:
FlashScreen; // Flash da 100ms
WriteError('Input non valido');
Su Linux usa la sequenza di escape ANSI reverse-video. Su Windows inverte i colori manualmente usando FillConsoleOutputAttribute.
Helper dei colori
// Ottieni il nome di una costante colore come stringa
WriteLn(ColorName(Green)); // 'Green'
WriteLn(ColorName(DarkGray)); // 'DarkGray'
// Salva/ripristina i colori manualmente (livello più basso di TConsoleColorStyle)
SaveColors;
TextColor(Red);
TextBackground(DarkBlue);
Write('Evidenziato');
RestoreSavedColors;
SaveColors e RestoreSavedColors operano su un unico slot globale (stessa avvertenza di SaveCursorPosition). Sono usati internamente da WriteColoredText e raramente servono direttamente.
Mettere tutto insieme: un pannello server
Ecco un esempio realistico che combina box, messaggi di stato, uno spinner, l’avanzamento e un menu in un semplice pannello di controllo server:
procedure RunServerPanel;
var
Menu: Integer;
S: ISpinner;
P: IProgress;
I: Integer;
Data: TStringMatrix;
Headers: TStringArray;
begin
EnableUTF8Console;
ClrScr;
// Intestazione
WriteHeader('Pannello di controllo DMVC Server', 80, Cyan);
WriteLn;
// Box di stato
Box('Stato corrente', [
'Web Server: ONLINE',
'Database: CONNESSO',
'Cache: AVVISO - 67% miss rate',
'Backup: INATTIVO'
], 50);
WriteLn;
// Tabella log
SetLength(Headers, 3);
Headers[0] := 'Ora'; Headers[1] := 'Livello'; Headers[2] := 'Messaggio';
SetLength(Data, 3);
Data[0] := ['14:32:01', 'INFO', 'Richiesta GET /api/users → 200 (12ms)'];
Data[1] := ['14:32:03', 'WARN', 'Cache miss: /api/products/42'];
Data[2] := ['14:32:07', 'ERROR', 'Timeout query DB dopo 5000ms'];
Table(Headers, Data, 'Voci di log recenti');
WriteLn;
// Menu principale
Menu := Menu('Azioni server', [
'Riavvia Web Server',
'Svuota Cache',
'Esegui migrazione database',
'Visualizza log completi',
'Esci'
]);
case Menu of
0: begin
// Riavvio con spinner
S := Spinner('Riavvio web server', ssDots, Cyan);
Sleep(2000);
S.Hide;
WriteSuccess('Web server riavviato con successo');
end;
1: begin
// Svuota cache con avanzamento
P := Progress('Svuotamento voci cache', 100);
for I := 1 to 100 do
begin
P.Update(I);
Sleep(15);
end;
P := nil;
WriteLn;
WriteSuccess('Cache svuotata: 4.832 voci rimosse');
end;
2: begin
if Confirm('Eseguire la migrazione database? Potrebbe richiedere diversi minuti.', False) then
begin
S := Spinner('Esecuzione migrazione', ssGrow, Yellow);
Sleep(3000);
S.Hide;
WriteSuccess('Migrazione completata: 12 script applicati');
end
else
WriteInfo('Migrazione annullata');
end;
-1, 4:
WriteInfo('Arrivederci!');
end;
end;
Riferimento rapido
| Categoria | Funzione / Tipo | Descrizione |
|---|---|---|
| Setup | EnableUTF8Console |
Abilita output UTF-8 (caratteri box, emoji) |
| Setup | EnableANSIColorConsole |
Abilita sequenze ANSI su Windows 10+ |
| Colore basso livello | Fore.Red, Back.Blue, Style.Bright |
Costanti ANSI escape inline |
| Output | WriteLine(text, fg, bg) |
Output colorato su tutta la riga |
| Output | WriteColoredText(text, color) |
Segmento colorato inline, senza a capo |
| Output | WriteAlignedText(text, width, align, color) |
Testo imbottito/allineato |
| Stato | WriteSuccess / WriteWarning / WriteError / WriteInfo |
Messaggi di stato con prefisso |
| Layout | WriteHeader(title, width, color) |
Titolo centrato con linee orizzontali |
| Layout | WriteSeparator(width, char) |
Linea separatrice orizzontale |
| Layout | WriteFormattedList(title, items, style) |
Lista a bullet / numerata / trattino / freccia |
| Widget | Box(title, lines, width) |
Area contenuto con bordi |
| Widget | DrawBox(x, y, w, h, style, title) |
Box grezzo a coordinate assolute |
| Widget | Table(headers, data, title) |
Tabella formattata statica |
| Widget | TableMenu(title, headers, data) |
Tabella navigabile da tastiera, restituisce indice riga |
| Widget | Menu(title, items, default) |
Menu navigabile da tastiera, restituisce indice voce |
| Animazione | Spinner(msg, style, color): ISpinner |
Spinner su thread in background (10 stili) |
| Animazione | Progress(msg, maxValue): IProgress |
Barra di avanzamento determinata / indeterminata |
| Input | GetKey: Integer |
Lettura tasto bloccante inclusi i tasti speciali |
| Input | KeyPressed: Boolean |
Controllo disponibilità tasto non bloccante |
| Input | WaitForReturn |
Blocca finché non viene premuto Invio |
| Input | Confirm(msg, default): Boolean |
Prompt S/N |
| Input | Choose(title, options): Integer |
Prompt scelta numerata |
| Cursore | HideCursor / ShowCursor |
Commuta visibilità cursore |
| Cursore | GotoXY(x, y) |
Sposta a colonna/riga assoluta |
| Cursore | GetCursorPosition: TMVCConsolePoint |
Legge le coordinate correnti del cursore |
| Schermo | ClrScr |
Cancella schermo, cursore a 0,0 |
| Schermo | ClearRegion(x, y, w, h) |
Cancella area rettangolare |
| Schermo | GetConsoleSize: TMVCConsoleSize |
Dimensioni visibili del terminale |
| Tema | ConsoleTheme: TConsoleColorStyle |
Colori globali e stile box per tutti i widget |
Domande frequenti
MVCFramework.Console richiede l’installazione completa di DMVCFramework?
No. La unit è autonoma e non ha dipendenze dal resto del framework. Copia MVCFramework.Console.pas in qualsiasi progetto console Delphi e aggiungila alla clausola uses. Non servono pacchetti, file BPL, né setup dell’IDE.
MVCFramework.Console è cross-platform?
Sì. La libreria compila ed esegue su Windows 10+ e Linux con la stessa API. Il supporto ai colori ANSI è nativo su Linux; su Windows viene abilitato automaticamente via EnableANSIColorConsole. I caratteri box-drawing e le emoji richiedono EnableUTF8Console all’avvio.
Quale versione di Delphi è necessaria?
Delphi 10 Seattle (D100) o successivo. La unit è inclusa in DMVCFramework 3.5.0 Silicon e versioni successive.
Come leggo i tasti freccia in un’applicazione console Delphi?
Usa GetKey: Integer. I tasti freccia restituiscono valori superiori a 255: KEY_UP = 256+38, KEY_DOWN = 256+40, KEY_LEFT = 256+37, KEY_RIGHT = 256+39. Usa IsSpecialKey(key) per distinguere i tasti speciali dai caratteri stampabili.
Come creo uno spinner non bloccante?
S := Spinner('Connessione...', ssDots, Cyan);
DoWork; // il thread principale continua; lo spinner anima in modo indipendente
S.Hide; // oppure: S := nil
Lo spinner gira su un thread in background. Sono disponibili dieci stili: ssLine, ssDots, ssBounce, ssGrow, ssArrow, ssCircle, ssClock, ssEarth, ssMoon, ssWeather.
Come cambio i colori e lo stile box di tutti i widget in una volta?
Modifica il record globale ConsoleTheme prima di chiamare qualsiasi funzione widget. Tutte le chiamate successive a Box, Menu, Table, WriteHeader e funzioni simili usano le impostazioni aggiornate. Salva e ripristina il record se hai bisogno di un cambio di stile temporaneo.
Come creo un menu interattivo navigabile da tastiera?
Selected := Menu('Opzioni', ['Avvia', 'Ferma', 'Esci']);
// I tasti freccia navigano, Invio conferma, Escape restituisce -1
Per una tabella interattiva con selezione delle righe, usa TableMenu(titolo, intestazioni, dati).
Riepilogo
MVCFramework.Console fornisce un toolkit completo per costruire applicazioni TUI in Delphi — da semplici messaggi di stato colorati a menu interattivi con navigazione da tastiera, spinner animati, barre di avanzamento e tabelle strutturate.
I punti chiave del design:
- Zero dipendenze dal framework. Inserisci la unit in qualsiasi progetto console.
- Cross-platform. Windows (10+) e Linux con la stessa API.
- Lifecycle basato su interfacce.
ISpinnereIProgresssi puliscono automaticamente quando rilasciati. - Sistema di temi. Un singolo record globale controlla tutti i colori dei widget e gli stili dei bordi.
- Due livelli. Basso livello (
TextColor,GotoXY,DrawBox) per layout personalizzati; alto livello (Menu,Table,Spinner) per i widget standard.
Il campione completo si trova in samples/console_sample/ConsoleDemo.dpr nel repository DMVCFramework su GitHub.
Questo post fa parte della serie DMVCFramework 3.5.0 “Silicon”. Per le API ANSI di basso livello (Fore, Back, Style) e il renderer di log HTTP in stile Gin, vedi il post precedente della serie.
Comments
comments powered by Disqus