Become a member!

Construyendo aplicaciones TUI con Delphi y DMVCFramework

🌐
Este artículo también está disponible en otros idiomas:
🇮🇹 Italiano  •  🇬🇧 English  •  🇩🇪 Deutsch

TUI: no es un paso atrás

Si has seguido el reciente desarrollo de DMVCFramework, ya sabes que las aplicaciones de consola están viviendo una segunda vida. En el post anterior sobre colores de consola, introduje la API ANSI de bajo nivel — los registros Fore, Back y Style — y el renderizador de logs HTTP estilo Gin. Esa era la base.

Este post trata sobre lo que se ha construido encima: la librería TUI completa integrada en MVCFramework.Console.pas.

TUI significa Text User Interface (Interfaz de Usuario de Texto). Es el mundo de htop, lazygit, k9s y todas las herramientas CLI que usan teclas de flecha, cajas coloreadas y barras de progreso. Estas aplicaciones no son “simples programas de consola” — son interfaces ricas e interactivas que se ejecutan en un terminal.

Con DMVCFramework 3.5.0 Silicon, los desarrolladores Delphi cuentan ahora con un toolkit TUI completo y multiplataforma:

  • Salida de texto coloreado con alineación
  • Menús interactivos navegados con teclas de flecha
  • Spinners animados con 10 estilos
  • Barras de progreso determinadas e indeterminadas
  • Tablas con visualización estática y selección interactiva de filas
  • Cajas con bordes y títulos opcionales
  • Control del cursor: ocultar, mostrar, guardar, restaurar, mover
  • Manejo de entrada de teclado incluyendo teclas especiales
  • Un sistema de temas global

Todo en una sola unit — MVCFramework.Console — con cero dependencias del resto del framework. Puedes copiarla en cualquier proyecto de consola Delphi y usarla inmediatamente.

Configuración

Añade MVCFramework.Console a tu cláusula uses. Para soporte Unicode completo (fotogramas de spinners, caracteres de dibujo de cajas, emojis), llama a EnableUTF8Console al inicio del programa:

program MyTUIApp;
{$APPTYPE CONSOLE}
uses
  System.SysUtils,
  MVCFramework.Console;

begin
  EnableUTF8Console;  // Necesario para caracteres de caja, spinners con emojis, etc.
  // ... tu código aquí
end.

En Windows, EnableUTF8Console llama a SetConsoleOutputCP(CP_UTF8). En Linux es un no-op ya que UTF-8 es el predeterminado. Si también planeas usar las primitivas ANSI (Fore, Back, Style) directamente con WriteLn, llama también a EnableANSIColorConsole. Sin embargo, todas las funciones de alto nivel de esta librería la llaman internamente, por lo que la mayoría de las veces no necesitas preocuparte por ello.

Colores ANSI: el enfoque colorama (Fore / Back / Style)

MVCFramework.Console incluye dos sistemas de color complementarios. El primero — cubierto en el post anterior — es una API ANSI de bajo nivel modelada sobre la librería colorama de Python. Vale la pena revisarlo aquí porque es una herramienta esencial para cualquier aplicación TUI, y se compone naturalmente con todo lo demás de esta unit.

La API consiste en tres registros con constantes de cadena:

Registro Propósito Ejemplo
Fore Color del texto (primer plano) Fore.Red, Fore.Green, Fore.Cyan
Back Color de fondo Back.DarkBlue, Back.DarkRed
Style Estilo de texto y reset Style.Bright, Style.Dim, Style.ResetAll

Cada constante es una cadena de secuencia de escape ANSI bruta. Los colores se componen con el operador +, y Style.ResetAll termina un segmento coloreado. Llama a EnableANSIColorConsole una vez al inicio (idempotente, no-op en Linux):

EnableANSIColorConsole;

// Colores de texto básicos
WriteLn(Fore.Red     + 'Error: conexión rechazada'      + Style.ResetAll);
WriteLn(Fore.Green   + 'OK: servidor iniciado en :8080' + Style.ResetAll);
WriteLn(Fore.Yellow  + 'Aviso: cache miss rate 67%'     + Style.ResetAll);
WriteLn(Fore.Cyan    + 'Info: usando config.env'        + Style.ResetAll);
WriteLn(Fore.DarkGray + '# salida de depuración'       + Style.ResetAll);
Colores de texto usando secuencias de escape Fore.*

Combinando color de texto, fondo y estilo

El verdadero poder está en la composición. Como las constantes son cadenas simples, cualquier combinación es una sola concatenación:

// Texto + fondo
WriteLn(Fore.White + Back.DarkBlue  + '  INFO  ' + Style.ResetAll + ' Servidor iniciado');
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 de memoria alto');

// Modificadores de estilo
WriteLn(Style.Bright + Fore.White + 'Encabezado blanco en negrita' + Style.ResetAll);
WriteLn(Style.Dim  + Fore.Gray  + 'Texto secundario atenuado' + Style.ResetAll);

// Mixto en línea
WriteLn(
  'Estado: ' + Fore.Green + 'ONLINE'  + Style.ResetAll +
  '  |  Req/s: ' + Fore.Cyan + '142' + Style.ResetAll +
  '  |  Errores: ' + Fore.Red + '0'  + Style.ResetAll
);
Badges de estado y colores mixtos en línea con Fore/Back/Style

