La librería BGI

Introducción

Mosaico

En esta pequeña serie de capítulos vamos a estudiar, finalizando con un ejemplo más o menos completo, la librería BGI de Borland para realizar aplicaciones gráficas en lenguaje C.

Antes de tratar esta librería, vamos a exponer unos primeros conceptos teóricos que nos ayudarán a aprender más rápidamente su manejo, así como el de otras de similar funcionamiento.

Cuando vamos a realizar una aplicación gráfica, necesitamos recurrir a unos elementos llamados primitivas para poder llevar a cabo los dibujos. Las primitivas de dibujo son los elementos básicos de los que disponemos para realizar cualquier representación gráfica: puntos, líneas, círculos, ... La primera de estas primitivas es muy dependiente del hardware de la máquina, del sistema operativo, del modo de vídeo, ... y dado que siempre podemos usar la función para este fin que viene en alguna librería de gráficos, vamos a tomarla como básica-disponible, es decir, cuando queramos dibujar algo, denotaremos PIXEL(x,y,color) a una función que vamos a tener a nuestra disposición siempre que la necesitemos.

La sintaxis de esta función en las distintas librerías no es muy distinta de la que adoptamos aquí como convenio. Así, a partir de ahora, en nuestros algoritmos, PIXEL(x,y,color) significa "pintar un punto en la posición (x,y) de la pantalla del color 'color'". Hay que hacer notar que en la pantalla, la esquina superior izquierda tiene coordenadas (0, 0), y que la coordenada x crece hacia la derecha mientras que la coordenada y crece hacia abajo. En la resolución en la que trabajemos, tendremos un máximo de XMAX puntos de ancho por YMAX puntos de alto, por tanto, el rango permitido para dibujar es el rectángulo de esquina superior izquierda (0, 0) y de esquina inferior derecha (XMAX-1, YMAX-1).

Una estrategia sencilla de programación gráfica podría esquematizarse en el siguiente gráfico:

Esquema general
Esquema general

Vamos a explicar cada una de estas etapas:

Detección del hardware: Trata de averiguar qué tipo de máquina se usa y qué características posee, pues esto nos determina qué modos gráficos podemos usar.

Por modo gráfico se entiende una configuración que nos informa de si es modo texto o gráfico, qué resolución tiene, si es paleta o true-color, y cuántos colores podemos usar.

Inicialización del modo gráfico: En esta fase escogemos las características del modo concreto que queramos usar. En modo texto tendremos como posibles resoluciones 80x25x16, 132x43x16,... y en modo gráfico tenemos 320x200x16, 320x200x256, 320x200x64K, 640x480x16, 800x600x16M, ... (hay muchas más posibilidades). Siempre los modos de 256 o menos colores son modos de paleta, y los modos de 32K o más colores, son modos true-color.

Según la librería, la función de inicialización de modo de vídeo es distinta. Suele usarse un identificador para cada modo distinto. Cuando no hablemos de una librería concreta, escribiremos:

Inicializar_Modo(Identificador)

y como 'Identificador' escribiremos una constante que empieza por m y, a continuación, el modo concreto siguiendo la notación MMMxNNNxCC, donde MMM es la resolución horizontal, NNN es la resolución vertical y CC es el número de colores. Por ejemplo, usaremos identificadores como:

m320x200x256
m640x480x16
m80x25x16
m800x600x32768
...

Así, en nuestra notación, si queremos inicializar el modo 640x400x256, escribiremos:

Inicializar_Modo(m640x400x256)

Una vez inicializado el modo, tenemos dos posibilidades: modificar el contexto gráfico o usar alguna función de dibujo. Se entiende por contexto gráfico un conjunto de propiedades que determinan con qué estilo se van a pintar las primitivas en pantalla.

Estilo son una serie de opciones que se aplican cuando tiene sentido aplicarse (ahora vemos qué quiere decir esto).

El estilo más básico es el color, que es aplicable a todas las primitivas, pues todas ellas son susceptibles de poder ser dibujadas con algún color.

