Você está na página 1de 5

033-036-curses

22.11.2004

17:48 Uhr

Seite 33

Interfaces

DESARROLLO

Un primer paso hacia curses

Interfaces Simples
Tal vez uno de los principales motivos para utilizar un interfaz de texto sea uno que dicta el sentido comn de la economa. Ms o menos nuestra mxima podra rezar as:Para qu ms?. Efectivamente: para qu? Qu necesidad tiene un almacn de piezas de recambio de automviles de un Pentium VII a un milln de gigaherzios con pantallas de plasma de tropocientos pixels y mogollomiles de colores? Tal vez sera ms realista escoger mquinas ms bsicas, incluso de las consideradas obsoletas, e invertir lo que se ahorre en hardware (y software, como despus veremos) en otro sitio. POR PAUL C. BROWN
ucho ms prctico, digo, es tener una pantalla en blanco y negro, con buen contraste, un ordenador sencillito. Sinceramente, los interfaces amigables con muchos colorines e iconos de diseo estn sobrevalorados. No slo eso, sino que pueden inducir a confusin. Si se piensa racionalmente, muchas aplicaciones (la mayora) no necesitan de interfaz grfico. Perfectamente podran apaarse con un interfaz de botones, mens y ventanas basadas en texto. Amn de resultar menos exigente con el hardware, tambin seran ms claros y ms sencillos de utilizar. Precisamente para desarrollar interfaces para aplicaciones de estas caractersticas existe curses, una librera que facilita enormemente la creacin de ventanas, mens y widgets en terminales de texto. Por supuesto que las libreras curses estn disponibles para la mayora de los Unixes, incluyendo Linux y suelen incorporarse con casi todas las distribuciones e incluso existen versiones para otras plataformas lo que asegura, hasta cierto punto, la portabilidad del cdigo (vase [1]). En este primer captulo destinado a curses vamos a ver como emplear la librera imbuida en la infraestructura de una aplicacin desarrollada en C++. Una advertencia: el C++ no es el entorno natural de curses y, al menos en un caso, no conseguiremos unas compilaciones cien por cien limpias (es decir, sin advertencias), si bien esto es ms bien un problema del compilador g++ que del cdigo de muestra o de un defecto de curses, como despus veremos.

Aplicndose a la tarea
Tal vez la manera ms racional de enfocar el desarrollo de la aplicacin (al menos es la que funciona para m) es el de concebirlo desde arriba hacia abajo. Es decir, piensa en como quieres que se vea y ya te encargars del trabajo sucio despus. Normalmente esto implica empezar con la funcin main() y reducirlo a unas pocas llamadas y cuando digo unas pocas, quiero decir unas poqusimas. Si la funcin main() puede contener como mucho 10 lneas de cdigo, es que vamos bien. Despus vamos implementando clases de las ms generales a las ms especficas, siendo estas ltimas las que de verdad interactan con las libreras especficas, curses en este caso. As que, yo cmo quera que se viese? Pues quera que en main() se inicializase un objeto x de una clase, llammosle application, y que esa clase se ocupase de mostrar la ventana principal de la aplicacin, colocara los mens etc. El resto de main() se ocupara con

un bucle while que se encargase de procesar los tecleos y pasarlos a la clase application y, al final, cuando se pulsase la tecla que precipitase la salida del bucle, se llamase al destructor del objeto y todo se recogiese limpia y ordenadamente. Sencillo. El corazn de dicha clase se puede apreciar en el Listado 1. No es muy impresionante verdad? Eso es por que la mayor parte del trabajo sucio se lleva a cabo en window, que al ser heredado por application, recibe una llamada a su constructor cuando se crea un objeto application. Y es en el constructor de window donde se inicializa toda la infraestructura de curses, preparndola para mostrar las ventana. Se puede apreciar la herencia de window por parte de application en el fichero de inclusin application.h, no listado en este artculo, pero que puede ser descargado desde la web de la revista en [2]. Un poco ms adelante volveremos sobre la clase window. Volviendo al constructor de application, lo primero que se

www.linuxmagazine.com.es

Nmero 01

33

033-036-curses

22.11.2004

17:48 Uhr

Seite 34

DESARROLLO

Interfaces

hace es realizar una llamada al macro de fabricacin casera makeString() que sirve para convertir una cadena con un nmero de argumentos opcionales en una sola cadena tipo string que se utilizar como ttulo (asignado, pues, a la propiedad de la clase title). Veremos ms sobre este macro en la seccin Parmetros Indefinidos ms abajo. Lo siguiente es abrir una ventana con la que podamos trabajar. Esta ventana ser la del fondo, la madre de todas las subsiguientes ventanas y donde se alojar el men principal del programa. La llamada es un mtodo de la clase window, heredada por application y que veremos en la siguiente seccin. El destructor de la clase contiene una nica instruccin, endwin(), la funcin de curses utilizado para recoger la basura y para devolver la terminal al estado que tena antes de la llamada a initscr(). De momento es todo lo que necesitaremos para salir elegantemente de curses.

