Este es otro tutorial viejito migrado desde la web Electrónica a Martillazos :D
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.
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.