Esempio: bmp_one

GDI

La cosa veramente meravigliosa di MS Windows è che diversamente da DOS, non è necessaria nessuna conoscenza sull’hardware video usato per visualizzare la grafica. Windows infatti fornisce delle API chiamate Graphic Device Interface, oppure GDI. Le GDI forniscono una serie di oggetti grafici generici che possono essere usati per disegnare sullo schermo, nella memoria, o addirittura sulle stampanti.

Device Contexts

Le GDI girano intorno ad un oggetto chiamato Device Context (DC), rappresentato da un datatype HDC (Handle to Device Context). Un HDC è sostanzialmente un handle di qualcosa su cui è possibile disegnare; può rappresentare lo schermo intero, una finestra intera, la client area di una finestra, una bitmap presente in memoria, o una stampante. La parte più bella di tutto ciò è che non abbiamo bisogno di conoscere dove stiamo disegnando, possiamo utilizzare i Device Contexts allo stesso modo per tutte le operazioni su tutte le periferiche. Egrave; semplicemente utilissimo.

Un HDC come molti oggetti GDI è opaco, vuol dire che non è possibile accedere ai dati direttamente… ma è possibile passarlo alle varie funzioni GDI che opereranno su di esso, sia per disegnarci qualcosa oppure per ricavare informazioni, o anche per effettuare delle modifiche all’oggetto.

Per esempio, se si vuole disegnare su una finestra, prima bisogna ricavarsi un HDC che rappresenta la finestra con GetDC(), dopodiché è possibile usare una qualsiasi funzione GDI che prende come parametro un HDC, tipo BitBlt() per disegnare le immagini, TextOut() per disegnare il testo, LineTo() per le linee e così via.

Bitmaps

Le Bitmaps possono essere caricate in modo molto simile a come abbiamo caricate le icone negli esempi precedenti, c’è una funzione LoadBitmap() per le funzionalità di base di caricare semplicemente una risorsa bitmap, e una funzione LoadImage() che può essere usata per caricare le bitmap da un file *.bmp proprio come con le icone.

Una peculiarità delle GDI consiste nel fatto che è possibile disegnare un oggetto bitmap (tipo HBITMAP) direttamente. Bisogna ricordare che le operazioni di disegno sono rese astratte dai Device Contexts, quindi per usare queste funzioni su una bitmap bisogna creare un Memory DC e dopodiché selezionarci dentro l’HBITMAP con SelectObject(). Ne risulta che il “device” a cui fa riferimento l’HDC è la bitmap in memoria, e quando si opera con l’HDC le operazioni grafiche risultanti vengono applicate alla bitmap. Come già detto è un sistema comodissimo per fare le cose, dato che in questo modo è possibile scrivere codice che disegna su un HDC e poterlo utilizzare su un Window DC o su un Memory DC senza effettuare cambiamento alcuno.

Abbiamo anche l’opzione di manipolare per conto nostro la bitmap in memoria; si può fare con le Device Independant Bitmaps(DIB); in questo modo si possono combinare le operazioni con GDI e le operazioni manuali tutte all’interno della DIB. Ad ogni modo queste operazioni semi-avanzate non rientrano negli obiettivi di questo tutorial che si prefigge di dare le basi sulla programmazione API in Windows. Vedremo le operazioni GDI più semplici.

GDI Leaks

Quando si è finito di operare con un HDC, è molto importante rilasciare l’oggetto (per farlo dipende da come è stato ricavato, e di questo ne parleremo in seguito). Gli oggetti GDI sono limitati in numero. Nelle versioni di Windows prima di Windows 95, non erano incredibilmente limitati ma venivano anche condivisi con il sistema, ma in questo modo se un programma ne utilizzava tanti, gli altri programmi non potevano disegnare nulla! Fortunatamente questo caso ora non si verifica più, soprattutto perchè Windows 2000 e XP effettuano controlli sulle risorse utilizzate dal programma prima che accada qualcosa di brutto… ma è facile dimenticare di liberare gli oggetti GDI e quindi sotto Windows 9x diventa facile che il programma termini gli oggetti GDI. Teoricamente non è possibile far danni nei i sistemi basati su architettura NT (NT/2K/XP) ma è possibile che capiti; come regola generale quindi, bisogna fare sempre attenzione a rilasciare gli oggetti allocati dopo averli usati.

Se il vostro programma gira bene per i primi minuti e ad un tratto inizia a disegnare in modo strano o a non disegnare affatto, è un buon segnale che ha qualche problema con le risorse GDI. Bisogna inoltre far conto che gli HDC non sono gli unici oggetti con cui bisogna stare attenti; anche le bitmap e i font concorrono agli sprechi di risorse: in linea generale è meglio lasciarli in memoria invece che ricaricarli ogni volta che se ne ha bisogno.

Inoltre, un HDC può contenere solo un tipo di oggetto (bitmap, font, pen…) per volta, quindi quando se ne seleziona uno nuovo esso ritorna l’ultimo caricato. E’ molto importante interagire con questo oggetto in modo adeguato; se lo si ignora completamente, esso rimarrà in memoria causando leaks. Quando un GDI viene creato ha alcuni oggetti di default selezionati all’interno… è generalmente una buona idea salvare questi oggetti in una variabile temporanea in modo da poterli riposizionare all’interno quando si è finito di utilizzare l’HDC; questa operazione non solo consente di rimuovere i propri oggetti dall’HDC (che è sempre una cosa buona) ma mantiene anche la disposizione corretta degli oggetti di default nel momento in cui l’HDC viene rilasciato o distrutto (e questa è sempre una cosa MOLTO buona).

Attenzione: Non tutti gli oggetti sono selezionati per default dentro l’HDC, bisogna fare riferimento a MSDN per i pochi che non lo sono.