Constantes de estilo reutilizables (como CSS para la consola)

En lugar de repetir Fore.White + Back.DarkGreen por todas partes, define constantes con nombre una sola vez. El compilador las resuelve en tiempo de compilación — cero overhead en tiempo de ejecución:

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;

// Código limpio y legible:
WriteLn(BADGE_OK   + ' PASS ' + RESET + '  TestUserAuth');
WriteLn(BADGE_FAIL + ' FAIL ' + RESET + '  TestPaymentTimeout');
WriteLn(BADGE_WARN + ' SLOW ' + RESET + '  La consulta tardó 3.2s');
WriteLn(BADGE_INFO + ' NOTE ' + RESET + '  Usando configuración de fallback');
WriteLn(MUTED      + '--- fin del informe ---' + RESET);
Constantes de badge reutilizables aplicadas a salidas de prueba y líneas de log

Cuándo usar Fore/Back/Style frente a TConsoleColor

Los dos sistemas son complementarios, no competidores:

Usa Fore / Back / Style cuando… Usa TConsoleColor / WriteLine cuando…
Necesitas coloración en línea dentro de un WriteLn Quieres salida coloreada en toda la línea
Estás construyendo badges o prefijos de log Estás usando Box, Table, Menu (con tema)
Quieres constantes de estilo en tiempo de compilación Quieres que se aplique el ConsoleTheme global
Necesitas Style.Bright o Style.Dim Necesitas color de fondo del tema

Ambos sistemas coexisten. Una aplicación TUI típica usa Fore/Back/Style para texto en línea personalizado y se apoya en las funciones basadas en TConsoleColor para los widgets estructurales (cajas, tablas, menús).

Salida de texto

La paleta de colores

La librería define TConsoleColor, una enumeración de 17 valores que cubre los clásicos 16 colores del terminal más UseDefault (hereda del tema activo):

Black, DarkBlue, DarkGreen, DarkCyan, DarkRed, DarkMagenta,
DarkYellow, Gray, DarkGray, Blue, Green, Cyan, Red, Magenta,
Yellow, White, UseDefault

En Windows se corresponden con la clásica Windows Console API, en Linux con los códigos ANSI. Los nombres son idénticos en ambas plataformas: Green siempre es verde, Red siempre es rojo.

WriteLine y WriteColoredText

La forma más sencilla de emitir texto coloreado:

// Texto simple (usa los valores predeterminados del terminal)
WriteLine('Iniciando la inicialización del servidor...');

// Texto con color de primer plano
WriteLine('Servidor iniciado con éxito', Green);

// Texto con color de primer plano y fondo
WriteLine(' CRÍTICO ', White, DarkRed);

// Segmento coloreado en línea (sin salto de línea)
WriteColoredText('[INFO] ', Cyan);
WriteLine('Escuchando en el puerto 8080', White);

WriteColoredText guarda y restaura automáticamente los colores actuales, por lo que es seguro anidarla o llamarla en secuencia sin preocuparse por la propagación de colores.

Terminal mostrando los 16 colores mediante WriteLine

Texto alineado

WriteAlignedText rellena el texto hasta un ancho fijo con alineación configurable:

// Centrar un título en un área de 80 caracteres de ancho
WriteAlignedText('DMVCFramework Server', 80, taCenter, Yellow);

// Alinear a la derecha un valor
WriteAlignedText('v3.5.0', 80, taRight, DarkGray);

// Alinear a la izquierda con color
WriteAlignedText('Estado: ONLINE', 80, taLeft, Green);

Los tres modos de alineación — taLeft, taCenter, taRight — cubren todas las necesidades de diseño. El parámetro Width controla el ancho de la columna de salida, no el ancho de la cadena: las cadenas más cortas se rellenan, las más largas se truncan.

CenterInScreen es un atajo de conveniencia que centra una cadena en la ventana física de la consola:

CenterInScreen('Presiona cualquier tecla para continuar');

Lee el tamaño actual de la consola con GetConsoleSize y calcula automáticamente las coordenadas GotoXY correctas.

Mensajes de estado

Cuatro funciones de estado predefinidas producen salida con estilo consistente:

WriteSuccess('Migración de base de datos completada en 1.2s');
WriteWarning('Cache miss rate superior al 50% — considera ajustar el TTL');
WriteError('Conexión rechazada en el puerto 5432');
WriteInfo('Usando configuración de fallback del entorno');

Cada una produce un badge de prefijo coloreado seguido de texto blanco del mensaje:

Función Badge Color del badge
WriteSuccess [SUCCESS] Verde
WriteWarning [WARNING] Amarillo
WriteError [ERROR] Rojo
WriteInfo [INFO] Cian
Las cuatro funciones de estado: WriteSuccess, WriteWarning, WriteError, WriteInfo

Encabezados y separadores

WriteHeader produce un título centrado enmarcado por líneas horizontales, usando el conjunto de caracteres de dibujo de cajas activo:

