Assembly Windows 32bit con MASM
Capitolo 7: Menu, icone, cursori, bitmap e dialoghi
Per rendere più intuitiva l'interazione tra l'utente e il computer, le interfacce grafiche
come quella di Windows mettono a disposizione una serie di oggetti che vengono
chiamati risorse; tra le risorse più conosciute si possono citare i Menu, le
Icone, i Cursori e le immagini in formato BitMap (mappa di bit).
Attraverso i menu l'utente ha a disposizione una serie di scelte grazie alle quali può
selezionare svariate funzionalità dell'applicazione che sta utilizzando; nel caso ad esempio
di un editor di testo, i menu vengono largamente utilizzati per permettere all'utente di
richiedere l'apertura o il salvataggio di un file, la ricerca di una parola chiave, le
operazioni di copia/incolla, la richiesta di aiuto (help), etc.
Le icone forniscono una rappresentazione visuale dei files e delle cartelle che si trovano
nell'hard disk; grazie alle icone è possibile distinguere i files dalle cartelle, e spesso
è anche possibile individuare il tipo di file con cui si ha a che fare (file di testo, file
eseguibile, file batch, documento di Word, foglio di calcolo di Excell, etc).
I cursori permettono di assegnare qualsiasi forma (shape) al puntatore del mouse; in
Windows l'utente ha a disposizione numerosi cursori predefiniti, con la possibilità
di creare anche dei cursori personalizzati. Attraverso la modifica della forma del cursore
del mouse, è possibile fornire precise informazioni all'utente; nel caso ad esempio di un
programma di elaborazione grafica (come Paint), il cursore del mouse cambia forma a
seconda dello strumento di disegno selezionato (penna, pennello, gomma da cancellare, spray,
etc). A partire da Windows 95 vengono supportati anche i cursori animati; un cursore
animato è formato da una sequenza di immagini che possono essere paragonate ai fotogrammi
di una pellicola cinematografica. Appena si carica un cursore animato, vengono fatti scorrere
automaticamente i vari fotogrammi che danno così l'idea di una animazione; nel seguito del
capitolo vedremo come si deve procedere per dotare di cursori animati le proprie applicazioni.
Le bitmap sono immagini grafiche memorizzate su disco sotto forma di mappe di bit; in
Windows il formato grafico predefinito è rappresentato proprio dalle bitmap che
vengono infatti utilizzate anche per memorizzare le icone e i cursori. Per migliorare le
prestazioni, Windows utilizza spesso bitmap prive di qualsiasi forma di compressione
grafica; rispetto ai formati grafici compressi come JPG, GIF, etc, lo svantaggio
di una bitmap non compressa è rappresentato dalle notevoli richieste di spazio su disco.
Le icone e i cursori non sono altro che bitmap di forma quadrata; nel caso generale invece
una bitmap è una immagine formata da una superficie rettangolare di pixel.
Gli aspetti relativi alla struttura interna delle risorse di Windows vengono discussi
in altri capitoli; in questo capitolo invece viene illustrato un procedimento che permette di
incorporare in modo molto semplice svariate risorse nei propri programmi. Viene anche illustrato
un modo per creare rapidamente una semplice finestra di dialogo (Dialog Box); le dialog
box vengono largamente utilizzate per permettere all'utente di interagire (dialogare) con una
applicazione.
Per mettere in pratica i concetti esposti in questo capitolo, viene presentato un programma
di esempio chiamato RISORSE.ASM, disponibile in versione MASM.
Ciascuno di questi file deve essere decompresso nella cartella di lavoro; gli esempi
presentati nella sezione Assembly Windows 32bit presuppongono che la cartella di lavoro
sia:
c:\win32asm
per il MASM.
Dalla decompressione del file cap07masm.zip si ottiene
una cartella Risorse che verrà quindi posizionata in:
c:\win32asm\Risorse
per il MASM.
All'interno della cartella Risorse sono presenti ben 15 files, la maggior
parte dei quali contengono le risorse utilizzate dal programma di esempio.
Se non si utilizzano i percorsi specificati in precedenza per le cartelle, è necessario
apportare le opportune modifiche ai percorsi indicati nel file RISORSE.BAT.
7.1 Il Resource File
Il metodo più semplice e più rapido per incorporare numerose risorse nei propri programmi,
consiste nel servirsi di appositi files che vengono chiamati Resource Files (files di
risorse); il Resource File è un file rigorosamente in formato
ASCII,
che attraverso una sorta di pseudo linguaggio di programmazione ci permette di dichiarare le
caratteristiche di tutte le risorse da incorporare in un programma.
Convenzionalmente per i Resource Files si utilizza l'estensione predefinita RC;
nel caso del nostro esempio il file di risorse si chiama RISORSE.RC.
In Figura 7.1 è mostrato il contenuto del file risorse.rc; proprio all'inizio del file
RISORSE.RC, troviamo la seguente istruzione:
#include <resource.h>
Come si può notare, questa è chiaramente una istruzione del linguaggio C; la direttiva
#include del C equivale ovviamente alla direttiva INCLUDE
dell'Assembly. La caratteristica fondamentale dei files di risorse consiste proprio
nel fatto che al loro interno dobbiamo utilizzare una sintassi presa in prestito da linguaggi
come il C e il Pascal; in questo modo si ottiene una sorta di pseudo linguaggio
attraverso il quale si possono codificare le risorse da inserire in un programma.
Il lavoro che con questo pseudo linguaggio viene svolto all'interno di un file di risorse, è
molto simile alla dichiarazione di una serie di macro alfanumeriche, ciascuna delle quali
rappresenta una risorsa; le varie costanti predefinite come WS_CHILD, WS_VISIBLE,
etc, che si utilizzano nel corpo di queste macro, vengono dichiarate all'interno dell'include
file principale windows.inc. Il Resource File richiede però
che queste costanti vengano dichiarate con la classica sintassi del C, per cui è
necessario disporre di una copia in versione C di windows.inc; il MASM
fornisce un apposito header file chiamato appunto resource.h, che si trova nella cartella
INCLUDE, e siccome la sintassi del C è standard, il file resource.h può
essere utilizzato con qualsiasi compilatore di risorse. Il contenuto di questo file
viene continuamente aggiornato dalla Microsoft man mano che escono nuove versioni di
Windows; è importante quindi procurarsi la versione più aggiornata possibile di
resource.h. Si consiglia di dare un'occhiata a questo file per prendere confidenza con
le numerose costanti simboliche presenti al suo interno.
Nel linguaggio C tutte le stringhe incluse tra /* e */ come ad esempio:
/* codici per i menu */
rappresentano un commento che può svilupparsi anche su più linee; si tratta quindi della
stessa situazione che in Assembly si ottiene con la direttiva COMMENT. Se il
commento si sviluppa su una sola linea, possiamo anche utilizzare la sintassi del C++
per i commenti scrivendo:
// codici per i menu
Proseguendo nella esplorazione del file RISORSE.RC, possiamo notare che dopo la
direttiva #include incontriamo una numerosa serie di istruzioni del tipo:
#define CM_MENU1a 101
Anche in questo caso abbiamo a che fare con istruzioni scritte in linguaggio C; la
direttiva #define del C equivale alla direttiva MACRO dell'Assembly,
e ci permette quindi di dichiarare macro alfanumeriche distribuite su una o più linee. Nel
nostro caso dobbiamo dichiarare una serie di semplici costanti numeriche, per cui la direttiva
precedente corrisponde in Assembly a:
CM_MENU1a = 101
Lo scopo di tutte queste costanti numeriche viene chiarito più avanti.
Dopo le direttive #define incontriamo la dichiarazione di una risorsa che rappresenta il
menu associato alla main window del nostro programma; la dichiarazione della struttura
di un menu inizia con il nome simbolico assegnato a questa risorsa, seguito dalla parola chiave
MENU. Nel nostro caso si ha:
RisorseMenu MENU
In pratica, la main window del nostro programma utilizza una risorsa menu chiamata
simbolicamente RisorseMenu; al posto della stringa RisorseMenu è possibile
specificare un valore numerico. Possiamo scrivere ad esempio:
3500 MENU
Il codice numerico deve essere compreso tra 0 e 65535; come vedremo in seguito,
l'utilizzo di un codice numerico al posto di una stringa ci permette di risparmiare diversi byte.
Per motivi di chiarezza è sempre meglio utilizzare nomi simbolici per identificare un valore
numerico esplicito; nel caso della dichiarazione precedente possiamo scrivere ad esempio:
#define ID_MENU 3500
Una volta che abbiamo dichiarato questa costante simbolica possiamo scrivere:
ID_MENU MENU
Seguendo sempre la classica sintassi del C, si può notare che il corpo della risorsa
menu inizia con una parentesi graffa aperta { e termina con una parentesi graffa chiusa
}; se questi due simboli non sono presenti sulla vostra tastiera, li potete ottenere
ugualmente ricordando che il codice
ASCII
di '{' è 123, e il codice
ASCII
di '}' è 125. Per ottenere la parentesi graffa aperta bisogna quindi
tenere premuto il tasto [Alt] sinistro e premere in sequenza i tasti [1],
[2] e [3] sul tastierino numerico; per ottenere la parentesi graffa chiusa bisogna
invece tenere premuto il tasto [Alt] sinistro e premere in sequenza i tasti [1],
[2] e [5] sul tastierino numerico. In alternativa, è possibile utilizzare la
parola chiave BEGIN al posto di {, e la parola chiave END al posto di
}; in questo modo si ottiene una sintassi simile a quella del Pascal.
Subito dopo l'intestazione di RisorseMenu, troviamo l'elenco dei vari popup
associati al menu; con il termine popup si indica la tipica finestra a discesa che
compare quando si clicca con il mouse sul menu di un programma (spesso si utilizza anche la
definizione di menu a tendina). Ciascun popup associato al menu viene specificato
con una dichiarazione del tipo:
POPUP "&Bitmap"
In questo modo, la stringa "Bitmap" comparirà nella menu bar (barra del menu)
del nostro programma; come si può notare, la stringa associata ad ogni popup deve essere
inserita tra doppi apici. Il simbolo '&' posto prima della B indica che questo
popup può essere selezionato non solo con il mouse ma anche con la sequenza di tasti
[Alt][B] (si tratta sempre del tasto [Alt] sinistro); il tasto da premere insieme
a [Alt] per selezionare un popup viene chiamato hot key (tasto caldo).
Ciascun popup è formato da una serie di menu items; con il termine menu
item si indica una delle varie opzioni (o voci) che si possono selezionare all'interno
di un popup. I vari menu items possono essere creati con dichiarazioni del tipo:
MENUITEM "&Penna", 304
In questo modo all'interno del popup "Cursori" comparirà l'opzione "Penna" con
hot key [P]; per selezionare un hot key all'interno di un popup non è
necessario premere [Alt]. Il valore 304 specificato nella precedente dichiarazione,
rappresenta il codice numerico che identifica il menu item "Penna"; anche in questo
caso, i vari codici numerici devono essere compresi tra 0 e 65535. Ogni volta che
selezioniamo un menu item, la window procedure del nostro programma riceve da
Windows un messaggio WM_COMMAND accompagnato dal codice numerico che abbiamo
assegnato allo stesso menu item; intercettando questo messaggio possiamo quindi rispondere
all'azione richiesta dall'utente attraverso il menu.
Come è stato ampiamente spiegato nei precedenti capitoli, l'utilizzo dei numeri espliciti
nei programmi aumenta notevolmente il rischio di commettere errori; se nel codice sorgente
del nostro programma inseriamo un numero sbagliato, non possiamo certo pretendere che
l'assembler ci segnali l'errore che abbiamo commesso. Per questo motivo, e anche per ragioni
di stile, si consiglia vivamente di utilizzare sempre costanti simboliche per gestire dei numeri
espliciti; le numerose direttive #define presenti nel file RISORSE.RC hanno
proprio lo scopo di dichiarare i codici numerici associati ai vari menu items del
menu RisorseMenu. In questo modo possiamo scrivere ad esempio:
MENUITEM "&Penna", CM_CURSOR4
Osservando il popup "Font", possiamo notare che tra i vari menu items è presente
anche un popup "Altri Font"; in questo modo possiamo dichiarare dei popup
innestati all'interno di altri popup. Osserviamo inoltre che anche la sequenza dei vari
menu items associati ad ogni popup deve essere racchiusa dalla solita coppia
{ }; alternativamente, anche in questo caso possiamo utilizzare le parole chiave
BEGIN e END.
Una considerazione finale sui menu riguarda il fatto che come si può notare dalle #define
presenti nel file RISORSE.RC, ad ogni popup viene assegnato un differente gruppo
di codici numerici, e all'interno di ogni popup i vari codici numerici sono consecutivi;
in questo modo è possibile apportare facilmente eventuali modifiche ad un popup, e si
possono anche ottenere notevoli vantaggi in fase di elaborazione del messaggio WM_COMMAND.
Dopo la dichiarazione del menu RisorseMenu, nel file RISORSE.RC incontriamo
una serie di altre dichiarazioni relative alle icone, ai cursori e alle bitmap che verranno
utilizzate nel programma.
Per incorporare una qualsiasi icona, dobbiamo utilizzare una dichiarazione del tipo:
NomeSimbolico ICON "nomefile.ico"
In pratica, questa dichiarazione è formata dal nome simbolico che intendiamo assegnare alla
risorsa, seguito dalla parola chiave ICON e dal nome del file contenente la bitmap
dell'icona (se il file si trova in un'altra cartella, bisogna specificare il percorso completo);
nel nostro caso vogliamo incorporare un'icona memorizzata nel file RISORSE.ICO, e
vogliamo assegnare a questa risorsa il nome simbolico RisorseIcon. Dobbiamo scrivere
quindi:
RisorseIcon ICON "risorse.ico"
Come vedremo in seguito, questa risorsa verrà utilizzata come icona personalizzata del programma
RISORSE.EXE.
Le icone utilizzate da Windows sono delle bitmap quadrate formate da 16x16,
24x24, 32x32 o 48x48 pixel; un file contenente un'icona per Windows
viene memorizzato su disco con estensione predefinita ICO, e contiene una struttura
formata da un header (intestazione) seguito dalla codifica della bitmap vera e propria.
L'icona associata ad una applicazione deve essere in formato 32x32, mentre l'icona
piccola che verrà visualizzata nella barra del titolo (in alto a sinistra) deve essere in
formato 16x16. L'icona associata ad una applicazione verrà utilizzata da Windows
per rappresentare graficamente l'applicazione stessa nel File Manager; nel caso del nostro
esempio vedremo che l'icona RISORSE.ICO verrà associata nel File Manager al
programma RISORSE.EXE.
Se vogliamo creare icone personalizzate, dobbiamo procurarci un apposito editor di icone; a
tale proposito possiamo effettuare una ricerca su Internet tenendo presente che l'editor
che ci serve deve essere in grado ovviamente di gestire il formato specifico ICO utilizzato
da Windows. Le icone sono immagini molto piccole, per cui sarebbe inutile crearle
utilizzando un numero elevato di colori; per risparmiare spazio su disco e in memoria, è
vivamente consigliabile l'utilizzo di una palette (tavolozza) a 16 colori.
Dopo la dichiarazione per l'icona RisorseIcon, incontriamo una serie di altre dichiarazioni
che elencano i vari cursori che verranno utilizzati dal nostro programma di esempio; come si
può notare, il procedimento da seguire è del tutto simile al caso delle icone. In pratica,
dobbiamo indicare il nome simbolico da assegnare al cursore, seguito dalla parola chiave
CURSOR e dal nome del file contenente la risorsa; nel nostro caso possiamo notare la
presenza di dichiarazioni del tipo:
RisorseCursor1 CURSOR "jet.cur"
I cursori utilizzati da Windows sono delle bitmap quadrate formate da 32x32
pixel; un file contenente un cursore per Windows viene memorizzato su disco con
estensione predefinita CUR, e contiene una struttura formata da un header seguito
dalla codifica della bitmap vera e propria. I files contenenti cursori animati vengono invece
memorizzati su disco con estensione predefinita ANI.
Se vogliamo creare cursori personalizzati, dobbiamo procurarci un apposito editor di cursori;
in genere, molti editor di icone sono in grado di gestire anche il formato CUR per
Windows, e gli editor più sofisticati supportano anche i cursori animati. In fase di
creazione di un cursore personalizzato, è necessario specificare una informazione
importantissima, rappresentata dalle coordinate X e Y del cosiddetto hot
spot (punto caldo); l'hot spot di un cursore è il punto della bitmap del cursore,
rispetto al quale Windows calcolerà istante per istante le coordinate del mouse. Nel
caso dell'applicazione RISORSE.EXE, viene utilizzato ad esempio un cursore a forma di
penna; in un caso del genere, il punto più ovvio da utilizzare come hot spot è
rappresentato naturalmente dalle coordinate della punta della penna.
Il Resource File può essere utilizzato anche per incorporare in una applicazione generiche
immagini bitmap; a tale proposito dobbiamo specificare come al solito il nome simbolico assegnato
alla risorsa, seguito dalla parola chiave BITMAP e dal nome del file contenente la
risorsa stessa. Nel caso del file RISORSE.RC notiamo ad esempio la presenza di una
dichiarazione del tipo:
RisorseLogo BITMAP "ramlogo.bmp"
Una bitmap generica è una immagine formata da un rettangolo di pixel; un file contenente una
immagine bitmap compatibile con Windows, viene memorizzato su disco con estensione
predefinita BMP, e contiene una struttura formata da un header seguito dalla
codifica della bitmap vera e propria. Per realizzare immagini bitmap si può utilizzare un
qualsiasi programma di grafica come ad esempio Paint; al momento di salvare l'immagine
su disco, bisogna richiedere il salvataggio in formato BMP per Windows.
Naturalmente il Resource File deve essere utilizzato per incorporare in un programma
piccole immagini bitmap; si tenga presente infatti che tutte le risorse presenti in un
Resource File vengono inserite direttamente nel file eseguibile finale, aumentandone
notevolmente le dimensioni. Non avrebbe nessun senso quindi dichiarare in un Resource File
un'immagine bitmap formata da 1024x768 pixel, con colori a 24 bit; in un caso del
genere la strada da seguire consiste nel lasciare la bitmap in un file separato e caricarla poi
in fase di esecuzione del programma.
Il file RISORSE.RC termina con la dichiarazione di una dialog box che verrà
utilizzata come finestra di About (informazioni) del programma; tutti gli aspetti relativi
a questa dialog box vengono discussi alla fine del capitolo.
Come è stato detto in precedenza, qualsiasi risorsa definita in un Resource File può
essere identificata non solo attraverso una stringa, ma anche attraverso un codice numerico
compreso tra 0 e 65535; consideriamo ad esempio il cursore che nel file
RISORSE.RC viene identificato con la stringa RisorseCursor2. Se vogliamo
identificare questo cursore attraverso il codice numerico 4002, possiamo dichiarare la
seguente costante numerica:
#define ID_CURSOR2 4002
A questo punto possiamo scrivere:
ID_CURSOR2 CURSOR "stella.cur"
Il file RISORSE.RC una volta completato viene passato ad un apposito compilatore chiamato
Resource Compiler (compilatore di risorse); nel caso del MASM il compilatore di
risorse si chiama RC.EXE, e come al solito si trova nella sottocartella BIN di
MASM32.
Per compilare il file RISORSE.RC con il MASM bisogna impartire il comando:
rc /v /i c:\masm32\include risorse.rc
Il parametro /v (verbose) fa in modo che RC.EXE visualizzi tutti i messaggi
relativi alla fase di compilazione di RISORSE.RC; in questo modo possiamo avere maggiori
informazioni nel caso in cui vengano trovati degli errori. Il parametro /i c:\masm32\include
specifica al compilatore di risorse dove cercare il file RESOURCE.H.
Si tenga presente che al posto del compilatore di risorse fornito da MASM
si possono utilizzare anche quelli forniti in dotazione ai vari ambienti di sviluppo per
Windows.
Se la compilazione va a buon fine si ottiene un file chiamato RISORSE.RES; il formato
interno di questo file è compatibile con il formato OBJ (object). Come si può facilmente
intuire, il file RISORSE.RES viene passato poi al linker insieme agli altri files in
formato object; il linker provvederà a collegare i vari object files producendo quindi
l'eseguibile finale.
7.2 L'include file RISORSE.INC
Per poter utilizzare le varie risorse dichiarate in RISORSE.RC, il modulo principale
RISORSE.ASM deve conoscere tutti i nomi e le variabili simboliche che abbiamo dichiarato
nel Resource File; in altre parole, il modulo principale RISORSE.ASM deve poter
essere in grado di interfacciarsi con il modulo RISORSE.RC. A tale proposito possiamo
notare che nel blocco dati inizializzati (_DATA) del modulo RISORSE.ASM sono
presenti le seguenti definizioni:
In questo modo vengono definite una serie di stringhe che contengono i nomi con i quali vengono
identificate le varie risorse dichiarate in RISORSE.RC; è importante notare che queste
stringhe devono essere in formato C, dotate cioè di zero finale.
In relazione ai cursori possiamo notare che solo due di essi vengono dichiarati nel file
RISORSE.RC; gli altri 3 vengono invece caricati direttamente dal disco in
fase di esecuzione del programma. Per gestire questi 3 cursori vengono definite nel
blocco _DATA le 3 variabili cursor3File, cursor4File e
animcurFile che devono puntare a stringhe C contenenti il nome del file CUR
o ANI; in sostanza il programmatore ha la possibilità di scegliere se incorporare
direttamente nel programma una risorsa di tipo cursore, oppure se lasciarla in un file separato e
caricarla poi in fase di esecuzione.
Il modulo RISORSE.ASM deve conoscere anche i codici numerici associati ai vari menu
items; per evitare di dover lavorare con valori numerici espliciti, possiamo ridichiarare
in RISORSE.ASM tutte le #define presenti in RISORSE.RC. Queste dichiarazioni
possono essere inserite nel blocco tipi e costanti simboliche del modulo RISORSE.ASM;
in alternativa, per rendere più semplici e snelli i vari moduli, possiamo servirci di un
apposito include file che nel nostro esempio viene chiamato RISORSE.INC. Analizzando il
file RISORSE.INC possiamo notare che al suo interno sono presenti le stesse #define
di RISORSE.RC, ridichiarate naturalmente in stile Assembly; nello stesso file
RISORSE.INC vengono inseriti anche i vari prototipi delle procedure utilizzate da
RISORSE.ASM. Per evitare errori, si consiglia di utilizzare un editor dotato dello
strumento copia/incolla; in questo modo si possono copiare le varie #define dal
file RISORSE.RC per incollarle poi nel file RISORSE.INC.
A questo punto dobbiamo includere tutte queste dichiarazioni nel modulo RISORSE.ASM; a tale
proposito, nella sezione inclusione librerie del modulo RISORSE.ASM possiamo notare
la presenza della direttiva:
INCLUDE risorse.inc
7.3 Struttura generale dell'applicazione RISORSE
Vediamo adesso la struttura del programma risorse.asm mostrato in Figura 7.2.
L'applicazione RISORSE è dotata di una semplice main window con barra dei menu
e icona personalizzata; appena si avvia l'esecuzione, viene mostrata la main window con
colore di sfondo personalizzato e con il classico cursore predefinito a forma di freccia.
Attraverso il menu l'utente può selezionare svariate risorse di tipo bitmap, cursore, font e
finestre di dialogo; sempre attraverso il menu è possibile richiedere la chiusura dell'applicazione.
Le bitmap selezionabili attraverso il popup "Bitmap" sono tutte da 128x128 pixel
e vengono utilizzate per modificare lo sfondo della client area della main window;
non si tratta quindi del riempimento della client area con una bitmap, ma di una vera
e propria modifica del campo hbrBackground della struttura WNDCLASSEX che definisce
lo sfondo della finestra.
Attraverso il popup "Cursori" è possibile assegnare al puntatore del mouse 5
forme differenti una delle quali è animata; dallo stesso popup si può tornare al cursore
predefinito a forma di freccia. I primi due cursori personalizzati sono stati incorporati nel
programma attraverso il file RISORSE.RC; gli altri tre cursori personalizzati vengono
invece caricati dal disco in fase di esecuzione.
Attraverso il popup "Font" si possono selezionare 9 font differenti che vengono
utilizzati per visualizzare una stringa; questa stringa viene posizionata al centro della
client area mediante la tecnica mostrata nel precedente capitolo.
Attraverso il popup "Informazioni" è possibile attivare una dialog box che
mostra una serie di informazioni relative all'applicazione e all'autore del programma; come
è stato detto in precedenza, tutti i dettagli relativi alla gestione delle dialog box
verranno illustrati alla fine del capitolo.
Per svolgere il suo lavoro l'applicazione RISORSE intercetta i 5 messaggi
WM_CREATE, WM_PAINT, WM_COMMAND, WM_CLOSE e WM_DESTROY;
analizziamo quindi il lavoro che viene svolto in fase di creazione della main window
e in fase di elaborazione dei messaggi.
7.4 Installazione del menu e dell'icona
L'installazione del menu e dell'icona personalizzata di un programma, avviene in modo molto
semplice; tutto questo lavoro infatti viene svolto durante la fase di inizializzazione della
main window. Nei precedenti capitoli abbiamo visto che questa fase consiste nel
riempimento di una struttura di tipo WNDCLASSEX; a tale proposito, supponiamo di
aver definito una variabile wc di tipo WNDCLASSEX. Tra i vari membri di
questa struttura possiamo notare che uno di essi viene chiamato:
wc.lpszMenuName
Questo membro è una DWORD che deve contenere l'identificatore della risorsa menu
associata alla window class che stiamo inizializzando; negli esempi dei precedenti
capitoli non abbiamo utilizzato nessuna risorsa menu, per cui questo membro veniva sempre
inizializzato con:
mov wc.lpszMenuName, NULL
Nell'esempio di questo capitolo abbiamo invece definito un menu identificato simbolicamente
dalla stringa RisorseMenu; per gestire questa stringa, nel blocco _DATA di
RISORSE.ASM è presente la definizione:
menuName db 'RisorseMenu', 0
A questo punto possiamo installare il menu della window class Risorse attraverso
l'istruzione:
mov wc.lpszMenuName, offset menuName
In precedenza è stato detto che una risorsa può essere identificata non solo attraverso una
stringa, ma anche attraverso un codice numerico; anche in questo caso l'installazione della
risorsa avviene in modo semplicissimo. Supponiamo ad esempio di voler gestire il menu del
programma attraverso un codice numerico; a tale proposito nel file RISORSE.RC inseriamo
ad esempio la dichiarazione:
#define ID_MENU 1001
Sempre nel file RISORSE.RC, l'intestazione del menu diventa:
ID_MENU MENU
Per poterci interfacciare a questa risorsa, nell'include file RISORSE.INC inseriamo la
dichiarazione:
ID_MENU = 1001
A questo punto nella fase di inizializzazione della window class Risorse possiamo scrivere
la semplicissima istruzione:
mov wc.lpszMenuName, ID_MENU
Come si può notare, questo secondo metodo è anche più semplice di quello precedente, e presenta
il vantaggio di farci risparmiare qualche byte; è chiaro infatti che la costante ID_MENU
richiede una quantità di memoria inferiore a quella richiesta dalla stringa:
menuName db 'RisorseMenu', 0
Passiamo ora all'installazione dell'icona personalizzata che nel file RISORSE.RC è
stata dichiarata come:
RisorseIcon ICON "risorse.ico"
Nella struttura WNDCLASSEX è presente un membro chiamato:
wc.hIcon
Questo membro è una DWORD che deve contenere l'handle dell'icona da assegnare alla
finestra che stiamo inizializzando; se vogliamo lasciare questa scelta a Windows possiamo
scrivere:
mov wc.hIcon, NULL
In questo caso Windows utilizza un'icona predefinita; generalmente si tratta di una icona
a forma di finestra. Se vogliamo utilizzare una specifica icona predefinita di Windows
possiamo scrivere ad esempio:
In questo caso alla finestra che stiamo inizializzando viene associata un'icona predefinita
contenente un segnale di pericolo con il punto esclamativo; è importante notare che nel
caso di installazione di icone predefinite, il primo parametro (hInstance) di
LoadIcon deve essere NULL.
Se invece vogliamo utilizzare l'icona personalizzata dichiarata in RISORSE.RC, dobbiamo
procedere esattamente come per il menu; prima di tutto nel blocco _DATA di
RISORSE.ASM inseriamo la definizione:
iconName db 'RisorseIcon', 0
A questo punto possiamo installare questa icona attraverso le istruzioni:
L'argomento hInstance passato a LoadIcon è naturalmente l'handle del
programma che viene reperito come sappiamo tramite GetModuleHandle.
Se vogliamo gestire l'icona personalizzata attraverso un codice numerico, dobbiamo seguire
lo stesso procedimento indicato per il menu; prima di tutto, nel file RISORSE.RC inseriamo
ad esempio la dichiarazione:
#define ID_PROGICON 2001
Sempre nel file RISORSE.RC, l'intestazione della risorsa icona diventa quindi:
ID_PROGICON ICON "risorse.ico"
Per poterci interfacciare a questa risorsa, nell'include file RISORSE.INC inseriamo la
dichiarazione:
ID_PROGICON = 2001
A questo punto nella fase di inizializzazione della window class Risorse possiamo scrivere
le semplicissime istruzioni:
La fase di installazione dell'icona di una applicazione, coinvolge anche l'icona piccola da
16x16 pixel che verrà visualizzata nella barra del titolo dell'applicazione stessa;
a tale proposito notiamo che tra i vari membri di WNDCLASSEX ne troviamo uno che
viene chiamato:
wc.hIconSm (handle to small icon)
Per installare l'icona piccola, dobbiamo seguire lo stesso identico procedimento descritto
per l'icona da 32x32 pixel; come è stato detto in un precedente capitolo, è possibile
delegare tutto questo lavoro a Windows scrivendo l'istruzione:
mov wc.hIconSm, NULL
In questo caso Windows utilizza per l'icona piccola una versione scalata a 16x16
pixel dell'icona da 32x32 pixel; tutto questo discorso naturalmente vale anche quando
utilizziamo una icona personalizzata da 32x32 pixel.
Nella fase di inizializzazione della window class è anche possibile installare un
cursore personalizzato per il mouse; il procedimento da seguire è identico a quello illustrato
per le icone. Volendo ad esempio installare il cursore identificato dalla stringa:
cursor1Name db 'RisorseCursor1', 0
possiamo sfruttare l'apposito membro:
wc.hCursor
In questo caso dobbiamo scrivere:
Nell'esempio RISORSE.ASM viene invece installato inizialmente il solito cursore
predefinito a forma di freccia; abbiamo quindi le classiche istruzioni:
Tutte le risorse di tipo cursore e bitmap che abbiamo dichiarato nel file RISORSE.RC,
vengono selezionate in fase di esecuzione attraverso il menu della main window; come
è stato detto in precedenza, la gestione del menu avviene tramite il messaggio WM_COMMAND.
7.5 Gestione del messaggio WM_CREATE
La window procedure della window class Risorse intercetta il messaggio
WM_CREATE per effettuare svariate inizializzazioni; a tale proposito la window
procedure chiama la procedura Risorse_OnCreate. Le caratteristiche dei parametri
ricevuti da questa procedura sono già state descritte nei precedenti capitoli; è importante
ribadire che la procedura che elabora il messaggio WM_CREATE, può essere considerata
come il costruttore della window class che ha ricevuto il messaggio stesso.
All'interno della procedura Risorse_OnCreate vengono caricate le risorse bitmap e le
risorse cursore utilizzate dal programma; viene inoltre riempita una struttura di tipo
LOGFONT necessaria per poter utilizzare i font di Windows.
Per caricare una risorsa bitmap e ottenere il suo handle, possiamo utilizzare la procedura
LoadBitmap; questa procedura viene definita nella libreria USER32.LIB e presenta
il seguente prototipo:
HBITMAP LoadBitmap(HINSTANCE hInstance, LPCTSTR lpBitmapName);
Il parametro hInstance come sappiamo è l'handle dell'applicazione che viene
reperito con GetModuleHandle.
Il parametro lpBitmapName è l'indirizzo del nome simbolico (o il codice numerico) che
identifica la risorsa bitmap.
La procedura LoadBitmap termina restituendo in EAX l'handle della bitmap
(tipo di dato HBITMAP) appena caricata.
Nel caso ad esempio della bitmap identificata nel blocco _DATA con:
bitmap1Name db 'RisorseBmp1', 0
possiamo scrivere:
Se invece nel file RISORSE.RC utilizziamo ad esempio la costante numerica ID_BITMAP1
per identificare questa bitmap, dobbiamo scrivere:
Per caricare le risorse di tipo cursore utilizziamo la procedura LoadCursor che già
conosciamo; come già sappiamo, se vogliamo caricare un cursore predefinito possiamo scrivere
ad esempio:
Se invece vogliamo caricare un cursore personalizzato dichiarato in un Resource File,
dobbiamo passare come primo argomento hInstance, e come secondo argomento l'indirizzo del
nome simbolico che identifica il cursore stesso; nel caso ad esempio del cursore identificato nel
blocco _DATA con:
cursor1Name db 'RisorseCursor1', 0
possiamo scrivere:
Come al solito, se nel file RISORSE.RC utilizziamo ad esempio la costante numerica
ID_CURSOR1 per identificare questo cursore, dobbiamo scrivere:
Se vogliamo caricare un cursore direttamente dal disco dobbiamo utilizzare la procedura
LoadCursorFromFile; questa procedura viene definita nella libreria USER32.LIB
e presenta il seguente prototipo:
HCURSOR LoadCursorFromFile(LPCTSTR lpFileName);
Il parametro lpFileName è l'indirizzo di una stringa C che contiene il nome del
file CUR o ANI.
La procedura LoadCursorFromFile termina restituendo in EAX l'handle del
cursore appena caricato.
All'interno di Risorse_OnCreate viene anche inizializzata la struttura di tipo
LOGFONT che definisce le caratteristiche del font che vogliamo utilizzare; questo
aspetto è stato già illustrato nel precedente capitolo.
7.6 Gestione del messaggio WM_COMMAND
Ogni volta che selezioniamo un menu item dal menu di una finestra, Windows invia
automaticamente un messaggio WM_COMMAND alla window procedure della finestra
stessa; come al solito, ulteriori informazioni associate ad un messaggio, vengono inviate alla
window procedure attraverso i due parametri wParam e lParam.
Nel caso di WM_COMMAND la window procedure riceve tre informazioni che vengono
chiamate id, hwndCtl e codeNotify; questi tre nomi sono stati "pescati"
come al solito dall'header file windowsx.h. Alternativamente le informazioni relative
agli argomenti associati ad un determinato messaggio possono essere ricavate anche dal solito
Win32 Programmer's Reference; nel nostro caso dobbiamo aprire il file Win32.hlp,
premere il pulsante Indice e richiedere la ricerca della parola chiave WM_COMMAND.
L'argomento id contiene un codice numerico che identifica il mittente del messaggio
WM_COMMAND; nel caso di un menu l'argomento id contiene il codice del menu
item che è stato selezionato dall'utente e che ha provocato quindi l'invio del messaggio
WM_COMMAND. L'argomento id viene inviato alla window procedure attraverso
la WORD meno significativa del parametro wParam; programmando in linguaggio
C si può sfruttare una macro di Windows chiamata LOWORD (Low Word = Word
Bassa), con la quale si può scrivere ad esempio:
id = LOWORD(wParam);
L'argomento codeNotify contiene un codice numerico che ci permette di sapere se il
messaggio WM_COMMAND proviene da un menu o da un cosiddetto controllo (Control); con il
termine Control si indicano vari dispositivi di controllo di Windows come i
pulsanti, le finestre di Edit, etc. Se il messaggio WM_COMMAND arriva
da un controllo l'argomento codeNotify vale 1; se invece il messaggio
WM_COMMAND arriva da un menu l'argomento codeNotify vale 0.
L'argomento codeNotify viene inviato alla window procedure attraverso la
WORD più significativa del parametro wParam; programmando in linguaggio
C si può sfruttare una macro di Windows chiamata HIWORD (High Word = Word
Alta), con la quale si può scrivere ad esempio:
codeNotify = HIWORD(wParam);
L'argomento hwndCtl contiene un codice numerico che rappresenta l'handle del
Control che ha inviato il messaggio WM_COMMAND; questo argomento quindi è
significativo solo per i controlli. Se il messaggio WM_COMMAND è stato inviato da
un menu, questo argomento vale NULL; l'argomento hwndCtl è una DWORD che
viene inviata alla window procedure attraverso il parametro lParam.
Seguendo le considerazioni esposte nei precedenti capitoli, possiamo utilizzare una apposita
procedura per la gestione del messaggio WM_COMMAND; il prototipo C per questa
procedura (ricavato dal file windowsx.h) è il seguente:
void Cls_OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify);
Nel file RISORSE.ASM questa procedura viene chiamata Risorse_OnCommand; in base
alle considerazioni appena esposte, all'interno della procedura WndProc possiamo
scrivere:
Come si può notare, per estrarre da wParam i due argomenti id e codeNotify
utilizziamo i due registri EAX e ECX; finché è possibile è meglio evitare
l'uso di registri come EBX, ESI e EDI il cui contenuto deve essere
preservato dalle procedure di callback come WndProc.
Appena entrati in Risorse_OnCommand, possiamo iniziare ad elaborare i codici che arrivano
dalla selezione dei vari menu items; a rigore sarebbe opportuno verificare prima di tutto
il valore contenuto in codeNotify. Come è stato detto in precedenza, se il messaggio
WM_COMMAND proviene da un controllo, allora l'argomento codeNotify vale
1, mentre se il messaggio WM_COMMAND arriva da un menu, allora l'argomento
codeNotify vale 0; nel nostro caso, non essendoci nessun controllo possiamo
dare per scontato che il messaggio WM_COMMAND sia stato inviato in seguito alla selezione
di un menu item.
Il menu associato all'applicazione RISORSE contiene ben 26 menu items, per cui
all'interno di Risorse_OnControl dobbiamo elaborare le 26 possibili scelte
effettuate dall'utente; attraverso il parametro id possiamo sapere quale menu item
è stato selezionato.
In precedenza è stato detto che è molto vantaggioso assegnare ad ogni popup un apposito
gruppo di codici numerici tra loro consecutivi; in questo modo si possono ottenere notevoli
semplificazioni nella fase di elaborazione dei vari codici, e si può rendere anche più compatta
la procedura Risorse_OnCommand. Per rendercene conto possiamo osservare il seguente codice
C che sfrutta appunto il fatto che i codici di ogni popup sono numeri interi
consecutivi (il simbolo && in C rappresenta l'AND logico):
In generale bisogna dire che in alcuni casi può essere conveniente elaborare in un colpo
solo un intero popup, mentre in altri casi può essere necessario elaborare singolarmente
i vari menu items presenti all'interno di un popup; nel seguito vengono illustrate
queste due situazioni.
Partiamo quindi con l'analisi delle elaborazioni relative al popup "Principale"; in
questo caso il parametro id può assumere uno tra i 4 codici che abbiamo chiamato
simbolicamente CM_MENU1a, CM_MENU1b, CM_MENU1c e CM_MENUEXIT.
L'elaborazione dei primi 3 menu items consiste nel mostrare una MessageBox che
visualizza il codice associato al parametro id; in questo modo possiamo constatare che
effettivamente il parametro id contiene proprio il codice che nel file RISORSE.RC
abbiamo associato al menu item appena selezionato. In questo caso è conveniente
elaborare questi primi 3 menu items in un unico blocco di istruzioni; così facendo
possiamo risparmiare parecchio codice inutile e dispersivo.
Per convertire il contenuto di id in una stringa, utilizziamo come al solito la procedura
wsprintf; la stringa così ottenuta viene poi inviata alla MessageBox. È
importante ribadire ancora una volta che in una situazione di questo genere, la MessageBox
è "figlia" della main window che la sta chiamando; di conseguenza il primo argomento
passato alla MessageBox deve essere l'handle (hwnd) della main window.
L'arrivo del codice id = CM_MENUEXIT indica che l'utente attraverso il menu ha richiesto
la chiusura dell'applicazione RISORSE.EXE; in una situazione del genere dovremmo in
teoria chiamare la procedura Risorse_OnClose che elabora il messaggio WM_CLOSE.
Questa soluzione però è da evitare in quanto le procedure che elaborano i vari messaggi
inviati ad una finestra, dovrebbero essere chiamate solo dalla relativa window procedure;
la soluzione ideale consiste allora nel forzare Windows a generare un messaggio
WM_CLOSE che verrà poi intercettato da WndProc. A tale proposito ci possiamo
servire dell'apposita procedura SendMessage che ci permette di inviare un messaggio ad
una window procedure; questa procedura viene definita nella libreria USER32.LIB e
presenta il seguente prototipo:
LRESULT SendMessage(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam);
hWnd è l'handle della finestra che ha chiamato SendMessage; in questo modo
SendMessage sa a quale window procedure deve inviare il messaggio.
Msg è il codice numerico del messaggio da inviare; nel nostro caso si tratta ovviamente
del codice WM_CLOSE.
wParam e lParam contengono informazioni aggiuntive da inviare alla window
procedure; nel nostro caso non dobbiamo inviare nessuna informazione aggiuntiva, per cui
questi due parametri valgono entrambi NULL.
SendMessage termina restituendo in EAX un codice che dipende dal messaggio che
abbiamo spedito.
L'istruzione:
invoke SendMessage, hwnd, WM_CLOSE, NULL, NULL
ha come effetto la chiamata diretta di WndProc; la window procedure riceve quindi
il messaggio WM_CLOSE e chiama Risorse_OnClose che elabora poi questo messaggio
nel modo già illustrato nei precedenti capitoli.
La procedura SendMessage lavora in modo sincrono con il nostro programma; questo significa
che SendMessage restituisce il controllo solo quando l'elaborazione di WM_CLOSE
è terminata. In alternativa Windows mette a disposizione anche la procedura
PostMessage; questa procedura ha gli stessi identici parametri di SendMessage,
ma presenta la caratteristica di lavorare in modo asincrono. In pratica PostMessage
invece di inviare direttamente Msg ad una window procedure, si limita ad inserire
Msg nella coda dei messaggi della stessa window procedure; dopo aver svolto questo
lavoro PostMessage restituisce subito il controllo al caller. In generale il programmatore
in base al contesto, deve essere in grado di stabilire se sia meglio chiamare SendMessage
o PostMessage; nel nostro caso è stata usata SendMessage in quanto l'utente ha
richiesto la chiusura dell'applicazione, e quindi prima di proseguire è necessario attendere
una eventuale conferma di questa decisione.
Passiamo ora all'elaborazione del popup "Bitmap"; in questo caso il parametro id
può assumere uno tra i 5 codici che abbiamo chiamato simbolicamente CM_BITMAP1,
CM_BITMAP2, CM_BITMAP3, CM_BITMAP4 e CM_BITMAP5. Attraverso i primi
4 menu items l'utente sta chiedendo di modificare lo sfondo della main window con
una apposita bitmap; attraverso il quinto menu item l'utente sta chiedendo di tornare
allo sfondo originale della finestra.
Gli handle delle bitmap selezionabili sono già stati caricati dalla procedura
Risorse_OnCreate; vediamo allora come si deve procedere per utilizzare queste bitmap
come sfondo della main window.
Nei precedenti capitoli abbiamo già conosciuto le due procedure CreateSolidBrush (crea
un pennello solido) e CreateHatchBrush (crea un pennello retinato); questa volta
utilizziamo invece una nuova procedura chiamata CreatePatternBrush. Questa procedura
viene definita nella libreria GDI32.LIB e presenta il seguente prototipo:
HBRUSH CreatePatternBrush(HBITMAP hBmp);
hBmp è l'handle di una bitmap che verrà utilizzata come tratto del pennello.
La procedura CreatePatternBrush termina restituendo in EAX l'handle del
pennello selezionato; in caso di errore si ottiene EAX=NULL.
In pratica, il pennello fornitoci da CreatePatternBrush ci permette di disegnare con
un tratto che assume la forma (pattern) della bitmap specificata come argomento; nel
caso di Windows 95 la bitmap deve essere da 8x8 pixel (se si utilizza una bitmap
più grande, Windows 95 taglia una porzione da 8x8 pixel della bitmap stessa).
Se vogliamo riempire lo sfondo della main window con la bitmap puntata da
handleBitmap1, possiamo scrivere:
invoke CreatePatternBrush, handleBitmap1
In questo modo otteniamo in EAX un pennello che dobbiamo passare alla procedura
SetClassLong; in fase di esecuzione di un programma, questa procedura ci permette di
modificare i vari membri della struttura WNDCLASSEX di una finestra (compresa la
window procedure). La procedura SetClassLong viene definita nella libreria
USER32.LIB e presenta il seguente prototipo:
DWORD SetClassLong(HWND hWnd, int nIndex, LONG dwNewLong);
hWnd è l'handle della window class da modificare.
nIndex è un codice numerico che ci permette di specificare il membro di WNDCLASSEX
che vogliamo modificare.
dwNewLong è una DWORD che contiene il nuovo valore da assegnare al membro di
WNDCLASSEX che vogliamo modificare.
La procedura SetClassLong termina restituendo in EAX il vecchio valore contenuto
nel membro di WNDCLASSEX che abbiamo modificato; in caso di errore viene restituito
EAX=0.
Il problema che si presenta è dato dal fatto che nel caso di modifica dello sfondo di una
finestra, la procedura SetClassLong non produce nessun effetto immediato; per veder
comparire il nuovo sfondo dobbiamo costringere Windows ad aggiornare sia lo sfondo
sia il contenuto della finestra. Un modo per ottenere facilmente questo risultato consiste nel
chiamare la procedura InvalidateRect; questa procedura viene definita nella libreria
USER32.LIB e presenta il seguente prototipo:
BOOL InvalidateRect(HWND hWnd, CONST RECT *lpRect, BOOL bErase);
hWnd è l'handle della finestra da aggiornare.
lpRect è l'indirizzo di una struttura RECT che contiene la porzione rettangolare
della finestra da aggiornare; se vogliamo aggiornare l'intera finestra dobbiamo passare
lpRect=NULL.
bErase è un flag che ci permette di richiedere o meno la cancellazione del vecchio
sfondo; se bErase=TRUE viene cancellato il vecchio sfondo.
Se la procedura InvalidateRect termina con successo restituisce un valore non nullo
in EAX; in caso contrario si ottiene EAX=0.
La chiamata:
invoke InvalidateRect, hwnd, NULL, TRUE
forza Windows ad inviare a WndProc i messaggi WM_ERASEBKGND e
WM_PAINT; il messaggio WM_ERASEBKGND comporta la richiesta di aggiornamento dello
sfondo di una finestra. Naturalmente il messaggio WM_ERASEBKGND non viene intercettato da
WndProc, per cui va a finire a DefWindowProc e viene quindi gestito direttamente
da Windows; il messaggio WM_PAINT viene invece smistato da WndProc
alla procedura Risorse_OnPaint che provvede ad aggiornare l'output della nostra
finestra.
In base alle considerazioni appena esposte, se vogliamo riempire lo sfondo con la bitmap
puntata da handleBitmap1 possiamo scrivere:
La costante simbolica GCL_HBRBACKGROUND ci permette di richiedere a SetClassLong
la modifica dello sfondo di una finestra; per conoscere le altre costanti simboliche si può
consultare il Win32 Programmer's Reference.
Come si può notare, è fondamentale passare a InvalidateRect l'argomento
bErase=TRUE; in caso contrario lo sfondo della finestra non viene aggiornato.
Quando l'utente seleziona il menu item associato al codice id=CM_BITMAP5, ci
sta chiedendo di ripristinare lo sfondo originario; in questo caso ci basta passare a
SetClassLong un pennello ottenuto con CreateSolidBrush e dotato dello stesso
colore che avevamo scelto in fase di riempimento della struttura WNDCLASSEX.
I concetti appena esposti, possono essere applicati anche per richiedere uno sfondo di
tipo bitmap in fase di riempimento della struttura WNDCLASSEX; possiamo scrivere
ad esempio:
Attraverso il popup "Cursori" l'utente può selezionare 6 forme differenti per
il cursore del mouse; i codici associati a questi 6 menu items vanno da CM_CURSOR1
a CM_CURSOR6. I codici da CM_CURSOR1 a CM_CURSOR5 permettono di selezionare
dei cursori personalizzati i cui handle sono stati già caricati da Risorse_OnCreate;
il codice CM_CURSOR6 permette di tornare al cursore predefinito a forma di freccia.
Anche per i cursori dobbiamo utilizzare la procedura SetClassLong; il secondo argomento
da passare a questa procedura è la costante simbolica GCL_HCURSOR, mentre il terzo
argomento è l'handle del nuovo cursore. Per sostituire ad esempio il cursore corrente
con il nuovo cursore puntato da handleCursor2 possiamo scrivere:
invoke SetClassLong, hwnd, GCL_HCURSOR, handleCursor2
Questa chiamata provoca l'immediata visualizzazione del nuovo cursore; il cursore animato
associato al codice CM_CURSOR5 è stato "preso in prestito" dalla cartella
windows\cursors di Windows XP.
Il codice CM_CURSOR6 permette di tornare al cursore predefinito a forma di freccia; in
questo caso bisogna passare a SetClassLong l'handle del cursore IDC_ARROW
che ci viene restituito come già sappiamo dala chiamata:
invoke LoadCursor, NULL, IDC_ARROW
Attraverso il popup "Font" è possibile selezionare 9 font differenti che verranno
utilizzati per visualizzare una stringa al centro della client area; i codici associati
a questi 9 menu items vanno da CM_FONT1 a CM_FONT9.
Appena viene selezionato un nuovo font viene chiamata la procedura SetFont che provvede
a caricare nel membro lfFaceName della struttura LOGFONT il nome del font
selezionato; subito dopo SetFont carica nella variabile globale handleFont
l'handle del nuovo font selezionato. Il procedimento è lo stesso già mostrato nel
precedente capitolo; nel nostro esempio è fondamentale che handleFont venga definito
nel blocco _DATA per poter essere inizializzato con 0. A questo punto bisogna
forzare Windows a generare un messaggio WM_PAINT in modo che la procedura
Risorse_OnPaint ridisegni la stringa con il nuovo font appena selezionato; è necessario
richiedere anche il ridisegno dello sfondo per evitare che la nuova stringa venga disegnata su
quella vecchia producendo così un pasticcio illegibile. Per ottenere questo risultato possiamo
sfruttare di nuovo la procedura InvalidateRect che deve ricevere TRUE come terzo
argomento (bErase).
La chiamata di InvalidateRect produce automaticamente un messaggio WM_PAINT che
viene intercettato da WndProc e inoltrato a Risorse_OnPaint; la procedura
Risorse_OnPaint verifica se handleFont è diverso da zero, e in caso affermativo
disegna una stringa al centro della client area attraverso la tecnica che già conosciamo.
L'ultimo popup chiamato "Informazioni" contiene due menu items; il primo
(CM_HELP) provoca la comparsa di una semplice MessageBox, mentre il secondo
(CM_ABOUT) provoca la comparsa di una dialog box che viene descritta più avanti.
7.7 Gestione del messaggio WM_DESTROY
Il messaggio WM_DESTROY viene gestito dalla procedura Risorse_OnDestroy; questa
procedura può essere vista come il distruttore della window class Risorse.
All'interno di Risorse_OnDestroy dobbiamo effettuare quindi tutte le necessarie
deinizializzazioni; in particolare dobbiamo restituire a Windows tutte le risorse non
più necessarie alla nostra applicazione. Analizzando il codice di RISORSE.ASM possiamo
notare che prima di tutto vengono distrutti con DeleteObject gli handle di tutte
le bitmap che abbiamo utilizzato nel programma; come già sappiamo, DeleteObject può
essere utilizzata per distruggere diverse risorse tra le quali si possono citare le penne,
i pennelli i font e le bitmap.
Gli handle dei cursori caricati con LoadCursor o LoadCursorFromFile, non
devono essere distrutti; questo lavoro infatti viene svolto da Windows in fase di
distruzione della window class.
Successivamente Risorse_OnDestroy distrugge con DeleteObject anche l'handle
che abbiamo utilizzato per gestire i font; l'ultimo compito svolto da Risorse_OnDestroy
consiste come al solito nel chiamare PostQuitMessage.
7.8 La dialog box RisorseAboutDlg
Come è stato detto in precedenza, le dialog box sono delle finestre che permettono
ad un utente di dialogare con una applicazione; l'esempio RISORSE.ASM utilizza una
semplice dialog box che può essere usata per mostrare alcune informazioni relative al
programma e all'autore del programma stesso.
Gli ambienti di sviluppo più sofisticati come Microsoft Visual C++, Borland C/C++
Compiler, Borland Delphi, etc, mettono a disposizione appositi strumenti (tools)
visuali che permettono di disegnare letteralmente le dialog box; in questo modo è
possibile creare in modo efficiente delle dialog box estremamente complesse, che verranno
poi tradotte automaticamente nel tipico pseudo codice che si utilizza nei files di risorse.
Se non si dispone di questi tools è ugualmente possibile progettare "a naso" le
proprie dialog box; a tale proposito ci possiamo munire di carta e penna per tracciare
uno schema della dialog box che vogliamo creare. Una volta che il disegno è pronto,
possiamo procedere alla traduzione "manuale" della dialog box in pseudo codice da
inserire nel resource file della nostra applicazione.
Nel file RISORSE.RC vediamo un esempio pratico su come sia possibile creare facilmente
delle semplici dialog box; il codice che dichiara una dialog box deve iniziare
con una intestazione del tipo:
nome_simbolico DIALOG x, y, larghezza, altezza
Il nome_simbolico ci permette di identificare la dialog box che stiamo creando;
anche in questo caso possiamo utilizzare una stringa oppure un codice numerico. Dopo la parola
chiave DIALOG incontriamo 4 parametri che rappresentano le coordinate x, y
del vertice in alto a sinistra della finestra, la larghezza e l'altezza della
finestra; le due coordinate x, y sono espresse in pixel e vengono calcolate rispetto al
vertice in alto a sinistra dello schermo. In relazione invece alla larghezza e all'altezza di
una dialog box è necessario fare una precisazione che assume una importanza enorme; a
differenza di quanto accade per le normali finestre, la larghezza e l'altezza di una dialog
box vengono espresse non in pixel ma in dialog units. Per le larghezze (asse
orizzontale) l'unità di misura usata è pari a 1/4 della larghezza media in pixel dei
caratteri del font utilizzato; per le altezze (asse verticale) l'unità di misura usata è pari
a 1/8 dell'altezza media in pixel dei caratteri del font utilizzato. Se non si seleziona
nessun font personalizzato, le unità di misura vengono riferite al font di sistema di
Windows (font predefinito); il font di sistema è un font a spaziatura fissa simile a
quello usato dal DOS. L'utilizzo delle dialog units si rende necessario perché in
questo modo le proporzioni delle dialog box vengono rese indipendenti (device
independent) dalla risoluzione grafica dello schermo; se non si adottasse questo accorgimento,
ogni volta che si modifica la risoluzione grafica dello schermo verrebbero completamente alterate
le proporzioni delle dialog box.
Tornando al file RISORSE.RC possiamo notare che dopo l'intestazione della dialog
box troviamo un'altra riga che inizia con la parola chiave STYLE; in questa
riga possiamo dichiarare una serie di aspetti stilistici della dialog box. A tale
proposito possiamo utilizzare una serie di costanti simboliche molte delle quali coincidono
con quelle utilizzate per la main window nella procedura CreateWindowEx; tutte
queste costanti devono essere combinate tra loro con l'operatore | del C
(codice
ASCII
124) che equivale all'operatore OR (bit-a-bit) dell'Assembly.
La costante DS_CENTER richiede che la dialog box venga centrata sullo schermo,
la costante WS_CAPTION abilita la barra del titolo, la costante WS_SYSMENU abilita
il menu di sistema, la costante WS_VISIBLE fa in modo che la dialog box diventi
visibile non appena viene attivata, etc; per maggiori dettagli su queste costanti si può
consultare il Win32 Programmer's Reference.
Dopo la riga STYLE incontriamo un'altra riga che inizia con la parola chiave
CAPTION; questa riga ci permette di specificare una stringa (tra doppi apici) che
comparirà nella barra del titolo della dialog box.
La riga successiva inizia con la parola chiave FONT e ci permette di specificare
l'altezza e il nome (tra doppi apici) del font da utilizzare all'interno della dialog
box; se il font che abbiamo richiesto non è disponibile, verrà utilizzato il solito
font di sistema. In base a quanto è stato detto in precedenza, l'altezza del font selezionato
influenzerà le dimensioni e l'aspetto generale della dialog box; di conseguenza, si
consiglia di scegliere dei font molto comuni, che siano disponibili su tutti i computers.
Se vogliamo aggiungere un menu alla nostra dialog box dobbiamo utilizzare la parola
chiave MENU seguita da una stringa tra doppi apici che identifica il menu stesso; a
titolo di curiosità, nella dichiarazione della nostra dialog box si può provare ad
aggiungere la riga:
MENU "RisorseMenu"
A questo punto inizia il corpo della dialog box; tutte le informazioni presenti nel
corpo devono essere racchiuse tra parentesi graffe { }, oppure tra le parole chiave
BEGIN END. All'interno del corpo di una dialog box possono comparire svariati
oggetti come bitmap, icone, pulsanti, stringhe di testo, controlli di input/output, etc;
questi oggetti vengono trattati come se fossero finestre figlie (child windows) della
stessa dialog box.
Come si può notare nel file RISORSE.RC, il corpo della nostra dialog box inizia
con una serie di oggetti di tipo CTEXT; un controllo di tipo CTEXT ci permette
di inserire una stringa di testo centrato nell'area rettangolare creata dal controllo stesso.
La dichiarazione di un controllo CTEXT assume in generale il seguente aspetto:
CTEXT "stringa di testo", id, x, y, larghezza, altezza, stile
La stringa tra doppi apici rappresenta il testo che verrà visualizzato all'interno di un
CTEXT.
id è il codice numerico che utilizziamo per identificare il controllo CTEXT;
questo codice viene inviato da Windows insieme al messaggio WM_COMMAND generato
dal controllo CTEXT. Siccome i controlli CTEXT hanno il compito di mostrare una
semplice stringa statica, il parametro id non ha nessun significato; in questo caso
bisogna utilizzare il valore predefinito -1.
x, y, larghezza, altezza rappresentano ovviamente le caratteristiche geometriche della
finestra associata al CTEXT, e vengono espresse in dialog units; anche x, y
quindi sono espresse in dialog units e sono riferite al vertice in alto a sinistra della
dialog box. Queste considerazioni sono valide per tutti gli oggetti presenti nel corpo
di una dialog box.
L'ultimo parametro di CTEXT definisce gli aspetti stilistici di questo controllo; la
costante WS_CHILD indica che la finestra del CTEXT è confinata all'interno della
dialog box, mentre la costante WS_BORDER permette di assegnare un bordo in 3d
alla finestra stessa.
Dopo i vari oggetti CTEXT incontriamo un oggetto di tipo PUSHBUTTON che dichiara
un controllo a pulsante; la dichiarazione di un controllo PUSHBUTTON assume in generale
il seguente aspetto:
PUSHBUTTON "stringa di testo", id, x, y, larghezza, altezza, stile
Il significato di questi parametri è identico al caso di CTEXT; questa volta però
il parametro id diventa importante in quanto ci permette di sapere quando il pulsante
è stato premuto. A tale proposito si può notare che viene utilizzata la costante simbolica
IDOK che è la stessa restituita da una MessageBox quando si preme il suo pulsante
Ok; in alternativa possiamo utilizzare anche un apposito codice numerico personalizzato.
Lo stile WS_TABSTOP indica tutti quei controlli che possiamo selezionare in sequenza
attraverso la pressione del tasto [Tab].
L'ultimo oggetto presente nella nostra dialog box viene identificato dalla parola chiave
CONTROL; la dichiarazione di un oggetto CONTROL assume in generale il seguente
aspetto:
CONTROL nome_simbolico, id, "tipo controllo", stile, x, y, larghezza, altezza
nome_simbolico è il nome che utilizziamo per identificare il controllo e può essere una
stringa tra doppi apici o un valore numerico.
id identifica il codice associato al controllo; se questo codice non è significativo
bisogna usare come al solito -1.
tipo_controllo è una parola chiave tra doppi apici che identifica il tipo di controllo;
nel nostro caso, dovendo definire un controllo di tipo bitmap dobbiamo utilizzare la parola
chiave STATIC.
Gli altri parametri hannno il solito significato; nel caso di CONTROL di tipo icona
o bitmap, i parametri larghezza e altezza vengono ignorati.
Per definire un controllo di tipo bitmap bisogna inserire la costante di stile SS_BITMAP;
per definire un controllo di tipo icona bisogna inserire la costante di stile SS_ICON.
Nel nostro caso viene definito un controllo di tipo bitmap che utilizza il nome simbolico
"RisorseLogo"; questo nome è stato dichiarato in precedenza nel file RISORSE.RC,
e identifica la bitmap "ramlogo.bmp".
Tutte le informazioni sulla dialog box dichiarata in RISORSE.RC vengono
utilizzate nel file RISORSE.ASM per gestire la dialog box stessa; vediamo
allora in dettaglio come si sviluppa questa gestione.
Con il menu item associato al codice CM_ABOUT l'utente sta richiedendo la
visualizzazione della nostra dialog box; per rispondere a questa richiesta possiamo
utilizzare la procedura DialogBoxParam. Questa procedura viene definita nella libreria
USER32.LIB e presenta il seguente prototipo:
hInstance è l'handle dell'applicazione a cui appartiene la dialog box;
nel nostro caso la dialog box appartiene all'applicazione RISORSE, per cui
dobbiamo utilizzare il solito hInstance che avevamo reperito con GetModuleHandle.
lpTemplateName è l'indirizzo del nome simbolico oppure il codice numerico che
identifica la dialog box; nel nostro caso utilizziamo il nome simbolico
RisorseAboutDlg.
hwndParent è l'handle della finestra "madre" che ha creato la dialog box;
nel nostro caso si tratta dell'handle della window class Risorse.
lpDialogFunc è l'indirizzo della window procedure che elabora i messaggi inviati
da Windows alla dialog box; come accade per tutte le finestre, anche le
dialog box devono specificare quindi la loro window procedure.
dwInitParam è una DWORD che ci permette di inviare ulteriori informazioni per
la fase di creazione della dialog box; se non esiste nessuna informazione aggiuntiva
da inviare, questo parametro deve valere NULL.
Se DialogBoxParam termina con successo restituisce in EAX il valore di
ritorno della dialog box; in caso contrario si ottiene EAX=-1.
All'interno della procedura Risorse_OnCommand la risposta al menu item CM_ABOUT
è rappresentata dalla chiamata:
invoke DialogBoxParam, hInstance, offset AboutDlgName, hwnd, offset AboutWinProc, NULL
La window procedure della nostra dialog box è rappresentata quindi dalla
procedura AboutWinProc; questa procedura ha una struttura del tutto simile a quella
di WndProc.
La procedura DialogBoxParam provvede anche a creare un loop dei messaggi per la
dialog box appena attivata; questo loop dei messaggi viene gestito direttamente da
Windows. La chiamata di DialogBoxParam determina l'invio da parte di
Windows del messaggio WM_INITDIALOG; questo messaggio è l'equivalente di
WM_CREATE e quindi viene inviato prima che la dialog box venga visualizzata.
In questo caso il parametro lParam di AboutWinProc riceve il contenuto del
parametro dwInitParam inviato da DialogBoxParam; l'elaborazione del messaggio
WM_INITDIALOG termina in genere con la restituzione del valore TRUE in
EAX.
A questo punto la nostra dialog box viene visualizzata sullo schermo, ed è pronta
per ricevere altri messaggi; la gestione di questi messaggi si svolge in modo del tutto
analogo a quanto abbiamo visto per WndProc, con la differenza che questa volta non
dobbiamo chiamare nessuna window procedure predefinita (come DefWindowProc).
Osserviamo in particolare che AboutWinProc intercetta il messaggio WM_COMMAND
che ci permette di interfacciarci con i vari controlli inseriti nella dialog box;
quando viene premuto ad esempio il pulsante 'Ok' della dialog box, viene
inviato il messaggio WM_COMMAND accompagnato dall'id del pulsante premuto.
Questo id si trova come al solito nella WORD meno significativa del parametro
wParam ricevuto da AboutWinProc; si tratta del codice IDOK che abbiamo
assegnato al PUSHBUTTON nel file RISORSE.RC.
Quando l'utente preme il pulsante di chiusura della dialog box, o seleziona Chiudi
dal menu di sistema, viene inviato un messaggio WM_COMMAND accompagnato da id =
IDCANCEL; in risposta all'arrivo del codice IDOK o IDCANCEL dobbiamo chiudere
la dialog box. A tale proposito ci dobbiamo servire della procedura EndDialog;
questa procedura viene definita nella libreria USER32.LIB e presenta il seguente
prototipo:
BOOL EndDialog(HWND hDlg, int nResult);
hDlg è l'handle che AboutWinProc ha ricevuto come primo argomento.
nResult è il valore di ritorno restituito dalla dialog box prima di
terminare; nel nostro caso la dialog box termina con il valore TRUE che
comunque viene ignorato.
Se EndDialog termina con successo restituisce un valore non nullo in EAX; in
caso contrario si ottiene EAX=0.
Un'ultima considerazione riguarda il fatto che la procedura DialogBoxParam crea una
dialog box di tipo modal (modale); una dialog box modale è quando viene
aperta e toglie il controllo alla finestra "madre" fino a quando non viene chiusa. All'estremo
opposto troviamo invece le dialog box di tipo modeless (non modali); questo tipo
di dialog box non toglie il controllo alla finestra "madre", e questo significa che
possiamo saltare dalla finestra "madre" alla finestra "figlia" o viceversa.
7.9 Generazione dell'eseguibile RISORSE.EXE
Per generare l'eseguibile RISORSE.EXE con il MASM bisogna uscire al prompt
del DOS, posizionarsi nella cartella:
c:\win32asm\Risorse
e digitare:
risorse.bat
Se analizziamo ora le dimensioni dei vari files, possiamo notare quanto segue (caso del
MASM):
Il file RISORSE.OBJ generato dall'assembler occupa su disco circa 5 KiB.
Il file RISORSE.RES generato dal compilatore di risorse occupa su disco circa
79 KiB.
Il file RISORSE.EXE generato dal linker occupa su disco circa 88 KiB.
Tutto questo ci fa capire che le risorse incorporate in un programma influiscono in modo
drastico sulle dimensioni dell'eseguibile finale; nel caso di RISORSE.EXE si vede
che le risorse occupano quasi il 90% dell'eseguibile.
In ogni caso, gli appena 88 KiB del nostro programma scritto in Assembly,
appaiono ridicoli al cospetto delle parecchie centinaia di KiB dello stesso programma
scritto in Java o in Visual Basic; non parliamo poi delle prestazioni perché
in questo caso il confronto diventa veramente penoso.
Codice sorgente per MASM:
Esempi capitolo 7 MASM