Apparentemente ci stava un bug in uno screensaver scritto da MS; il bug consisteva proprio in un mancato rilascio della bitmap di default, e per questo motivo le risorse GDI terminavano presto. Per l’ennesima volta, state attenti! E’ facile commettere errori.

Visualizzare le Bitmaps

Ok, possiamo passare alla pratica. L’operazione di disegno più semplice su una finestra si effettua processando WM_PAINT. Quando la finestra è visualizzata la prima volta, ripristinata dopo essere stata minimizzata o anche scoperta dopo essere stata sotto un’altra finestra, Windows invia il messaggio WM_PAINT alla finestra per farle sapere cha ha bisogno di ridisegnare i contenuti. Quando disegniamo qualcosa sullo schermo infatti questo non è permanente, rimane semplicemente lì finché qualcos’altro non ci disegna sopra, per questo motivo bisogna ridisegnare quando necessario.

  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;

Il primo passaggio è ovviamente quello di caricare la bitmap, è abbastanza semplice con una risorsa, non ci sono differenze significative dal caricamento di altri tipi di risorsa. Ora possiamo procedere a disegnare…

    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;

Ricavare il Window DC

Per iniziare dichiariamo un paio di variabili che ci serviranno. Notare che la prima è una BITMAP, non una HBITMAP. BITMAP è una struttura che mantiene le informazioni su una HBITMAP che rappresenta l’oggetto GDI attuale. Abbiamo bisogno di un modo per ottenere l’altezza e la larghezza della HBITMAP, quindi utilizziamo GetObject() che al contrario di cosa ci possa far intuire il nome non ricava esattamente un oggetto, bensì le informazioni su un oggetto esistente, effettivamente il nome più appropriato sarebbe stato “GetObjectInfo” o qualcosa del genere. GetObject() funziona per i vari tipi di oggetto GDI che possono essere distinti in base al valore del secondo parametro, la grandezza della struttura.

La PAINTSTRUCT è una struttura che contiene informazioni relative alla finestra che viene fisegnata e cosa esattamente sta succedendo con il messaggio di disegno. Per le operazioni più semplici, si può tranquillamente ignorare le informazioni contenute, ma è richiesta per la chiamata a BeginePaint(). BeginPaint(), come suggerisce il nome è designata specificatamente per il messaggio WM_PAINT. Quando non si processa WM_PAINT, bisogna usare GetDC(), e questo lo vedremo presto nell’esempio di animazione con il timer… ma in WM_PAINT, è importante usare BeginPaint() ed EndPaint()

BeginPaint() restituisce un HDC che rappresenta l’HWND che gli passiamo, quello per cui viene processato WM_PAINT. Ogni operazione che effettuiamo su questo HDC verrà visualizzata immediatamente sullo schermo

Configurare un Memory DC per la Bitmap

Come detto in precedenza, per disegnare oppure per lavorare con le bitmap, abbiamo bisogno di creare un DC in memoria… il modo più semplice per farlo è utilizzare CreateCompatibleDC() con un DC che già possediamo. In questo modo otteniamo un Memory DC che è compatibile con i colori e le proprietà di visualizzazione dell’HDC per la finestra.

Dopodiché chiamiamo SelectObject() per selezionare la bitmap dentro il DC stando attenti a salvare la bitmap di default in modo da poterla ripristinare in seguito e non causare scompensi con gli oggetti GDI.

Disegnare

Dopo aver ottenuto le fimensioni della bitmap riempita dentro la struttura BITMAP, possiamo chiamare BitBlt() per copiare l’immagine dal Memory DC al Window DC, quindi visualizzarla sullo schermo. Come sempre si può ottenere la spiegazione di ogni parametro tramite MSDN, ma in breve essi sono: La destinazione, la posizione e la grandezza, la sorgente e la posizione della sorgente e infine la Raster Operation (ROP code), che specifica come fare la copia. In questo caso vogliamo una semplice ed esatta copia della sorgente, nessuna operazione particolare.

BitBlt() è probabilmente la funzione più allegra dentro le API di Windows ed è la dieta quotidiana per chi scrive giochi o applicazioni grafiche in windows. E’ probabilmente la prima API di cui ho memorizzato i parametri

Cleanup

A questo punto la bitmap dovrebbe essere sullo schermo, e abbiamo bisogno di rilasciare le risorse utilizzate. La prima cosa da fare è rimettere il Memory DC allo stesso stato di quando ci è stato dato, questo significa rimpiazzare la nostra bitmap con quella di default che abbiamo salvato. Dopodiché possiamo cancellare il tutto con DeleteDC().

Infine rilasciamo il Window DC che ci è stato restituito da BeginPaint() usando EndPaint().

Distruggere un HDC può confondere in quando ci sono almeno 3 modi per farlo a seconda di come è stato ottenuto per la prima volta. Segue una lista dei metodi più comuni per ottenere un HDC e di come rilasciarli una volta finito di operare

  • GetDC() - ReleaseDC()
  • BeginPaint() - EndPaint()
  • CreateCompatibleDC() - DeleteDC()

Infine, quando il nostro programma termina, vogliamo liberare tutte le risorse allocate, Parlando tecnicamente non è assolutamente richiesto, in quando le piattaforme Windows moderne, sono concepite abbastanza decentemente da liberare tutte le risorse quando il programma termina, ma è sempre una buona idea tenere traccia degli oggetti perché altrimenti se si diventa pigri e si perdono per strada ri rischia di impazzire. E inoltre è ben noto che in alcune versioni di Windows, specialmente le più vecchie, a causa di alcuni bug, gli oggetti GDI non vengono rilasciati correttamente se non lo si fa a mano

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