Become a member!

Costruire applicazioni TUI con Delphi e DMVCFramework

🌐
Questo articolo è disponibile anche in altre lingue:
🇬🇧 English  •  🇪🇸 Español  •  🇩🇪 Deutsch

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);
Colori del testo usando le sequenze di escape Fore.*

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
);
Badge di stato e colori inline misti con Fore/Back/Style

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);
Costanti badge riutilizzabili applicate a output di test e righe di log

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.

Terminale che mostra tutti i 16 colori tramite WriteLine

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
Tutte e quattro le funzioni di stato: WriteSuccess, WriteWarning, WriteError, WriteInfo

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
WriteHeader sopra un blocco di contenuto, chiuso da WriteSeparator

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 >
Tutti e quattro gli stili di lista: bullet, numerata, trattino, freccia

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');   // ┏━┓ ┃ ┗━┛
I quattro stili box: bsSingle, bsDouble, bsRounded, bsThick

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 statica con quattro colonne e righe dati renderizzata da Table()

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.

Tabella interattiva con navigazione a frecce e riga evidenziata

Puoi pre-selezionare una riga:

SelectedRow := TableMenu('Seleziona', Headers, Data, 2); // Inizia con la riga 2 selezionata

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.

Menu interattivo navigato con i tasti freccia, selezione confermata con Invio
// 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;
Barra di avanzamento determinata che si riempie dallo 0% al 100%

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);
Tutti i 10 stili spinner animati simultaneamente

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;
Un menu renderizzato in modo pulito con il cursore nascosto

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).

Layout multi-pannello costruito con DrawBox, DrawHorizontalLine e DrawVerticalLine

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']);
Gli stessi widget renderizzati con il tema predefinito e un tema scuro personalizzato

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;
Pannello di controllo DMVC Server completo: intestazione, box stato, tabella log, navigazione menu

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).

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. ISpinner e IProgress si 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