Lezione 3 – I controlli e messaggi di Windows



By admin on settembre 12, 2010


Nel file MIODIALOGO.RC usato alla Lezione 2 per preparare il nostro primo dialogo, avevamo descritto una finestra di dialogo vuota, poiché nulla era elencato fra le righe di BEGIN ed END. Per rimediare a questo, basta inserire in questa zona delle righe che descrivano i controlli che la nostra finestra di dialogo deve contenere.
Un "controllo" è normalmente una finestra che è progettata appositamente per essere "figlia" di un'altra; di solito, specificamente, figlia di una finestra di dialogo. Il controllo e la finestra si scambiano messaggi, attraverso i quali comunicano; il controllo, inoltre, ha di solito, fra le proprie caratteristiche, quella di interagire con l'utente sia "mostrandogli" qualcosa, sia, spesso, ricevendone l'input attraverso mouse e tastiera, e queste interazioni generano, alla fin fine, altri messaggi dal controllo verso la finestra.
Avremo dunque, dopo le istruzioni RC che danno i vari dettagli della finestra, qualcosa come:

BEGIN
  CONTROL  "stringa", nId, classe, stile, x, y, larg, alt
END

dove nId è un arbitrario numero d'identificazione (dovremo poi usarlo, nel nostro codice, quando vorremo fare riferimento a questo controllo); classe può essere una delle parole-chiave seguenti: BUTTON, COMBOBOX, EDIT, LISTBOX, SCROLLBAR, STATIC (vi sono altre possibilità, ma per ora è decisamente meglio limitarci a queste!); stile è una combinazione di "codici di stile", come si è già visto per l'istruzione STYLE della finestra di dialogo (a ogni classe di controllo sono applicabili stili diversi); x e y sono le coordinate, entro la finestra di dialogo, dell'angolo in alto a sinistra del controllo; larg e alt, la larghezza e l'altezza del controllo. 
I controlli STATIC sono forse i più semplici, e fra i più usati. Uno STATIC può essere un campo di testo in solo output, o un semplice rettangolo che si staglia sul nostro dialogo, o può mostrare una immagine (bitmap), a seconda del suo "stile".
Gli usi più frequenti degli STATIC sono i testi. SS_LEFT, SS_CENTER, e SS_RIGHT, sono i tre tipici stili usati per mostrare del testo in sola uscita, testo che verrà, rispettivamente, allineato a sinistra, o centrato, o allineato a destra; esistono altri stili di testo, ma non occorre preoccuparsene. Il testo mostrato, naturalmente, è quello passato come primo argomento all'istruzione CONTROL.
Altro uso semplice, e tipico, di STATIC, è quello di mostrare, sul dialog, semplici rettangoli. I rettangoli "pieni" sono disegnati dagli STATIC con stile SS_BLACKRECT, SS_GRAYRECT, SS_WHITERECT; quelli col solo "perimetro", invece, usando lo stile SS_BLACKFRAME, SS_GRAYFRAME, SS_WHITEFRAME. Non fatevi ingannare dal significato letterale di BLACK ("nero", ecc); i GRAY usano il colore di sfondo dello schermo (grigio solo per default, ma l'utente può averlo cambiato come preferenza!), e similmente i BLACK usano il colore dei bordi delle finestre, i WHITE il colore degli sfondi delle finestre.
Per tutte le versioni di Win32 uscite negli ultimi anni, cioè Win/95, Win/NT 4.0, e successivi, si possono usare anche altri stili interessanti: SS_BITMAP, per avere uno static che mostra una bitmap, o anche SS_OWNERDRAW, per impegnarci a disegnarlo noi stessi; inoltre, ogni stile può essere combinato (col solito | alla C) con SS_SUNKEN, per dare al rettangolo dello STATIC l'aspetto "affossato" nel tipico look 3D; e con SS_NOTIFY, che dice al controllo di "avvertire" la finestra di dialogo, con opportuno messaggio, se l'utente fa clic del mouse sull'area del controllo stesso.
E dunque: modifichiamo solo il nostro file di risorse, non il nostro programma; nel file .RC, inseriamo fra BEGIN e END la singola riga (qui divisa in più righe, per comodità di formato; il resource compiler permette di dividere una sola riga logica in più righe fisiche):

CONTROL "Ciao!", 101, "STATIC",  SS_CENTER|WS_BORDER, 60, 40, 40, 10

Lo stile WS_BORDER, applicabile a qualsiasi finestra, indica che vogliamo un "bordo" (cioè un contorno) per questa scritta.
Compilato questo .RC in un .RES, e fatto quanto occorra, col nostro RC e/o linker, per sostituire questo file di risorse a quello che avevamo già definito in precedenza entro il nostro EXE, possiamo fare nuovamente eseguire l'EXE stesso.
Mentre la nostra finestra era, prima, vuota (con lo stesso codice eseguibile), adesso ha, un po' a sinistra del centro, la scritta "Ciao!", con attorno un bordo rettangolare. è interessante ribadire che il nostro codice eseguibile non è cambiato; è solo la situazione delle nostre risorse, quindi, che determina quale finestra di dialogo il nostro programma stia mostrando all'utente.
Questo dialogo, naturalmente, resta piuttosto "statico", per quanti "static" possiamo mettergli dentro, con scritte di vari stili. Perché il nostro programma faccia qualcosa, sarà necessario che scriviamo del codice per rispondere agli eventi, così come abbiamo dovuto gestire il messaggio WM_CLOSE per chiudere il dialogo (e quindi il programma) in risposta al click dell'utente sulla crocetta in altro a destra del dialogo.
Proseguiremo, quindi, esaminando come le azioni dell'utente si traducono in messaggi alle finestre del programma, e alcune azioni che il programma può fare in risposta a questi eventi (messaggi).

 

Rispondere ai messaggi
Abbiamo accennato al fatto che un controllo static, se fra i suoi bit di stile ha SS_NOTIFY, ci "notifica" (cioè, "ci avverte") se e quando l'utente fa click con il mouse su di esso. Più precisamente, quello che succede in questo caso è che lo static (per intenderci, il codice eseguibile associato alla finestra di classe "static", codice che il sistema ci fornisce) spedisce alla finestra di dialogo che lo contiene un messaggio WM_COMMAND. Più in generale, WM_COMMAND viene spedito ad una finestra di dialogo a fronte di vari possibili eventi, che, per modo di dire, possiamo considerare eventi "di comando", tipo le selezioni su di un menu, e varie azioni dell'utente su controlli contenuti nelle finestre. Nei due argomenti, wParam ed lParam, che accompagnano il messaggio WM_COMMAND, sono contenuti tre elementi di informazione. Questo "impacchettamento" è uno schema che ritroviamo abbastanza spesso in Windows; qui, in particolare, è basato sul fatto di considerare i 32 bit di wParam come composti da due parole di 16 bit l'una. Le macro HIWORD e LOWORD possono essere applicate a qualsiasi "longword" ("parola lunga"), che è di 32 bit, per ottenere le due "parole" ("word", di 16 bit l'una) che la compongono.
Per WM_COMMAND, in particolare:

