Tutorial WINAPI C++ 2.1 (Creación del ObjetoEscena)
Categorías : Windows, Programación, C y C++.

Esta parte del tutorial consistira en crear un objeto que se encargara de gestionar la escena. La escena seran todos aquellos graficos que se tengan que pintar en la venana, y en este caso consistira en un fondo y varias 'ventanas' translucidas.
Necesitaremos crear cuatro modelos de ventanas translucidas por lo que sera una buena idea hacer un modelo base del cual poder derivar nuestras futuras tres ventanas translucidas, que seran : Marcador, Tablero, Mensaje y Records.
Bueno para empezar todos conoceréis el típico juego de la serpiente que va comiendo fichas, y se hace mas grande, en el que la única dificultad reside en no chocar con ella misma o con los muros.
El juego en si no es muy complicado de programar, pero lo que se pretende es tener en primera instancia una base para un entorno grafico que nos pueda servir tanto para este juego, como para otro tipo de aplicaciones.
Lo primero será definir que queremos que haga esta interfaz grafica, y como estructurarla de forma que luego resulte simple añadir nuevas funciones a esta.
Lo que se busca es : Una ventana padre que pueda contener un fondo, y que simule dentro de ella unas ventanas translucidas que se usaran en este caso para mostrar el juego, el marcador, mensajes como la pausa, y los records.
Para cumplir estos objetivos se diseñaran 2 clases básicas : ObjetoEscena y ObjetoEscena_VentanaTranslucida.
El ObjetoEscena deberá ser capaz de mostrar una o mas ventanas translucidas, y el ObjetoEscena_VentanaTranslucida se encargara de pintar los gráficos que necesitemos para dicha ventana. Por ello necesitaremos crear estas dos clases paralelamente ya que no podrán funcionar una sin la otra.
Veamos la declaración de ObjetoEscena :
// Tipo para el vector de ventanastypedef std::vector<ObjetoEscena_VentanaTranslucida *> VectorVentanas;// Clase que hereda ObjetoVentana y contiene los datos de la escenaclass ObjetoEscena : public ObjetoVentana {public : ///////////////////////////////////////// Miembros publicos// -ConstructorObjetoEscena(void);// -Destructor~ObjetoEscena(void);// Función para crear la ventanaHWND CrearEscena( HWND hWndParent, UINT Estilos, const TCHAR *nTitulo,const int cX, const int cY,const int cAncho, const int cAlto,HMENU nMenu = NULL, DWORD nEstiloExtendido = NULL,const int nIconoRecursos = 32512 );//////////////////// Funciones para obtener los eventos// -Función enlazada al mensaje WM_ERASEBKGNDvirtual LRESULT Evento_BorrarFondo(HDC hDC);// -Función enlazada al mensaje WM_PAINTvirtual LRESULT Evento_Pintar(HDC hDC, PAINTSTRUCT &PS);//////////////////// Funciones para la escena// -Función que pinta la escenavoid Escena_Pintar(HDC hDCDestino);// -Función que agrega una ventana translucida a la escenavoid Escena_AgregarVentana(ObjetoEscena_VentanaTranslucida *Ventana);// -Función que carga la imagen para el fondo de la escenaBOOL Escena_ImagenFondo(const UINT BmpID);// -Color para el fondo (el mismo que el fondo de la imagen)COLORREF ColorFondo;protected : ///////// Miembros protegidos// -Vector que contiene nuestras ventanas translucidasVectorVentanas Ventanas;// -Variables que mantendran la imagen de fondo cargada en memoriaHBITMAP BmpFondo;HBITMAP ViejoFondo;HDC BufferFondo;};
Esta clase en esencia tiene una función para crear la ventana, 2 funciones / eventos que re-emplazaran los eventos de pintado, y varias funciones para interactuar con la escena. Además también contiene un vector de ObjetoEscena_VentanaTranslucida en el cual se almacenaran todas las ventanas translucidas que necesitemos crear.
La idea es que esta clase pinte un fondo partiendo de una imagen BMP, y luego pinte encima cada ventana translucida de forma ordenada. Para entenderlo mejor pintaremos todos los objetos que contiene nuestra escena, empezando por el último objeto visible hasta el primero.
// -Función que pinta la escena con todas sus ventanas translucidasvoid ObjetoEscena::Escena_Pintar(HDC hDCDestino) {DWORD Tiempo = GetTickCount();HDC hDC;if (hDCDestino == NULL) hDC = GetDC(_hWnd);else hDC = hDCDestino;RECT RC;GetClientRect(_hWnd, &RC);// Creo un buffer de pintadoHDC Buffer = CreateCompatibleDC(hDC);HBITMAP Bmp = CreateCompatibleBitmap(hDC, RC.right, RC.bottom);HBITMAP Viejo = static_cast<HBITMAP>(SelectObject(Buffer, Bmp));// Pinto el fondoHBRUSH BrochaFondo = CreateSolidBrush(ColorFondo);FillRect(Buffer, &RC, BrochaFondo);DeleteObject(BrochaFondo);// Pinto la imagen de fondoif (BufferFondo != NULL) BitBlt(Buffer, 0, 0, RC.right, RC.bottom, BufferFondo, 0, 0, SRCCOPY);// Pinto cada una de las ventanas translucidas (NOTA NO TOCAR EL INT PARA HACERLO UNSIGNED)for (int i = static_cast<int>(Ventanas.size()) -1; i > -1; i--) Ventanas[i]->Pintar(Buffer);// Pinto el buffer en la ventanaBitBlt(hDC, 0, 0, RC.right, RC.bottom, Buffer, 0, 0, SRCCOPY);// Elimino de memoria los objetos GDISelectObject(Buffer, Viejo);DeleteObject(Bmp);DeleteDC(Buffer);// Si miramos la variable tiempo podemos saber cuantos milisegundos se tarda en pintar toda la escenaTiempo = GetTickCount() - Tiempo;}
Como se ve en el código anterior, nos creamos un buffer para pintar los gráficos, pintamos el fondo, y luego llamamos a la función pintar de cada ventana translucida.
Hasta aquí parece fácil la cosa, ahora veamos la función pintar del ObjetoEscena_VentanaTranslucida :
// Estructura que se usara para almacenar un pixel de un BMPstruct EstructuraBitmap32Bits {char B;char G;char R;char Valor;};// Definicion que podemos modificar si necesitamos mas de 10 pixeles cuadrados para el escaneo de las puntas#define ESPACIO_REDONDEADO 10// Función para pintar la ventana translucidavoid ObjetoEscena_VentanaTranslucida::Pintar(HDC hDCDestino) {// Si no es visible salimosif (Visible == FALSE) return;// Espacio para nuestro bufferRECT RC = { 0, 0, Espacio.right - Espacio.left, Espacio.bottom - Espacio.top };// Creo un buffer para el fondoHDC BufferFondo = CreateCompatibleDC(hDCDestino);HBITMAP BmpFondo = CreateCompatibleBitmap(hDCDestino, RC.right, RC.bottom);HBITMAP ViejoFondo = static_cast<HBITMAP>(SelectObject(BufferFondo, BmpFondo));SetBkColor(BufferFondo, EscenaPadre->ColorFondo);// Creo un buffer para la ventana translucidaHDC BufferVentana = CreateCompatibleDC(hDCDestino);HBITMAP BmpVentana = CreateCompatibleBitmap(hDCDestino, RC.right, RC.bottom);HBITMAP ViejoVentana = static_cast<HBITMAP>(SelectObject(BufferVentana, BmpVentana));SetBkColor(BufferVentana, EscenaPadre->ColorFondo);// Creo una region para usarla en la ventana translucidaHRGN Region = CreateRoundRectRgn(0, 0, RC.right + 1, RC.bottom + 1, 20, 20);// Pinto el fondo del hDCDestino en el bufferBitBlt(BufferFondo, 0, 0, RC.right, RC.bottom, hDCDestino, Espacio.left, Espacio.top, SRCCOPY);// Pinto fondo de la ventana padreFillRect(BufferVentana, &RC, static_cast<HBRUSH>(GetStockObject(BLACK_BRUSH)));// Pinto el fondo de esta ventanaHBRUSH Brocha = CreateSolidBrush(ColorFondo);FillRgn(BufferVentana, Region, Brocha);DeleteObject(Brocha);// Pinto el borde de esta ventanaBrocha = CreateSolidBrush(ColorBordeI);FrameRgn(BufferVentana, Region, Brocha, 2, 2);DeleteObject(Brocha);Brocha = CreateSolidBrush(ColorBordeE);FrameRgn(BufferVentana, Region, Brocha, 1, 1);DeleteObject(Brocha);// Pintamos los graficos extras de las ventanas que hereden de esta clasePintar_AlphaBlend(BufferVentana);// Pinto la ventana encima del fondo de forma translucidaBLENDFUNCTION BF;BF.AlphaFormat = 0;BF.BlendOp = AC_SRC_OVER;BF.BlendFlags = NULL;BF.SourceConstantAlpha = 200;BOOL A = AlphaBlend(BufferFondo, 0, 0, RC.right, RC.bottom, BufferVentana, 0, 0, RC.right, RC.bottom, BF);// Rellenamos la estructura BITMAPINFOHEADER para poder obtener los datos del BMPBITMAPINFOHEADER bi;bi.biSize = sizeof(BITMAPINFOHEADER);bi.biWidth = RC.right;bi.biHeight = RC.bottom;bi.biPlanes = 1;bi.biBitCount = 32; // Debe ser 32 para que la alineacion sea 8B+8G+8R+8BitImagenbi.biCompression = BI_RGB;bi.biSizeImage = (RC.right * RC.bottom) * 4;bi.biXPelsPerMeter = 0;bi.biYPelsPerMeter = 0;bi.biClrUsed = 0;bi.biClrImportant = 0;LONG nx, ny, nxy;nxy = 0;// El siguiente código se ha optimizado para obtener la maxima velocidad// Inicialmente se escaneaban todos los pixels y se miraba si estaban dentro de la region con PtInRgn.// Pero de esa forma si la region tiene 1000*1000 pixeles estamos haciendo un millon de iteraciones cuando con unas 400 bastaria// Para solucionar esto escanearemos 10 pixeles cuadrados por cada punta del rectangulo.// La efectividad de esta optimización se nota en la función Pintar_Escena la cual tardaba 32ms, y ahora tarda de 0 a 1msEstructuraBitmap32Bits *Bmp = new EstructuraBitmap32Bits[RC.right * RC.bottom];GetDIBits(BufferFondo, BmpFondo, 0, (UINT)RC.bottom, Bmp, (BITMAPINFO *)&bi, DIB_RGB_COLORS);// Punta 7 (del teclado numerico)for (ny = 0; ny < ESPACIO_REDONDEADO; ny ++) {for (nx = 0; nx < ESPACIO_REDONDEADO; nx ++) {if (PtInRegion(Region, nx, ny) == FALSE) {nxy = (ny * RC.right) + nx;Bmp[nxy].R = COLOR_TRANSPARENTE_R;Bmp[nxy].G = COLOR_TRANSPARENTE_G;Bmp[nxy].B = COLOR_TRANSPARENTE_B;}}}// Punta 9 (del teclado numerico)for (ny = 0; ny < ESPACIO_REDONDEADO; ny ++) {for (nx = RC.right - ESPACIO_REDONDEADO; nx < RC.right; nx ++) {if (PtInRegion(Region, nx, ny) == FALSE) {nxy = (ny * RC.right) + nx;Bmp[nxy].R = COLOR_TRANSPARENTE_R;Bmp[nxy].G = COLOR_TRANSPARENTE_G;Bmp[nxy].B = COLOR_TRANSPARENTE_B;}}}// Punta 1 (del teclado numerico)for (ny = RC.bottom - ESPACIO_REDONDEADO; ny < RC.bottom; ny ++) {for (nx = 0; nx < ESPACIO_REDONDEADO; nx ++) {if (PtInRegion(Region, nx, ny) == FALSE) {nxy = (ny * RC.right) + nx;Bmp[nxy].R = COLOR_TRANSPARENTE_R;Bmp[nxy].G = COLOR_TRANSPARENTE_G;Bmp[nxy].B = COLOR_TRANSPARENTE_B;}}}// Punta 3 (del teclado numerico)for (ny = RC.bottom - ESPACIO_REDONDEADO; ny < RC.bottom; ny ++) {for (nx = RC.right - ESPACIO_REDONDEADO; nx < RC.right; nx ++) {if (PtInRegion(Region, nx, ny) == FALSE) {nxy = (ny * RC.right) + nx;Bmp[nxy].R = COLOR_TRANSPARENTE_R;Bmp[nxy].G = COLOR_TRANSPARENTE_G;Bmp[nxy].B = COLOR_TRANSPARENTE_B;}}}// Asignamos el nuevo BMP al HDC que hace de bufferSetDIBits(BufferFondo, BmpFondo, 0, (UINT)RC.bottom, Bmp, (BITMAPINFO *)&bi, DIB_RGB_COLORS);// Borramos los datos del Bmp de memoriadelete Bmp;// Llamamos a la funcion Pintar_Terminado para pintar aquellos graficos que no queramos incluir con AlphaBlendPintar_Terminado(BufferFondo);// Pintamos el Bitmap con las puntas transparentesTransparentBlt( hDCDestino, Espacio.left, Espacio.top, Espacio.right - Espacio.left, Espacio.bottom - Espacio.top,BufferFondo, 0, 0, RC.right, RC.bottom, RGB(COLOR_TRANSPARENTE_R, COLOR_TRANSPARENTE_G, COLOR_TRANSPARENTE_B) );// Elimino la region de la memoriaDeleteObject(Region);// Elimino buffers de la memoriaSelectObject(BufferFondo, ViejoFondo);DeleteObject(BmpFondo);DeleteDC(BufferFondo);SelectObject(BufferVentana, ViejoVentana);DeleteObject(BmpVentana);DeleteDC(BufferVentana);}
Para empezar se han creado 2 buffers en memoria para pintar gráficos, en el primero pintaremos la porción del fondo de la escena que corresponde con la ubicación de la ventana translucida, y en el segundo buffer pintaremos una recta redondeada que nos servirá de ventana. Una vez tenemos estos dos elementos preparados empezamos a fusionar los gráficos, para ello se utilizara la API AlphaBlend. Esta API lo que hace es pintar un DC translucido encima de otro, de forma que veamos el fondo algo borroso, y nuestra ventana por encima.