También podemos establecer la forma en que las líneas serán dibujadas: grosor y trazo continuo o discontinuo, pudiendo elegir entre varias posibilidades en este último. El estilo de trazo de líneas pone de manifiesto que cualquier estilo no es susceptible de ser aplicable a cualquier primitiva: por ejemplo, no tiene sentido aplicar un estilo de trazo de líneas a un punto. De todas formas, no hay que preocuparse por qué primitivas admiten un estilo y qué primitivas no lo admiten: el esquema general de una librería gráfica en este aspecto es, primero, fijar el estilo, para después dibujar la primitiva con ese estilo, si es posible aplicarlo, y si no es posible, la dibuja sin aplicarlo, únicamente empleará aquello aplicable a la primitiva en cuestión.

Como decíamos antes, además de tener grosores distintos, las líneas pueden tener varios tipos de trazo. A continuación mostramos unos ejemplos de trazos y los identificadores que usaremos (pues luego las librerías gráficas dan completa información y ejemplos sobre las posibilidades):

Estilos de líneas
Estilos de líneas

El último estilo que vamos a ver es aplicable a figuras que pueden rellenarse, y se conoce como patrón de relleno. Posibles patrones de relleno (y sus identificadores para nosotros) son, por ejemplo, SOLIDO, que hace referencia al relleno normal, TRAMADO, que hace una trama de líneas horizontales y verticales en lugar del relleno sólido, TRAMADO_OBL, que hace una trama de líneas oblícuas, PUNTEADO, que hace una trama de puntos, TRANSPARENTE, que no rellenará la figura, etc...

Ahora que hemos estudiado los posibles estilos, vamos a ver la notación que vamos a usar para las funciones que cambian los estilos:

   Cambiar_Color(ID_COLOR);
   Cambiar_Trazo_Linea(ID_LINEA,ID_GROSOR);
   Cambiar_Patron_Relleno(ID_RELLENO);

Notar lo siguiente: si pintamos un rectángulo, su borde se pintará con el estilo del trazo de líneas, mientras que su interior se pintará con el estilo del patrón de relleno. Si no especificamos estilo alguno, las librerías toman por defecto los siguientes valores:

   ID_COLOR es el identificativo del color negro.
   ID_LINEA es el identificativo del trazo continuo.
   GROSOR es el trazo identificativo de 1 píxel de grosor.
   ID_RELLENO es el identificativo del relleno sólido.

Estudiadas las posibilidades que tenemos para dibujar primitivas, entramos ya en las primitivas que tenemos disponibles:

Pixel(x,y,color);
Pinta un pixel en la posición (x,y) de la pantalla, del color especificado en color. Es la única primitiva a la que no se le aplica estilo alguno.
Linea(x0,y0,x1,y1);
Pinta una línea que va del punto (x0,y0) al punto (x1,y1) con el estilo que se haya definido. Si no hay estilos definidos, se aplica el estilo por defecto.
Rectangulo(x0,y0,x1,y1);
Pinta un rectángulo cuya esquina superior izquierda tiene por coordenadas (x0,y0) y cuya esquina inferior derecha tiene por coordenadas (x1,y1). El trazo de las cuatro líneas será el que venga dado por el estilo de línea. Si no hay estilo de línea definido, se usará el estilo por defecto, trazo continuo.
RectanguloR(x0,y0,x1,y1);
Igual que Rectangulo, pero la figura se rellenará con el estilo de relleno que haya definido en ese momento.
Circulo(x,y,radio);
Pinta un círculo de centro (x,y) y radio 'radio'. El valor del radio debe ser un número positivo para que tenga sentido. El círculo se pinta con el estilo que haya definido en ese momento o con el estilo por defecto si no se han definido estilos.
CirculoR(x,y,radio);
Igual que Circulo, pero además se rellenará con el estilo de relleno que haya definido en ese momento.
Elipse(x,y,a,b);
Pinta una elipse de centro (x,y) y semiejes mayor y menor a y b, respectivamente. Ambos deben ser números positivos para que tenga sentido. La elipse se pinta con el estilo definido en ese momento o con el estilo or defecto si no hay ninguno definido.

Y muchas otras que concretaremos para el caso de la BGI.

La librería BGI: Modos de vídeo y colores

Nota previa: para utilizar cualquiera de estas funciones, primero deberá incluirse el correspondiente fichero de cabecera 'graphics.h'. Además, esta librería no se enlaza automáticamente si no se lo especificas así, para lo que hay que hacer una de las dos cosas siguientes:

  1. Si tienes un Borland C con entorno de programación, en las opciones de compilación, concretamente,
    OPTIONS -> LINKER -> LIBRARIES, debes marcar la casilla que pone 'Graphics Library' para que enlace con la librería de gráficos al compilar el programa.
  2. Para enlazar desde línea de comandos, tendrás que escribir:
    TC -L GRAPHICS.LIB PROGRAMA.C

Como comentábamos en el capítulo anterior, lo primero que había que hacer era detectar el hardware para después inicializar el modo de vídeo. Para hacer esto tenemos dos posibilidades. Podemos asignar directamente el tipo de tarjeta de vídeo y el modo que usaremos de los soportados por esa tarjeta, para lo cual necesitamos saber qué codigos asignar a unas variables que llamaremos 'driver' y 'modo', que deben ser de tipo entero, o bien podemos dejar que sea una función de la librería la que detecte el modo gráfico máximo que soporta nuestro hardware, y esa función se encargue de realizar las asignaciones de los valores correspondientes a las variables 'driver' y 'modo', para después pasarlas nosotros a la función que inicializa el modo gráfico.

Para consultar los modos disponibles, tenemos la función:

   void detectgraph(int *driver, int *modo);

que almacenará en 'driver' el código del fichero driver requerido y en 'modo' el código de la máxima resolución disponible con ese driver. Si preferimos asignar directamente esos modos, a continuación vemos qué valores tenemos disponibles.

Valores para driver=CODIGO_DRIVER:

   DETECT
   CGA
   MCGA
   EGA
   EGA64
   EGAMONO
   IBM8514
   VGA

El driver DETECT lo que significará es que se escogerá el modo más alto soportado por nuestra tarjeta, de entre todos los disponibles. Los valores para modo=CODIGO_MODO:

   CGA         CGAC0
               CGAC1
               CGAC2
               CGAC3
               CGAHI
   MCGA        MCGAC0
               MCGAC1
               MCGAC2
               MCGAC3
               MCGAMED
               MCGAHI
   EGA         EGALO
               EGAHI
   EGA64       EGA64LO
               EGA64HI
   EGAMONO     EGAMONOHI
   VGA         VGALO
               VGAMED
               VGAHI
   IBM8514     IBM8514LO
               IBM8514HI

Hoy en día, únicamente usaremos (casi seguro) el driver VGA con el modo VGAHI, puesto que el resto se trata de modos de vídeo con resoluciones bajas y pocos colores que ahora no tienen sentido por las capacidades de las tarjetas gráficas, pero que hace unos años era lo único que se podía hacer. Incluso el driver VGA sabrá a poco en muchas ocasiones, para lo que existe una librería llamada SVGABGI que implementa las mismas funciones que la BGI de Borland, pero no está desarrollada por Borland, y nos da facilidades para usar modos de vídeo con más colores y mayor resolución. Puedes bajarla de aquí y usarla de la misma manera que explicaremos aquí el uso de la BGI. De todas formas, consulta la documentación de la librería, en ella verás más detalles.

Tras la consulta de los modos de vídeo (bien por llamar a la función detectgraph, bien por asignación directa), ya podemos inicializar el modo de vídeo deseado, llamando a la función:

   void initgraph(int *driver, int *modo, char *ruta);

que utiliza los códigos de 'driver' y 'modo' para seleccionar la configuración, y la cadena de caracteres 'camino' para hallar el fichero que contiene al driver adecuado.

La librería BGI dispone de varios ficheros con extensión .BGI que tendrán que estar localizables por el programa que creemos pues, en caso contrario, no podremos inicializar el modo de video y, consecuentemente, no podremos crear gráficos. Además, si el programa no encuentra los ficheros BGI, terminará dando un error en el que dice que no ha podido inicializar el modo gráfico. Estos ficheros BGI se encuentran en el directorio BGI de la distribución correspondiente del compilador de Borland.

Suponiendo que hemos inicializado correctamente el modo gráfico, ya podemos definir el área de dibujo (también llamada viewport), que es el rectángulo que podemos modificar con nuestros gráficos, y que no tiene por qué coincidir con la totalidad de la pantalla. Esto se realiza con la función:

   void setviewport(int x0, int y0, int x1, int y1, char Corte);

que establece el rectángulo de dibujo entre un punto superior izquierdo (de coordenadas (x0, y0)) y otro inferior derecho (de coordenadas (x1, y1)). Corte es un booleano que indica si se eliminan los dibujos que salgan fuera del área o no. Si no se usa setviewport, el área de trabajo será, por defecto, toda la pantalla. Otra función de gran utilidad, que nos permite borrar todo el contenido del área de trabajo, asignándole el color de fondo (después vemos cómo especificar este color) es la siguiente:

   void clearviewport();

Una vez tenemos definida el área de trabajo deberemos entrar en la definición del contexto gráfico (recordemos lo dicho en el capítulo anterior sobre esto), es decir, de todas aquellas variables que van a determinar cómo aparecen dibujadas las cosas (si no se hacen cambios, se usan valores por defecto que son automáticamente asignados al inicializar el modo gráfico) (colores, estilo y anchura de las líneas,...). Normalmente será prioritario escoger los colores con los que se va a dibujar.

En el capítulo anterior decíamos que existen dos modos de paleta de colores: el modo paleta (propiamente dicho) y el modo true-color. El modo paleta es indexado, es decir, existe una tabla con las componentes RGB de los colores de los que se compone la paleta, y a cada color le corresponde un índice. Por ejemplo, el color negro tiene por componentes RGB (0,0,0), y suele tener el índice 0 en las paletas de color. Otro ejemplo, el color rojo tiene por componentes RGB (255,0,0), y puede tener, por ejemplo, el índice 40 en la paleta de colores. Hay unos colores por defecto, pero podemos cambiarlos. El modo true-color no es indexado, si queremos especificar un color, tenemos que decir sus componentes RGB directamente.

La librería BGI únicamente está preparada para trabajar en modo paleta. Según el modo gráfico seleccionado, la paleta será de más o menos colores. Con los BGI estándar de Borland, el modo VGA sólo tiene 16 colores. Con la librería SVGA BGI podemos definir hasta 256 colores en la paleta. El primer paso será, pues, introducir en la paleta aquellos tonos de color que queremos utilizar, a no ser que nos quedemos con los colores por defecto.

Si queremos quedarnos con los colores por defecto, cuando queramos establecer el estilo 'color', podremos pasarle como argumento el índice del color en la paleta, o podemos pasarle unas constantes predefinidas que hacen referencia al color. Estas constantes (con sus correspondientes índices entre paréntesis) son:

   BLACK (0)     BLUE (1)          GREEN (2)       CYAN (3)
   RED (4)       MAGENTA (5)       BROWN (6)       LIGHTGRAY (7)
   DARKGRAY (8)  LIGHTBLUE (9)     LIGHTGREEN (10) LIGHTCYAN (11)
   LIGHTRED (12) LIGHTMAGENTA (13) YELLOW (14)     WHITE (15)

Si preferimos definir nuestros propios colores, tenemos las funciones:

   void setpalette(int numcolor, int codigocolor);
   void setRGBcolor(int numcolor, int r, int g, int b);

que sitúa en el lugar 'numcolor' de la paleta (en una pluma) el color (tinta) definido por 'codigocolor'. En las primeras versiones, el color no se especificaba por sus componentes RGB, sino haciendo uso de una tabla de colores predefinida, donde podemos encontrar sólo determinados tonos con los correspondientes códigos. Posteriormente se añadió la posibilidad de seleccionar por RGB, pero utilizando solo 6 bits por pixel, o sea, 64 combinaciones por componente.

Tras definir la paleta de colores, tenemos que asignar un color de dibujo y un color de fondo (que se usa en las instrucciones de borrado o como fondo cuando dibujamos texto), indicando qué posiciones del mapa de color utilizamos para cada uno. Esto se hace con las funciones respectivas:

   void setcolor(int numcolor);
   void setbkcolor(int numcolor);

Seguimos en el próximo capítulo, viendo qué sintaxis tienen las primitivas de dibujo.

La librería BGI: Primitivas de dibujo y relleno de figuras

El paso siguiente a la selección de colores consiste en modificar los colores de los pixels de pantalla deseados mediante las funciones de dibujo. Esto se puede hacer cambiando directamente los pixels, dibujando primitivas gráficas (líneas, rectángulos,...) o texto. La operación de cambio de color de un pixel puede realizarse con:

   void putpixel(int x, int y, int numcolor);

que pone el color correspondiente a 'numcolor' en la posición (x,y) de la pantalla (debemos tener presente que el sistema de coordenadas en TurboC, y en casi todas las librerías gráficas 2D, tiene su eje Y vertical, pero con el sentido positivo de arriba hacia abajo). Igual que muchas funciones BGI, 'putpixel' tiene su versión recíproca, 'getpixel', para averiguar el color de una posición de pantalla determinada (getpixel(x,y), que devuelve un entero con el índice del color en la paleta). Se pueden escribir o leer rectángulos de pixels completos (pixmaps o imágenes) usando las funciones:

   void putimage(int x0, int y0, unsigned char *pixmap, int tipo);
   void getimage(int x0, int y0, int x1, int y1, unsigned char *pixmap);

En putimage se especifican las coordenadas a partir de la cual se copia la imagen almacenada en la matriz 'pixmap'. 'tipo' indica el código de la función lógica que se va a efectuar entre los nuevos valores de los pixels y los antiguos. Estos valores son:

   COPY_PUT   Copia el pixmap tal como es
   XOR_PUT    Copia haciendo una O exclusiva con el fondo
   OR_PUT     Copia haciendo una O con el fondo
   AND_PUT    Copia haciendo una Y con el fondo (máscara)
   NOT_PUT    Copia invirtiendo la imagen

La forma común de dibujar es, sin embargo, usar primitivas gráficas o formas predefinidas. Veamos algunas de ellas:

   void line(int x0, int y0, int x1, int y1);
   void rectangle(int x0,int y0,int x1,int y1);
   void circle(int x,int y,int radio);
   void arc(int x,int y,int angulo_ini,int angulo_fin,int radio);

Todas ellas dibujan trazos, pero sin rellenar. Algunas tienen además versiones con relleno (filling), y también existe una función general para rellenar el interior de cualquier figura:

   void floodfill(int x, int y, int color_borde);

que rellena una figura comenzando por el punto (x,y) hasta que se encuentre con un contorno de color_borde. Antes de usarla hay que asegurarse de que (x,y) está en el interior del contorno a rellenar, no fuera o justo en el borde. También hay que asegurarse de que el contorno está perfectamente cerrado, sin aberturas.

Hay una primitiva que sí se dibuja rellena, es:

   void bar(int x0, int y0, int x1, int y1);

que dibuja un rectángulo relleno con el estilo actual.

En esta librería, no se distingue la modificación del estilo de dibujo de líneas con la modificación del estilo de relleno, teniendo una única función que cambia la forma de rellenar las primitivas. Si llamamos a la siguiente función antes de dibujar primitivas, las que se dibujen serán de acuerdo al estilo definido por ella (hasta una nueva llamada a dicha función que vuelva a modificar el estilo):

   void setfillstyle(int patron, int numcolor);

La variable 'patron' es un número que lo identifica, y 'numcolor' es el color con el que se efectuará el relleno. En el fichero de cabecera 'graphics.h' vienen definidas unas constantes para los valores posibles que puede tomar 'patron', valores que vemos a continuación:

   EMPTY_FILL (0)       Relleno con el color de fondo
   SOLID_FILL (1)       Relleno de color liso
   LINE_FILL (2)        Relleno con líneas
   LTSLASH_FILL (3)     Relleno con barras finas
   SLASH_FILL (4)       Relleno con barras
   BKSLASH_FILL (5)     Relleno con barras invertidas
   LTBKSLASH_FILL (6)   Relleno con barras invertidas finas
   HATCH_FILL (7)       Relleno con trama fina
   XHATCH_FILL (8)      Relleno con trama
   INTERLEAVE_FILL (9)  Relleno dejando espacios
   WIDE_DOT_FILL (10)   Relleno con puntos muy espaciados
   CLOSE_DOT_FILL (11)  Relleno con puntos poco espaciados
   USER_FILL (12)       Rellenar con patrón definido por el usuario

Si escogemos 'USER_FILL', debemos usar la función 'setfillpattern()', cuya cabecera es:

   void setfillpattern(char *patron, int numcolor);

donde 'patron' es un array de 8 bytes. Este patrón se distribuirá como un patrón de 8x8 bits. Si un bit está activado, se muestra el pixel del color que especifica 'numcolor', en caso contrario, se muestra el color del fondo.

Otra importante parte de las librerías gráficas se dedica a la representación de textos en la ventana gráfica. Algunas de las funciones más elementales serían:

   void settextstyle(int fuente, int dirección, int tamaño);

que indica el estilo actual para escribir texto, qué fuente de texto se utiliza, si la dirección es horizontal(0) o vertical(1) y el tamaño (de 1 a 10) de las letras. Los códigos y las constantes definidas para la fuente a usar los tenemos a continuación:

   DEFAULT_FONT (0)
   TRIPLEX_FONT (1)
   SMALL_FONT (2)
   SANS_SERIF_FONT (3)
   GOTHIC_FONT (4)

Para escribir cadenas de caracteres se usan las funciones:

   void outtext(char *cadena);
   void outtextxy(int x, int y, char *cadena);

Mientras que outtext muestra el texto a partir de la posición de la última operación gráfica, outtextxy lo hace a partir de la posición (x,y) indicada.

Por último, debemos saber cómo cerrar el modo gráfico cuando nuestra aplicación termine. Para ello, debemos usar la función:

   void closegraph(void);

que devuelve al sistema al modo de vídeo en el que estaba antes de llamar a la función 'initgraph'. Podemos usar, en lugar de 'closegraph', la función void restorecrtmode(void) si el programa ya está terminado. En este caso, se libera de forma automática toda la memoria asignada.

La librería BGI: Un ejemplo

Para finalizar este breve estudio de la librería BGI (se puede encontrar más información en la propia ayuda de Turbo C, o en manuales sobre este compilador), vamos a poner un código de ejemplo y una captura con la pantalla producida por el programa, para ver el uso de algunas de estas funciones:

#include <stdio.h>
#include <graphics}.h>
#include <stdlib}.h>
#include <conio}.h>

void Dibuja(void);
void Ventana(int,int,int,int);

int main(void)
  {
  int driver, modo;

  driver=VGA;
  modo=VGAHI;

  initgraph(&driver,&modo,"");  /* La ruta es "", es decir, el
                                   directorio en el que esté el
                                   programa */
  Dibuja();
  fflush(stdin);
  getch();
  restorecrtmode();

  return 0;
  }

void Dibuja()
  {
  char Patron[8] = {0x00,0x64,0x62,0x02,0x02,0x62,0x64,0x00};

  Ventana(20,20,200,100);
  setfillpattern(Patron,RED);
  circle(400,240,100);
  floodfill(400,240,WHITE);
  setfillstyle(SOLID_FILL,RED);
  circle(40,200,20);
  floodfill(40,200,WHITE);
  settextstyle(SANS_SERIF_FONT,0,4);
  outtextxy(20,300,"Texto de prueba");
  }

void Ventana(int x0, int y0, int x1, int y1)
  {
  setfillstyle(SOLID_FILL,1);
  bar(x0+2,y0+2,x1-2,y0+20);
  setfillstyle(SOLID_FILL,7);
  bar(x0+2,y0+22,x1-2,y1-2);
  setcolor(0);
  rectangle(x0,y0,x1,y1);
  setcolor(15);
  rectangle(x0+1,y0+1,x1-1,y1-1);
  line(x0+2,y0+21,x1-2,y0+21);
  }

Nota: En el mismo directorio en el que está el ejecutable, deben estar los ficheros 'EGAVGA.BGI' y 'SANS.CHR', necesarios para inicializar el modo de vídeo, el primero, y para usar la fuente Sans Serif, el segundo. El resultado de este programa podemos verlo en la siguiente imagen:

Ejemplo de BGI
Ejemplo de BGI

Descargas

Nombre Fichero Fecha Tamaño Contador Descarga
Librería SVGA BGI V4 SVGA_BGI_4.zip 2001-09-15 125389 bytes 457