Abriendo la Ventanas
En la siguiente capa de nuestra aplicacin, estara la clase que administrara cada una de las ventanas que se fuesen creando, incluyendo la principal. Esta capa viene representada por la clase window, la implementacin de la cual se puede ver en el Listado 2 (o al menos parte. Se han dejado fuera los mtodos get y set correspondientes a varios atrib-

Listado 1: application.cpp
01 #include "application.h" 02 03 application::application(bool frame=TRUE,string app_Name="Untitled"...) 04 { 05 makeString(app_Name); 06 title=app_Name; 07 08 window app_window(COLS,LINES,0,0,TRU UE,title); 09 } 10 11 application::~application() 12 { 13 endwin(); 14 } 15 16 // ... Y 'gets' varios

utos. Ver [2]). Fijmonos que window cuenta con dos constructores sobrecargados. Uno, el que no tiene parmetros, sirve de constructor cuando se crea un objeto application y crea el entorno curses. Este constructor se llamar una sola vez por aplicacin y monta la ventana stdscr, el contenedor de todas las dems ventanas. En el cdigo se aprecia como usa los mtodos estndar de inicializar la pantalla y librera de curses utilizando (initscr()) y como se activa el mapeado del teclado (keypad(), esto sirve para que las teclas de funcin, cursores, etc. devuelvan caracteres que el programa pueda procesar, permitiendo, por ejemplo, que si el usuario pulsa la tecla F1, se active el sistema de ayuda, etc.). A continuacin, deshabilitamos la secuencia de Nueva Lnea + Retorno de Carro cada vez que se produce una salida con la llamada a nonl() y le decimos a la aplicacin que ha de capturar las pulsaciones en cuanto se produzcan sin esperar a un carcter de nueva lnea con el procedimiento cbreak(). Esto ltimo nos permitir procesar cada tecleo del usuario como es debido, activando alguna funcionalidad del programa si pulsa un tecla de un carcter no imprimible o colocando una letra en la ventana apropiada si el usuario desea escribir algo. Por fin, evitamos que se visualicen inmediatamente las entradas desde el teclado con la funcin noecho(). De esta manera, podremos capturar los tecleos y procesarlos como nos convenga en el contexto de la aplicacin. Todas estas funciones pertenecen a la librera de curses y su uso es bastante estndar en el arranque de cualquier aplicacin que utilice el paquete. El segundo constructor, window::window( int wide = COLS,int high = LINES, int posX = 0,int posY = 0, bool frame = TRUE, string win_Name = "Untitled"...), sirve para crear las ventanas con las que podremos interactuar. Si nos fijamos, el constructor de la clase application utiliza este constructor para generar el fondo visible de la aplicacin, con un marco (establecido por el parmetro frame) y de un tamao igual a la del terminal donde se mostrar. El tamao de la terminal se establece en lneas (LINES) y columnas (COLS), siendo estas dos variables generadas por curses a la hora de inicializar el motor y para la ventana principal de la

aplicacin vamos a ocupar todo el terminal visible. Por ello, al invocar al constructor de la ventana de fondo de la aplicacin en el constructor de application, utilizamos el alto y ancho mximo disponible al arrancar el programa. De hecho, lo primero que se hace en este segundo constructor es crear una ventana con la funcin curses newwin() y asignar el puntero que devuelve al atributo w_Handle. Este atributo nos ser til ms adelante para referirnos a cada una de las ventanas cuando contemos con ms de uno. A continuacin, procesamos el ttulo de la ventana de manera similar que hacamos en el constructor de application y establecemos al atributo frame para dibujar (o no) un marco alrededor de la ventana. Lo siguiente es asignar la ventana a un panel. Los paneles en curses aaden propiedades a las ventanas permitiendo que estas se apilen por capas y se solapen de manera consistente. Para entendernos, sera muy difcil tener ventanas de dilogos, ventanas mviles y ventanas apiladas sin que estas participaran de las funcionalidades que les atribuyen los paneles. La conversin de una ventana normal en un ventana panel es tan sencilla como se ve en el listado: basta invocar a la funcin curses new_panel() con el handle de la ventana a convertir. La funcin devuelve un handle al panel creado que guardamos en el atributo p_Handle para referencias futuras. Luego llamamos al mtodo de la clase putTitle() (que se ve un poco ms abajo en el listado). ste mtodo, como su nombre indica, coloca el ttulo de la ventana en el ngulo superior izquierdo y, de paso, dibuja el marco para la ventana. El siguiente mtodo, showPanels(), tambin perteneciente a la clase window, muestra el panel llamando a update_panels() (que actualiza el aspecto del panel) y doupdate() (que actualiza la pantalla con la nueva informacin), ambas funciones pertenecientes a la librera curses. El mtodo putTitle() a su vez invoca al mtodo wWrite(), que es el mtodo de la clase encargado gestionar el paso de cadenas a las ventanas de curses. Recibe tres o ms parmetros: los dos primeros establecen la posicin x e y de la cadena (tercer parmetro) que se montar junto con los parmetros indefinidos que le siguen. La funcin mvwprintw(), perteneciente a la

