NovaMonoFix
Errores PHP
X
Usuario
Password
0 FPS

Tutorial WINAPI C++ 2.1 (Creación del ObjetoEscena)

05 de Mayo del 2010 por Josep Antoni Bover, 33 visitas, 0 comentarios, 0 votos
Categorías : Windows, Programación, C y C++.
La web está en segundo plano, animación en pausa.
Cargando animación...
Tutorial WINAPI C++ 2.1 (Creación del ObjetoEscena)

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 :

Archivo : ObjetoEscena.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// Tipo para el vector de ventanas
typedef std::vector<ObjetoEscena_VentanaTranslucida *> VectorVentanas;
// Clase que hereda ObjetoVentana y contiene los datos de la escena
class ObjetoEscena : public ObjetoVentana {
public : ///////////////////////////////////////// Miembros publicos
// -Constructor
ObjetoEscena(void);
// -Destructor
~ObjetoEscena(void);
// Función para crear la ventana
HWND 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_ERASEBKGND
virtual LRESULT Evento_BorrarFondo(HDC hDC);
// -Función enlazada al mensaje WM_PAINT
virtual LRESULT Evento_Pintar(HDC hDC, PAINTSTRUCT &PS);
//////////////////// Funciones para la escena
// -Función que pinta la escena
void Escena_Pintar(HDC hDCDestino);
// -Función que agrega una ventana translucida a la escena
void Escena_AgregarVentana(ObjetoEscena_VentanaTranslucida *Ventana);
// -Función que carga la imagen para el fondo de la escena
BOOL 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 translucidas
VectorVentanas Ventanas;
// -Variables que mantendran la imagen de fondo cargada en memoria
HBITMAP 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.

Archivo : ObjetoEscena.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// -Función que pinta la escena con todas sus ventanas translucidas
void 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 pintado
HDC Buffer = CreateCompatibleDC(hDC);
HBITMAP Bmp = CreateCompatibleBitmap(hDC, RC.right, RC.bottom);
HBITMAP Viejo = static_cast<HBITMAP>(SelectObject(Buffer, Bmp));
// Pinto el fondo
HBRUSH BrochaFondo = CreateSolidBrush(ColorFondo);
FillRect(Buffer, &RC, BrochaFondo);
DeleteObject(BrochaFondo);
// Pinto la imagen de fondo
if (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 ventana
BitBlt(hDC, 0, 0, RC.right, RC.bottom, Buffer, 0, 0, SRCCOPY);
// Elimino de memoria los objetos GDI
SelectObject(Buffer, Viejo);
DeleteObject(Bmp);
DeleteDC(Buffer);
// Si miramos la variable tiempo podemos saber cuantos milisegundos se tarda en pintar toda la escena
Tiempo = 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 :

Archivo : ObjetoEscena_VentanaTranslucida.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
// Estructura que se usara para almacenar un pixel de un BMP
struct 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 translucida
void ObjetoEscena_VentanaTranslucida::Pintar(HDC hDCDestino) {
// Si no es visible salimos
if (Visible == FALSE) return;
// Espacio para nuestro buffer
RECT RC = { 0, 0, Espacio.right - Espacio.left, Espacio.bottom - Espacio.top };
// Creo un buffer para el fondo
HDC 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 translucida
HDC 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 translucida
HRGN Region = CreateRoundRectRgn(0, 0, RC.right + 1, RC.bottom + 1, 20, 20);
// Pinto el fondo del hDCDestino en el buffer
BitBlt(BufferFondo, 0, 0, RC.right, RC.bottom, hDCDestino, Espacio.left, Espacio.top, SRCCOPY);
// Pinto fondo de la ventana padre
FillRect(BufferVentana, &RC, static_cast<HBRUSH>(GetStockObject(BLACK_BRUSH)));
// Pinto el fondo de esta ventana
HBRUSH Brocha = CreateSolidBrush(ColorFondo);
FillRgn(BufferVentana, Region, Brocha);
DeleteObject(Brocha);
// Pinto el borde de esta ventana
Brocha = 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 clase
Pintar_AlphaBlend(BufferVentana);
// Pinto la ventana encima del fondo de forma translucida
BLENDFUNCTION 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 BMP
BITMAPINFOHEADER 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+8BitImagen
bi.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 1ms
EstructuraBitmap32Bits *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 buffer
SetDIBits(BufferFondo, BmpFondo, 0, (UINT)RC.bottom, Bmp, (BITMAPINFO *)&bi, DIB_RGB_COLORS);
// Borramos los datos del Bmp de memoria
delete Bmp;
// Llamamos a la funcion Pintar_Terminado para pintar aquellos graficos que no queramos incluir con AlphaBlend
Pintar_Terminado(BufferFondo);
// Pintamos el Bitmap con las puntas transparentes
TransparentBlt( 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 memoria
DeleteObject(Region);
// Elimino buffers de la memoria
SelectObject(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