WriteHeader('Herramienta de migración de base de datos');
WriteHeader('Resumen de configuración', 60);
WriteHeader('Resultados', 60, Green);  // Color personalizado

El ancho predeterminado es 80 columnas. El carácter de línea horizontal se adapta a ConsoleTheme.BoxStyle (ver la sección Temas más adelante).

WriteSeparator dibuja una línea horizontal simple — útil para dividir visualmente las secciones de salida:

WriteSeparator;          // 60 guiones
WriteSeparator(80);      // 80 guiones
WriteSeparator(40, '='); // 40 signos de igual
WriteHeader sobre un bloque de contenido, cerrado por WriteSeparator

Listas formateadas

WriteFormattedList renderiza una lista con título y estilo de viñeta configurable:

var Features: TStringArray;
SetLength(Features, 4);
Features[0] := 'Menús interactivos con navegación por teclas de flecha';
Features[1] := 'Spinners no bloqueantes en hilo en segundo plano';
Features[2] := 'Tablas con visualización estática e interactiva';
Features[3] := 'Sistema de temas global';

WriteFormattedList('Novedades en DMVCFramework 3.5.0:', Features, lsBullet);
WriteFormattedList('Pasos para actualizar:', Features, lsNumbered);
WriteFormattedList('Funciones eliminadas:', Features, lsDash);
WriteFormattedList('Próximos pasos:', Features, lsArrow);

Los cuatro estilos de lista:

Estilo Prefijo
lsBullet *
lsNumbered 1.
lsDash -
lsArrow >
Los cuatro estilos de lista: viñeta, numerada, guión, flecha

Cajas

La función Box renderiza un área de contenido con bordes, auto-dimensionándose al ancho del contenido de forma predeterminada:

// Caja simple, sin título, ancho predeterminado (60)
Box(['Servidor: ONLINE', 'Base de datos: CONECTADA', 'Memoria: 65%']);

// Caja con título
Box('Estado del sistema', ['Servidor: ONLINE', 'Base de datos: CONECTADA']);

// Caja con título y ancho personalizado
Box('AVISO', ['Servidor de caché no responde', 'Comprueba la conexión de red'], 50);

El contenido de la caja se rellena automáticamente a la izquierda dentro del borde. Los caracteres del borde se adaptan a ConsoleTheme.BoxStyle.

Para un control preciso del dibujo, usa directamente la primitiva de bajo nivel DrawBox:

// Dibuja una caja 40x10 comenzando en la columna 5, fila 3
DrawBox(5, 3, 40, 10, bsDouble, 'Panel de depuración');

Estilos de caja

Hay cuatro estilos de borde disponibles mediante TBoxStyle:

DrawBox(0, 0, 30, 5, bsSingle,  'Simple');      // ┌─┐ │ └─┘
DrawBox(0, 6, 30, 5, bsDouble,  'Doble');        // ╔═╗ ║ ╚═╝
DrawBox(0, 12, 30, 5, bsRounded, 'Redondeado'); // ╭─╮ │ ╰─╯
DrawBox(0, 18, 30, 5, bsThick,   'Grueso');      // ┏━┓ ┃ ┗━┛
Los cuatro estilos de caja: bsSingle, bsDouble, bsRounded, bsThick

El estilo de caja predeterminado para todas las funciones de alto nivel (Box, Menu, Table) está controlado por ConsoleTheme.BoxStyle. Cambiarlo una vez afecta a todos los widgets:

ConsoleTheme.BoxStyle := bsDouble;
Box('Ahora todo usa bordes dobles', ['Elemento 1', 'Elemento 2']);

Tablas

Tabla estática

Table renderiza una tabla formateada con columnas de tamaño automático:

var
  Headers: TStringArray;
  Data: TStringMatrix;

SetLength(Headers, 4);
Headers[0] := 'ID'; Headers[1] := 'Nombre';
Headers[2] := 'Framework'; Headers[3] := 'Lenguaje';

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#'];

// Sin título
Table(Headers, Data);

// Con título
Table(Headers, Data, 'Equipo de desarrollo');

Los anchos de columna se calculan automáticamente: cada columna es tan ancha como su celda más ancha (encabezado o datos), más 2 caracteres de relleno interno. La fila de encabezado se renderiza con ConsoleTheme.TextHighlightColor para distinción visual.

Tabla estática con cuatro columnas y filas de datos renderizada por Table()

Tabla interactiva

TableMenu convierte una tabla en un selector interactivo navegado con teclas de flecha:

var SelectedRow: Integer;

SelectedRow := TableMenu('Selecciona un desarrollador', Headers, Data);

if SelectedRow >= 0 then
  WriteSuccess('Has seleccionado: ' + Data[SelectedRow][1])
else
  WriteWarning('Selección cancelada');

La fila seleccionada se resalta usando ConsoleTheme.BackgroundHighlightColor y ConsoleTheme.TextHighlightColor. Teclas de navegación:

Tecla Acción
↑ / ↓ Mueve la selección arriba/abajo
Enter Confirma la selección, devuelve el índice de fila
Escape Cancela, devuelve -1

El cursor se oculta automáticamente durante la interacción y se restaura al salir.

Tabla interactiva con navegación por teclas de flecha y fila resaltada