34

Nmero 01

www.linuxmagazine.com.es

033-036-curses

22.11.2004

17:48 Uhr

Seite 35

Interfaces

DESARROLLO

Listado 2: window.cpp
01 02 03 04 05 06 07 08 09 10 11 12 #include "window.h" window::window() { w_Handle=initscr(); keypad(stdscr, TRUE); nonl(); cbreak(); noecho(); } 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 } void window::showPanels() { update_panels(); doupdate(); } void window::wWrite(int px, int py, string my_String...) { if (has_Frame) { px++;py++; } makeString(my_String); mvwprintw(w_Handle,py,px,my_String.c_str()); wrefresh(w_Handle); showWindow(); } void window::showWindow() { nodelay(w_Handle,TRUE); wgetch(w_Handle); nodelay(w_Handle,FALSE); showPanels(); } void window::drawFrame() { wborder(w_Handle, ACS_VLINE, ACS_VLINE, ACS_HLINE, ACS_HLINE, ACS_ULCORNER, ACS_URCORNER, ACS_LLCORNER, ACS_LRCORNER); } int window::wGetch() { return(wgetch(w_Handle)); } // Gets y sets varios aqu

window::window(int wide=COLS,int high=LINES,int posX=0,int posY=0,bool frame=TRUE,string win_Name="Untitled"...) 13 { 14 w_Handle=newwin(high,wide,posY,posX); 15 16 makeString(win_Name); 17 title=win_Name; 18 has_Frame=frame; 19 20 p_Handle=new_panel(w_Handle); 21 22 putTitle(); 23 showPanels(); 24 } 25 26 27 void window::closeWindow() 28 { 29 delwin(w_Handle); 30 refresh(); 31 } 32 33 void window::putTitle() 34 { 35 int px=1,py=0; 36 if(has_Frame) 37 { 38 drawFrame(); 39 px=0;py=-1; 40 } 41 wWrite(px,py,title);

73 74 75 76 77 78 79 80 81

librera curses, es la encargada de colocar la cadena en la ventana indicada por el handle que se le pasa. Ntese como, a pesar de que la mayora nosotros estamos acostumbrados a referirnos primero al eje de las x y despus al eje de las y, curses lo hace a la inversa, ms bien porque cuando hablamos de terminales de texto, es habitual hablar primero de lneas (componente vertical) y despus de columnas (componente horizontal) en ese orden. Despus de colocar la

cadena, llamamos wrefresh() que refresca la ventana que acabamos de modificar, que no la pantalla. El refresco de la pantalla para que se puedan ver las modificaciones se produce en el mtodo showWindow(), que llamamos a continuacin. El mtodo showWindow() es un parche en la ya larga tradicin de los famosos parches de Paul C. Brown. Naci cuando me d cuenta de que curses no me actualizaba satisfactoriamente las ventanas si no haba una entrada de

datos desde el teclado. Bueno, ms bien, si no se intentaba conseguir una entrada desde el teclado. Por ello lo nico que hace esta rutina es anular la espera para una entrada (nodelay()), leer el bfer de entrada (wgetch() normalmente dicho bfer estar vaco, puesto que no espera a que el usuario teclee nada) y volver a activar la espera de entrada para, a continuacin, actualizar los paneles llamando a showPanels(). Sin este procedimiento, los paneles no se actualizan

www.linuxmagazine.com.es

Nmero 01

35

033-036-curses

22.11.2004

17:48 Uhr

Seite 36

DESARROLLO

Interfaces

que fuera iterando sobre la lista de arguque acepta, adems de una serie de hasta una nueva entrada por parte del mentos hasta que o (a) se leyese hasta el parmetros definidos, un nmero usuario y no se mostrarn de motu pronmero de argumentos preestablecido o indefinido de otros parmetros. Hay pio cosa como los ttulos o el borde. En (b) se llegase hasta el argumento que varias maneras de leer los parmetros cuanto a drawFrame(), su nombre lo fuese igual al argumento definido como indefinidos a variables. Una sera la de dice todo: dibuja un marco alrededor de el ltimo. Ambos mtodos ofenden mi pasar como argumento definido el la ventana utilizando para ello la rutina sentido de la esttica de la progranmero de argumentos indefinidos. Otra curses wborder() y los valores premacin, ya que qu pasa si ni el mismo la de incluir como definido un argudefinidos de curses para la barra vertical programador sabe cuantos argumentos mento que fuese igual al ltimo de los (ACS_VLINE), tanto para el borde van se indefinidos? Yo opto por el argumentos indefinidos. En ambos casos izquierdo, como para el derecho; la barra mtodo (c), que consiste en que todos crearamos un bucle (en el primer caso horizontal (ACS_HLINE) para el borde los argumentos se vuelcan en una lista un bucle for y en el segundo uno while) superior e inferior ; y los grficos de las (de tipo va_list) y se van esquinas ACS_ULCORNER leyendo e integrando en el (esquina superior Listado 3: va.h argumento plantilla. Esto no izquierdo), ACS_ 01 #include <stdlib.h> sera un problema si no fuera URCORNER (esquina su02 porque con curses, el uso de perior derecho), ACS_ 03 #define makeString(return_String) \ una cadena de longitud LLCORNER (esquina infe04 { \ indefinida (tipo char *cadena) rior izquierdo), ACS_ 05 char *fmt; \ genera un fallo de segLRCORNER (esquina infe06 if ((fmt=(char*) mentacin. En concreto, si rior derecho). Por fin, el malloc(sizeof(char)*return_String.length()))==NULL)\ utilizamos una cadena de lonmtodo wGetch() se en07 printf("ERROR: Memoria insuficiente");\ gitud indefinida *buf en un carga de recoger las 08 else \ programa C o C++ normal entradas procedentes del 09 { \ (es decir, que no cargue librteclado referidas a la ven10 strcpy(fmt,return_String.c_str()); \ era raros como curses) no tana para que puedan ser 11 va_list args; \ pasa nada: procesadas. 12 char *p; \

Parmetros indefinidos
Conviene explicar en un aparte lo referente a las funciones con parmetros indefinidos. Estas funciones permiten que funcionen, por ejemplo, el socorrido printf() de C. Si nos fijamos en como funciona, si hacemos
printf("Tengo %iU mueca vestida deU %s...",1,"azul");

devolver la cadena Tengo 1 mueca vestida de azul... y comprobamos que a printf(), que no es ms que otra funcin de las libreras estndar de C, puede recibir tantos parmetros como gustemos siempre y cuando el primero sea el que indique el formato del mensaje a mostrar. Eso es por que printf() es una funcin

13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42

int n, size=10; \ if((p = (char*) malloc(size)) == NULL)\ printf("ERROR: Memoria insuficiente");\ else \ { \ while(1) \ { \ va_start(args,fmt); \ n=vsnprintf(p,size,fmt,args); \ va_end(args); \ if (n < size) \ { \ break; \ } \ else \ { \ size = n+1; \ if((p = (char *) realloc(p,size)) == NULL)\ { \ printf("ERROR: Memoria Insuficiente");\ break; \ } \ } \ } \ } \ return_String=p; \ free(p); \ free(fmt); \ } \ }