Una vez hecho esto se nos presenta un gran problema, ya que Windows pinta prácticamente todo a base de rectángulos, pero nosotros no queremos que nuestra ventana translucida sea rectangular.
La mejor solución que se me ocurrió fue utilizar la API TransparentBlt en vez de usar la API BitBlt. La API TransparentBlt en esencia actúa como BitBlt, pero le podemos indicar un color como de la imagen para que lo trate como si fuera totalmente transparente.
Pero con esto no solucionamos el problema aun. Al usar la API AlphaBlend los colores de las imágenes originales no tienen porque seguir siendo los mismos, por ejemplo queremos que el color transparente sea el negro, y tenemos una imagen con fondo negro, pero le sobreponemos otra imagen con AlphaBlend que tenia el fondo gris, el resultado de nuestro fondo sera un gris mas oscuro que no será negro.
Lo que habrá que hacer es editar los pixeles que queremos que sean transparentes para asignarles el color que le indicaremos a la API TransparentBlt para usar como transparente. En este caso usaremos una tonalidad de color negro RGB(10,10,10) como en la foto de la derecha para lo que sobre de los bordes.

Como podemos reconocer los pixeles que queremos que sean transparentes?
En primera instancia se me ocurrió cambiar todos los pixeles que tuvieran el mismo color que el pixel x-0 y-0 de nuestra ventana translucida, pero esta solución no nos sirve si queremos sobreponer una ventana sobre otra, o si tenemos un fondo que no tiene porque ser de un solo color (una imagen por ejemplo). Así que ya podemos descartar esta alternativa.
Lo siguiente que pensé fue en comparar con la API PtInRegion cada pixel para ver si ese pixel estaba dentro o fuera de la región, y en caso de estar fuera cambiaríamos el pixel al color que elegimos como transparente.
Esta solución me dio los resultados que buscaba en cuanto a mostrar bien los gráficos, pero resultaba ser lenta si se escaneaban todos los pixeles de la imagen. Por ello tuve que optimizar esta solución de forma que solo se miren 10 pixeles cuadrados de cada esquina de la ventana.
En la foto de la izquierda podeis ver como la parte negra exterior de los bordes desaparece para convertirse en transparente. Si os fijais el los dos circulos rojos superiores vereis que aunque el fondo no sea de un unico color los bordes quedan perfectamente.
El resultado es que se necesita mas código, pero el ordenador sufre mucho menos. Si por ejemplo la ventana es de 300*300 el código inicial necesitaría hacer unas 90000 veces las comprobaciones, una por cada pixel. Pero como sabemos que los únicos pixeles que queremos comprobar son los que están en las esquinas de las ventanas, si hacemos 4 bucles (uno para cada esquina) que miren 10 pixeles cuadrados será mas que suficiente (lo que serian unas 400 comprobaciones en vez de 90000).
Por último para acceder a la lista de pixeles que contiene un BMP debemos usar la API GetDIBits, dicha api es un poco complicada de utilizar la primera vez, ya que nos pide un buffer para almacenar los bits del BMP en forma de void. Esto lo han hecho así porque los mapas de bits no tienen una longitud fija, y sus atributos pueden contener valores de 4, 8, 16, 24 y 32 bits. En este ejemplo forzamos a la API a que nos retorne el mapa de bits en formato de 32bits, que viene a ser una estructura así : 8bits canal B (azul), 8bits canal V (verde), 8bits canal R (rojo) y 8bits para datos extra EN ESTE ORDEN.
Una vez retocados los pixeles de la imagen, asignamos nuestro nuevo array de pixeles con la API SetDIBits al HBITMAP que queremos pintar, y ya podemos utilizar la API TransparentBlt para terminar de pintar la ventana translucida.
Por ultimo he creado dos funciones virtuales dentro de ObjetoEscena_VentanaTranslucida, que son : Pintar_AlphaBlend y Pintar_Terminado. La primera se usara para pintar graficos adicionales antes de pasar la función AlphaBlend, y la segunda función se usara para pintar aquellos graficos que queramos pintar opacos sin añadirlos al AlphaBlend.
Con todo esto hecho ya podemos empezar a crear nuestros objetos ObjetoEscena_VentanaTranslucida, de forma que podamos crear varios modelos distintos dependiendo de lo que necesitemos.
En el ejemplo 2.1 podemos ver la ventana ObjetoEscena que tiene como imagen de fondo el logo de www.devildrey33.es y que luego pinta 2 ObjetoEscena_VentanaTranslucida solapadas por encima del logo.
En la siguiente parte del tutorial veremos como crear un tablero y un marcador para el juego : 2.2 - Creación del tablero, el marcador, y el mensaje.
Descargar tutorial WinAPI completo | Snake compilada |