Puedes preseleccionar una fila:

SelectedRow := TableMenu('Selecciona', Headers, Data, 2); // Empieza con la fila 2 seleccionada

Menús interactivos

Menu muestra una lista navegable por teclado encerrada en una caja. Aparece en la posición actual del cursor y se limpia cuando el usuario hace una selección o pulsa Escape.

var
  Items: TStringArray;
  Selected: Integer;

SetLength(Items, 5);
Items[0] := 'Iniciar servidor';
Items[1] := 'Detener servidor';
Items[2] := 'Ver registros';
Items[3] := 'Configuración';
Items[4] := 'Salir';

// Sin título
Selected := Menu(Items);

// Con título
Selected := Menu('Menú principal', Items);

// Con título y elemento preseleccionado
Selected := Menu('Menú archivo', Items, 2); // Empieza con 'Ver registros' seleccionado

Valor devuelto: el índice del elemento seleccionado (0-based), o -1 si el usuario pulsó Escape.

Navegación:

Tecla Acción
Mueve arriba (con wrap)
Mueve abajo (con wrap)
Enter Confirmar
Escape Cancelar

El elemento seleccionado se renderiza con colores invertidos (fondo resaltado). El menú se ajusta automáticamente en ancho al elemento más largo más el relleno. Si el menú sobrepasaría el fondo del terminal, se reposiciona automáticamente hacia arriba.

Menú interactivo navegado con teclas de flecha, selección confirmada con Enter
// Ejemplo práctico: bucle de despacho principal
while True do
begin
  Selected := Menu('Control del servidor', ['Iniciar', 'Detener', 'Reiniciar', 'Salir']);
  case Selected of
    0: StartServer;
    1: StopServer;
    2: begin StopServer; StartServer; end;
    3, -1: Break;
  end;
end;

Barras de progreso

Progreso determinado

Progress con MaxValue > 0 produce una barra de progreso determinada:

var
  P: IProgress;
  I: Integer;

P := Progress('Descargando archivos', 100);
for I := 1 to 100 do
begin
  P.Update(I);
  Sleep(20); // Simula trabajo
end;
P := nil; // Llama a Complete automáticamente mediante el destructor

La barra se muestra como [==== ] 45%. La parte rellena se calcula como (Actual * Ancho) div MaxValue. El ancho predeterminado de la barra es 50 caracteres.

La interfaz IProgress expone:

Método Descripción
Update(Value) Establece el valor actual (absoluto)
Increment(Amount) Incrementa en Amount (predeterminado 1)
SetMessage(Msg) Actualiza la etiqueta del título
Complete Marca como hecho, imprime “¡Hecho!” en verde

Establecer P := nil activa el destructor, que llama a Complete si aún no se ha hecho. Esto significa que envolver el progreso en un bloque try/finally es opcional — la limpieza está garantizada.

// Estilo de incremento
P := Progress('Procesando registros', 1000);
for I := 1 to 1000 do
begin
  ProcessRecord(I);
  P.Increment;
end;
Barra de progreso determinada llenándose del 0% al 100%

Progreso indeterminado

Progress con MaxValue = 0 produce un spinner indeterminado dentro de un par de corchetes:

P := Progress('Cargando configuración');
// Haz el trabajo...
while StillLoading do
begin
  P.Update(0); // Avanza el spinner
  Sleep(50);
end;
P.Complete;

El spinner dentro de los corchetes [|] cicla a través de los caracteres |/-\. Llama a Update periódicamente para avanzar la animación. Este es un patrón bloqueante — el spinner solo avanza cuando llamas a Update. Para un spinner completamente no bloqueante, usa Spinner en su lugar (ver abajo).

Spinners

La función Spinner crea un spinner animado no bloqueante que se ejecuta en un hilo en segundo plano. Tu hilo principal continúa trabajando mientras el spinner anima de forma independiente:

var S: ISpinner;

S := Spinner('Cargando datos', ssLine, DarkGray);
// Haz tu trabajo aquí — el spinner anima por sí solo
FetchRemoteData;
S.Hide; // O: S := nil (llama al destructor → Hide)

Liberar la interfaz (estableciéndola a nil) también llama a Hide, que detiene el hilo y borra el spinner del terminal.

Los 10 estilos de spinner

TSpinnerStyle ofrece 10 estilos de animación:

Estilo Caracteres Intervalo Descripción
ssLine -\│/ 100ms Spinner de terminal clásico
ssDots Patrones Braille 80ms Animación de puntos fluida
ssBounce Rebote Braille 80ms Punto rebotante
ssGrow Elementos de bloque 120ms Barra creciente
ssArrow Caracteres de flecha 100ms Flecha giratoria
ssCircle Cuartos de círculo 100ms Círculo giratorio
ssClock 🕐🕑🕒… 200ms 12 caras de reloj
ssEarth 🌍🌎🌏 200ms Rotación del globo
ssMoon 🌑🌒🌓… 200ms 8 fases lunares
ssWeather 🌤🌧⛈… 200ms Iconos meteorológicos

Cada estilo usa automáticamente el intervalo de animación apropiado. No es necesario ajustar los valores de Sleep.

// Spinner de línea (predeterminado)
S := Spinner(ssLine);