function ejemplo2U (char *plant, ...) { char *buf; va_list args; va_start(args, plant); vsprintf(buf,plant,args); va_end(args); return(buf); }

El cdigo anterior funcionar sin ningn problema. Pero con curses, si se utiliza una cadena de longitud indeterminada y despus se utiliza sprintf() o vsprinf() con una plantilla y varios argumentos, el resultado es el dichoso error de fallo de segmentacin, uno de los errores ms irritantes que tiene la insidiosa costumbre de aparecer en el momento de la ejecucin, despus de una compilacin exitosa. Otra pega un tanto engorrosa es que va_start() y va_end() han de estar en la misma funcin que recibe los parmetros indefinidos, por tanto veris la

36

Nmero 01

www.linuxmagazine.com.es

033-036-curses

22.11.2004

17:48 Uhr

Seite 37

Interfaces

DESARROLLO

misma secuencia de comandos repetida una y otra vez a lo largo de las clases que estamos analizando y no hay manera de separarlos en una funcin aparte. El tercer problema es que slo se nos permite operar con cadenas tipo char *, lo que tericamente nos despoja del privilegio de utilizar cadenas de tipo string, con la de ventajas y facilidades que aportan, caray. Todos y cada uno de estos inconvenientes se resuelven con Figura 1: Nuestra primera aplicacin, con marco y ttulo. el macro que se puede ver en el Listado 3. Cmo funciona? Pues, como que size, es que se ha truncado la se puede observar en el listado, lo cadena. En este caso lo que hemos de primero que hacemos es asignar espacio hacer es aumentar el valor de size hasta a una variable de tipo char* llamado fmt, n+1 (la longitud total ms el crcter de hecho tanto espacio como caracteres nulo /0) y reasignar memoria por el contenga el argumento string nuevo valor a p. Es lo que se hace a con(return_String). A continuacin copitinuacin. Una vez que tenemos el amos el contenido de return_String a tamao adecuado, volvemos a asignar la fmt. a continuacin asignamos un poco cadena a p y podemos salir del bucle y de espacio (size en bytes) a otra variable asignar la cadena contenida en p a char* llamado p. Esta variable contendr return_String y proceder a liberar la la cadena procesada. Seguidamente, memoria asignada a p y a fmt. Este ingecreamos la cadena final con un lmite de nioso truco viene de la pgina man de size bytes. La funcin vsnprintf() asigna vsnprintf(), si bien ha sido adaptado tantos bytes a p como los indicados en para que por un extremo entre una varisize, por tanto, no es posible excedernos able tipo string sin los otros parmetros de la cantidad de memoria asignada a p para formatear y por el otro salga una y evitamos posibles fallos de segvariable string con todos los parmetros mentacin. Ahora bien, si toda la cadena dispuestos limpiamente en su interior. no cabe en esos size bytes, vsnprintf() Toda la basura se recoge, la memoria devuelve el nmero de bytes que hubiera reservada para las cadenas se libera, cabido. Es decir, si n resulta ser mayor todo queda inmaculadamente limpio tras su ejecucin. Este macro no produce una compilacin limpia. A la hora de compiListado 4: principal.cpp lar con g++ genera una advertencia 01 #include "application.h" all donde se emplee que reza:
02 03 int main() 04 { 05 int ch; 06 application my_App(TRUE,"Mi Aplicacin nmero %i",1); 07 08 while((ch=my_App.wGetch())!=KE Y_HOME) 09 { 10 11 } 12 13 my_App.~application(); 14 exit(0); 15 }

