Introduzione alle API Win32

Capitolo 18: timer e subclassing

Come potremmo, ci chiedevamo alla fine del capitolo precedente, ottenere una specie di "auto-ripetizione" dell'effetto dei nostri pushbutton, se l'utente tiene premuto su di uno di essi il tasto del mouse...?

Studiando la documentazione del SDK di Windows, e specificamente i messaggi di notifica spediti dai bottoni, può sembrare che ci sia una possibilità.

Sono, infatti, documentati (sia pure con avvertimenti di non usarli...!) due messaggi, BN_PUSHED e BN_UNPUSHED, che sembrano fare proprio al caso nostro; se i nostri bottoni ci spedissero BN_PUSHED quando sono premuti, e BN_UNPUSHED quando sono rilasciati, sarebbe infatti possibile realizzare una "auto-ripetizione".

Supponiamo che questi messaggi arrivassero; come si realizzerebbe, dunque, l'auto-ripetizione dei clic?

Per questo scopo, potremmo usare uno dei timer che Windows mette a disposizione. Un timer è un meccanismo che permette di chiamare a ripetizione, alla frequenza richiesta, una nostra funzione di "call-back", che possiamo scrivere secondo il prototipo:

VOID CALLBACK ProceduraTimer(HWND hwnd, // finestra associata UINT uMsg, // sempre il codice WM_TIMER UINT idEvent, // identificatore del timer DWORD dwTime // "istante attuale" ("tempo di sistema") );

Un timer si attiva chiamando:

SetTimer(hwnd, idEvent, cMillisec, ProceduraTimer); dove cMillisec è il periodo, in millisecondi, che dovrà passare fra due successive chiamate alla ProceduraTimer da parte del sistema; il timer si disattiva chiamando: KillTimer(hwnd, idEvent); (SetTimer e KillTimer tornano entrambe zero in caso di errore; la GetLastError darà allora il codice d'errore).

Ad esempio, quindi, noi potremmo:

Non è difficile inserire questi elementi nel nostro programmino, e invitiamo appunto il lettore a provare a farlo.

Purtroppo, essi non "agiranno": infatti (come si può confermare, ad esempio, ponendo una chiamata a MessageBox nel punto del nostro codice che, se mai ne arrivasse una, gestirebbe la notifica di BN_PUSHED; oppure, usando il prezioso strumentino "Spy++" che la Microsoft fornisce come parte dell'SDK che già più volte abbiamo nominato) questi messaggi di notifica non vengono in realtà mai generati dai nostri bottoni.

Sembra dunque che siamo "bloccati"... come fare, se i bottoni standard fanno quasi al caso nostro, ma mancano di una feature che ci serve, come, qui, quella di spedire questi messaggi di notifica...? Dovremmo, forse, scriverci da zero la nostra "classe" di controlli, riproducendo tutte le utili caratteristiche già presenti nei bottoni, e aggiungendo quelle supplementari che ci servono e ad essi mancano...?

Sarebbe un grosso spreco di energia riscrivere tanto codice già esistente; non c'è modo di riutilizzarlo, limitandoci, cioè, a scrivere il poco codice supplementare che serve a implementare quelle poche, piccole feature mancanti...?

Eccome se c'è; anzi, Windows offre più di una valida alternativa per questa esigenza. Ci limitiamo, per ora, a illustrare la più semplice e flessibile, il subclassing di istanza (altre, pure utili, che possono avere vantaggi in certe situazioni ma ricalcano sostanzialmente idee simili, sono il subclassing di classe, e il superclassing, che per ora non illustreremo).

 

Abbiamo già accennato all'esistenza di "classi di finestre", e come tutte le funzionalità che abbiamo usato sinora poggino su classi standard, predefinite per noi da Windows: precisamente, le classi "dialogo", "static", e "bottone". Una classe di finestre ha varie caratteristiche, ma, fra esse, la più rilevante è la window procedure associata alla classe, una funzione analoga alla "dialog procedure" che già abbiamo visto per i dialoghi, di prototipo:

LRESULT CALLBACK WindowProc(HWND hwnd, // la finestra UINT uMsg, // il messaggio e i suoi parametri: WPARAM wParam, LPARAM lParam);

A differenza della dialog procedure, la window procedure non può semplicemente tornare FALSE per dire "non ho gestito questo messaggio, pensaci tu"; il risultato che essa torna deve infatti dipendere dal messaggio ricevuto; può, però, esplicitamente delegare i messaggi che non ha finito di gestire ad una procedura di default, con:

return DefWindowProc(hwnd, uMsg, wParam, lParam); (la DefWindowProc è un'API di Windows, studiata proprio a questo scopo, che esegue una "elaborazione minima possibile" su ogni tipo di messaggio).

Alternativamente, se la nostra window procedure (che non vuole far nulla di particolare per gestire un dato messaggio) ha a propria disposizione un puntatore a un'altra window procedure (cioè una variabile, diciamo wp, di tipo WINPROC), può delegare ad essa invece che alla DefWindowProc; questo tipo di delega da una window procedure ad un'altra si fa, però, non chiamando direttamente la "altra procedura", bensì usando un'API apposita:

return CallWindowProc(wp, hwnd, uMsg, wParam, lParam); (in tanti casi, si potrebbe anche fare la chiamata diretta, ma vi sono anche parecchi casi anomali in cui ciò non funzionerebbe, ed è meglio abituarsi ad usare sempre l'API CallWindowProc).

 

Dovrebbe già essere abbastanza chiaro come questo concetto ci permetta di "scrivere solo il codice supplementare" per caratteristiche da aggiungere o modificare: se ho il puntatore a una window procedure esistente, che faccia "quasi" già quel che desidero, basterà infatti che la mia window procedure intercetti i soli messaggi d'interesse, delegando all'altra, con CallWindowProc, per tutti quelli che ignora, o, comunque, che non gestisce interamente.

Resta da capire come ottenere questo "puntatore a window procedure esistente" -- e, naturalmente, come far sì che la mia window procedure specializzata vada in esecuzione, per una data finestra, al posto della sua window procedure originale.

 

L'API di Windows permette di accedere a vari dati relativi a una finestra attraverso una singola funzione:

LONG GetWindowLong(HWND hWnd, // la finestra int nIndex // codice del valore che interessa ); I valori così accessibili sono parecchi, e, in particolare, usando come codice GWL_WNDPROC, si ottiene appunto il puntatore alla window procedure della finestra (bisogna, naturalmente, castare il LONG ritornato a WNDPROC, per usarlo).

Inoltre, si possono anche modificare questi dati per una data finestra, ottenendo come effetto collaterale il valore che il dato aveva prima della modifica:

LONG SetWindowLong(HWND hWnd, // la finestra int nIndex, // codice del dato che interessa LONG dwNewLong // nuovo valore per il dato ); (qui, il "nuovo valore" va naturalmente castato a LONG, qualsiasi sia il suo vero tipo, per poterlo impostare).

Quando SetWindowLong è usata per impostare una window procedure (che delega in parte a quella che era impostata precedentemente sulla finestra), si parla di subclassing per la finestra così modificata.

 

Ecco dunque come possiamo "arricchire" il comportamento dei bottoni per ottenere da essi notifiche BN_PUSHED e BN_UNPUSHED come desideriamo:

La WpSuper è facile da scrivere sapendo quali sono i messaggi che arrivano al bottone alla pressione ed al rilascio del mouse su di esso: sono rispettivamente WM_LBUTTONDOWN e WM_LBUTTONUP (per il pulsante principale, cioè "di sinistra", del mouse, che è quello che ci interessa usare):

LRESULT WpSuper(HWND hwnd,UINT uMsg,WPARAM wParam,LPARAM lParam) { UINT code = 0; switch(uMsg) { case WM_LBUTTONDOWN: code = BN_PUSHED; break; case WM_LBUTTONUP: code = BN_UNPUSHED; break; } if(code) SendMessage(GetParent(hwnd), WM_COMMAND, MAKELONG(GetDlgCtrlID(hwnd),code), (LPARAM)hwnd); if(wpBase) return CallWindowProc(wpBase,hwnd,uMsg,wParam,lParam); return 0; } La macro MAKELONG (sempre dagli header di Windows) compone una longword (ULONG) da due parole di 16 bit l'una, prendendo come primo argomento la "meno significativa", cioè quella da inserire nella LOWORD del risultato.

La precauzione di controllare che wpBase non sia nullo, prima di delegargli l'esecuzione, può non essere strettamente necessaria, ma è in generale un buon stile; in alternativa, si può usare un'asserzione assert(wpBase) all'ingresso in WpSuper, poichè essa non dovrebbe mai trovarsi ad eseguire senza che wpBase sia già stata impostata.

 

Con quest'ultima modifica, il nostro programma, finalmente, fa proprio il lavoro che desideravamo. C'è ancora un singolo, grave difetto, un difetto strutturale: è proprio questa variabile globale wpBase, che abbiamo appena inserito "per comodità"... le variabili globali sono una iattura, e Windows ci dà tutti gli strumenti necessari per farne quasi totalmente a meno. Nel prossimo capitolo, riproporremo l'intero programma, nel "giusto ordine", e, in particolare, risolveremo questo problema.


Capitolo 17: alcune migliorie
Capitolo 19: l'esempio rivisitato (1)
Elenco dei capitoli