Eccoci, dunque, al punto di prendere una decisione strutturale importante per il progetto: come incapsulare il "subclassing" che abbiamo effettuato? C'è modo di renderlo più usabile, più generale, riusabile in una qualche misura? Altra considerazione importante -- possiamo farlo senza usare variabili globali, con tutti i problemi che si portano dietro?
Vediamo anzitutto quest'ultimo aspetto. Facciamo del subclassing, quindi, all'arrivo di un messaggio su di un controllo che abbiamo subclassato, andrà in esecuzione la nostra window procedure; questa ha bisogno di trovare dei dati (tipo, la window procedure precedente, cui delegare il grosso del lavoro) -- dove, se non in variabili globali...? Mica controlliamo gli argomenti passati alla nostra window procedure... dove altro potremmo mettere, dunque, i dati di cui ha bisogno...?
È vero che non controlliamo quali argomenti vengano
passati alla nostra window procedure a fronte di ogni
dato messaggio. Ma è vero anche che, in ciascun caso,
uno di questi argomenti altro non è che l'HWND
della
finestra che abbiamo subclassato; se avessimo modo di,
diciamo, "associare" tutti i dati per noi rilevanti
a questa HWND
, la soluzione sarebbe limpida: ogni
finestra subclassata si porterebbe dietro l'"associazione"
a tutti i dati che sono necessari alla window procedure
di subclassing -- problema risolto.
Abbiamo già visto che Windows ci offre le API
SetWindowLong
e GetWindowLong
, con le quali si può
accedere (in sola lettura, o lettura e scrittura,
rispettivamente) a tanti dati
associati a ogni HWND
-- e uno dei valori definiti per
l'argomento "indice" ha il promettente nome di
GWL_USERDATA
, "dati dell'utente"... non potremmo
forse usare quella longword, per metterci i dati che
ci fanno comodo...?
In generale, questa potrebbe essere la soluzione migliore; come vedremo, infatti, quando creeremo le nostre classi di finestre, potremo anzi addirittura riservare più spazio (sempre accedibile con SetWindowLong ecc), rispetto alla singola longword normalmente disponibile, proprio per facilitare questo uso.
La soluzione, però, non sarebbe affidabile e generale per
il caso specifico del subclassing. Infatti, per finestre
subclassate, non possiamo "aumentare lo spazio" a
disposizione -- e l'unica longword (che al limite potrebbe
bastare, usandola per tenere l'indirizzo di una struttura
che contiene tutti i dati che ci interessano) potrebbe
essere già usata, per scopi suoi, dalla window procedure
preesistente, cui vogliamo "passare la palla" della gestione
della gran parte
dei messaggi... teniamo presente, infatti, che la window
procedure preesistente "non sa" che sarà subclassata, o come
lo potrà essere; è responsabilità del "subclassatore",
dunque, quella di mantenere per la wp preesistente l'"ambiente",
la "normalità", sulla quale essa può fare conto... compreso,
in molti casi, il suo uso privato e personale del GWL_USERDATA
.
Non possiamo, quindi, chiaramente, usare impunemente noi stessi quella stessa longword. E non sarebbe furbo testare una data versione di, ad esempio, "Button", anche se si scoprisse che quella data versione pare non usare quella longword -- altre versioni, magari più aggiornate, che il nostro programma potrebbe certo incontrare in futuro nelle DLL di una qualche versione più aggiornata di Windows, potrebbero invece anche usarla...!.
L'API di Windows prevede un meccanismo apposito per
questi casi; un meccanismo che
esegue la associazione di arbitrari dati con una
HWND
senza "toccare" la struttura dell'HWND
stessa --
ideale, dunque, per il subclassing!
Si tratta del meccanismo delle "Window Properties". Le API fondamentali di questo meccanismo sono:
hData
,
basta un opportuno cast);
Esiste inoltre una ulteriore API, EnumPropsEx
, che permette
di esaminare tutte le proprietà associate a una
data finestra, ma qui essa non ci servirà.
Il meccanismo delle Window Properties è appena
appena più oneroso, in termini di spazio e tempo di
esecuzione, rispetto a quello di SetWindowLong
, ma
è anche molto più flessibile, e normalmente le sue
prestazioni sono comunque del tutto accettabili.
Useremo dunque questo schema per associare alle nostre finestre subclassate i dati che servono, come, ad esempio, l'indirizzo della window-procedure preesistente, cui delegare i messaggi non di specifico interesse.
Per ottenere codice riusabile, è bene, spesso, fare una riflessione sulla generalità di quanto si scrive.
Quello che abbiamo già scritto è codice con un
compito abbastanza preciso: far sì che un bottone
mandi certe "notifiche" (WM_COMMAND
, con i
codici BN_PUSHED
, ecc) al dialogo che lo contiene, a
fronte di certi eventi (up e down del bottone sinistro del
mouse, nel nostro caso).
Questo ha già la sua utilità, ma un minimo della riflessione suddetta dovrebbe permettere facilmente di vedere che una semplice e utile generalizzazione di questa funzione è di non "cablare" direttamente nel codice l'elenco degli eventi cui rispondere, e di quali codici di notifica usare per comunicare questi eventi al dialogo "genitore" del controllo; questa semplice corrispondenza può invece fruttuosamente essere codificata in una struttura dati, che viene passata al momento della richiesta del subclassing, e contestualmente associata alla finestra; il chiamante può anche modificare quella struttura dati, per cambiare i dettagli di comportamento del controllo subclassato!
Una struttura dati semplice ed efficace per lo scopo
è un array di LONG
, che alternativamente indichino
l'evento WM_xxx
cui rispondere, e il codice di notifica
da usare per esso, array "logicamente terminato" da un
valore di zero nel campo corrispondente al codice
WM_xxx
(indice pari nell'array).
Con questa convenzione, ecco come potremmo impostare l'implementazione del nostro subclassing:
Facciamo notare, di passaggio, che, come già detto, questo codice è del tutto insoddisfacente dallo specifico punto di vista del controllo degli errori -- tutto questo esempio ha precauzioni davvero al di sotto del minimo professionalmente accettabile contro errori imprevisti di tutti i tipi. Il problema generale, e importantissimo, della gestione degli errori, ci potrebbe però qui portare un po' troppo lontani dallo scopo primario di imparare a usare le API di Windows; affronteremo ancora questo problema più avanti.
Se non altro, controllo di errori a parte, stiamo iniziando ad
accumulare una piccola ma utile librerietta di procedure, di
codice riusabile per rivestire ed arricchire l'uso delle API di
Win32 -- in parte, codice banale (che è comunque meglio
scrivere "una volta per tutte", quando ciò risulta fattibile),
in parte, come per la WinMove
e la Aggiungi_Notifiche
, non
esattamente banale, benchè, certo, neppure particolarmente
astruso o "sublime".
Notiamo che è un tipico sintomo di un programmatore già esperto, alle prese con un ambiente di programmazione per lui nuovo, quello di accumulare gradualmente (e raffinare, e perfezionare, via via) un "corpus" di codice riusabile per il nuovo ambiente, che concretizza i tipici idiomi che facilitano l'uso dell'ambiente stesso -- sia in termini di macro, ovvero di funzioni, o, ancora, di classi, se si dispone di un linguaggio di programmazione ad oggetti. Il programmatore esperto non riscrive più e più volte le stesse cose -- riusa, invece, quelle che ha già scritto, o, meglio ancora, che hanno scritto altri, se fanno al caso suo -- e neppure egli commette l'errore di limitare questo riuso al "copia e incolla", che, egli lo sa bene, a fronte di un'apparente promessa di utilità "illico et immediate", è però fonte di elevati costi di manutenzione futuri.
"Armati", dunque, di questa piccola libreria, troveremo quindi assai facile completare, al prossimo capitolo, il nostro esempietto.
Capitolo 19: l'esempio rivisitato (1)
Capitolo 21: l'esempio rivisitato (2)
Elenco dei capitoli