HIWORD(wParam) -> "codice di notifica"
LOWORD(wParam) -> "identificatore"
lParam         -> HWND del controllo

L'identificatore ID è il numero che abbiamo associato al controllo nel file .RC (il nostro solo e unico STATIC, ad esempio, era stato creato con id 101); lParam su cui sarà necessario un cast, (HWND)lParam, per usarlo, ci informa della "window handle" con la quale possiamo "rispondere" al controllo che ci sta notificando.
Il "codice di notifica" identifica quale evento, esattamente, sia successo. Ad esempio, per il click su di uno STATIC, il codice di notifica (cioè la parola alta di wParam) vale STN_CLICKED (che e' naturalmente una macro definita negli header file di Windows).
Una volta avvertito dell'evento, il nostro codice è padrone di rispondere ad esso nel modo più opportuno, a seconda della nostra applicazione.
Per esempio, possiamo rispondere al click dell'utente sul nostro STATIC, cambiando il testo dello STATIC stesso. Per cambiare il testo di una finestra, si può spedire alla finestra stessa il messaggio WM_SETTEXT, con 0 in wParam, e, in lParam, l'indirizzo della stringa (terminata da 0) che si vuole impostare. Viceversa, per chiedere a una finestra quale sia il suo attuale testo, si può spedirle WM_GETTEXT, con, in lParam, l'indirizzo iniziale di un nostro buffer di caratteri, e, in wParam, la lunghezza di questo buffer.
Per spedire messaggi a una finestra, possiamo usare l'API SendMessage: il suo primo argomento è l'HWND della finestra, il secondo il codice del messaggio, il terzo wParam, il quarto lParam. La SendMessage ci ritorna, come risultato (di tipo LRESULT), la "risposta" della finestra a questo messaggio.
Supponiamo, dunque, che il nostro scopo sia di commutare il messaggio dello static, alternativamente, fra "Ciao!" e "Salve", quando ci arriva la notifica del click dell'utente sul controllo. Possiamo farlo, nel modo più semplice (cioè, senza salvarci un bit di "stato" che ci dica a quale messaggio tocca!), come segue:

  • chiedere alla finestra qual'è il suo testo attuale,
  • esaminare la risposta, e impostare l'altra stringa come testo della finestra.

Notiamo, come considerazione generale, che è di solito meglio, quando sia possibile, evitare di duplicare informazioni, come si farebbe qui tenendo un "bit di stato" per codificare la stessa informazione che già possiamo recuperare chiedendo e riesaminando il testo attuale della finestra del controllo; la "ottimizzazione" consistente nel mantenere dello "stato" ridondante per evitare di "ricalcolarlo", così come tutte le ottimizzazioni, va applicata con la massima parsimonia e solo a fronte di necessità effettivamente controllate e verificate.
Dovremo dunque fare due piccoli cambiamenti; uno al nostro file .RC, cambiando l'istruzione STATIC in:

CONTROL "Ciao!", 101, "STATIC", SS_CENTER|WS_BORDER|SS_NOTIFY, 60, 40, 40, 10

 

e uno al nostro file sorgente, cambiando la dialog procedure in:

BOOL CALLBACK unaDlgProc(HWND hwndDlg,UINT uMsg, WPARAM wParam, LPARAM lParam)
{
 if(uMsg==WM_CLOSE)
   {
    EndDialog(hwndDlg, 0);
    return TRUE;
   }
 else
  if(uMsg==WM_COMMAND)
    {
     WORD wNotifica = HIWORD(wParam);
     WORD wId = LOWORD(wParam);
     if(wNotifica == STN_CLICKED && wId == 101)
       {
        HWND hStat = (HWND)lParam;
        char buff[6];
        const char* pTesto;
        SendMessage(hStat, WM_GETTEXT, 6, (LPARAM)buff);
        if(buff[0]=='C') pTesto = "Salve";
        else pTesto = "Ciao!";
        SendMessage(hStat, WM_SETTEXT, 0, (LPARAM)pTesto);
        return TRUE;
       }
    }
 return FALSE; 
}

 
Esaminando questo piccolo programma, e riflettendo su di esso, vengono in mente parecchie considerazioni importanti; troppo importanti, però, per "comprimerle" nella coda di questo capitolo, le rimanderemo a un capitolo successivo. Per ora, invece, ci lasciamo con un primo, piccolo esercizio: cambiare le risorse, ed il programma, in modo che nello static sia sempre mostrato il numero di volte che l'utente ha cliccato su di esso, in altri termini, un contatore di click.

 

Identificare le risorse
Come accennato in chiusura del precedente paragrafo, l'esame del programmino ivi esemplificato dovrebbe sollevare un paio di dubbi in termini di "struttura": non ci sono, in questi schemi, le radici implicite di futuri problemi, una volta che il programma dovesse crescere ed espandersi? Sì, ci sono almeno due ordini di problemi, uno piuttosto semplice e chiaro, che affronteremo in questo paragrafo, e uno più sottile e profondo, che rimanderemo al prossimo paragrafo.
Il primo problema, in un certo senso banale, è dovuto alla necessità di avere, nei due sorgenti .RC e .C (o .CPP), valori identici per i nomi delle risorse e i numeri di identificazione dei controlli; tutto bene, per "programmini" di poche righe, ma come potremo tenere sotto controllo questa necessaria identità in un programma appena un po' più sostanziale? La soluzione più semplice è esattamente la stessa che si pone all'analogo problema di usare gli "stessi" valori in diversi file di sorgenti C (o C++): invece di ripetere in ciascun file-sorgente il valore letterale di una costante (col rischio di non "tenere sotto controllo" questo valore), basta usare una macro del preprocessore C, definita con #define in un header file (che, nello specifico caso di controlli e risorse, è di solito chiamato resource.h).
Il C++ offre anche l'alternativa, analoga ma più elegante e certo migliore, di definire questi valori letterali con la parola-chiave const, sempre in un header file; tuttavia, la soluzione del #define è tutto sommato adeguata, e l'unica possibile nel caso che ci occupa, quello della collaborazione fra il compilatore C e il resource-compiler RC. Il file delle definizioni, resource.h, andrà poi incluso, con #include, sia nel .C, sia nel .RC. Fortunatamente, il resource compiler RC usa i semplici costrutti del preprocessore C, per cui le cose si incastrano bene, naturalmente, non è certo un caso che ciò accada: il compilatore di risorse, RC, è stato deliberatamente e appositamente progettato per condividere macro con i compilatori C e C++ attraverso l'uso di #define e #include, proprio allo scopo di permettere questo uso. Per la stessa ragione, RC ignora silenziosamente la maggior parte della sintassi C, o C++, che non conosce e gli è irrilevante, come le varie dichiarazioni di tipi e funzioni, anche se resta comunque molto consigliabile non aver altro, in "resource.h", che #define e commenti.
Sempre allo scopo di facilitare questo approccio, è possibile usare per i nomi di risorse dei numeri anziché delle stringhe; in questo caso, si userà la macro MAKEINTRESOURCE(numero) per ottenere la "stringa" da passare come nome di risorsa alle API che vogliono appunto valori di questo tipo. Questi sono, quindi, problemi abbastanza semplici, cui si può dare una soluzione univoca, tradizionale, e chiaramente OK. In futuro, daremo per scontato che sia incluso un header file resource.h, nel quale saranno state definite macro con nomi come "IDD_qualcosa" per le risorse finestre di dialogo, e nomi come "IDC_qualcosaltro" come identificatori dei vari controlli, ecc., e useremo direttamente negli esempi delle macro di questo tipo. Il programma del procedente paragrafo, come già accennato, dovrebbe tuttavia suggerire anche riflessioni di un altro tipo.

 

Problemi di struttura
Il programmino presentato nel precedente Capitolo ci ha già suggerito un tipo di problema, quello di garantire l'univocità di identificazione delle risorse, cui abbiamo proposto una soluzione nel precedente paragrafo.
Più sottile è un secondo tipo di problema, la cui radice sta nel semplice "if ... else if ..." che funge da "corpo" della dialog procedure e non è che sostituire questo costrutto con uno switch migliorerebbe di molto le cose!
La "natura profonda" del problema è questa: una dialog procedure può ricevere centinaia di tipi diversi di messaggi a cui rispondere e alcuni di essi, come WM_COMMAND, prevedono poi a loro volta molti sotto-casi, completamente distinti l'uno dall'altro dal punto di vista della logica funzionale del programma.
Inserire in una sola funzione, come si può, ahimè sin troppo facilmente!, essere tentati di fare, tutta questa logica disparata e varia, sarebbe però un gravissimo errore strutturale: una funzione dovrebbe, infatti, "svolgere un compito", non centinaia di compiti diversi a seconda degli argomenti! Il "motto" da ricordare e seguire è: "una funzione ha una funzione" (leggendo la parola "funzione" in due sensi diversi dai due lati del verbo "ha": a sinistra, quello del linguaggio C o C++, a destra, quello di "scopo", "ruolo", "compito").
Se si adotta invece la "struttura spontanea" per una dialog procedure, imperniata su di un unico grosso switch, si buttano tutti i principi di modularità, e, con essi, ogni chance di scrivere un programma di buona qualità, tale da essere ragionevolmente "usabile" in futuro.
La soluzione non è difficile e infatti, è stata re-inventata più volte, indipendentemente. Si tratta, in sostanza, di approcci alternativi alla soluzione del problema, di per se assai generale, di "eseguire compiti diversi a seconda di un valore di input (o più di uno)".
L'approccio "ingenuo" usa switch, o if/else if, per dirimere fra gli input, e, una volta determinato in questo modo quale caso si sia presentato, esegue codice nel corpo stesso dello switch (o if/else); quello "semi-ingenuo", non molto migliore, usa lo stesso tipo di switch, ma, una volta determinato il caso che si è presentato, chiama poi una funzione per fare "il lavoro vero". (Ci sono anche sub-approcci "più svegli", imperniati su quello "semi-ingenuo" ma con una qualche "idea salvatrice", di varie possibili nature, che li rende un poco migliori; ma quello che tutti hanno in comune è che la logica di base della "scelta del compito da eseguire" è espressa in termini di "codice eseguibile" -- il famoso switch o if/else).
Gli approcci migliori si basano su una struttura dati per registrare la corrispondenza fra "caso che si è presentato" e "lavoro da fare in quel caso"; i puntatori a funzione servono, in C, proprio per accedere all'istante a questo "lavoro da fare", espresso, normalmente, in una funzione (una funzione che, come da motto, ha una funzione, specificamente quella di gestire un singolo, specifico messaggio, o un piccolo gruppo di messaggi fortemente correlati fra di loro).
In C++, sono possibili approcci abbastanza diversi anche se, alla fin fine, anch'essi si concretano in "tabelle di puntatori a funzione" ma molto può, se vogliamo, gestircelo rapidamente il compilatore, in modo implicito, elegante, e sicuro, ma penso che, visto che molti utenti C++ non hanno comunque un chiaro modello mentale di cosa corrisponde a ogni costrutto del linguaggio (testo caldamente suggerito: "Inside the C++ Object Model", S. Lippmann, Addison-Wesley!), sia più istruttivo esaminare solo approcci C.
Un framework di questo tipo, nei primi anni 90, lo promulgò ad esempio A. Schulmann (per Windows 3.0, un prodotto a 16 bit), nel suo ottimo testo "Undocumented Windows". Nello schema di Schulmann, si associa ad ogni finestra un semplice array, che usa come indice il codice di messaggio ricevuto; ogni cella dell'array contiene, semplicemente, il puntatore alla funzione da eseguire, oppure 0 per indicare "nulla di speciale, usa il default". Ci sono circa un migliaio di messaggi, quindi 4,000 puntatori per finestra (oggi, a 32 bit, 16Kbyte) è l'overhead di spazio di questo metodo; a fronte dello spazio notevole, bisogna ammettere la sua semplicità, rapidità, flessibilità.
In termini moderni, avremmo qualcosa come:

typedef DLGPROC dlgarray[WM_USER];
dlgarray gestori;


BOOL CALLBACK genericDP(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
 DLGPROC delega = 0;
 if(uMsg<WM_USER) delega = gestori[uMsg];
 if(delega) return delega(hwndDlg,uMsg,wParam,lParam);
 return FALSE;
}

C'è ancora un piccolo problema: questa cosiddetta generica dialog procedure usa lo stesso array di puntatori a funzione per tutti i dialoghi che gestisce, soluzione poco elegante, in quanto imperniata su di un array globale, e, per la stessa ragione, assai poco flessibile e generale.
Per migliorare la flessibilità, si può associare a ogni HWND di un dialogo gestito, un diverso array di puntatori a funzione (dlgarray); a questo scopo si può usare qualsiasi struttura dati che implementi una corrispondenza (o "mappa", o "array associativo", ecc, che dir si voglia) o, in alternativa, si può utilizzare uno dei vari tipi di corrispondenze HWND → dati-utente che l'API di Windows mette a disposizione.
Vi sono inoltre dei problemi di prestazioni. La grande maggioranza dei dialoghi hanno effettiva necessità di gestire un modesto numero di messaggi, sulle centinaia che sono possibili, per la grande maggioranza dei messaggi, la funzionalità di default che si ottiene ritornando FALSE ("non gestito") dalla dialog procedure è proprio quello che si desidera. Ma l'approccio "array di gestori" obbliga comunque a riservare migliaia di byte di memoria nell'array di puntatori a funzioni, anche se la grande maggioranza delle entry saranno zero, e solo poche avranno effettivamente dei puntatori "utili". Lo spazio sprecato implica anche spreco di tempo, a causa dell'uso della memoria virtuale.
Questo è il classico "problema della matrice sparsa", ed è suscettibile delle classiche soluzioni a questo problema. Una di esse, fra le più semplici, è di sostituire all'array una lista di coppie (indice,contenuto); invece di array[indice] si usa un ciclo di ricerca, in pseudo-codice:

    esamina tutte le coppie nella lista:
      se l'indice che cerchiamo e' il
        primo elemento della coppia,
      abbiamo trovato, torna il contenuto
    se non trovato, torna zero

 
Naturalmente, se vi sono troppe coppie nella lista, questa ricerca lineare può dare tempi troppo lunghi; possono usarsi tutti i classici schemi di algoritmi e strutture dati applicabili in questi casi, dalla ricerca binaria su array ordinato, alle tabelle di hash, agli alberi red-black (o altre variazioni), ecc. In pratica, visti i piccoli numeri di messaggi tipicamente gestiti (al massimo qualche decina), la ricerca lineare va benissimo, magari con un minimo di cura nel mettere verso l'inizio della lista quei messaggi da gestire che sappiamo essere particolarmente frequenti, o particolarmente vincolati a tempi di risposta più stretti.
Vista l'importanza centrale di questo tema per l'organizzazione di "grosse" applicazioni Windows, esistono molti altri schemi, e variazioni su di essi, comprese "raffinatezze" ovvero "complicazioni" di ogni tipo. Accenneremo più avanti a uno schema semplice, e built-in in Windows, detto "message crackers", che presenta vantaggi d'altro tipo.
Il punto importante, in quest'ottica, è un altro: non ci preoccuperemo più di tanto, nel seguito, del tema "come determinare quale codice eseguire a fronte di un certo messaggio o comando", dando invece per scontato che sia in piedi un qualche "schema di gestione", tra i vari su elencati, che, per una via o per l'altra, sia in grado di "consegnare" alla funzione più appropriata il messaggio da gestire, coi relativi parametri; e ci interesseremo, invece, sostanzialmente a quali siano questi messaggi, e quale lavoro debbano fare le nostre funzioni scritte per gestirli.

 

Controlli: I BUTTON
Gli STATIC, benché utili e flessibili, non sono certo gli unici tipi di controlli predefiniti che Windows ci mette a disposizione. I "controlli elementari", infatti, comprendono anche BUTTON, COMBOBOX, EDIT, LISTBOX, e SCROLLBAR, mentre i più avanzati controlli comuni iniziano con il "Rich Edit" (normalmente nelle due versioni, rispettivamente ANSI e UNICODE, RichEdit20A e RichEdit20W; la macro RICHEDIT_CLASS, definita, naturalmente, negli header file di sistema, permette di usare il nome di classe migliore in ciascun caso.
I controlli comuni proseguono, poi, con Animate, HotKey, Date-Time Picker, ProgressBar, ReBar, StatusBar, Tooltip, Trackbar, UpDown, Combo Box estesa, Header, IP-Address, Listview, Pager, TabControl, e TreeView (e altri ancora, in versioni più recenti di Win32!). I nomi delle classi di tutta questa varietà di controlli "comuni" tendono a cambiare spesso, per cui per essi si usano normalmente delle macro definite, assieme a tante altre cose, nell'header file di sistema <commctrl.h>.
L'enorme varietà e ricchezza di questi controlli ci impedirà, naturalmente, di darne una trattazione soddisfacente soprattutto considerando quale vasta, enorme ricchezza funzionale di dettaglio c'è dietro ciascuno di questi controlli predefiniti!
Presenteremo, invece, i controlli più importanti ed elementari, ed eventualmente qualche cenno ed esempio di argomenti più avanzati; quanto basta per indirizzare il lettore verso un uso proficuo dell'help in linea del Platform SDK, di MSDN, e di altri testi disponibili su questi temi.
I controlli BUTTON sono abbastanza semplici e versatili. Un BUTTON è quasi sempre un modo per l'utente di comunicare al nostro programma, "cliccando" (col tasto sinistro del mouse, o con la tastiera) sul bottone stesso; a seconda del suo "stile", può essere un "pushbutton", una "checkbox", o un "radiobutton", con aspetti e significati diversi.
Esistono inoltre i pulsante "owner-drawn" (cioè "disegnati dal proprietario"), nel qual caso il nostro programma si rende pienamente responsabile per l'aspetto del pulsante e per il suo comportamento; e i pulsante "group box", che, pur appartenendo alla stessa classe di finestre, non ricordano per nulla i pulsanti, bensì, se mai, gli static, poiché si tratta di pure "decorazioni" della finestra di dialogo, senza alcuna interazione con l'utente, l'aspetto è quello di un rettangolo, normalmente posto attorno a un gruppo di altri controlli, appunto allo scopo di "raggrupparli" visivamente, con un'etichetta di testo in alto a sinistra.
Un "push button" è il più tipico e riconoscibile dei "bottoni". Il suo stile-base è BS_PUSHBUTTON; uno, e uno solo, dei bottoni dei dialogo può invece essere designato come BS_DEFPUSHBUTTON, cioè "bottone di default", esso verrà disegnato in modo leggermente diverso ed "enfatizzato", e, se l'utente aziona il tasto Enter (Return) dovunque nel dialogo, sarà "proprio come se" avesse cliccato il pulsante di default.
Un push button mostra il proprio testo (primo argomento dell'istruzione CONTROL nel file RC) centrato nel "pulsante" vero e proprio. Quando viene cliccato, come tutti i pulsanti, avverte il dialogo che lo contiene con il messaggio WM_COMMAND, codice di notifica BN_CLICKED (proprio come uno STATIC con il bit di stile SS_NOTIFY, se cliccato, avverte il dialogo con lo stesso WM_COMMAND, ma codice di notifica SN_CLICKED).
Una "check box" è una casella che può essere "spuntata" ("checked") o meno, con un testo esplicativo a fianco. Con lo stile BS_CHECKBOX, si ottiene il comportamento-base: ogni volta che l'utente clicca, il dialogo riceve il WM_COMMAND, ed è responsabile di gestire lo stato on/off della casella "check box". Spesso utile è invece lo stile BS_AUTOCHECKBOX, in cui la casella "commuta" automaticamente il proprio stato on/off (oltre a spedire il solito WM_COMMAND). Esistono anche caselle "a tre stati" (BS_3STATE, e BS_AUTO3STATE) che, oltre agli stati on e off, prevedono un "terzo stato indeterminato" (che è rappresentato "ingrigendo" la casella).
Un "radio button" è simile a una checkbox (gli stili sono BS_RADIOBUTTON e BS_AUTORADIOBUTTON), con un aspetto un poco diverso (rotondo, con o senza "pallino", invece che quadrato con o senza "spuntatura"), e una cruciale differenza di comportamento: un radiobutton non si "spegne" quando l'utente vi clicca, bensì quando si clicca su di un altro radiobutton, che si accende. I radiobutton indicano, insomma, una situazione in cui l'utente può scegliere una tra le N possibilità offerte, reciprocamente esclusive.
La determinazione "automatica" di quali radio button di un dialogo siano nello stesso "gruppo" si appoggia sul loro flag di stile WS_GROUP e sull'ordine in cui sono stati creati nel dialogo. Un "gruppo" comincia dal primo controllo creato, e continua, nell'ordine di creazione, sino a che non si incontra un controllo con WS_GROUP, quest'ultimo è escluso dal gruppo; ciascun gruppo successivo al primo inizia da un controllo con WS_GROUP, incluso, e prosegue, sempre nell'ordine di creazione, sino al prossimo controllo che ha WS_GROUP, escluso.
Questo concetto di "gruppo", tra l'altro, oltre alla sua funzione cruciale per gruppi di radio button, è anche assai rilevante relativamente alle possibilità di "navigazione" all'interno del dialogo, da parte dell'utente che voglia usare la tastiera invece del mouse: un "gruppo" di controlli (come qui definito) è anche l'insieme di controlli fra cui l'utente può muoversi con i tasti-freccia della tastiera. L'ordine di creazione dei controlli (che, usando un file .RC, è esattamente l'ordine con cui i controlli sono elencati fra il BEGIN e l'END del dialogo) è poi anche lo stesso ordine in cui l'utente si può muovere da un controllo all'altro usando TAB, gruppi a parte (usando il tasto TAB, l'utente potrà raggiungere direttamente solo i controlli che hanno il bit di stile WS_TABSTOP).
Se non usiamo gli stili AUTO, lo stato on/off di check box e radio button dipende solo dai messaggi che il nostro codice invia ai controlli stessi. Il messaggio BM_SETCHECK, con lParam obbligatoriamente 0, contiene in wParam il valore che vogliamo impostare per lo stato di check del check box o radio button cui lo spediamo: BST_CHECKED fà sì che il bottone appaia "impostato" (col segno di spuntatura, o il pallino, secondo il suo stile), mentre BST_UNCHECKED fà sì che il bottone appaia "non impostato" ("vuoto").
Si può anche chiedere ad un bottone il suo stato di check, spedendogli il messaggio BM_GETCHECK; lo stato sarà allora comunicato come valore di ritorno della API SendMessage usata per spedire questo messaggio (i possibili valori di ritorno sono, nuovamente, BST_CHECKED e BST_UNCHECKED).
Lo stile BS_BITMAP e quello BS_ICON sono simili a quelli SS_BITMAP e SS_ICON: il bottone mostra un'immagine (bitmap, o icona, rispettivamente) invece di un testo. L'immagine può essere impostata col messaggio BM_SETIMAGE (così come, per uno static, può esserlo col messaggio STM_SETIMAGE).
Ma torneremo su questi temi più avanti, quando inizieremo ad affrontare i problemi della grafica in Windows; similmente, rimandiamo la gestione degli "owner-drawn" (sia per i pulsanti, sia per gli static), visto che, sino ad allora, non sapremmo ancora come e cosa disegnare. Lo stesso vale per i messaggi di tipo WM_CTLCOLOR..., che permettono ad una finestra di dialogo di stabilire i colori dei controlli che contiene.
Così come uno STATIC può essere abilitato a spedire alcuni messaggi (tipo SN_CLICKED) alla finestra di dialogo col bit di stile SS_NOTIFY, così un BUTTON può essere abilitato a spedire altri messaggi (oltre a BN_CLICKED, che esso spedisce comunque!) alla sua finestra di dialogo, con il bit di stile BS_NOTIFY. I messaggi che così si ottengono sono:

  • BN_DOUBLECLICKED, il "doppio click" (la documentazione Microsoft dice che si tratta di un messaggio obsoleto presente solo per compatibilità, ma questo è un errore della documentazione stessa!)
  • BN_SETFOCUS e BN_KILLFOCUS, spediti quando il bottone ottiene e perde il "focus di input"
  • vari altri cambiamenti di stato

Viceversa, se un'applicazione, per qualche strana ragione, vuole simulare il click su di un pulsante da parte dell'utente, può farlo spedendo al pulsante stesso il messaggio BM_CLICK (con entrambi i parametri obbligatoriamente pari a zero); questo messaggio deve essere spedito solo ai pulsanti di un dialogo che è attualmente attivo.
Naturalmente, anche altri stili (come WS_BORDER, cui già abbiamo accennato), applicabili a generiche finestre, sono, in particolare, applicabili anche ai pulsanti; così pure, altri messaggi (come WM_GETTEXT e WM_SETTEXT, cui pure accennammo), sono, come a tante altre finestre, applicabili anche ai pulsanti.

 

ID e HWND

Parlando di spedizione messaggi, è il caso di evidenziare un'altra particolarità. I messaggi (almeno per quanto abbiamo visto sino ad ora!) si spediscono sempre ad una finestra, e richiedono, quindi, l'HWND di quella finestra. Tutto bene, dunque, se l'HWND della finestra ci viene resa disponibile nel contesto in cui vogliamo interagire con essa, ad esempio, come abbiamo visto precedentemente, quando, a fronte di un WM_COMMAND, vogliamo agire sulla finestra (controllo) che ce l'ha spedito, poiché la HWND di quel controllo è l'lParam del messaggio stesso.
Ma spesso, naturalmente, a fronte di eventi su di un certo controllo X, noi vorremo agire su di un diverso controllo Y; ad esempio, cambiare la scritta su di uno STATIC, a fronte di click, non sullo static stesso, ma su di un certo BUTTON con stile push button. Come si fa, allora, a recuperare l'HWND dello STATIC, quando nel messaggio WM_COMMAND ci arriva invece quella del BUTTON? Dello STATIC, e in generale dei nostri controlli, noi conosciamo a tempo di compilazione l'ID associato al controllo. L'HWND, invece, cambia ad ogni diversa esecuzione del programma, non c'è modo di determinarla a priori, una volta per tutte, mentre il programma viene compilato!
Windows, naturalmente, ci fornisce la funzionalità richiesta: risalire dall'ID di un controllo alla sua HWND (e viceversa, avendo una HWND, ottenere l'ID del controllo cui essa corrisponde).
Le API fondamentali per questo compito sono:

HWND GetDlgItem(HWND hDlg, // il dialogo
    int nIDDlgItem         // ID del controllo
);

torna l'HWND desiderata, o 0 se non esiste in quel dialogo un controllo con quell'ID;

int GetDlgCtrlID( HWND hwndCtl // il controllo 
);

questa va "nel verso opposto", cioè recupera l'ID conoscendo l'HWND del controllo.
Infine, l'API...:

HWND GetParent(HWND hWnd // il controllo
);

ci permette, data l'handle di una finestra figlia, come ad esempio un controllo, di risalire a quella della finestra che la contiene (e questa API, affidabilmente, torna zero se non c'è nessuna "finestra-genitrice" per la HWND passatale come argomento).
Grazie alla presenza di queste API, non abbiamo bisogno di "registrarci" da nessuna parte queste fondamentali relazioni ID<->HWND<->genitore: possiamo sempre recuperarle dinamicamente a seconda delle esigenze. Nulla ci impedisce in effetti di "salvarci" da qualche parte una copia di queste info, ma non è strategia particolarmente consigliabile: il "costo" in termini di tempo per recuperarle è molto modesto, da bilanciare contro il "costo" in termini di spazio di salvarle da qualche parte (e in un sistema a memoria virtuale, dobbiamo ricordarlo, sprecare spazio significa alla fin fine sprecare tempo!); e soprattutto, per principio generale, è meglio non tenere copie duplicate e ridondanti di nessun tipo di informazioni, onde evitare errori di "disallineamento" fra le diverse "versioni" delle informazioni stesse.
Ecco, dunque, un esempio di gestore di WM_COMMAND che interagisce con molteplici controlli: se viene cliccato il pushbutton di identificatore IDC_PRIMO, il gestore scrive, nello STATIC con identificatore IDC_SCRITTA, il testo "Primo", se invece viene cliccato quello con identificatore IDC_SECONDO, scrive nello stesso STATIC, il testo "Secondo". Come abbiamo accennato, usiamo due piccole comodità del C++: commenti con //, e dichiarazione delle variabili solo quando servono; per usare C puro, cambiare tutti i commenti alla forma /* ... */, e spostare tutte le dichiarazioni all'inizio della funzione.

BOOL TreControlli(HWND hDialogo, UINT nMessage, WPARAM wParam, LPARAM lParam)
{
 // verifiche -- magari ridondanti -- che questo sia proprio
 // un caso che gestiamo: e' proprio un WM_COMMAND?
 if(nMessage != WM_COMMAND) return FALSE;

 // e' proprio il clic di un bottone?
 UINT codice = HIWORD(wParam);
 if(codice != BN_CLICKED) return FALSE;

 // e' proprio un bottone di nostro interesse?
 int idCliccato = LOWORD(wParam);
 if(idCliccato!=IDC_PRIMO && idCliccato!=IDC_SECONDO) return FALSE

 // OK, il messaggio e' per noi -- gestiamolo!
 HWND hScritta = GetDlgItem(hDialogo,IDC_SCRITTA);
 if(!hScritta) return FALSE;    // ulteriore controllo...!

 // scritta, e simultaneo "ultimo controllo d'errore":
 return SetWindowText(hScritta, idCliccato==IDC_PRIMO?"Primo":"Secondo");
}


Abbiamo qui usato la suaccennata funzione SetWindowText, invece che spedire un messaggio WM_SETTEXT, se non altro per la comodità di averne già un ritorno TRUE, o FALSE, direttamente usabile come risultato da ritornare dal nostro gestore.
Windows offre ulteriori funzioni API che possono tornar comode, benché non siano certo indispensabili, per interagire con i controlli di un dialogo. Ad esempio, invece di trovare l'HWND con GetDlgItem, poi settare il testo con la SetWindowText, si può direttamente chiamare l'API:

BOOL SetDlgItemText(HWND hDlg, // il dialogo
    int nIDDlgItem,            // ID del controllo
    LPCTSTR lpString           // testo da impostare
);

Il vantaggio è puramente uno di comodità, ma può valere la pena; le ultime tre istruzioni del nostro gestore diventerebbero dunque l'unica:

return SetDlgItemText(hDialogo,  IDC_SCRITTA,
  idCliccato==IDC_PRIMO?"Primo":"Secondo");

Similmente abbiamo altre "API di comodità", come la GetDlgItemText, SendDlgItemMessage, eccetera; sono comodità, certo piccole, ma non del tutto trascurabili, quindi potremo farne uso.
Abbiamo anche "API di comodità" specializzate per certi tipi di controllo, come, per esempio:

BOOL CheckDlgButton(HWND hDlg, // il dialogo
    int nIDButton,             // ID del bottone
    UINT uCheck                // 0 o 1 (reset/set)
);

equivalente a GetDlgItem seguito dalla SendMessage di un BM_SETCHECK,

UINT IsDlgButtonChecked(HWND hDlg, // il dialogo
    int nIDButton                  // ID del bottone
);

simile "rivestimento" del BM_GETCHECK, e l'API, che incapsula funzionalità assai maggiori:

BOOL CheckRadioButton(HWND hDlg, // il dialogo
    int nIDFirstButton, // ID del 1° bottone del gruppo
    int nIDLastButton, // ID dell'ultimo bottone
    int nIDCheckButton // ID bottone da settare
);

Quest'ultima equivale ad un ciclo sull'intero gruppo di radio-button, resettando tutti (senza pallino) con l'eccezione dell'unico scelto (cui il pallino viene invece messo).
L'ordine dei bottoni, nei cui termini determiniamo il primo e l'ultimo del gruppo, è, lo ricordiamo, l'ordine di creazione dei controlli entro la finestra di dialogo; usando il file .RC, esso coincide con l'ordine in cui ivi elenchiamo i bottoni fra le righe BEGIN e END del dialogo (per le funzionalità "autoradio", avremo normalmente anche il bit di stile WS_GROUP nel primo radio del gruppo, e, se questi radio button non sono gli ultimi del dialogo, sul primo del gruppo ad essi successivo).
Per "navigare", dal nostro codice, su questi "ordinamenti" e "raggruppamenti" (cosa che non occorre certo fare di frequente, ma solo per certe funzionalità avanzate), le API, di uso non del tutto semplice, sono GetNextDlgGroupItem e GetNextDlgTabItem che non le approfondiremo ulteriormente.





Lascia un Commento