Esempio: anim_one

Preparazione

Prima di animare qualcosa, dobbiamo preparare una struttura per memorizzare la posizione della palla tra un aggiornamento e l’altro. Questa struttura conterrà la posizione e la dimensione correnti della palla, oltre ai valori delta, cioè di quanto vogliamo spostarla a ogni frame.

Dopo aver dichiarato il tipo della struttura, dichiariamo anche un’istanza globale. Va bene così perché abbiamo una sola palla; se dovessimo animarne parecchie, probabilmente vorreste usare un array o un altro contenitore (come una linked list in C++) per memorizzarle in modo più comodo.

const int BALL_MOVE_DELTA = 2;

typedef struct _BALLINFO 
{
    int width;
    int height;
    int x;
    int y;

    int dx;
    int dy;
}BALLINFO;

BALLINFO g_ballInfo;

Abbiamo anche definito una costante BALL_MOVE_DELTA, che indica di quanto vogliamo spostare la palla a ogni aggiornamento. Memorizziamo i delta anche nella struttura BALLINFO perché vogliamo poter muovere la palla a destra o a sinistra e in alto o in basso in modo indipendente; BALL_MOVE_DELTA è solo un nome comodo da dare al valore, così da poterlo cambiare in seguito se vogliamo.

Ora dobbiamo inizializzare questa struttura dopo aver caricato le bitmap:

 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 palla parte dall’angolo in alto a sinistra e si muove verso destra e verso il basso in base ai membri dx e dy di BALLINFO.

Impostare il Timer

Il modo più semplice per aggiungere un timer a un programma con finestre è SetTimer(). Non è il migliore, e non è consigliato per vere applicazioni multimediali o giochi completi, ma è sufficiente per animazioni semplici come questa. Quando vi serve qualcosa di meglio, date un’occhiata a timeSetEvent() su MSDN: è più preciso.

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);

Qui abbiamo dichiarato un id per il timer, così da potervi fare riferimento in seguito (per fermarlo), e poi abbiamo impostato il timer nell’handler di WM_CREATE della finestra principale. Ogni volta che il timer scade, invia un messaggio WM_TIMER alla finestra, passandoci l’ID in wParam. Dato che abbiamo un solo timer l’ID non ci serve, ma è utile se impostate più timer e dovete distinguerli.

Abbiamo impostato il timer in modo che scada ogni 50 millisecondi, il che corrisponde a circa 20 frame al secondo. Circa, perché come ho detto SetTimer() è un po’ impreciso, ma questo non è codice critico e qualche millisecondo in più o in meno non ci ucciderà.

Animare in WM_TIMER

Ora, quando riceviamo WM_TIMER, vogliamo calcolare la nuova posizione della palla e disegnarla nella posizione aggiornata.

    case WM_TIMER:
    {
        RECT rcClient;
        HDC hdc = GetDC(hwnd);

        GetClientRect(hwnd, &rcClient);

        UpdateBall(&rcClient);
        DrawBall(hdc, &rcClient);

        ReleaseDC(hwnd, hdc);
    }
    break;

Ho messo il codice per aggiornare e disegnare la palla in funzioni separate. È una buona pratica e ci permette di disegnare la palla sia da WM_TIMER che da WM_PAINT senza duplicare codice. Notate che il metodo usato per ottenere l’HDC è diverso nei due casi, quindi è meglio lasciare questo codice negli handler dei messaggi e passare il risultato alla funzione 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;
    }
}

Questa funzione fa solo un po’ di matematica di base: aggiungiamo il valore delta alla posizione x per spostare la palla. Se la palla esce dalla client area, la riportiamo all’interno e invertiamo il valore delta, così che la palla “rimbalzi” sui bordi.

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);
}

Questo è essenzialmente lo stesso codice di disegno degli ultimi esempi, con l’eccezione che posizione e dimensioni della palla vengono lette dalla struttura BALLINFO. C’è però una differenza importante…

Double Buffering

Quando si disegna direttamente sull’HDC della finestra, è del tutto possibile che lo schermo venga aggiornato prima che abbiate finito… per esempio, dopo aver disegnato la maschera e prima di aver disegnato sopra l’immagine a colori, l’utente potrebbe vedere per un attimo lo sfondo nero prima che il programma riesca a ricoprirlo con i colori. Più il computer è lento e più operazioni di disegno fate, più lo sfarfallio sarà evidente, finché il risultato non sembrerà un gran pasticcio.

Tutto ciò è terribilmente fastidioso, e possiamo risolverlo semplicemente eseguendo prima tutto il disegno in memoria e poi copiando il capolavoro completato sullo schermo con un’unica BitBlt(), in modo che lo schermo passi direttamente dalla vecchia immagine alla nuova immagine completa senza che nessuna delle singole operazioni sia visibile.

Per farlo, creiamo in memoria un HBITMAP temporaneo con le stesse dimensioni dell’area dello schermo su cui disegneremo. Ci serve anche un HDC per poter usare BitBlt() sulla bitmap.

    HDC hdcBuffer = CreateCompatibleDC(hdc);
    HBITMAP hbmBuffer = CreateCompatibleBitmap(hdc, prc->right, prc->bottom);
    HBITMAP hbmOldBuffer = SelectObject(hdcBuffer, hbmBuffer);

Ora che abbiamo uno spazio in memoria su cui disegnare, tutte le operazioni di disegno usano hdcBuffer invece di hdc (la finestra) e i risultati vengono memorizzati nella bitmap in memoria finché non abbiamo finito. A quel punto possiamo copiare tutto sulla finestra in un colpo solo.

    BitBlt(hdc, 0, 0, prc->right, prc->bottom, hdcBuffer, 0, 0, SRCCOPY);

Tutto qui; poi, come al solito, liberiamo i nostri HDC e HBITMAP.

Double Buffering più veloce

In questo esempio creo e distruggo a ogni frame la bitmap usata per il double buffering. L’ho fatto principalmente perché volevo poter ridimensionare la finestra, ed è più semplice creare sempre un nuovo buffer che tenere traccia di quando cambia la dimensione della finestra e ridimensionare il buffer. Sarebbe più efficiente creare una bitmap globale per il double buffering e non permettere il ridimensionamento della finestra, oppure ridimensionare la bitmap solo quando la finestra viene ridimensionata, invece di crearla e distruggerla continuamente. Sta a voi implementarlo se volete ottimizzare il disegno per un gioco o qualcosa di simile.

Fermare il Timer

Quando la finestra viene distrutta, è buona norma rilasciare tutte le risorse usate, e in questo caso tra queste c’è anche il timer che abbiamo impostato. Per fermarlo basta chiamare KillTimer() passandogli l’ID usato al momento della creazione.

    KillTimer(hwnd, ID_TIMER);