Ejemplo: anim_one
Una vez que hemos declarado el tipo de la estructura, también declaramos una instancia global de la misma. Esto está bien, debido a que solo tenemos una esfera, si fueramos a animar varias de ellas deberiamos probablemente usar un arreglo o una lista para contenerlas de una forma mas conveniente.
También hemos definido una constante BALL_MOVE_DELTA
la cual indica cuan lejos queremos que la esfera se desplace en cada actualización. La razón para almacenar deltas en la estructura BALLINFO
es que queremos mover la esfera hacia arriba o hacia abajo, hacia izquierda o derecha, de forma independiente, BALL_MOVE_DELTA
es sólo un nombre significativo que nos facilitará la tarea de cambiar el valor en el futuro.
Ahora necesitamos inicializar la estructura después de que cargamos nuestros bitmaps:
BITMAP bm;
GetObject(g_hbmBall, sizeof(bm), &bm); ZeroMemory(&g_ballInfo, sizeof(g_ballInfo)); g_ballInfo.width = bm.bmWidth; g_ballInfo.height = bm.bmHeight; g_ballInfo.dx = BALL_MOVE_DELTA; g_ballInfo.dy = BALL_MOVE_DELTA;
La esfera comienza en la esquina superior izquierda, moviéndose hacia la derecha y bajando de acuerdo a los miembros dx
y dy
de la estructura BALLINFO
.
SetTimer()
, aunque no es la mejor forma y tampoco es la forma recomendada para aplicaciónes multimedia o juegos, sin embargo es lo suficientemente buena para aplicaciones simples como ésta. Cuando necesites algo mejor, échale una mirada a timeSetEvent()
en MSDN, la cual es mas apropiada.
const int ID_TIMER = 1;
ret = SetTimer(hwnd, ID_TIMER, 50, NULL); if(ret == 0) MessageBox(hwnd, "Could not SetTimer()!", "Error", MB_OK | MB_ICONEXCLAMATION);
Aquí hemos declarado un ID para el timer para que podamos referirnos a éste posteriormente (para eliminarlo) y pusimos el timer en el manejador del mensaje WM_CREATE
de nuestra ventana principal. Cada vez que transcurre un determinado tiempo, el timer envía un mensaje WM_TIMER
a la ventana y pasa como parámetro en wParam
su ID. Debido a que sólo tenemos un timer no necesitamos el ID, pero es muy útil si utilizamos mas de uno.
Hemos configurado el timer para que envíe una señal cada 50 milisegundos, lo cual resulta en aproximadamente 20 cuadros por segundo. He dicho aproximadamente debido a que, como dije, SetTimer( )
es un poco impresiso, pero este no es un código crítico donde milisegundos mas milisegundos menos causarán estragos, por lo tanto podemos usarlo.
Ahora, cuando obtenemos el mensaje WM_TIMER
queremos calcular la nueva posición de la esfera y dibujarla en la nueva posición.
case WM_TIMER: { RECT rcClient; HDC hdc = GetDC(hwnd); GetClientRect(hwnd, &rcClient); UpdateBall(&rcClient); DrawBall(hdc, &rcClient); ReleaseDC(hwnd, hdc); } break;
He puesto el código para actualizar y dibujar la esfera en sus respectivas funciones. Esto es una buena práctica y nos permite dibujar la esfera fuera de WM_TIMER
o de WM_PAINT
sin necesidad de duplicar el código. Observa que el método que utilizamos para obtener el HDC en cada caso es diferente , por lo tanto es mejor dejar este código en los manejadores de mensajes y pasar el resultado dentro de la función DrawBall( )
.
void UpdateBall(RECT* prc) { g_ballInfo.x += g_ballInfo.dx; g_ballInfo.y += g_ballInfo.dy; if(g_ballInfo.x < 0) { g_ballInfo.x = 0; g_ballInfo.dx = BALL_MOVE_DELTA; } else if(g_ballInfo.x + g_ballInfo.width > prc->right) { g_ballInfo.x = prc->right - g_ballInfo.width; g_ballInfo.dx = -BALL_MOVE_DELTA; } if(g_ballInfo.y < 0) { g_ballInfo.y = 0; g_ballInfo.dy = BALL_MOVE_DELTA; } else if(g_ballInfo.y + g_ballInfo.height > prc->bottom) { g_ballInfo.y = prc->bottom - g_ballInfo.height; g_ballInfo.dy = -BALL_MOVE_DELTA; } }
Todo lo que esto hace es básicamente matemático, sumamos el valor delta a la posición x para mover la esfera. Si el valor pasa afuera del área cliente lo ponemos nuevamente en el rango y cambiamos el valor delta a la dirección opuesta para que la esfera "rebote" en los bordes.
void DrawBall(HDC hdc, RECT* prc) { HDC hdcBuffer = CreateCompatibleDC(hdc); HBITMAP hbmBuffer = CreateCompatibleBitmap(hdc, prc->right, prc->bottom); HBITMAP hbmOldBuffer = SelectObject(hdcBuffer, hbmBuffer); HDC hdcMem = CreateCompatibleDC(hdc); HBITMAP hbmOld = SelectObject(hdcMem, g_hbmMask); FillRect(hdcBuffer, prc, GetStockObject(WHITE_BRUSH)); BitBlt(hdcBuffer, g_ballInfo.x, g_ballInfo.y, g_ballInfo.width, g_ballInfo.height, hdcMem, 0, 0, SRCAND); SelectObject(hdcMem, g_hbmBall); BitBlt(hdcBuffer, g_ballInfo.x, g_ballInfo.y, g_ballInfo.width, g_ballInfo.height, hdcMem, 0, 0, SRCPAINT); BitBlt(hdc, 0, 0, prc->right, prc->bottom, hdcBuffer, 0, 0, SRCCOPY); SelectObject(hdcMem, hbmOld); DeleteDC(hdcMem); SelectObject(hdcBuffer, hbmOldBuffer); DeleteDC(hdcBuffer); DeleteObject(hbmBuffer); }
Este es escencialmente el mismo código de dibujo de los ejemplos anteriores, con la excepción de que obtiene la posición y las dimensiones de la esfera a partir de la estructura BALLINFO
. Sin embargo hay una diferencia importante...
HDC
de la ventana, es totalmente posible que la pantalla se actualice antes de que lo hagamos... por ejemplo, después de que dibujemos la máscara y antes que dibujemos la imagen original encima el usuario puede ver un parpadeo del fondo antes de que nuestro programa tenga una chance de de dibujar sobre éste la imagen en color. Mientras mas lerda sea la computadora y mientras mas operaciones de dibujo realicemos, aparecerán mas parpadeos.
Esto es terriblemente drástico y podemos resolverlo simplemente haciendo primero todos los dibujos en memoria y luego copiar a la pantalla la obra maestra completa en un solo BitBlt()
para que la pantalla sea actualizada directamente de dicha imagen, sin que sean visibles ninguna de las operaciones intermedias.
Para hacer esto creamos en memoria un HBITMAP
temporal que es del tamaño exacto del área que vamos a dibujar sobre la pantalla. También necesitamos un HDC para que podamos usar BitBlt()
sobre el bitmap.
HDC hdcBuffer = CreateCompatibleDC(hdc); HBITMAP hbmBuffer = CreateCompatibleBitmap(hdc, prc->right, prc->bottom); HBITMAP hbmOldBuffer = SelectObject(hdcBuffer, hbmBuffer);
Ahora que tenemos un lugar en memoria donde dibujar, todas las operaciones de dibujo usan hdcBuffer
en vez de hdc
(la ventana) y los resultados son almacenados sobre el bitmap en memoria hasta que hallamos terminado. Luego podemos copiar dicha imagen a la ventana en un solo "disparo".
BitBlt(hdc, 0, 0, prc->right, prc->bottom, hdcBuffer, 0, 0, SRCCOPY);Por último podemos eliminar nuestro
HDC
y HBITMAP
de manera usual.
Es una buena idea liberar todos los recursos cuando la ventana es destruida y en este caso también se incluye el timer que hemos usado. Para detenerlo, simplemente llamamos a KillTimer( )
y le pasamos el ID que hemos usado para crearlo.
KillTimer(hwnd, ID_TIMER);
Versión en español: Federico Pizarro - 2003