[ contenidos | #winprog]

Bitmaps, Dispositivos de Contexto y BitBlt

Ejemplo: bmp_one

[images/bmp_one.gif]

GDI

Una cosa buena que tiene MS Windows, a diferencia de DOS, es que para mostrar gráficos no necesitamos conocer nada acerca del hardware de video que estamos usando. Por el contrario, Windows provee una API llamada Graphics Device Interface (Interface del Dispositivo Gráfico) o GDI, en forma abreviada. LA GDI usa un conjunto de objetos gráficos genéricos que pueden ser usados para dbujar en la pantalla, en la memoria, o en la impresora.

Dispositivo de Contextos

La GDI gira entorno a un objeto llamado Dispositivo de Contexto (DC), representado por el tipo de dato HDC (Handle al Dispositivo de Contexto). Un HDC es básicamente un handle a algo en lo que podemos dibujar, puede representar la pantalla entera, una ventana completa, el área cliente de una ventana, un bitmap almacenado en memoria o puede ser una impresora. La parte buena es que no necesitamos saber a cuál de todas estas cosas hace referencia, se usa básicamente de la misma manera, lo cual es muy útil para escribir funciones de dibujo que podemos usar en cualquiera de estos dispositivos sin necesidad de cambiarla para cada uno de estos.

Un HDC, al igual que la mayoría de los objetos GDI, es opaco. Esto significa que no podemos acceder a sus datos de forma directa... pero podemos pasárselo a varias funciones GDI que operarán en sus datos, ya sea para dibujar algo, para obtener información a cerca de éste, o para cambiar de alguna manera el objeto.

Por ejemplo, si queremos dibujar en una ventana, primero debemos obtener un HDC que represente la ventana, para esto utilizamos GetDC( ). Luego podemos usar cualquiera de las funciones GDI que toman un HDC, como por ejemplo BitBlt( ) para dibujar imágenes, TextOut( ) para dibujar texto, LineTo( ) para lineas, etc...

Bitmaps

Los bitmaps puden ser cargados en su mayoría como los íconos de los primeros ejemplos. Hay una función LoadBitmap( ) para simplemente cargar un recurso bitmap y una función LoadImage( ) para cargar bitmaps desde cualquier archivo *.bmp.

Una de las sutilezas de la GDI es que no podemos dibujar directamente sobre objetos bitmap (HBITMAP). Recuerda que las operacioens de dibujo son abstractas y dicha abstracción la realiza el Dispositivo de Contextos, por lo tanto para usar cualquiera de estas funciones de dibujo sobre un bitmap necesitamos crear un Memory DC y luego seleccionar el HBITMAP dentro de éste usando SelectObject( ). El efecto es que el "dispositvo" al cual el HDC hace referencia es el bitmap en memoria y cuando operamos sobre el HDC los resutados de las funcioens gráficas son aplicados al bitmap. Como vimos, esto es una forma muy conveniente de hacer las cosas porque podemos escribir código que dibuja sobre un HDC y luego usarlo sobre un Window DC o sobre un Memory DC sin tener que realizar cambios o chequeos.

Tenemos la opción de manipular nosotros mismos los datos del bitmap en memoria. Podemos hacer esto con el Dispositivo de Bitmaps Independientes (DIB) y podemos aún combinar operaciones GDI y operaciones manuales en el DIB. Sin embargo, por el momento, esto está mas allá del alcance de este tutorial básico y por ahora sólo cubriremos las operaciones GDI mas simples.

Liberación de Objetos GDI

Una vez que hemos finalizado de usar un HDC es muy importante liberarlo (cómo lo hacemos depende de cómo lo obtuvimos, hablaremos de esto dentro de un momento). Los objetos GDI están limitados en número. En versiones de Windows anteriores a Windows 95, no sólo estaban increíblemente limitados sinó que también compartían ampliamente los recursos del sistema, por lo tanto, si un programa usaba demasiados, ninguno de los demás programas podía ser capaz de dibujar nada! Afortunadamente, en Windows 2000 o XP, esto ya no sucede más y podemos obtener una gran cantidad de recursos sin que suceda algo malo... pero aún así es fácil olvidarse de liberar objetos GDI y rápidamente podemos obtener un agotamiento de los recursos en sistemas como Windows 9x. Teóricamente no deberíamos poder agotar los recursos GDI en sistemas basados en tecnología NT (NT/2K/XP) pero puede llegar a suceder en casos extremos, o si acertamos en el bug apropiado...

Si nuestro programa se ejecuta bien algunos minutos y luego comienza a dibujar extrañamente o de forma incompleta, es una buena señal de que estás reteniendo objetos GDI. Los HDCs no son los únicos objetos con los que necesitamos ser cuidadosos con respecto a su liberación, en general es bueno mantener cosas como bitmaps y fuentes hasta el final de la ejecución del programa, debido a que es mucho más eficiente que recargarlos cada vez que los necesitamos.

Actualización Importante: Windows tiene asignados (retenidos) ciertos objetos por default en los HDC, pero hay unos pocos objetos que nunca utiliza para esto y puedes buscar cuáles son en MSDN. Debido a esto, tenía la incertidumbre si HBITMAP era uno de ellos, ya que parece no haber una documentación definitiva sobre éste y los ejemplos (aún los de Microsoft) suelen ignorar el bitmap por default. De la escritura del tutorial original hace varios años, me han confirmado que hay un bitmap que necesita ser liberado. Esta información es cortesía de Shaun Ivory, un ingeniero de software de MS y un amigo mio de #winprog.

Aparentemente hay un bug en un protector de pantalla escrito por MS y esto se debe a que el bitmap por default no fué reemplazado o destruído y eventualmente provoca un desborde de los recursos GDI. Debes estar atentos!, es un error fácil de cometer.

Como Mostrar Bitmaps

Ok, volvamos al tema. Las operaciones más simple de dibujo sobre una ventana se realizan procesando el mensaje WM_PAINT. Cuando nuestra ventana es mostrada por primera vez, o es restaurada o minimizada, o es descubierta luego de haber estado tapada por otra ventana, Windows le envía a la ventana el mensaje WM_PAINT para hacerle saber que necesita redibujar su contenido. Cuando dibujamos algo sobre la pantalla NO ES permanente, simplemente permanece hasta que algo se dibuja encima y luego, cuando se destapa, sólo es necesario redibujarlo.
HBITMAP g_hbmBall = NULL;
    case WM_CREATE:
        g_hbmBall = LoadBitmap(GetModuleHandle(NULL), MAKEINTRESOURCE(IDB_BALL));
        if(g_hbmBall == NULL)
            MessageBox(hwnd, "Could not load IDB_BALL!", "Error", MB_OK | MB_ICONEXCLAMATION);
    break;
El primer paso, por supuesto, es cargar el bitmap. Esto es simple cuando tenemos un recurso bitmap ya que no hay diferencias significantes de cuando cargamos otro tipo de recursos.
    case WM_PAINT:
    {
        BITMAP bm;
        PAINTSTRUCT ps;

        HDC hdc = BeginPaint(hwnd, &ps);

        HDC hdcMem = CreateCompatibleDC(hdc);
        HBITMAP hbmOld = SelectObject(hdcMem, g_hbmBall);

        GetObject(g_hbmBall, sizeof(bm), &bm);

        BitBlt(hdc, 0, 0, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY);

        SelectObject(hdcMem, hbmOld);
        DeleteDC(hdcMem);

        EndPaint(hwnd, &ps);
    }
    break;

Obtener el Window DC

Para comenzar, declaramos algunas variables. Observa que la primera es un BITMAP y no un HBITMAP. BITMAP es una estructura que almacena información acerca de un HBITMAP el cual es el verdadero objeto GDI. Necesitamos una forma de obtener el alto y ancho del HBITMAP, para esto usamos GetObject( ), el cual, contrariamente a su nombre, no obtiene un objeto pero si obtiene información acerca de uno ya existente. "GetObjectInfo" podría haber sido un nombre más apropiado. GetObject( ) funciona con varios tipos de objetos GDI, que puede distinguir usando el segundo parámetro, el tamaño de la estructura.

PAINTSTRUCT es una estructura que contiene información acerca de la ventana que esta siendo pintada, que es la que continúa con el mensaje paint. Para las tareas más simples, podemos ignorar la información que esta estructura contiene, pero ésta es necesaria en la llamada a BeginPaint( ). BeginPaint( ) está diseñada para procesar especifícamente el mensaje WM_PAINT. Cuando no procesamos el mensaje WM_PAINT podemos usar GetDC( ), el cual veremos en los ejemplos de la animación dentro de un momento... pero en el mensaje WM_PAINT es importante usar BeginPaint( ) y EndPaint( ).

BeginPaint( ) retorna un HDC que representa el HWND que le pasamos, el de la ventana que está procesando el WM_PAINT. Cualquier operación de dibujo que realizemos sobre este HDC será inmediatamente mostrada en la pantalla.

Preparando un Memory DC para el Bitmap

Como mencioné anteriormente, para poder dibujar sobre bitmaps o con bitmaps, necesitamos crear un DC en memoria... la forma más fácil de hacerlo aquí es usando CreateCompatibleDC( ) con el que ya tenemos. Con esto obtenemos un Memory DC que es compatible con el color y las propiedades de dibujo del HDC de la ventana.

Ahora llamamos a SelectObject( ) para seleccionar el bitmap dentro del DC, siendo cuidadosos de almacenar el bitmap por default para que podamos reemplazarlo posteriormente y no retener objetos GDI.

Dibujo

Una vez que hemos llenado los valores ancho y alto dentro de la estructura BITMAP podemos llamar a BitBlt( ) para copiar la imagen desde nuestro Memory DC hasta el Window DC y luego mostrarlo en pantalla. Como siempre puedes buscar cada parámetro en MSDN, pero acá están resumidos: El destino, la posición, el tamaño, el origen y la posición origen y finalmente el Raster Operation (código ROP), el cual especifica como hacer la copia. En ese caso queremos una copia exacta del recurso, nada más.

BitBlt probablemente sea la función mas feliz de toda la API Win32 y es la dieta pricipal de todos aquellos que están aprendiendo a escribir juegos o alguna otra aplicación gráfica en Windows. Esta fué probablemente la primer API de la cual he memorizado todos los parámetros.

Limpieza

Hasta este punto el bitmap debería estar en pantalla y es necesario que lo borremos nosotros mismos. Lo primero que hay que hacer es restaurar el Memory DC al estado en el que estaba cuando lo obtuvimos, para esto, reemplazamos nuestro bitmap con el bitmap por default que habíamos guardado. Luego podemos borrarlo por completo usando DeleteDC( ).

Finalmente liberamos el Window DC que obtuvimos con BeginPaint( ) usando EndPaint( ).

Destruir un HDC a veces es un poco confuso debido a que hay al menos 3 formas de hacerlo que dependen de como lo obtuvimos. Aquí hay una lista de los métodos más comunes para obtener un HDC y luego liberarlo.

Por último, al final de nuestro programa, liberamos todos los recursos que tenemos asignados. Técnicamente hablando, esto no es absolutamente necesario debido a que las plataformas modernas de Windows son muy buenas liberando todos los recursos cuando nuestro programa termina, aunque siempre es conveniente hacerlo uno mismo ya que hay viejas versiones de Windows que no hacen esto de manera efectiva.

    case WM_DESTROY:
        DeleteObject(g_hbmBall);
        PostQuitMessage(0);
    break;

Copyright © 1998-2003, Brook Miles (theForger). All rights reserved.

Versión en Español: Federico Pizarro - 2003