Construyendo aplicaciones TUI con Delphi y DMVCFramework
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);
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
);
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);
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.
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 |
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
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 |
> |
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'); // ┏━┓ ┃ ┗━┛
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 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.
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.
// 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;
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);
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;
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).
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 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;
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.
ISpinnereIProgressse 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