Menú en la terminal de GNU/Linux con C++ y códigos de escape

El hacer un programa en modo texto para GNU/Linux en C++ que muestre un menú con una barra que se controle con las flechas del teclado implica más trabajo del que se requiere para lograr lo mismo si nuestro programa fuera a ejecutarse en un sistema operativo basado en DOS (ya sea MS-DOS, Windows, etc.)

A continuación muestro una posible solución de como lograrlo sin utilizar ncurses, únicamente códigos de escape, esto tiene la pequeña desventaja de que no funciona para cualquier terminal, pero para xterm y algunas otras no debe haber problema

Crearemos una fución que espere a que el usuario pulse una tecla y regrese el valor correspondiente a esa tecla, la función debe tomar en cuenta las teclas que al ser pulsadas regresan tres valores, siendo el primer valor el que corresponde a la tecla ESC (que es el 27)

int lee_caracter()
{
 struct termios config_ant;
 struct termios config_nueva;
 int caracter;

 tcgetattr(STDIN_FILENO, &config_ant);
 config_nueva = config_ant;                /* Guarda los valores actuales de configuración de la terminal */
 config_nueva.c_lflag &= ~(ICANON | ECHO); /* resetea las banderas canonical y echo para que no se muestre en pantalla lo que se teclea*/
 tcsetattr(STDIN_FILENO, TCSANOW, &config_nueva); /* Establece los nuevos valores para la terminal */
 caracter = getchar();

 // si se pulsa una tecla que empieza con un código de escape
 if (caracter==27)
    {
     caracter=getchar();
     if (caracter==91)
         caracter=getchar(); //caracter ahora tiene el valor de la tecla pulsada
    }

 tcsetattr(STDIN_FILENO, TCSANOW, &config_ant); /* Vuelve a poner los valores originales de configuración de la terminal */

 return caracter;
}

Los atributos y colores que podemos manejar son los siguientes:

#define RESET        0
#define BRIGHT       1
#define DIM          2
#define UNDERLINE    3
#define BLINK        4
#define REVERSE      7
#define HIDDEN       8
#define BLACK        0
#define RED          1
#define GREEN        2
#define YELLOW       3
#define BLUE         4
#define MAGENTA      5
#define CYAN         6
#define WHITE        7

Necesitamos también una función que nos permita establecer el atributo, color de la fuente y color de fondo que vamos a utilizar antes de escribir un texto

void ColorTexto(int atributo, int texto, int fondo)
{
 std::string color = "\033["+toString(atributo)+";"+toString(texto+30)+";"+toString(fondo+40)+"m";
 cout<<color;
}

Este programa utiliza las dos funciones descritas anteriores para mostrar un menú vertical, se asume una terminal de 80 columnas

/*
Debido a que este programa usa el archivo de cabecera termios.h,
sólo funciona en sistemas operativos que cumplan con el estándar POSIX
Es decir, cualquier distribución de GNU/Linux o cualquier Unix.
*/

#include <iostream>
#include <sstream>    /* Para poder usar istringstream */
#include <string>
#include <termios.h>  /* Para poder usar las estructuras termios */
#include <stdio.h>    /* Para poder usar getchar() */
#include <iomanip>    /* Para poder usar setw */

#define RESET        0
#define BRIGHT       1
#define DIM          2
#define UNDERLINE    3
#define BLINK        4
#define REVERSE      7
#define HIDDEN       8

#define BLACK        0
#define RED          1
#define GREEN        2
#define YELLOW       3
#define BLUE         4
#define MAGENTA      5
#define CYAN         6
#define WHITE        7

const std::string default_console = "\033[0m";

using std::cout;
using std::endl;
using std::setw;

template <typename T>
static std::string toString (T numero)
{
 std::ostringstream ss;
 ss << numero;
 return ss.str();
}

void ColorTexto(int atributo, int texto, int fondo);
int lee_caracter();
void ImprimeMenu(std::string menu[], int tamanio, int op);

