Introduzione alle API Win32

Capitolo 30: il disegno in Windows

Il "modello concettuale" con cui funziona la grafica di Windows (e quella di altri ambienti, per altri versi tutt'altro che simili, come l'X Window System universalmente diffuso su Unix e Linux) non è molto simile a quello che la maggior parte dei programmatori troverebbe più spontaneo e intuitivo, per cui è particolarmente importante farsene un'idea chiara.

Il "modello spontaneo e intuitivo" sarebbe uno del tipo: quando la mia applicazione ha dei nuovi dati da mostrare, li "mette", in forma grafica opportuna, "da qualche parte", e, da quel momento in avanti, non ha più bisogno di preoccuparsene, se non per modificarli. In effetti, è su questa falsariga che funzionano normalmente i vari controlli predefiniti: se ho del testo da mostrare, lo mando con SetWindowText alla finestra dove voglio mostrarlo (ad esempio, un controllo static); se ho una bitmap da mostrare, la mando, con un opportuno messaggio, al controllo static dove la voglio mostrare.

Ma le cose funzionano così solo perchè le window procedure di questi controlli si memorizzano le informazioni necessarie per ricreare il "rendering" grafico (cioè il disegno vero e proprio) se e quando serve, e lo fanno "per conto loro", in modo, cioè, "trasparente" al nostro codice; questa comodità concettuale, in altri termini, ci è offerta da alcune window procedure per vari controlli esistenti, ma non è il "vero modo" in cui le cose, "sotto sotto", funzionano in Windows.

La "vera" grafica di Windows è, invece, ad eventi, cioè basata su messaggi, come tante altre cose che già abbiamo visto. Quando il sistema determina che una certa finestra si deve "ri-dipingere", in tutto o in parte, glielo manda a dire, spedendole un apposito messaggio; la finestra deve allora gestire quel messaggio, eseguendo le "azioni" grafiche di disegno corrispondenti a quello che dovrà essere il suo aspetto (naturalmente, qui e nel seguito, quando diciamo che una finestra deve gestire un messaggio, intendiamo che quel messaggio deve essere gestito da opportuno codice nella window procedure della finestra stessa, o da essa invocato; ci si concederà, speriamo, questa piccola scorciatoia linguistica, senza lasciarsene confondere:-).

Il sistema, ad esempio, spedisce questo tipo di messaggi ("ridisègnati, prego") quando un'altra finestra, che non ha probabilmente nulla a che vedere con la nostra applicazione, ha coperto (magari solo parzialmente) e poi nuovamente scoperto una delle "nostre" finestre; questo, proprio perchè l'"altra" finestra non è per nulla sotto il nostro controllo, può succedere in qualsiasi momento, e dunque in qualsiasi momento le nostre finestre devono essere pronte a "ridisegnarsi".