nuestro programa es harto sencillo. Incluimos el fichero de inclusin application.h que contiene la definicin de la clase application. A continuacin, declaramos una variable tipo char para contener las pulsaciones del teclado y un objeto tipo application. Llegados a este punto, se visualizar en la terminal la ventana principal de la aplicacin, tal y como se ve en la Figura 1. Seguidamente, entramos en un bucle a la espera de que el usuario pulse la tecla INICIO o HOME, lo que cierra el bucle, desencadenando la destruccin del objeto application y cerrando el programa.

Compilacin
Para la compilacin con curses, hemos de enlazar con la librera ncurses. Si adems empleamos paneles para poder solapar ventanas, hemos de incluir la librera panels y, por supuesto, hemos de enlazar las clases que hemos creado. Para todo ello existe un sencillo Makefile que se puede descargar junto con el resto del cdigo fuente de [2].

Conclusin
Si bien parece que es poco lo obtenido hasta ahora, hemos sentado las bases para una aplicacin mucho ms compleja. Podramos tomar lo desarrollado como una plantilla para un programa funcional e ir insertando nuevas funcionalidades con un de mnimo esfuerzo. El mes que viene seguiremos utilizando curses para implementar una aplicacin y veremos como crear ventanas modales de dilogo, como crear botones y otras funcionalidades que nos demostrarn que los interfaces de texto siguen estando vivitos y coleando

warning: second parameter ofU `va_start' not lastnamedU argument

Esta advertencia surge debido a que el compilador es incapaz de reconocer una cadena de tipo string como el ltimo argumento definido antes de la ristra de argumentos indefinidos. Sin embargo, el programa acaba compilando y se ejecuta sin problemas.

RECURSOS
[1] Curses en plataformas Windows: http://www.funet.fi/pub/win-nt/curses/ http://www.crystalcom.com/crs_swin. htm http://www.eunet.bg/simtel.net/ msdos/screen.html [2] Las fuentes completas, con ficheros de inclusin, para el programa descrito en este artculo http://www.linuxnewmedia. es/magazine/numero1/descargas/curses

Explotacin
Por fin hemos llegado al momento de ver nuestras clases en accin. Si miramos el Listado 4, vemos que la funcin main de

www.linuxmagazine.com.es

Nmero 01

37

Você também pode gostar