Introduzione alle API Win32

Capitolo 20: un subclassing migliore

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:

BOOL SetProp( HWND hWnd, // la finestra LPCTSTR lpString, // nome da dare alla proprietà HANDLE hData // valore da dare alla proprietà ); che crea, o modifica, una proprietà con un certo nome da noi scelto, associandola alla generica "handle" che le passiamo (è specificamente documentato che al posto di una handle si possono usare 32 bit qualsiasi come hData, basta un opportuno cast); HANDLE GetProp( HWND hWnd, // la finestra LPCTSTR lpString // nome della proprietà ); che torna il valore della proprietà richiesta (0 se non esiste -- attenzione, dunque, che una proprietà settata a zero è difficile da distinguere da una non esistente); HANDLE RemoveProp( HWND hWnd, // la finestra LPCTSTR lpString // nome della proprietà ); che opera come GetProp, ma, inoltre, rimuove la proprietà, in modo che a future richieste la finestra risulterà non avere più una proprietà di quel nome (per pulizia, un programma dovrebbe togliere tutte le proprietà dalle finestre cui ne ha assegnate, prima che esse siano distrutte).

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:

// aggnot.h // // aggiunge a un controllo la capacita` di inviare notifiche, via // WM_COMMAND, al dialogo che lo contiene, sulla base di // eventi Windows WM_xxx ricevuti dal controllo -- la codifica // della corrispondenza evento->notifica e` nell'array pCodici, // che agli indici pari ha codici WM_xxx, a quelli dispari subito // dopo il codice di notifica da usare, terminato logicamente da // uno 0 a indice pari; l'array puo` venire modificato nel corso // dell'esecuzione, per cambiare il comportamento del controllo // void Aggiungi_Notifiche(HWND hwndDialogo, int idControllo, long* pCodici); // aggnot.c // // vedi descrizione della funzionalita` nell'header file aggnot.h // #define STRICT #define WIN32_LEAN_AND_MEAN #include <windows.h> #include "aggnot.h" // la window procedure di superclassing static LRESULT CALLBACK WpSuper(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { // recupera i codici di corrispondenza messaggio->notifica long* pCodici = (long*)GetProp(hwnd,"AN_DT"); if(pCodici) { // scandisce l'array sino a una corrispondenza, o // sino allo 0 di terminazione int i=0; while(pCodici[i]) { if(pCodici[i] == (long)uMsg) { // trovata la corrispondenza! SendMessage(GetParent(hwnd), WM_COMMAND, MAKELONG(GetDlgCtrlID(hwnd),pCodici[i+1]), (LPARAM)hwnd); break; } i += 2; } } // recupera la window procedure che ha subclassato WNDPROC wp = (WNDPROC)GetProp(hwnd,"AN_WP"); if(uMsg == WM_DESTROY) { // alla distruzione, deve fare pulizia RemoveProp(hwnd, "AN_WP"); RemoveProp(hwnd, "AN_DT"); } if(wp) return CallWindowProc(wp, hwnd, uMsg, wParam, lParam); else return DefWindowProc(hwnd, uMsg, wParam, lParam); } // // istalla la procedura di subclassing e le due proprieta` AN_WP // (precedente window procedure) e AN_DT (puntatore ai codici) // void Aggiungi_Notifiche(HWND hwndDialogo, int idControllo, long* pCodici) { HWND hwnd = GetDlgItem(hwndDialogo,idControllo); WNDPROC wp = (WNDPROC)SetWindowLong(hwnd,GWL_WNDPROC,(LONG)WpSuper); SetProp(hwnd,"AN_WP",(HANDLE)wp); SetProp(hwnd,"AN_DT",(HANDLE)pCodici); }

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