// Dots con mensaje
S := Spinner('Conectando a la base de datos', ssDots, Cyan);

// Rotación de la Tierra, sin mensaje
S := Spinner(ssEarth);

// Fases lunares con color personalizado
S := Spinner('Sincronizando', ssMoon, Blue);
Los 10 estilos de spinner animados simultáneamente

Spinner en un bucle de trabajo

Un patrón común: muestra un spinner mientras se ejecuta una operación asíncrona, luego reemplázalo con un mensaje de estado.

var S: ISpinner;
begin
  S := Spinner('Conectando al servidor', ssDots, Cyan);
  try
    ConnectToServer; // Bloquea, pero el spinner se ejecuta en un hilo separado
    S.Hide;
    WriteSuccess('Conectado con éxito');
  except
    S.Hide;
    WriteError('Conexión fallida: ' + E.Message);
  end;
end;

Confirm y Choose

Confirm

Confirm solicita al usuario una respuesta sí/no con un valor predeterminado opcional:

// Predeterminado: Sí
if Confirm('¿Quieres continuar?') then
  StartOperation
else
  Writeln('Cancelado.');

// Predeterminado: No (más seguro para acciones destructivas)
if Confirm('¿Eliminar todos los registros?', False) then
  DeleteAllRecords;

El prompt muestra [S/N] (S): o [S/N] (N): según el predeterminado. Pulsar Enter sin escribir nada acepta el predeterminado.

Choose

Choose presenta una lista numerada y lee una entrada numérica. Útil cuando el número de opciones es pequeño y se quiere una UX más simple que un menú completo con teclas de flecha:

var
  Options: TStringArray;
  Choice: Integer;

SetLength(Options, 3);
Options[0] := 'Modo rápido';
Options[1] := 'Modo normal';
Options[2] := 'Modo seguro';

Choice := Choose('Selecciona el modo de procesamiento:', Options);
if Choice >= 0 then
  WriteSuccess('Has elegido: ' + Options[Choice]);

Salida:

Selecciona el modo de procesamiento:
  [1] Modo rápido
  [2] Modo normal
  [3] Modo seguro
Tu elección: _

Devuelve el índice 0-based de la opción seleccionada, o -1 si la entrada no es válida.

Control del cursor

Ocultar y mostrar

Al dibujar elementos de UI interactivos (menús, barras de progreso, spinners), ocultar el cursor elimina el ruido visual del cursor parpadeante saltando por la pantalla:

HideCursor;
try
  DrawDashboard;
  WaitForKey;
finally
  ShowCursor; // Restaura siempre en un bloque finally
end;
Un menú renderizado limpiamente con el cursor oculto

Las funciones de alto nivel (Menu, TableMenu, Spinner) gestionan internamente la visibilidad del cursor. Solo necesitas llamar a HideCursor manualmente cuando usas directamente las primitivas de dibujo de bajo nivel.

GotoXY

GotoXY mueve el cursor a una posición absoluta columna/fila (base 0):

// Mover a la columna 10, fila 5
GotoXY(10, 5);
Write('Valor: ');
GotoXY(18, 5);
Write(CurrentValue:6:2);

Esta es la base de todas las actualizaciones de pantalla en el lugar. Combinado con GetCursorPosition, puedes registrar una posición y volver a ella más tarde:

var Pos: TMVCConsolePoint;

Pos := GetCursorPosition;
Write('Procesando...');
DoSomeWork;
GotoXY(Pos.X, Pos.Y);
Write('¡Hecho!         '); // Sobrescribe el texto anterior

Guardar y restaurar la posición del cursor

SaveCursorPosition y RestoreCursorPosition almacenan/restauran coordenadas en una variable global:

SaveCursorPosition;
// ... dibuja algo en otro lugar
RestoreCursorPosition;
Write('¡De vuelta aquí!');

Nota: estas usan un único slot de almacenamiento global, por lo que no son reentrantes. Para diseños complejos con múltiples posiciones guardadas, usa directamente GetCursorPosition / GotoXY.

Primitivas de dibujo

Para diseños que requieren más que los widgets de alto nivel, puedes usar las funciones de dibujo en bruto:

DrawBox

Dibuja una caja en coordenadas absolutas sin el relleno y la gestión de color del Box de alto nivel:

DrawBox(5, 2, 40, 10, bsSingle, 'Título del panel');
DrawBox(50, 2, 30, 10, bsDouble);

Parámetros: X, Y (esquina superior izquierda), Width, Height, Style (opcional), Title (opcional, centrado en el borde superior).

DrawHorizontalLine y DrawVerticalLine

// Dibuja un separador de 60 caracteres en la fila 12
DrawHorizontalLine(0, 12, 60, bsSingle);

// Dibuja un divisor vertical en la columna 40, desde la fila 2, de 20 caracteres de alto
DrawVerticalLine(40, 2, 20, bsSingle);

Usa bsUseDefault para heredar el estilo de ConsoleTheme.BoxStyle.

ClearRegion

Limpia un área rectangular sobreescribiéndola con espacios:

// Limpia una región 40x10 comenzando en (5, 3)
ClearRegion(5, 3, 40, 10);