int main()
{
 std::string str_menu[4] = {"1. Registrar", "2. Consultar", "3. Modificar", "4. Salir    "};
 int tecla, opcion=0, seleccion=1;

do
 {
  ImprimeMenu(str_menu,4,seleccion);
  tecla = lee_caracter();

  switch (tecla)
         {
          case 10:
                  // Enter
                  opcion = seleccion;
                  break;
          case 65:
                  // Flecha arriba
                  seleccion = (seleccion==1 ? 4 : seleccion-1);
                  opcion = 0;
                  break;
          case 66:
                  // Flecha abajo
                  seleccion = (seleccion==4 ? 1 : seleccion+1);
                  opcion = 0;
                  break;
          default:
                  opcion = tecla-48;
                  if (opcion>=1 && opcion<=4)
                     {
                      seleccion = opcion;
                      ImprimeMenu(str_menu,4,seleccion);
                     }
         }
  if (opcion>=1 && opcion<=4)
     {
      cout<<"Opcion "<<opcion<<endl;
      lee_caracter();
     }
  }
 while (opcion!=4);

 return 0;
}

void ColorTexto(int atributo, int texto, int fondo)
{
 std::string color = "\033["+toString(atributo)+";"+toString(texto+30)+";"+toString(fondo+40)+"m";
 cout<<color;
}

int lee_caracter()
{
 struct termios config_ant;
 struct termios config_nueva;
 int caracter;

 tcgetattr(STDIN_FILENO, &config_ant);
 config_nueva = config_ant;                /* Guarda los valores actuales de configuración de la terminal */
 config_nueva.c_lflag &= ~(ICANON | ECHO); /* resetea las banderas canonical y echo para que no se muestre en pantalla lo que se teclea*/
 tcsetattr(STDIN_FILENO, TCSANOW, &config_nueva); /* Establece los nuevos valores para la terminal */
 caracter = getchar();

 // si se pulsa una tecla que empieza con un código de escape
 if (caracter==27)
    {
     caracter=getchar();
     if (caracter==91)
         caracter=getchar(); //caracter ahora tiene el valor de la tecla pulsada
    }

 tcsetattr(STDIN_FILENO, TCSANOW, &config_ant); /* Vuelve a poner los valores originales de configuración de la terminal */

 return caracter;
}

void ImprimeMenu(std::string menu[], int tamanio, int op)
{
 int x=0, pos;
 unsigned int longitud=0;

 // Obtiene la longitud de la opción del menú con más caracteres
 for (x=0; x<tamanio; ++x)
     {
      if (menu[x].length() > longitud)
          longitud = menu[x].length();
     }
 pos = (80 - longitud) / 2;

 // Limpia la pantalla
 cout << "\033[2J\033[1;1H";

 for (x=0; x<tamanio; ++x)
     {
      // Deja el número de espacios adecuado para que el menú quede centrado (se asume una pantalla de 80 caracteres)
      cout<<default_console;
      cout<<setw(pos)<<" ";

      if (x+1==op)
          ColorTexto(BRIGHT,YELLOW,MAGENTA);
      else
          ColorTexto(BRIGHT,WHITE,BLUE);
      cout<<menu[x]<<endl;
     }
 cout<<default_console<<endl;
}

menu

Ahora la versión para mostrar el menú en forma horizontal

/*
 Debido a que este programa usa el archivo de cabecera termios.h,
 sólo funciona en sistemas operativos que cumplan con el estándar POSIX
 Es decir, cualquier distribución de GNU/Linux o cualquier Unix.
*/

#include <iostream>
#include <sstream>    /* Para poder usar istringstream */
#include <string>
#include <termios.h>  /* Para poder usar las estructuras termios */
#include <stdio.h>    /* Para poder usar getchar() */
#include <iomanip>    /* Para poder usar setw */

#define RESET        0
#define BRIGHT       1
#define DIM          2
#define UNDERLINE    3
#define BLINK        4
#define REVERSE      7
#define HIDDEN       8