Ogni finestra deve dunque "tenersi", in qualche modo, tutti i dati che le servono per "ridisegnare" il proprio aspetto in qualsiasi istante. Windows ci incoraggia inoltre ad usare lo stesso schema (di "ri"-disegno) anche per quello che è in realtà il "primo" disegno di una certa finestra sulla base di certi dati (alla creazione, o quando i dati che ne determinano l'aspetto sono appena stati cambiati): invece di procedere subito al nuovo disegno, la finestra può venire in tutto o in parte "invalidata", proprio "come se" fosse stata coperta e poi nuovamente scoperta da un'altra, e riceverà dunque il messaggio che le dice "devi ridisegnarti" -- sarà solo in risposta a questo messaggio, che essa provvederà, appunto, a disegnarsi.

È possibile anche disegnare in modo molto dinamico e transitorio, ad esempio in immediata, interattiva risposta ad azioni dell'utente, ma non è, per lo più, il modo "normale" di agire in Windows, e, per ora, non avremo bisogno di occuparcene.

Lo "invalidarsi" di una finestra (che ciò sia dovuto allo spostamento di altre, o ad apposite API chiamate per forzare il ridisegno) può essere parziale -- è comune che sia solo un certo sotto-rettangolo della finestra a dovere veramente essere di nuovo disegnato; è possibile, dunque, ottimizzare le operazioni grafiche, determinando quale sia questo sottoinsieme della finestra che ne ha davvero necessità, ed eseguendo, di conseguenza, solo un sottoinsieme delle "azioni grafiche" che servirebbero per ridisegnare la finestra tutta.

Queste ottimizzazioni, tuttavia, sono opportune solo in casi particolari (finestre molto grandi, operazioni di disegno veramente onerose -- e, in quest'ultimo caso, è probabibile che sia meglio puntare invece sulla riduzione di questo onere, attraverso il pre-calcolo di quanto serva precalcolare, e la scelta di strutture opportune per i dati di disegno), e quindi, ancora una volta, scegliamo di rimandare lo studio di questi aspetti (la vastità del tema "programmazione per Windows" è così ampia da lasciare a volte sbalorditi, e, nell'ambito di un tutorial, non possiamo certo approfondire più di tanto uno qualsiasi dei suoi sotto-temi... si fà già fatica a riuscire a nominarli tutti!-).

 

Il messaggio che, più spesso, dice ad una finestra "ridisègnati", è WM_PAINT. Ci sono tuttavia vari altri casi, e il caso che qui esaminiamo, quello di un controllo owner-drawn, è appunto uno di questi; la finestra-controllo riceve, sì, il WM_PAINT, ma essa, sapendo di essere owner-drawn, risponde a questo messaggio spedendo al dialogo che la contiene un altro messaggio, WM_DRAWITEM, che dice al dialogo "ridisègnami"; il WM_DRAWITEM è, naturalmente, accompagnato da tutti i dati che servono al dialogo per determinare cosa disegnare, nonchè in quali esatte circostanze si presenta la necessità di disegnare.

Specificamente, il wParam che accompagna il messaggio WM_DRAWITEM è l'ID del controllo che deve essere ridisegnato; l'lParam è (il cast ad LPARAM di) un puntatore ad una struttura di tipo:

typedef struct tagDRAWITEMSTRUCT { UINT CtlType; UINT CtlID; UINT itemID; UINT itemAction; UINT itemState; HWND hwndItem; HDC hDC; RECT rcItem; DWORD itemData; } DRAWITEMSTRUCT;

Il campo CtlID di questa struttura ripete l'informazione già contenuta nel wParam; itemID identifica, per quei controlli che hanno dei "sotto-elementi" (come i due che ancora non abbiamo visto, le liste ed i combo), di quale sotto-elemento si parli.

CtlType identifica il tipo di controllo: ODT_STATIC, ODT_BUTTON, eccetera -- dato, anche questo, che dovrebbe essere già noto sulla base del CtlID; così pure per hwndItem, che è l'HWND del controllo.

itemAction è una maschera di bit che dicono se il controllo debba essere interamente ridisegnato (cioè se ha ricevuto un vero WM_PAINT), se abbia cambiato (guadagnato o perso) focus, se abbia cambiato stato (bottone da premuto a non, o viceversa, ad esempio).

Per il nostro static (disabled, che quindi non può mai prendere il focus), questo non ci interessa, ma in altri casi potrebbe naturalmente essere importante. Così pure per itemState, che definisce lo "stato" attuale del controllo (o suo eventuale sotto-elemento).

itemData è un arbitrario valore di 32 bit che può essere stato associato all'elemento da parte dell'applicazione, ma esso non si applica a bottoni e static.

 

Ciò ci lascia, dunque, con due soli dati veramente cruciali ai nostri fini immediati: hDC, la "handle di device context" ("contesto di dispositivo") su cui disegnare, e rcItem, lo specifico rettangolo di questo DC sul quale dobbiamo disegnare.

Tutte le operazioni grafiche, in Windows, si svolgono sempre su di un device context (DC), acceduto attraverso una handle ad esso (tipo HDC). I device context sono l'astrazione fondamentale della grafica di Windows: ogni volta che 'disegno su di una finestra', in realtà lo faccio ottenendo in qualche modo un DC per quella finestra, poi disegnando sul DC (e normalmente, a disegno finito, liberando il DC stesso, ma, in questo caso di owner-drawn, a questo ci pensa invece il sistema -- nostra responsabilità è solo quella di "lasciare il DC come l'abbiamo trovato" in termini del suo "stato", concetti che, naturalmente, torneremo a vedere meglio fra breve).

Normalmente, ottenuto un DC, esso è "tutto nostro" per disegnarci (se "usciamo" dai suoi veri confini, ci pensa Windows a "potare" ["clipping"] i nostri eccessi); in questo caso, però, riceviamo anche un rettangolo entro il DC, con l'ingiunzione di limitare le nostre attività grafiche a quel rettangolo, che, ci viene assicurato, rappresenta "tutto" quel che dobbiamo disegnare -- non è questione di ottimizzazione, ma di correttezza (in realtà, in molti casi, Windows può comunque compensare nostri eventuali errori, ma è meglio non farci conto). Le unità di misura del rettangolo sono quelle del DC (altro dettaglio su cui torneremo); non occorre, dunque, che ce ne occupiamo direttamente, salvo casi particolarissimi. Come al solito, infine, quello che ci viene passato è un rettangolo di cui i bordi superiore e sinistro solo inclusi, quelli inferiore e destro esclusi; ma anche tutte le nostre chiamate di API di disegno rispetteranno questa stessa, solita convenzione, e quindi, ancora una volta, non c'è, in questo, particolare rischio d'errore.

Resta, dunque, solo da determinare cosa e come si può disegnare in un DC in queste condizioni... ma, al solito, "il seguito alla prossima puntata"!-)


Capitolo 29: GUI per grafico di funzione
Capitolo 31: disegno: fondamenti
Elenco dei capitoli