Útil para actualizar partes de la pantalla sin llamar a ClrScr (que limpia todo y reposiciona el cursor en 0, 0).

Diseño multi-panel construido con DrawBox, DrawHorizontalLine y DrawVerticalLine

Temas

Todos los widgets de alto nivel leen sus colores y estilo de caja del registro global ConsoleTheme:

type
  TConsoleColorStyle = record
    TextColor: TConsoleColor;           // Texto del cuerpo en cajas y tablas
    BackgroundColor: TConsoleColor;     // Fondo (usado en Linux)
    DrawColor: TConsoleColor;           // Bordes de cajas y separadores
    SymbolsColor: TConsoleColor;        // Viñetas de listas, prefijos
    BackgroundHighlightColor: TConsoleColor; // Fondo del elemento seleccionado
    TextHighlightColor: TConsoleColor;  // Texto del elemento seleccionado, texto de encabezado
    BoxStyle: TBoxStyle;                // Estilo de borde predeterminado para todos los widgets
  end;

El tema predeterminado:

ConsoleTheme.TextColor               := Cyan;
ConsoleTheme.BackgroundColor         := Black;
ConsoleTheme.DrawColor               := White;
ConsoleTheme.SymbolsColor            := Gray;
ConsoleTheme.BackgroundHighlightColor := Cyan;
ConsoleTheme.TextHighlightColor      := Blue;
ConsoleTheme.BoxStyle                := bsRounded;

Cambiar el tema afecta a todas las llamadas posteriores a Box, Table, Menu, WriteHeader, WriteFormattedList y funciones similares:

// Estilo de terminal oscuro
ConsoleTheme.TextColor               := White;
ConsoleTheme.DrawColor               := DarkGray;
ConsoleTheme.BackgroundHighlightColor := DarkBlue;
ConsoleTheme.TextHighlightColor      := White;
ConsoleTheme.BoxStyle                := bsDouble;

// A partir de aquí, todos los widgets usan el nuevo tema
Box('Info del sistema', ['CPU: 12%', 'RAM: 4.2GB']);
Los mismos widgets renderizados con el tema predeterminado y un tema oscuro personalizado

Los temas son una variable global. Si necesitas cambiar de tema temporalmente, guarda y restaura:

var SavedTheme: TConsoleColorStyle;

SavedTheme := ConsoleTheme;
ConsoleTheme.BoxStyle := bsThick;
ConsoleTheme.DrawColor := Red;
Box('ERROR', ['Fallo crítico detectado']);
ConsoleTheme := SavedTheme; // Restaurar

Entrada de teclado

GetKey y GetCh

GetKey bloquea hasta que el usuario presiona una tecla y devuelve un código entero:

  • Caracteres imprimibles: Ord('A') = 65, Ord(' ') = 32, etc.
  • Teclas especiales: valores superiores a 255, definidos como constantes con nombre
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
  // Carácter normal
  ProcessChar(Chr(Key));
end;

GetCh es un atajo que devuelve Char en lugar de Integer. Para teclas especiales devuelve #0 (carácter nulo) — usa GetKey si necesitas distinguir las teclas de flecha.

Constantes de teclas especiales

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) devuelve True cuando KeyCode > 255, es decir, cuando el valor representa una tecla de flecha u otra tecla virtual en lugar de un carácter imprimible.

Verificación de tecla no bloqueante

KeyPressed devuelve True si hay una tecla esperando en el buffer de entrada, sin consumirla:

// Bucle animado que sale al presionar cualquier tecla
while not KeyPressed do
begin
  UpdateAnimation;
  Sleep(50);
end;
Key := GetKey; // Consume la tecla pendiente

WaitForReturn

WaitForReturn bloquea hasta que el usuario presiona Enter — una alternativa más limpia a ReadLn para los prompts de “presiona Enter para continuar”:

WriteInfo('Revisión completa. Presiona ENTER para continuar.');
WaitForReturn;

Capacidades del terminal

Tamaño de la consola

GetConsoleSize devuelve las dimensiones visibles de la ventana del terminal:

var Size: TMVCConsoleSize;

Size := GetConsoleSize;
WriteLn(Format('Terminal: %d x %d', [Size.Columns, Size.Rows]));

GetConsoleBufferSize devuelve el tamaño completo del buffer (en Windows, el buffer de desplazamiento puede ser más grande que la ventana). Para diseños TUI, usa siempre GetConsoleSize — quieres el área visible.

Detección del terminal

if IsTerminalCapable then
  StartInteractiveMode
else
  // Redirigido a archivo — omite las funciones interactivas
  WritePlainOutput;

WriteLn('Ejecutándose en: ' + GetTerminalName);
// Windows → 'Windows Console'
// Linux   → valor de la variable $TERM (ej. 'xterm-256color')

Colores y ANSI

// Habilita colores ANSI (necesario antes de usar Fore/Back/Style directamente)
EnableANSIColorConsole;

// Comprueba si ANSI está activo
if IsANSIColorConsoleEnabled then
  WriteLn(Fore.Green + '¡Colores activos!' + Style.ResetAll);

Funciones de utilidad

ClrScr

