Introduzione alle API Win32

Capitolo 31: disegno: fondamenti

La varietà di funzioni che le API di Windows mettono a nostra disposizione per analizzare e modificare un device context (DC) [e, naturalmente, per disegnarci sopra], è spaventosamente vasta. Per fortuna, non avremo bisogno di altro che un manipolo di esse per il nostro attuale compito, che è in effetti abbastanza semplice: quello, ricordiamo, di disegnare su di un controllo static owner-drawn il grafico di una funzione.

Uno dei compiti del gestore di un messaggio WM_DRAWITEM è quello di "lasciare il DC come lo si trova". Un device context ha svariati elementi di "stato" (modificare questi elementi, che hanno effetto su varie successive operazioni di disegno, è più semplice e rapido che passare tutti i parametri di stato a ciascuna di queste operazioni); il gestore in questione può cambiarli, ma, se lo fa, deve poi anche ripristinarli come li ha trovati.

Può essere scomodo tenere traccia di tutte le modifiche che si sono fatte; fortunatamente, due specifiche API ci risparmiano questo lavoro (a prezzo di un eventuale, ma comunque modesto, appesantimento del carico computazionale):

int SaveDC(HDC hdc); salva lo stato attuale del DC, e torna un intero che identifica questo fra vari altri eventuali "stati salvati" per il DC (zero in caso d'errore, come al solito); BOOL RestoreDC(HDC hdc, int nSavedDC); ripristina il DC allo stato identificato dall'intero passato come secondo parametro.

Per comodità ancora maggiore, se nSavedDC è negativo, esso è preso come riferimento relativo; RestoreDC(hdc,-1), ad esempio, ripristina lo stato salvato dalla SaveDC(hdc) più recente, il che ci risparmia persino la modesta fatica di salvarci il valore ritornato dalla SaveDC stessa!

Se si ritiene opportuno modificare lo stato del DC, è dunque opportuno, per semplicità, avere una coppia di chiamate, SaveDC(hdc) all'ingresso e RestoreDC(hdc,-1) all'uscita, per garantire di ripristinarlo correttamente. Noi, in realtà, non ne avremo bisogno, ma è una tecnica molto comoda (e ancora più se incapsulata in un costruttore e un distruttore di un oggetto locale in C++, naturalmente).

 

Fra gli elementi dello "stato" di un DC ci sono vari "oggetti grafici", e in particolare una "penna", che determina il "tratto" col quale curve e linee vengono disegnate, ed un "pennello", che determina il modo in cui vengono riempite le aree. Se i colori "normali" che presenta un DC non ci soddisfano, un modo di cambiarli è quello di cambiare penna e pennello correnti del DC stesso. Ancora una volta, è una tecnica che noi qui non useremo (ma il lettore è invitato a fare esperimenti con essa...!), ma è importante averla presente.

Cosa useremo, dunque...? Beh, ragioniamo su cosa dobbiamo fare... possiamo avere un colore di sfondo sul rettangolo del nostro static owner-drawn; e vogliamo tracciare su di esso una serie di linee (in realtà, una sola linea di molti segmenti, poichè ciascuno inizia dove il successivo finisce). Supponendo di accettare la penna di default (sottile, probabilmente nera), che è tutto sommato perfettamente ragionevole per un semplice grafico, possiamo mantenere la piccola ambizione di decidere il colore di sfondo.

C'è un'API comodissima per questo scopo...:

int FillRect(HDC hDC, CONST RECT *lprc, HBRUSH hbr); le passiamo il DC, un puntatore al rettangolo, e una HBRUSH ("handle di un pennello"), e lei fa tutto. Il DC e il RECT di nostro interesse, come ricorderete, li abbiamo già a mano -- sono i campi hDC e rcItem della struttura, di tipo DRAWITEMSTRUCT, alla quale viene passato un puntatore (in lParam) assieme al messaggio WM_DRAWITEM che supponiamo di stare gestendo.

E un HBRUSH, come possiamo ottenerlo...?

Anche qui, i modi sono tanti (ma tanti), ma, fra i più semplici, abbiamo la API...:

HBRUSH CreateSolidBrush(COLORREF crColor); per avere un "pennello" che rappresenta un colore uniforme; un COLORREF è la descrizione del colore che desideriamo, e possiamo ottenerlo, ad esempio, con la macro: RGB(r,g,b) dove r, g, e b, sono i valori, ciascuno fra 0 e 255, delle componenti rossa, verde, e blu, del colore di nostro interesse.

L'HBRUSH così creato, naturalmente, è poi nostra responsabilità gettarlo, chiamando su di esso l'API:

BOOL DeleteObject(HGDIOBJ hObject); dove un HGDIOBJ è una generica "handle ad oggetto grafico" (di cui l'HBRUSH è un caso particolare).

Riassumendo, dunque, il codice, nel nostro gestore di WM_DRAWITEM, per riempire l'item di un colore di sfondo desiderato, è davvero semplice...:

void OnDlgDrawItem(HWND hwnd, const DRAWITEMSTRUCT *lpDrawItem) { // copie locali per comodita` HDC hdc = lpDrawItem->hDC; RECT* pr = &lpDrawItem->rcItem; HBRUSH hb = CreateSolidBrush(RGB(200,180,50)); if(hb) { FillRect(hdc, pr, hb); DeleteObject((HGDIOBJ)hb); }

Una volta tanto, abbiamo esplicitamente controllato che il pennello sia stato creato (ricordiamo che, di solito, per semplicità, omettiamo i vari controlli di errore nei nostri esempietti!); la creazione potrebbe risultare impossibile se, ad esempio, lo schermo sul quale stiamo disegnando non avesse a disposizione una sufficiente varietà di colori, nel qual caso, in questo esempio, scegliamo di lasciare semplicemente il rettangolo nel suo normale colore di fondo (la maggior parte degli ambienti Win32 supporta in modo implicito algoritmi, detti di "dithering", per "simulare" colori che non sono in realtà disponibili, ma non tutti tali ambienti hanno questa possibilità).

 

Per disegnare una linea a più segmenti, Windows ci offre una API altrettanto comoda e semplice:

BOOL Polyline(HDC hdc, const POINT *lppt, int cPoints); Basta dunque che forniamo un array di strutture di tipo POINT (ciascuna, ricordiamo, con due coordinate LONG, x e y), e Windows farà il resto, disegnando sul DC i segmenti di retta che connettono questi punti (il primo al secondo, il secondo al terzo, eccetera).

Ci rimane soltanto, dunque, il problema di come preparare, e come mettere a disposizione della funzione che gestisce WM_DRAWITEM, questo array di punti e la sua lunghezza. Per la seconda metà, conosciamo già la soluzione: WM_DRAWITEM riceve come argomento la HWND del nostro dialogo, quindi può recuperare informazioni che, altrove nel nostro programma, abbiamo "connesso" al dialogo, o come "window property", o, in modo marginalmente più efficiente, con SetWindowLong (l'indice DWL_USER è riservato appositamente per una longword determinata dalla applicazione, cioè noi, e associata al dialogo); il normale modus operandi è di porre in questo LONG il cast del puntatore ad una struttura che contiene tutte le informazioni d'interesse. Il codice completo del gestore potrebbe dunque essere qualcosa come, per una qualche opportuna struttura:

typedef tagStru { // le info d'interesse } stru; e mantenendoci un po' di generalità, che spiegheremo nel testo immediatamente successivo, avremmo dunque: void OnDlgDrawItem(HWND hwnd, const DRAWITEMSTRUCT *lpDrawItem) { stru* pSt = (stru*) GetWindowLong(hwnd, DWL_USER); // copie locali per comodita` HDC hdc = lpDrawItem->hDC; RECT* pr = &lpDrawItem->rcItem; COLORREF cr; // accettiamo eventuale colore di fondo gia` assegnato if(pSt) cr = pSt->sfondo; else cr = RGB(200,120,40); HBRUSH hb = CreateSolidBrush(cr); if(hb) { FillRect(hdc, pr, hb); DeleteObject((HGDIOBJ)hb); } // eventuale preparazione dei dati BOOL bNonPronto = TRUE; if(pSt && pSt->prepara) { bNonPronto = !pSt->prepara(pSt, pr); } if(bNonPronto) { // disegno della scritta "non pronto" static char* szNonPronto = "Grafico non pronto"; static int cNonPronto = 0; if(!cNonPronto) cNonPronto=strlen(szNonPronto); TextOut(hdc,pr->left+5,pr->top+5, szNonPronto, cNonPronto); } else { // disegno del grafico vero e proprio Polyline(hdc, pSt->punti, pSt->nPunti); } };

Qui abbiamo tenuto conto di varie possibilità, e ipotizzato una struttura assai elegante e avanzata: anzitutto, se il puntatore a stru manca, usiamo l'API TextOut per informare l'utente che non siamo ancora pronti per tracciare il grafico; se c'è, supponiamo vi siano inclusi il colore di sfondo (come campo di tipo COLORREF), due campi che danno l'array di punti e il numero di punti, ed il puntatore a una funzione che, chiamata con due argomenti (il puntatore alla struttura, che conterrà tutti i campi che le servono, e quello al rettangolo di disegno, dato geometrico assai importante), torna un BOOL -- vero se il grafico è pronto per essere disegnato, falso altrimenti (implicitamente, essa provvederà a renderlo pronto, se possibile, nel caso che ancora non lo fosse).

Questo è uno schema, non propriamente object-oriented, ma almeno object-based, che ci permette di incapsulare nel gestore del messaggio WM_DRAWITEM tutto e solo quello che serve per disegnare, demandando invece ogni altra decisione a un'altra funzione, trovata, con massima flessibilità, grazie all'uso del puntatore-a-funzione contenuto nella struttura.

 

Un vantaggio di questo approccio avanzato è che il lettore può già mettere insieme molto agevolmente tutto il resto del programma, lasciando a zero la longword DWL_USER del dialogo, e verificare che appaia il giusto messaggio di "non pronto"; mettendo nella struttura l'opportuno campo cr, si può anche verificare il comportamento con vari colori di fondo.

Noi, naturalmente, continueremo a tratteggiare il resto della struttura di codice sottesa da questo (sempre nell'ipotesi semplificativa di usare i message cracker di WindowsX.h, naturalmente), ma... alla prossima puntata!


Capitolo 30: il disegno in Windows
Capitolo 32: l'impostazione del grafico
Elenco dei capitoli