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:
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