Limpia toda la consola y mueve el cursor a 0, 0. Úsalo al inicio de una pantalla para tener un lienzo limpio:

ClrScr;
WriteHeader('Panel de control del servidor');

Beep

Emite un pitido del sistema. Útil como señal de audio de error:

if CriticalError then
begin
  Beep;
  WriteError('¡Fallo crítico!');
end;

En Windows llama a WinAPI.Windows.Beep(800, 200). En Linux escribe #7 en stdout.

FlashScreen

Invierte brevemente los colores de la pantalla como alerta visual:

FlashScreen; // Flash de 100ms
WriteError('Entrada no válida');

En Linux usa la secuencia de escape ANSI reverse-video. En Windows invierte los colores manualmente usando FillConsoleOutputAttribute.

Ayudantes de color

// Obtiene el nombre de una constante de color como cadena
WriteLn(ColorName(Green)); // 'Green'
WriteLn(ColorName(DarkGray)); // 'DarkGray'

// Guardar/restaurar colores manualmente (nivel más bajo que TConsoleColorStyle)
SaveColors;
TextColor(Red);
TextBackground(DarkBlue);
Write('Resaltado');
RestoreSavedColors;

SaveColors y RestoreSavedColors operan en un único slot global (misma advertencia que SaveCursorPosition). Los usa internamente WriteColoredText y raramente se necesitan directamente.

Poniéndolo todo junto: un panel de servidor

Aquí hay un ejemplo realista que combina cajas, mensajes de estado, un spinner, progreso y un menú en un simple panel de control de servidor:

procedure RunServerPanel;
var
  Menu: Integer;
  S: ISpinner;
  P: IProgress;
  I: Integer;
  Data: TStringMatrix;
  Headers: TStringArray;
begin
  EnableUTF8Console;
  ClrScr;

  // Encabezado
  WriteHeader('Panel de control DMVC Server', 80, Cyan);
  WriteLn;

  // Caja de estado
  Box('Estado actual', [
    'Servidor web:  ONLINE',
    'Base de datos: CONECTADA',
    'Caché:         AVISO - 67% miss rate',
    'Copia de seg.: INACTIVA'
  ], 50);
  WriteLn;

  // Tabla de registro
  SetLength(Headers, 3);
  Headers[0] := 'Hora'; Headers[1] := 'Nivel'; Headers[2] := 'Mensaje';

  SetLength(Data, 3);
  Data[0] := ['14:32:01', 'INFO',  'Petición GET /api/users → 200 (12ms)'];
  Data[1] := ['14:32:03', 'WARN',  'Cache miss: /api/products/42'];
  Data[2] := ['14:32:07', 'ERROR', 'Timeout de consulta DB tras 5000ms'];

  Table(Headers, Data, 'Entradas de registro recientes');
  WriteLn;

  // Menú principal
  Menu := Menu('Acciones del servidor', [
    'Reiniciar servidor web',
    'Vaciar caché',
    'Ejecutar migración de base de datos',
    'Ver registros completos',
    'Salir'
  ]);

  case Menu of
    0: begin
         // Reinicio con spinner
         S := Spinner('Reiniciando servidor web', ssDots, Cyan);
         Sleep(2000);
         S.Hide;
         WriteSuccess('Servidor web reiniciado con éxito');
       end;
    1: begin
         // Vaciar caché con progreso
         P := Progress('Vaciando entradas de caché', 100);
         for I := 1 to 100 do
         begin
           P.Update(I);
           Sleep(15);
         end;
         P := nil;
         WriteLn;
         WriteSuccess('Caché vaciada: 4.832 entradas eliminadas');
       end;
    2: begin
         if Confirm('¿Ejecutar migración de base de datos? Puede tardar varios minutos.', False) then
         begin
           S := Spinner('Ejecutando migración', ssGrow, Yellow);
           Sleep(3000);
           S.Hide;
           WriteSuccess('Migración completada: 12 scripts aplicados');
         end
         else
           WriteInfo('Migración cancelada');
       end;
    -1, 4:
       WriteInfo('¡Hasta luego!');
  end;
end;
Panel de control DMVC Server completo: encabezado, caja de estado, tabla de registro, navegación por menú

Referencia rápida