#define BLACK        0
#define RED          1
#define GREEN        2
#define YELLOW       3
#define BLUE         4
#define MAGENTA      5
#define CYAN         6
#define WHITE        7

const std::string default_console = "\033[0m";

using std::cout;
using std::endl;
using std::setw;

template <typename T>
static std::string toString (T numero)
{
 std::ostringstream ss;
 ss << numero;
 return ss.str();
}

void ColorTexto(int atributo, int texto, int fondo);
int lee_caracter();
void ImprimeMenu(std::string menu[], int tamanio, int op);

int main()
{
 std::string str_menu[4] = {"1. Registrar", "2. Consultar", "3. Modificar", "4. Salir"};
 int tecla, opcion=0, seleccion=1;

 do
   {
    ImprimeMenu(str_menu,4,seleccion);
    tecla = lee_caracter();

    switch (tecla)
           {
            case 10:
                    // Enter
                    opcion = seleccion;
                    break;
            case 67:
                    // Flecha derecha
                    seleccion = (seleccion==4 ? 1 : seleccion+1);
                    opcion = 0;
                    break;
            case 68:
                    // Flecha izquierda
                    seleccion = (seleccion==1 ? 4 : seleccion-1);
                    opcion = 0;
                    break;
            default:
                    opcion = tecla-48;
                    if (opcion>=1 && opcion<=4)
                       {
                        seleccion = opcion;
                        ImprimeMenu(str_menu,4,seleccion);
                       }
           }
    if (opcion>=1 && opcion<=4)
       {
        cout<<"Opcion "<<opcion<<endl;
        lee_caracter();
       }
   }
 while (opcion!=4);

 return 0;
}

void ColorTexto(int atributo, int texto, int fondo)
{
 std::string color = "\033["+toString(atributo)+";"+toString(texto+30)+";"+toString(fondo+40)+"m";
 cout<<color;
}

int lee_caracter()
{
 struct termios config_ant;
 struct termios config_nueva;
 int caracter;

 tcgetattr(STDIN_FILENO, &config_ant);
 config_nueva = config_ant;                /* Guarda los valores actuales de configuración de la terminal */
 config_nueva.c_lflag &= ~(ICANON | ECHO); /* resetea las banderas canonical y echo para que no se muestre en pantalla lo que se teclea*/
 tcsetattr(STDIN_FILENO, TCSANOW, &config_nueva); /* Establece los nuevos valores para la terminal */
 caracter = getchar();

 // si se pulsa una tecla que empieza con un código de escape
 if (caracter==27)
    {
     caracter=getchar();
     if (caracter==91)
         caracter=getchar(); //caracter ahora tiene el valor de la tecla pulsada
    }

 tcsetattr(STDIN_FILENO, TCSANOW, &config_ant); /* Vuelve a poner los valores originales de configuración de la terminal */

 return caracter;
}

void ImprimeMenu(std::string menu[], int tamanio, int op)
{
 int x=0, pos;
 unsigned int longitud=0;

 // Obtiene la longitud total del menú dejando dos espacios entre cada opción
 for (x=0; x<tamanio; ++x)
     {
      longitud += menu[x].length();
      if (x!=tamanio-1)
          longitud +=2;
     }

 pos = (80 - longitud) / 2;

 // Limpia la pantalla
 cout << "\033[2J\033[1;1H";

 // Deja el número de espacios adecuado para que el menú quede centrado (se asume una pantalla de 80 caracteres)
 cout<<setw(pos)<<" ";
 for (x=0; x<tamanio; ++x)
     {
      if (x+1==op)
          ColorTexto(BRIGHT,YELLOW,MAGENTA);
      else
          ColorTexto(BRIGHT,WHITE,BLUE);
      cout<<menu[x];
      if (x!=tamanio-1)
        {
         ColorTexto(BRIGHT,WHITE,BLUE);
         cout<<"  ";
        }
     }
 cout<<default_console<<endl;
}

menuh

Etiquetas: , ,

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s


A %d blogueros les gusta esto: