TUTORIAL DE WIN32 Y EJEMPLO DEL USO DE LAS APIS BITBLT, GETDIBITS Y SETDIBITS EN VISUAL C++ 2008 EXPRESS

En este tutorial la idea es manipular imágenes y mostrarlas en una ventana usando solamente apis de Windows. La manera más fácil es empleando el Visual C++ de Microsoft, el cual ya instala los archivos de encabezado necesarios y genera automáticamente el código para crear una ventana vacía.

Primero abrimos el Visual C++ (yo uso la versión 2008 Express), elegimos Nuevo Proyecto->Win32->Proyecto de Win32. A mi proyecto le he puesto el nombre de "FirstTry".


Como tipo de aplicación elegimos "Aplicación para Windows". Por ser la versión Express, no nos permite usar las librerías MFC ó ATL, pero no importa porque no las vamos a necesitar (y yo tampoco sé usarlas :P)

Al presionar el botón de "Finalizar", el Visual C++ nos crea varios archivos, con extensión .cpp y extensión .h. Donde debemos mirar es el código del archivo cpp que tiene el mismo nombre de nuestro proyecto. Para este caso es FirstTry.cpp. Con lo primero que nos encontramos es que el Visual C++ ha generado casi doscientas líneas de código. Todo ese código es necesario para mostrar una sola ventana, simple y vacía. Para entender en detalle qué significa, recomiendo este tutorial: The Forger's Win32 Api Tutorial.

COmo resumen, voy a explicar un poco qué hace este código:

Hay cuatro funciones a las qué prestar atención: MyRegisterClass, InitInstance, WndProc y _tWinMain.

MyRegisterClass: En esta función se establecen las características de la ventana: colores, íconos, etc. También se le envía el nombre de la función (WndProc) donde se procesarán los eventos que ocurran en la ventana (al redimensionarla, maximizarla, hacerle click, cerrarla, etc).

InitInstance: Nuestro programa en ejecución (que por ahora sólo muestra una ventana vacía) se le conoce como instancia. Una aplicación puede tener varias instancias: por ejemplo, dos ventanas del Block de Notas abiertas son dos instancias de la aplicación Block de Notas. La función InitInstance inicializa la instancia actual de nuestra aplicación, crea la ventana llamando a la api CreateWindow y retorna un valor Verdadero o Falso dependiendo si la ventana pudo crearse con éxito o no.

WndProc: Aquí se procesa todo lo que ocurre en la ventana. Ese "todo lo que ocurre" se conoce como Eventos, y pueden ser desde entradas del teclado o clicks del ratón, hasta el cierre de la ventana y el final de la instancia de la aplicación.

_tWinMain: Función principal y desde dónde arranca nuestra aplicación. Esta función llama a MyRegisterClass y a InitInstance. Si todo sale bien, entrará al bucle de mensajes. Como en MyRegisterClass ya se asoció a la función WndProc con la ventana actual, no es necesario llamarla desde _tWinMain, de esto se encarga automáticamente el bucle de mensajes. Cuando ocurre un evento, la ejecución del programa "salta" a WndProc junto con un número que identifica al evento ocurrido.


Dentro de WndProc siempre hay un "switch" que evalúa el valor que acompaña a los eventos lanzados, de esta forma se puede identificar al evento y ejecutar el código correspondiente:


¿Complicado? ¡Claro que sí! Comparado con el Form_Load de Visual Basic o C#, esto es una pesadilla. Pero es así como el sistema operativo Windows maneja las ventanas de TODAS las aplicaciones. En Windows, no importa si se programó en .Net, VB6, Java o Borland C++, Delphi... las librerías que usan estos lenguajes no son más que "envolturas" (wrappers) de la api de Windows. En sus profundidades, todas las aplicaciones de Windows que tengan ventanas terminan en un bucle de mensajes.

Nota: Nótese que repito "Windows" muchas muchas veces, pues este tutorial es exclusivamente para este sistema operativo. En Linux las ventanas se crean con librerías como GTK, las cuales también poseen un bucle, pero las funciones y la lógica son totalmente distintas.

Para los ejemplos en este tutorial el evento que nos interesa es WM_PAINT, el cual se lanza cada vez que se maximiza o minimiza la ventana, cuando se la redimensiona, o se la vuelve a mostrar luego de estar tapada por otra ventana, en otras palabras, cada vez que se la "repinta".

 

Primer Ejemplo: Mostrar en la ventana una imagen en formato BMP.

Para cargar una imagen en el proyecto, debemos ir a donde dice Archivos de Recursos, hacer click derecho->Agregar->Elemento existente. Se abrirá una ventana desde la que elegiremos una imagen en formato BMP. En este caso he elegido un archivo llamado p1.bmp.


Luego le hacemos click derecho al archivo FirstTry.rc->Ver Código y añadimos la siguiente línea donde se declara el nombre con el que identificaremos a la imagen p1.bmp:

 

Luego vamos a Resources.h y definimos un valor único para el nombre que identifica a la imagen p1.bmp:


Estos pasos se deben repetir para cada imagen que se carga al proyecto. Para este ejemplo he cargado una segunda imagen llamada "IDI_CMC" y cuyo valor en Resources.h es 100.

A continuación vamos a FirstTry.cpp y, en la función WndProc agregamos el siguiente código en el "case" de WM_PAINT:


Este código crea un "handle" a la imagen ya definida en los archivos de recursos. Luego se crea un "Device Context" llamado hdcMem el cual se "enlaza" al handle de la imagen. Los "Device Context" (Dispositivos de Contexto) son áreas de memoria donde podemos dibujar y copiar imágenes. Luego se llama a la api GetObject para obtener las dimensiones de la imagen, éstos valores se guardarán en el objeto BITMAP llamado bm.

La imagen está cargada en algún lugar de la memoria RAM. Para que aparezca en la ventana debemos copiarla. Eso se hace con la api BitBlt cuyos parámetros son: destino (el handle del Device Context de la ventana, y que es el valor devuelto por la api BeginPaint y guardado en la variable hdc), coordenada "x" donde se ubicará la esquina superior izquierda de la imagen copiada, coordenada "y" donde se ubicará la esquina superior izquierda de la imagen copiada, ancho de la imagen, alto de la imagen, handle del Device Conext con la imagen a copiar, coordenada "x" de la esquina superior izquierda de la imagen a copiar, coordenada "y" de la esquina superior izquierda de la imagen a copiar, operación de copiado. Más información de esta api aquí.

Luego de terminar de utilizar los handles, bitmaps y demás objetos, se los debe borrar de la memoria. Al programar directamente con la Api de Windows usamos código no administrado. Es decir: no hay un recolector de basura que libere la memoria. Si una función en código no administrado no libera todos los recursos utilizados tendrá "fugas de memoria" (memory leaks) las cuales se irán acumulando cada vez que se llame a la función. En el peor de los casos terminará agotando la memoria asignada a nuestra aplicación y haciendo que se cuelgue.

Al ejecutar la aplicación se mostrará la imagen en la ventana. Cada vez que ésta se "repinte" ejecutará el código que vuelve a cargar y copiar la imagen. Si no fuera así, la imagen desaparecería si se minimiza y maximiza la ventana, se la redimensiona, etc.

El resultado final es:

 

Segundo Ejemplo: Copiar una imagen de forma que cubra toda la ventana.

Para tener el código de forma más ordenada, vamos a crear un nuevo archivo cpp y llamar sus funciones desde el "case" WM_PAINT en WndProc. Para ello hacemos click derecho en "Archivos de Código Fuente" y elegimos la opción Agregar->Nuevo Elemento:


En la ventana de "Agregar Nuevo Elemento" elegimos la opción Código->Archivo C++:

 

Esto creará un archivo con extensión cpp en blanco. Primero le agregamos las referencias a los archivos de encabezado stafx.h y el que lleva el nombre del proyecto:

Luego, en el archivo de encabezado con el nombre del proyecto, se añaden las declaraciones de las funciones que irán en el nuevo archivo cpp (para este caso lo he llamado Image.cpp). La función que dibujará y copiará una imagen en toda la ventana se llamará ImagenCopiar. En general, las funciones que dibujan en una ventana necesitan 3 parámetros: La instancia actual de la aplicación (para cargar la imagen dentro de un recurso), el handle de la ventana (hWnd) y el handle del Device Context (hdc) de la ventana (donde copiaremos la imagen).

A continuación muestro el código de ImagenCopiar. Buena parte del código simplemente se copió del ejemplo anterior, mas se han añadido la declaración de un objeto RECT y la llamada a la api GetWindowsRect; esto es para poder obtener las dimensiones de la ventana. Las dimensiones de la imagen se guardan en el objeto BITMAP llamado bm. Para este ejemplo he usado la imagen llamada p1.bmp declarada como IDB_BITMAP1.

El código incluye dos bucles anidados, los que recorren el ancho y el alto de la ventana en incrementos iguales al ancho y al alto de la imagen a copiar, la cual es mucho más pequeña que la ventana donde va a mostrarse. Dentro de los bucles se va alternado una llamada a la api BitBlt que copia exactamente la imagen a la ventana, y otra llamada a la misma api que copia la imagen pero con los colores invertidos. El resultado es:

 

Tercer Ejemplo: Manipular los píxeles de una imagen. Uso de las apis GetDIBits y SetDIBits.

Para este ejemplo declaramos una segunda función llamada ImagenManipular, la cual recibe los mismos parámetros que ImagenCopiar, convertirá la imagen p1.bmp a escala de grises y la mostrará en la ventana de la aplicación.

Para poder procesar individualmente cada píxel de p1.bmp, debemos extraerlos, guardarlos temporalmente en un array, realizar las operaciones necesarias con ellos, y devolverlos a una nueva imagen la cual se mostrará en la ventana.

Cada píxel de una imagen está compuesto de 3 ó 4 bytes que corresponden a los colores rojo, verde y azul, más el canal alpha que indica la transparencia, el cual no es usado en todos los formatos de imagen. En este ejemplo sólo se trabajará con los valores de los colores. Para el canal alpha existen otras apis como TransparentBlt ó AlphaBlend (la última es la más recomendable de usar) pero no nos ocuparemos de ellas en este ejemplo.

El código de ImagenManipular es el siguiente:

Usando la api GetDIBits extraemos los píxeles de la imagen, como esta api también necesita de una "cabecera" donde esté la información de la imagen (bits por píxel, dimensiones, compresión, etc), esta información la guardamos en las variables tipo BITMAPINFO, una para la imagen original y otra para la imagen ya convertida a escala de grises.. Las dimensiones de p1.bmp las obtenemos con la api GetObject. Leyendo tutoriales y otros códigos de ejemplo, supe que a las variables BITMAPINFO se les establece la altura de la imagen como un valor negativo ya que su sistema de coordenadas para la altura está invertido.

Las dos imágenes (la original y la que estará en escala de grises) necesitan su propio handle y su propio Device Context.

Para recorrer los píxeles de la imagen uso dos punteros: uno que siempre apuntará al inicio del array (buf2), y otro (buf) que lo recorrerá incrementando su valor. Luego coloco los píxeles en otra imagen mediante la api SetDIBits, la cual requiere también otra variable tipo BITMAPINFO para "construir" la nueva imagen en escala de grises.

SetDIBits crea (o "dibuja") la imagen en escala de grises en un espacio en memoria. Aún falta colocarla en la ventana para poder verla. Esto se hace con una simple copia usando la api BitBlt.

El resultado es:

 

Si llamo a la función ImagenCopiar y luego a ImagenManipular el resultado será:

Esto es porque ImagenManipular copia a p1.bmp encima de lo que ha sido dibujado por ImagenCopiar.

Una imagen cualquiera posee mucha más información que sólo los píxeles que la componen. Todos los archivos digitales poseen una cabecera que le dice al sistema operativo o a una aplicación qué tipo de archivo es. En el caso de imágenes, la cabecera almacena información de los bits por píxel, dimensiones, formato, tipo de compresión etc. E el caso de archivos jpg, también se almacena la Data Exif, que contiene información del dispositivo de captura (como una cámara digital), el software de edición, la fecha de creación, etc.  Las apis GetDIBits y SetDIBits nos permiten obtener sólo los píxeles de una imagen y guardarlos en un array para después procesarlos.

El proyecto con los tres ejemplos se puede descargar de aquí. Se compila con Visual C++ Express 2008.

Un detalle más: el código de estos ejemplos no está optimizado. Lo ideal es que las imágenes procesadas se guarden en un Device Context global y simplemente copiarlo al Device Context de la ventana cada vez que ésta se repinta.