Categoría Función / Tipo Descripción
Configuración EnableUTF8Console Habilita salida UTF-8 (caracteres de caja, emojis)
Configuración EnableANSIColorConsole Habilita secuencias ANSI en Windows 10+
Color bajo nivel Fore.Red, Back.Blue, Style.Bright Constantes de escape ANSI en línea
Salida WriteLine(text, fg, bg) Salida coloreada en toda la línea
Salida WriteColoredText(text, color) Segmento coloreado en línea, sin salto de línea
Salida WriteAlignedText(text, width, align, color) Texto relleno/alineado
Estado WriteSuccess / WriteWarning / WriteError / WriteInfo Mensajes de estado con prefijo
Diseño WriteHeader(title, width, color) Título centrado con líneas horizontales
Diseño WriteSeparator(width, char) Línea separadora horizontal
Diseño WriteFormattedList(title, items, style) Lista con viñetas / numerada / guión / flecha
Widgets Box(title, lines, width) Área de contenido con bordes
Widgets DrawBox(x, y, w, h, style, title) Caja en bruto en coordenadas absolutas
Widgets Table(headers, data, title) Tabla formateada estática
Widgets TableMenu(title, headers, data) Tabla navegable por teclado, devuelve índice de fila
Widgets Menu(title, items, default) Menú navegable por teclado, devuelve índice de elemento
Animación Spinner(msg, style, color): ISpinner Spinner en hilo en segundo plano (10 estilos)
Animación Progress(msg, maxValue): IProgress Barra de progreso determinada / indeterminada
Entrada GetKey: Integer Lectura de tecla bloqueante incluyendo teclas especiales
Entrada KeyPressed: Boolean Verificación de disponibilidad de tecla no bloqueante
Entrada WaitForReturn Bloquea hasta que se pulsa Enter
Entrada Confirm(msg, default): Boolean Prompt S/N
Entrada Choose(title, options): Integer Prompt de elección numerada
Cursor HideCursor / ShowCursor Alterna la visibilidad del cursor
Cursor GotoXY(x, y) Mueve a columna/fila absoluta
Cursor GetCursorPosition: TMVCConsolePoint Lee las coordenadas actuales del cursor
Pantalla ClrScr Limpia la pantalla, cursor a 0,0
Pantalla ClearRegion(x, y, w, h) Limpia área rectangular
Pantalla GetConsoleSize: TMVCConsoleSize Dimensiones visibles del terminal
Tema ConsoleTheme: TConsoleColorStyle Colores globales y estilo de caja para todos los widgets

Preguntas frecuentes

¿MVCFramework.Console requiere la instalación completa de DMVCFramework?

No. La unit es autónoma y no tiene dependencias del resto del framework. Copia MVCFramework.Console.pas en cualquier proyecto de consola Delphi y añádela a la cláusula uses. No se necesitan paquetes, archivos BPL ni configuración del IDE.

¿Es MVCFramework.Console multiplataforma?

Sí. La librería compila y se ejecuta en Windows 10+ y Linux con la misma API. El soporte de colores ANSI es nativo en Linux; en Windows se habilita automáticamente mediante EnableANSIColorConsole. Los caracteres de dibujo de cajas y los emojis requieren EnableUTF8Console al inicio.

¿Qué versión de Delphi se necesita?

Delphi 10 Seattle (D100) o posterior. La unit se incluye en DMVCFramework 3.5.0 Silicon y versiones posteriores.

¿Cómo leo las teclas de flecha en una aplicación de consola Delphi?

Usa GetKey: Integer. Las teclas de flecha devuelven valores superiores a 255: KEY_UP = 256+38, KEY_DOWN = 256+40, KEY_LEFT = 256+37, KEY_RIGHT = 256+39. Usa IsSpecialKey(key) para distinguir las teclas especiales de los caracteres imprimibles.

¿Cómo creo un spinner no bloqueante?

S := Spinner('Conectando...', ssDots, Cyan);
DoWork;       // el hilo principal continúa; el spinner anima de forma independiente
S.Hide;       // o: S := nil

El spinner se ejecuta en un hilo en segundo plano. Hay diez estilos disponibles: ssLine, ssDots, ssBounce, ssGrow, ssArrow, ssCircle, ssClock, ssEarth, ssMoon, ssWeather.

¿Cómo cambio los colores y el estilo de caja de todos los widgets de una vez?

Modifica el registro global ConsoleTheme antes de llamar a cualquier función widget. Todas las llamadas posteriores a Box, Menu, Table, WriteHeader y funciones similares usan la configuración actualizada. Guarda y restaura el registro si necesitas un cambio de estilo temporal.

¿Cómo creo un menú interactivo navegable por teclado?

Selected := Menu('Opciones', ['Iniciar', 'Detener', 'Salir']);
// Las teclas de flecha navegan, Enter confirma, Escape devuelve -1

Para una tabla interactiva con selección de filas, usa TableMenu(título, encabezados, datos).

Resumen

MVCFramework.Console proporciona un toolkit completo para construir aplicaciones TUI en Delphi — desde simples mensajes de estado coloreados hasta menús interactivos con navegación por teclado, spinners animados, barras de progreso y tablas estructuradas.

Los puntos clave del diseño:

  • Cero dependencias del framework. Añade la unit a cualquier proyecto de consola.
  • Multiplataforma. Windows (10+) y Linux con la misma API.
  • Ciclo de vida basado en interfaces. ISpinner e IProgress se limpian automáticamente cuando se liberan.
  • Sistema de temas. Un único registro global controla todos los colores de los widgets y los estilos de borde.
  • Dos niveles. Bajo nivel (TextColor, GotoXY, DrawBox) para diseños personalizados; alto nivel (Menu, Table, Spinner) para los widgets estándar.

El ejemplo completo está en samples/console_sample/ConsoleDemo.dpr en el repositorio DMVCFramework en GitHub.


Este post forma parte de la serie DMVCFramework 3.5.0 “Silicon”. Para la API ANSI de bajo nivel (Fore, Back, Style) y el renderizador de logs HTTP estilo Gin, consulta el post anterior de la serie.

Comments

comments powered by Disqus