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:
cMillisec
è il periodo, in millisecondi, che dovrà
passare fra due successive chiamate alla ProceduraTimer
da parte del sistema; il timer si disattiva chiamando:
SetTimer
e KillTimer
tornano entrambe zero in caso di
errore; la GetLastError
darà allora il codice d'errore).
Ad esempio, quindi, noi potremmo:
BN_PUSHED
, attivare
un timer sulla finestra del dialogo, con id pari all'id
del bottone, e un periodo, ad esempio, di 200 mSec;
BN_UNPUSHED
, disattivare questo timer;
HWND
del dialogo, e id del bottone, saranno fra i
parametri della procedura di timer; da questi, possiamo
ottenere anche l'HWND
del bottone, con GetDlgItem
, e
infine chiamare
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:
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:
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:
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:
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
,
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:
wpBase
, di tipo WNDPROC
, inizializzata a zero;
WpSuper
, che tiene conto delle pressioni e
dei rilasci dei bottoni ma delega poi comunque la
gestione di ogni evento a wpBase
(anche pressioni e
rilasci non vengono gestiti interamente da WpSuper
:
la wpBase
va comunque informata di questi eventi,
poichè provvede a ridisegnare il bottone in stato
premuto o non-premuto, e a generare la notifica di clic);
OnDlgInitDialog
,
aggiungiamo la "istallazione" del subclassing:
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):
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