Assembly Windows 32bit con MASM
Capitolo 5: La Main Window
Nel precedente capitolo sono state presentate alcune piccolissime applicazioni per Win32
come ad esempio PRIMO.ASM; l'estrema semplicità di un programma come PRIMO.ASM è
legata principalmente al fatto che abbiamo utilizzato un servizio come la finestra dei messaggi
(message box), interamente gestito da Win32. In sostanza, tutta la reale complessità
del programma è nascosta all'interno della procedura MessageBox; questa procedura esegue
un lavoro piuttosto impegnativo che consiste nell'inizializzare, registrare, attivare e gestire
una finestra chiamata appunto finestra dei messaggi. Nel caso più generale, quando si realizza
un'applicazione standard per Win32, tutto questo lavoro spetta invece al programmatore;
in particolare, in questo capitolo viene esaminato il procedimento che bisogna seguire per
realizzare una applicazione per Win32 dotata di una propria finestra. Come è stato già
spiegato nei precedenti capitoli, ogni applicazione standard per Win32 è dotata di una
finestra principale chiamata appunto main window; la main window rappresenta simbolicamente
un vero e proprio schermo virtuale attraverso il quale un'applicazione interagisce con l'utente.
D'altra parte, lo scopo fondamentale delle interfacce grafiche (GUI) è proprio quello di
realizzare un tipo di interazione visuale tra computer e utente; per raggiungere questo obiettivo
si utilizzano appunto le finestre che al loro interno possono contenere diversi oggetti come le
icone, i bottoni, i menu, le immagini, etc. In commercio esistono numerosissimi libri che
illustrano in dettaglio questi concetti; nella sezione Assembly per Windows a 32 bit ci occuperemo invece
degli aspetti pratici che riguardano noi programmatori Assembly.
Nella realizzazione di un'applicazione standard per Win32 un programmatore Assembly
deve gestire principalmente le seguenti fasi:
- Inizializzazione della main window.
- Registrazione della main window.
- Attivazione della main window.
- Gestione della main window.
La fase più importante è rappresentata sicuramente dalla gestione della main window; questa
fase infatti ci permette di capire il principio fondamentale su cui si basano le GUI dei
SO come Windows, OS/2, Unix/Linux, MacOS, etc.
5.1 I messaggi di Windows
All'interno di Win32, qualunque evento viene convertito in un codice numerico chiamato
messaggio; lo spostamento del mouse, la pressione o il rilascio di un pulsante del mouse,
la pressione o il rilascio di un tasto della tastiera, il trascinamento di una finestra, la
chiusura di una finestra, etc, sono tutti eventi che Win32 converte in appositi messaggi.
Non appena un'applicazione Win32 viene caricata in memoria per l'esecuzione, inizia da
parte del SO un vero e proprio bombardamento di messaggi destinato all'applicazione stessa;
la gestione della main window da parte del programmatore consiste proprio nel decidere quali
messaggi accettare e quali messaggi restituire invece al mittente (e cioè al SO).
La ricezione e l'elaborazione dei messaggi destinati ad una finestra avviene all'interno di
un'apposita procedura chiamata window procedure (procedura di finestra); la window procedure
ha un'importanza enorme in quanto rappresenta di fatto il cuore di un'applicazione Win32.
Un'applicazione Win32 può essere dotata di una o più finestre; una di esse rappresenta
la finestra principale (main window), mentre le altre svolgono il ruolo di finestre secondarie
(o finestre figlio). Qualsiasi finestra, principale o secondaria che sia, deve essere dotata
di una propria window procedure; la window procedure di una finestra deve essere notificata al
SO durante la fase di registrazione della finestra stessa. Il SO si serve proprio
delle window procedure per inviare i messaggi alle corrispondenti finestre; le procedure di questo
tipo, e cioè le procedure che vengono utilizzate dal SO per inviare indirettamente
messaggi ad una applicazione, vengono chiamate procedure di callback. Il programmatore
Assembly è libero di assegnare qualsiasi nome ad una window procedure; tradizionalmente la
window procedure associata alla main window viene chiamata WndProc.
Cominciamo quindi l'analisi dettagliata delle quattro fasi che ci portano a realizzare una
applicazione standard per Win32; prima di tutto vediamo come è strutturato l'esempio
di questo capitolo; la Figura 5.1 mostra infatti il listato dell'esempio mainwin.asm per MASM.
In Figura 5.1 si può notare che il codice sorgente ha dimensioni ben più consistenti rispetto a quelle degli esempi mostrati
nel precedente capitolo; si tenga presente in ogni caso che MAINWIN.ASM contiene la
struttura generale di un'applicazione standard per Win32, per cui il 100% di questo
codice può essere riciclato e impiegato per scrivere altre applicazioni.
5.2 La procedura WinMain
Analizzando il listato di Figura 5.1 si nota che le fasi di inizializzazione,
registrazione, attivazione e gestione della main window sono state inserite all'interno di una
procedura chiamata WinMain; in questo modo stiamo simulando la classica struttura di una
applicazione Win32 scritta in linguaggio C.
Un programma in linguaggio C per l'ambiente DOS o per la console di
Unix/Linux, deve contenere una procedura che deve chiamarsi obbligatoriamente main
(in C le procedure vengono chiamate funzioni); questa procedura ha un ruolo molto
importante in quanto rappresenta la procedura principale del programma e cioè il punto in cui il
programmatore riceve il controllo. Il vero entry point del programma si trova invece in
un apposito modulo gestito direttamente dal compilatore; nel caso ad esempio del Borland C/C++
3.1, questo modulo si chiama C0.ASM ed è scritto chiaramente in Assembly.
All'interno di C0.ASM troviamo l'entry point del programma, chiamato STARTX
più una serie di importanti inizializzazioni che nel loro insieme formano lo startup code;
tra le varie inizializzazioni si possono citare la creazione del segmento di stack del programma e
l'individuazione di eventuali parametri passati al programma dalla linea di comando (command
line). Supponiamo ad esempio di avere un programma C chiamato TESTC.EXE;
digitando:
TESTC param1 param2 param3 param4
e premendo [Invio], stiamo avviando l'esecuzione di TESTC.EXE e stiamo passando al
programma i quattro parametri param1, param2, param3, param4. La
stringa dei parametri viene inserita nel campo CommandLineParameters del PSP del
programma (vedere il Capitolo 13 della sezione Assembly Base); questo campo conterrà
quindi la stringa
ASCII:
' param1 param2 param3 param4', 0Dh
Il campo CommandLineParmLength del PSP contiene invece la lunghezza in byte della
stringa dei parametri (nel nostro caso 28); il compilatore C elabora questi due
campi per individuare le informazioni da inviare alla procedura main. Viene creata una
variabile (tipo intero) chiamata arguments counter (contatore del numero di argomenti)
contenente il numero di argomenti passati al programma dalla linea di comando (nel nostro caso
4); viene poi creata una seconda variabile (tipo vettore di puntatori a stringhe) chiamata
arguments vector (vettore degli argomenti) contenente una sequenza di puntatori ai
parametri. Una volta che la fase di inizializzazione è terminata, dal modulo C0.ASM viene
chiamata la procedura main; il prototipo C di questa procedura è:
int main(int argc, char *argv[])
In sostanza il compilatore utilizza la procedura main per cedere il controllo al
programmatore; nel caso del nostro esempio (TESTC.EXE), il parametro argc contiene
il valore 4 (numero di parametri passati a TESTC.EXE), mentre il parametro
argv è un vettore di 4 puntatori a stringhe, con argv[0] che punta a
'param1', argv[1] che punta a 'param2', argv[2] che punta a
'param3', argv[3] che punta a 'param4' e argv[4] che per convenzione
deve contenere il valore NULL. Una volta che main ha terminato il suo lavoro cede
nuovamente il controllo al modulo C0.ASM; all'interno di questo modulo vengono effettuate
tutte le necessarie deinizializzazioni e infine viene terminato il programma con la conseguente
restituzione del controllo al SO (nel caso del DOS viene chiamato come al solito
il servizio 4Ch dell'INT 21h).
Un programma in linguaggio C per l'ambiente Win32 deve contenere una procedura che
deve chiamarsi obbligatoriamente WinMain; la procedura WinMain ricopre in
Win32 lo stesso ruolo di procedura principale ricoperto da main in ambiente
DOS. Anche in questo caso quindi, il vero entry point del programma si trova in
un'altro modulo gestito direttamente dal compilatore; nel caso ad esempio del Borland C/C++
Compiler 5.x, questo modulo si chiama C0W32. All'interno di questo modulo vengono
effettuate come al solito le necessarie inizializzazioni che prevedono in particolare
l'acquisizione dei parametri da passare alla procedura WinMain; terminata la fase di
inizializzazione il modulo C0W32 chiama la procedura WinMain e cede il controllo
al programmatore. Quando WinMain ha esaurito il suo compito restituisce il controllo al
modulo C0W32; questo modulo, dopo aver effettuato le varie deinizializzazioni, richiede
a Win32 la terminazione del nostro programma.
Un programma in linguaggio Assembly per l'ambiente Win32 non è obbligato a seguire
il procedimento appena descritto; in Assembly siamo liberi di seguire lo stile che più si
adatta ai nostri gusti. Naturalmente però siamo tenuti a rispettare le regole che definiscono la
struttura di un'applicazione Win32 standard; questa struttura può essere schematizzata
attraverso tre blocchi fondamentali:
- Il primo blocco contiene la cosiddetta fase di startup dell'applicazione; in
questa fase dobbiamo reperire in particolare alcune informazioni molto importanti.
- Il secondo blocco contiene le quattro fasi che portano alla creazione della main
window del programma; come già sappiamo queste quattro fasi consistono nell'inizializzazione,
registrazione, attivazione e gestione della main window.
- Il terzo blocco contiene la fase di deinizializzazione dell'applicazione; in questa
fase in particolare dobbiamo richiedere al SO la terminazione del nostro programma.
In base a queste considerazioni risulta evidente che un'applicazione Assembly per
Win32 contiene il vero entry point e il vero exit point dell'applicazione
stessa; questo significa che le fasi di inizializzazione e deinizializzazione del programma
spettano a noi.
Quando si utilizza un linguaggio criptico come l'Assembly, è molto importante seguire uno
stile di programmazione chiaro e ordinato; per raggiungere questo obiettivo si possono utilizzare
ad esempio le procedure, che hanno il pregio di rendere evidente la suddivisione del nostro
programma in tanti blocchi funzionali. In questo modo tra l'altro, si rende molto più semplice la
verifica e la manutenzione del codice sorgente; proprio per questo motivo, tutti gli esempi
presentati nella sezione Assembly per Windows a 32 bit, seguendo le convenzioni dei linguaggi di alto
livello utilizzano una procedura che ricopre il ruolo di procedura principale dell'applicazione. In
particolare, in base alle convenzioni del linguaggio C utilizzeremo proprio la procedura
WinMain; naturalmente siamo liberi anche di adottare le convenzioni seguite da altri
linguaggi come Visual Basic, Delphi, Java, etc, e siamo anche liberi di
non seguire nessuna convenzione.
A questo punto possiamo procedere con l'analisi delle caratteristiche della procedura
WinMain; consultando il Win32 Programmer's Reference possiamo notare che questa
procedura viene dichiarata con il seguente prototipo C:
int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrevInst, LPSTR lpCmdLine, int nCmdShow);
Il termine WINAPI indica le convenzioni seguite da una procedura per il passaggio dei
parametri e per la pulizia dello stack, ed è perfettamente equivalente a STDCALL; infatti
nel file windows.inc è presente una dichiarazione del tipo:
WINAPI equ STDCALL
Come si può notare, la procedura WinMain termina restituendo un valore di tipo int
(intero con segno a 32 bit); questo valore è l'exit code che verrà restituito al
SO dopo la terminazione della nostra applicazione. Come è stato detto nei precedenti
capitoli, l'exit code viene totalmente ignorato da Win32; in ogni caso, il Win32
Programmer's Reference, a proposito dell'exit code, fornisce alcuni utili consigli che
vengono illustrati più avanti.
Passiamo ora all'analisi dettagliata dei parametri richiesti da WinMain.
hInst è l'handle che identifica l'istanza corrente della nostra applicazione, e
cioè il codice numerico che il SO assegna alla nostra applicazione al momento del
caricamento in memoria per la fase di esecuzione; come è stato spiegato nel precedente capitolo,
in Win32 questo codice vale sempre 00400000h e rappresenta l'offset a 32
bit da cui parte l'applicazione in memoria. Si tratta in sostanza del base address
di un'applicazione all'interno del segmento virtuale da 4 GiB.
hPrevInst è l'handle che identifica eventuali altre istanze della nostra
applicazione già in fase di esecuzione; come sappiamo, questa informazione è importante solo
in ambiente Win16. In Win32 il concetto di istanze multiple di un'applicazione
non ha nessun significato; se eseguiamo contemporaneamente 10 copie di un'applicazione,
otteniamo 10 applicazioni che girano in 10 macchine virtuali indipendenti tra loro.
Ogni applicazione è convinta quindi di essere l'unica in esecuzione e di avere tutto il computer
a sua disposizione; per questo motivo in Win32 il parametro hPrevInst di
WinMain deve valere sempre NULL.
lpCmdLine è un puntatore FAR ad una stringa C contenente gli eventuali
parametri passati alla nostra applicazione dalla command line; come già sappiamo in Win32
la distinzione tra puntatori NEAR o FAR non ha significato. In sostanza il tipo
LPSTR (long pointer to string) rappresenta un offset a 32 bit, e cioè un dato
di tipo DWORD; il parametro lpCmdLine non viene quasi mai utilizzato in quanto
è rarissimo che un'applicazione Win32 riceva l'input dalla linea di comando.
nCmdShow è un codice numerico a 32 bit che ci permette di specificare lo stato
iniziale della main window della nostra applicazione; i codici più utilizzati vengono indicati
con i nomi simbolici SW_SHOWMAXIMIZED, SW_SHOWMINIMIZED e SW_SHOWNORMAL
dichiarati come al solito in windows.inc (SW = Show Window). Se si utilizza
SW_SHOWMAXIMIZED la nostra applicazione parte con la main window massimizzata; se si
utilizza SW_SHOWMINIMIZED la nostra applicazione parte con la main window minimizzata
(cioè ridotta ad icona nella barra delle applicazioni). Nel nostro esempio utilizziamo
SW_SHOWNORMAL che lascia a Win32 il compito di stabilire le dimensioni iniziali
e la posizione iniziale della main window del programma; per conoscere gli altri codici si
consiglia di consultare il Win32 Programmer's Reference.
In base a tutte le considerazioni appena esposte, il prototipo di WinMain puo essere
scritto come:
WinMain PROTO :DWORD, :DWORD, :DWORD, :DWORD
o se preferiamo anche come:
WinMain PROTO :HINSTANCE, :HINSTANCE, :LPSTR, :SDWORD
Appare evidente inoltre che subito dopo l'entry point (start) del nostro programma,
dobbiamo effettuare le necessarie inizializzazioni che consistono nell'acquisizione delle
informazioni da passare a WinMain; le due informazioni da reperire sono l'handle
dell'applicazione e l'indirizzo della stringa contenente la command line.
Per reperire l'handle dell'applicazione ci serviamo della procedura GetModuleHandleA
già descritta nel precedente capitolo; questa procedura restituisce in EAX l'informazione
richiesta. Il valore restituito in EAX viene salvato nella variabile hInstance
definita nel segmento dati non inizializzati del nostro programma.
Per reperire l'indirizzo della command line ci serviamo invece della procedura
GetCommandLineA definita nella libreria KERNEL32; il prototipo di questa procedura è:
LPTSTR GetCommandLine(void);
Come si può notare, GetCommandLineA non richiede nessun parametro e quando termina
restituisce in EAX l'offset a 32 bit della command line; questa informazione
viene salvata nella variabile commandLine definita nel segmento dati non
inizializzati del nostro programma.
A questo punto abbiamo a disposizione tutte le informazioni necessarie per poter chiamare
WinMain; la parte iniziale del nostro programma assume quindi la struttura mostrata
in Figura 5.2; (in versione MASM):
Come si può notare, grazie all'uso di WinMain il codice appare estremamente chiaro e
ordinato; i tre blocchi evidenti sono:
- Acquisizione di hInstance e commandLine.
- Chiamata di WinMain.
- Terminazione dell'applicazione.
L'applicazione viene terminata come al solito da ExitProcess che utilizza il contenuto
di EAX come exit code; naturalmente EAX in quel preciso istante contiene
il valore di ritorno restituito da WinMain.
Passiamo ora all'analisi del codice racchiuso dalla procedura WinMain; come è stato detto
in precedenza, questo codice ci permette di creare la Main Window della nostra applicazione.
5.3 Inizializzazione della Main Window
Il primo passo da compiere consiste nell'inizializzazione della Main Window; questa fase consiste
in una vera e propria progettazione della finestra principale della nostra applicazione.
Progettare la Main Window significa definire le caratteristiche generali della finestra, come
ad esempio, il colore di sfondo, l'icona associata, la window procedure, etc; tutte queste
informazioni devono essere passate al SO attraverso un'apposita struttura dichiarata
con il nome WNDCLASSEX. Per analizzare questa struttura, apriamo con un editor l'include
file principale windows.inc, e chiediamo all'editor stesso di
cercare il nome WNDCLASSEX; ad esempio, con QEDITOR.EXE dobbiamo selezionare il
menu Edit + Find Text. Informazioni dettagliate su questa struttura possono essere reperite
nel Win32 Programmer's Reference; la struttura WNDCLASSEX assume in Assembly
il seguente aspetto:
Prima di tutto nel nostro programma definiamo un'istanza wc di questa struttura; questa
istanza può essere definita nel blocco dati non inizializzati scrivendo:
wc WNDCLASSEX < ? >
Alternativamente, seguendo lo stile dei linguaggi di alto livello, possiamo definire wc
come variabile locale di WinMain; in questo caso la memoria per wc viene allocata
nello stack scrivendo:
LOCAL wc :WNDCLASSEX
Analizziamo in dettaglio il significato dei singoli campi di questa struttura.
cbSize è una DWORD destinata a contenere la dimensione in byte della struttura;
per inizializzare questo campo possiamo utilizzare l'operatore SIZEOF del MASM.
Con il MASM possiamo scrivere:
mov wc.cbSize, SIZEOF WNDCLASSEX
style è una DWORD che ci permette di definire alcuni aspetti stilistici della
finestra; nel nostro esempio utilizzeremo i due codici CS_HREDRAW e CS_VREDRAW.
Il codice CS_HREDRAW determina l'aggiornamento della finestra in caso di modifica della
sua larghezza; il codice CS_VREDRAW determina l'aggiornamento della finestra in caso di
modifica della sua altezza. I due codici devono essere combinati tra loro attraverso l'operatore
OR dell'Assembly; in definitiva possiamo scrivere:
mov wc.style, CS_HREDRAW OR CS_VREDRAW
Nei capitoli successivi utilizzeremo anche altri codici di stile.
lpfnWndProc è l'indirizzo della window procedure associata alla nostra finestra; come
è stato detto in precedenza, il SO utilizza la window procedure di una finestra per
inviare i messaggi alla finestra stessa. Nel caso del nostro esempio la window procedure si
chiama WndProc e viene descritta più avanti; il tipo di dato WNDPROC non è
altro che una DWORD contenente l'offset di WndProc. Possiamo scrivere quindi:
mov wc.lpfnWndProc, offset WndProc
cbClsExtra contiene il numero extra di byte da riservare per la classe finestra; questo
campo (tipo DWORD) verrà illustrato in altri capitoli. Nel nostro caso non ci serve nessun
byte extra per cui scriviamo:
mov wc.cbClsExtra, 0
cbWndExtra contiene il numero extra di byte da riservare per l'istanza della finestra;
anche questo campo (tipo DWORD) verrà illustrato in altri capitoli. Nel nostro caso non ci
serve nessun byte extra per cui scriviamo:
mov wc.cbWndExtra, 0
hInstance rappresenta l'handle della nostra applicazione; in precedenza, con la chiamata
di GetModuleHandleA avevamo già reperito questa informazione che era stata salvata nella
variabile hInstance. Possiamo scrivere quindi:
mov wc.hInstance, hInstance
Naturalmente questa istruzione produce un errore dell'assembler in quanto non è possibile
effettuare un trasferimento dati da memoria a memoria; per evitare il fastidio di dover effettuare
ogni volta due trasferimenti (da SRC a reg e da reg a DEST), possiamo
utilizzare la seguente macro:
A questo punto possiamo scrivere:
MOV32 wc.hInstance, hInstance
La macro MOV32 potrebbe essere scritta anche come:
Questo metodo però è sconsigliabile in quanto il registro EAX (o qualsiasi altro registro
generale) potrebbe essere già in uso e quindi non utilizzabile.
hIcon contiene l'handle dell'icona (32x32 pixel) associata alla nostra applicazione;
il tipo HICON equivale come al solito a DWORD. Per reperire questa informazione
dobbiamo servirci della procedura LoadIconA definita nella libreria USER32.LIB; il
prototipo di questa procedura è:
HICON LoadIconA(HINSTANCE hInst, LPCTSTR lpIconName);
Il parametro hInst rappresenta l'handle della nostra applicazione, mentre il parametro
lpIconName rappresenta il nome o il codice che identifica l'icona. Queste informazioni
sono necessarie solo quando vogliamo utilizzare un'icona personalizzata; nel nostro caso invece
viene utilizzata l'icona predefinita IDI_WINLOGO che mostra il classico logo di
Windows. Quando si utilizza un'icona predefinita il parametro hInst deve valere
NULL; la procedura LoadIconA termina restituendo in EAX l'handle richiesto.
In definitiva possiamo scrivere (in versione MASM):
I codici delle altre icone predefinite sono reperibili nel Win32 Programmer's Reference;
utilizzando i vari codici disponibili possiamo sperimentare i diversi tipi di icone predefinite.
hCursor contiene l'handle del cursore del mouse associato alla nostra applicazione; il tipo
HCURSOR equivale a DWORD. Per reperire questa informazione dobbiamo servirci della
procedura LoadCursorA definita nella libreria USER32.LIB; il prototipo di questa
procedura è:
HCURSOR LoadCursorA(HINSTANCE hInst, LPCTSTR lpCursorName);
Il parametro hInst rappresenta l'handle della nostra applicazione, mentre il parametro
lpCursorName rappresenta il nome o il codice che identifica il cursore. Queste informazioni
sono necessarie solo quando vogliamo utilizzare un cursore personalizzato; nel nostro caso invece
viene utilizzato il cursore predefinito IDC_ARROW che mostra la classica freccia.
Quando si utilizza un cursore predefinito il parametro hInst deve valere NULL; la
procedura LoadCursorA termina restituendo in EAX l'handle richiesto. In definitiva
possiamo scrivere (in versione MASM):
Anche in questo caso possiamo divertirci a sperimentare altri codici che ci permettono di
utilizzare cursori predefiniti a forma di clessidra, di punto interrogativo, etc.
hbrBackground contiene il colore di sfondo della finestra; il tipo di dato HBRUSH
equivale a DWORD e rappresenta l'handle del pennello da utilizzare per colorare uno
sfondo. Anche in questo caso Win32 fornisce una numerosa serie di colori predefiniti;
ciascun colore predefinito è rappresentato da un codice numerico al quale bisogna sommare
1. Utilizzando ad esempio il colore predefinito COLOR_WINDOW (colore di sistema per
lo sfondo delle finestre), possiamo scrivere:
mov wc.hbrBackground, COLOR_WINDOW + 1
lpszMenuName è un codice numerico o un puntatore ad una stringa C che identifica la
risorsa menu da collegare alla nostra applicazione; se l'applicazione non ha un menu
questo campo deve valere NULL. Nel nostro caso quindi possiamo scrivere:
mov wc.lpszMenuName, NULL
lpszClassName è un puntatore ad una stringa C che contiene il nome da assegnare
alla classe della finestra (window class) che stiamo inizializzando; il termine window
class si riferisce alla categoria a cui appartiene la finestra da definire. Ricorrendo ad una
pratica molto diffusa tra i programmatori, utilizziamo lo stesso nome dell'applicazione; nel
blocco dati inizializzati viene definita la stringa:
className db 'MainWin', 0
Possiamo scrivere quindi:
mov wc.lpszClassName, offset className
hIconSm contiene l'handle dell'icona piccola (16x16 pixel) associata alla nostra
applicazione; si tratta della piccola icona che compare all'estremità sinistra della barra del
titolo di una finestra. Cliccando su questa icona con il pulsante sinistro del mouse compare il
menu di sistema (system menu) che Win32 assegna automaticamente ad ogni finestra;
un doppio click su questa icona provoca invece una richiesta di chiusura della finestra. Se non
vogliamo utilizzare nessuna icona piccola, questo campo deve valere NULL; in questo caso
il SO utilizza come icona piccola una versione rimpiccolita dell'icona individuata dal
campo hIcon. Nel nostro caso possiamo scrivere:
mov wc.hIconSm, NULL
A questo punto abbiamo esaminato tutti i campi della struttura WNDCLASSEX, per cui possiamo
dire che la fase di inizializzazione della Main Window è terminata; il passo successivo da
compiere consiste nell'inviare al SO una richiesta di registrazione della finestra appena
inizializzata.
5.4 Registrazione della Main Window
Per effettuare la richiesta di registrazione di una finestra dobbiamo servirci della procedura
RegisterClassExA definita nella libreria USER32.LIB; questa procedura ha il seguente
prototipo:
ATOM RegisterClassExA(CONST WNDCLASSEX *lpwcx);
Come si può notare, il parametro lpwcx deve contenere l'indirizzo di una struttura di
tipo WNDCLASSEX; il qualificatore CONST del linguaggio C impone che
lpwcx sia un puntatore costante, nel senso che non è possibile modificare l'indirizzo a
cui punta lpwcx. Nel caso del nostro esempio dobbiamo passare a RegisterClassExA
l'offset di wc; il procedimento che dobbiamo seguire varia a seconda che wc sia
stata definita nel segmento dati del programma (variabile globale) o nello stack (variabile
locale). Vediamo come ci si deve comportare nel caso del MASM.
Per quanto riguarda il MASM; questo assembler
ci mette a disposizione l'operatore ADDR che utilizzato in combinazione con la
direttiva INVOKE permette la scrittura di codice più compatto. L'operatore ADDR
applicato ad una locazione di memoria, si comporta come OFFSET per le variabili globali,
e come LEA per le variabili locali; nel nostro caso possiamo scrivere quindi:
invoke RegisterClassExA, ADDR wc
questa istruzione corrisponderà al codice macchina della sequenza di
istruzioni:
Queste regole devono essere rispettate in modo rigoroso e valgono ovviamente per tutte quelle
istruzioni che operano sull'indirizzo di una variabile locale; la non osservanza di queste regole
può portare alla scrittura di programmi contenenti bugs molto pericolosi e difficili da scovare.
La procedura RegisterClassExA termina restituendo in EAX un valore di tipo
ATOM che equivale a DWORD; se la registrazione ha successo, EAX contiene
un codice numerico che identifica in modo univoco la window class appena registrata.
Se la registrazione fallisce EAX vale zero; in questo caso il Win32 Programmer's
Reference consiglia di terminare il programma proprio con exit code uguale a zero.
In definitiva, il codice relativo alla registrazione della main window assume il seguente
aspetto (versione MASM):
In caso di errore la procedura WinMain termina con EAX=0; in una eventualità del
genere sarebbe opportuno informare l'utente con un'apposita MessageBox. Il lettore può
provare per esercizio ad inserire il codice necessario per la gestione degli errori; questo
codice può essere inserito prima della chiamata di ExitProcess.
5.5 Attivazione della Main Window
Se la fase di registrazione è terminata con successo, possiamo passare alla fase di attivazione
della main window; questa fase si svolge fondamentalmente in tre passi:
- Creazione della finestra.
- Visualizzazione della finestra.
- Aggiornamento dell'area di output della finestra.
La creazione materiale della finestra avviene attraverso la procedura CreateWindowExA
definita nella libreria USER32.LIB; l'impressionante prototipo di questa procedura è il
seguente:
Tutti i parametri richiesti da questa procedura sono di tipo DWORD; analizziamo in dettaglio
il loro significato.
dwExStyle è un codice numerico che rappresenta lo stile esteso della finestra; modificando
questo codice è possibile alterare completamente l'aspetto esteriore della finestra. Nel nostro
esempio viene utilizzato il codice WS_EX_OVERLAPPEDWINDOW (WS = Window Style) che
crea una finestra con bordi tridimensionali; questo codice è formato da un OR tra
WS_EX_CLIENTEDGE (area utente incavata) e WS_EX_WINDOWEDGE (bordo sporgente). Per
gli altri numerosi codici si veda il Win32 Programmer's Reference o l'include file
principale windows.inc.
lpClassName è l'indirizzo di una stringa C contenente il nome assegnato alla
window class; si tratta dello stesso parametro già utilizzato per inizializzare la
struttura wc.
lpWindowName è l'indirizzo di una stringa C che verrà visualizzata nella barra del
titolo della finestra; nel nostro esempio utilizziamo la stringa winTitle.
dwStyle è un codice numerico che rappresenta lo stile ordinario della finestra; nel
nostro esempio utilizziamo lo stile WS_OVERLAPPEDWINDOW che è formato da una combinazione
attraverso l'operatore OR dei codici WS_BORDER (finestra con bordo),
WS_CAPTION (finestra con title bar), WS_SYSMENU (finestra con menu di sistema),
WS_TICKFRAME (finestra dimensionabile), WS_MINIMIZEBOX (finestra con bottone
minimizza) e WS_MAXIMIZEBOX (finestra con bottone massimizza).
x è l'ascissa iniziale del vertice superiore sinistro della finestra; nel nostro esempio
utilizziamo il codice predefinito CW_USEDEFAULT che lascia la scelta di x al
SO.
y è l'ordinata iniziale del vertice superiore sinistro della finestra; nel nostro esempio
utilizziamo il codice predefinito CW_USEDEFAULT che lascia la scelta di y al
SO.
nWidth è la larghezza iniziale della finestra; nel nostro esempio utilizziamo il codice
predefinito CW_USEDEFAULT che lascia la scelta di nWidth al SO.
nHeight è l'altezza iniziale della finestra; nel nostro esempio utilizziamo il codice
predefinito CW_USEDEFAULT che lascia la scelta di nHeight al SO.
hWndParent è l'handle della finestra "madre" di cui è "figlia" la nostra finestra; nel
nostro caso stiamo creando proprio la finestra madre (main window), per cui questo parametro
deve valere NULL.
hMenu è un codice numerico che identifica un menu o una finestra figlia; nel nostro
caso non esistono ne menu ne finestre figlie per cui questo parametro deve valere NULL.
hInstance è l'handle della nostra applicazione; questa informazione è già stata
descritta in precedenza.
lpParam è l'indirizzo di una struttura di tipo CREATESTRUCT contenente informazioni
ulteriori per la fase di creazione della finestra; quando si chiama con successo la procedura
CreateWindowEx, il SO invia alla nostra window procedure, il messaggio
WM_CREATE (WM = Window Message). Se intendiamo gestire questo messaggio, dobbiamo
inizializzare il parametro lpParam facendolo puntare ad una struttura contenente le
necessarie informazioni; nel nostro esempio, il messaggio WM_CREATE viene rispedito al
mittente per cui il parametro lpParam deve valere NULL.
Se la procedura CreateWindowEx termina con successo, ci restituisce in EAX l'handle
che il SO ha assegnato alla finestra che abbiamo appena creato; questa informazione può
servirci anche nel seguito del programma per cui la salviamo nella variabile hWindow. Se
invece CreateWindowEx fallisce il suo compito, ci restituisce in EAX il valore
NULL; in questo caso dobbiamo terminare il programma con exit code zero.
Se tutto è filato liscio possiamo compiere i due passi successivi che consistono nella
visualizzazione della finestra e nell'aggiornamento del suo contenuto; la visualizzazione della
finestra viene ottenuta attraverso la procedura ShowWindow definita nella libreria
USER32.LIB. Il prototipo di questa procedura è il seguente:
BOOL ShowWindow(HWND hWnd, int nCmdShow);
Il parametro hWnd è l'handle della finestra che in precedenza avevamo salvato in
hWindow; il parametro nCmdShow individua lo stato iniziale della finestra ed è
stato già descritto in precedenza per la procedura WinMain. La procedura ShowWindow
termina restituendo in EAX un valore booleano TRUE (non zero) o FALSE (zero);
il valore restituito dipende dallo stato iniziale (visibile/invisibile) della finestra. Questo
significa che FALSE non deve essere interpretato come un codice di errore.
A questo punto dobbiamo procedere con l'aggiornamento dell'area di output della finestra; nel
gergo di Windows quest'area viene chiamata client area. L'aggiornamento della
client area viene svolto dalla procedura UpdateWindow definita nella libreria
USER32.LIB. Il prototipo di questa procedura è il seguente:
BOOL UpdateWindow(HWND hWnd);
Il parametro hWnd è il solito handle della finestra già descritto in precedenza; questa
procedura invia alla nostra window procedure il messaggio WM_PAINT. Intercettando questo
messaggio possiamo eseguire il codice necessario per aggiornare la client area della nostra
finestra; questo stesso messaggio viene inviato automaticamente dal SO ogni volta che la
client area della nostra finestra viene "sporcata". Anche UpdateWindow termina restituendo
in EAX un valore booleano TRUE (non zero) o FALSE (zero); questa volta però
il valore FALSE (zero) rappresenta un codice di errore. In questo caso dobbiamo terminare
la nostra applicazione con exit code zero; in definitiva, la fase di attivazione della
main window assume la struttura mostrata in Figura 5.3 (versione MASM).
A questo punto se tutto è filato liscio possiamo passare alla fase più importante del nostro
programma, e cioè alla fase di gestione della main window.
5.6 Gestione della Main Window
La gestione della main window consiste principalmente in un loop chiamato message loop
(loop dei messaggi); all'interno di questo loop si sviluppa il flusso dei messaggi che il
SO invia alla window procedure della main window. Possiamo dire quindi che in questa fase
il protagonista principale è il messaggio; le caratteristiche dei messaggi di Win32
vengono specificate dalla struttura MSG dichiarata in windows.inc. Questa struttura
(che viene riempita dal SO) presenta il seguente aspetto:
Analizziamo in dettaglio il significato dei vari campi che formano questa struttura.
hwnd è l'handle della finestra destinataria del messaggio.
message è il codice numerico a 32 bit che identifica il messaggio; nel file
windows.inc è presente un vasto elenco di costanti simboliche che rappresentano i vari
messaggi. I nomi di queste costanti iniziano generalmente con WM_ (Window Message).
wParam e lParam sono due DWORD contenenti informazioni addizionali sul
messaggio; la natura di queste informazioni è legata al codice presente nel campo
messaggio.
time rappresenta l'ora esatta in cui il SO ha inviato il messaggio.
pt è una struttura di tipo POINT contenente la posizione (x, y) del cursore
del mouse al momento dell'invio del messaggio; la struttura POINT viene dichiarata in
windows.inc come:
Tutti i messaggi che il SO invia alle applicazioni in esecuzione, vengono inseriti in
un'apposita coda di attesa; il nostro compito consiste nell'attivare un loop all'interno del
quale questi messaggi vengono estratti in sequenza dalla coda, decodificati e smistati ai vari
destinatari (finestre). Possiamo individuare quindi tre passi fondamentali che dobbiamo compiere
all'interno del loop:
- Estrazione del prossimo messaggio dalla coda di attesa.
- Decodifica del messaggio.
- Invio del messaggio al destinatario.
A ciascuno di questi tre passi è associata una ben precisa procedura. Per l'estrazione dei
messaggi dalla coda dobbiamo utilizzare la procedura GetMessage definita nella libreria
USER32.LIB; il prototipo di questa procedura è il seguente:
BOOL GetMessage(LPMSG lpMsg, HWND hWnd, UINT wMsgFilterMin, UINT wMsgFilterMax);
lpMsg è una DWORD contenente l'indirizzo di una struttura di tipo MSG;
i campi che formano questa struttura vengono riempiti da GetMessage.
hWnd è l'handle della finestra destinataria del messaggio; quando questo parametro vale
NULL la procedura GetMessage estrae i messaggi legati esclusivamente all'applicazione
che ha chiamato la procedura stessa. Nel caso del nostro esempio è fondamentale che il parametro
hWnd valga NULL.
wMsgFilterMin e wMsgFilterMax rappresentano rispettivamente il codice più piccolo
e il codice più grande dei messaggi che bisogna estrarre dalla coda di attesa; assegnando il
valore zero ad entrambi i parametri si indica a GetMessage di estrarre messaggi aventi
qualsiasi codice.
La procedura GetMessage termina restituendo in EAX un valore non nullo; l'unica
eccezione è rappresentata dall'estrazione dalla coda del messaggio WM_QUIT che indica
l'imminente chiusura della nostra applicazione. In questo caso GetMessage restituisce
EAX=0; questa è proprio la condizione di uscita dal loop dei messaggi. L'uscita dal
loop determinata dalla ricezione di WM_QUIT indica la terminazione regolare del nostro
programma; in questo caso il Win32 Programmer's Reference consiglia di utilizzare il
campo wParam della struttura MSG come exit code.
Se nell'estrazione di un messaggio dalla coda si verifica un errore, GetMessage
restituisce il numero negativo -1; si tratta di un'eventualità estremamente improbabile,
ma conoscendo Windows è meglio non sottovalutare la situazione.
Per la decodifica del messaggio appena estratto da GetMessage si utilizza la procedura
TranslateMessage definita nella libreria USER32.LIB; il prototipo di questa
procedura è il seguente:
BOOL TranslateMessage(CONST MSG *lpMsg);
lpMsg è l'indirizzo della struttura di tipo MSG appena restituitaci da
GetMessage; la procedura TranslateMessage provvede alla decodifica del messaggio
corrente. Ad esempio, se si preme un tasto della tastiera, Win32 associa questo evento
ad un messaggio che rappresenta in forma "criptata" il codice del tasto appena premuto; questo
codice viene chiamato virtual key (tasto virtuale). La procedura TranslateMessage
provvede a "decriptare" il virtual key ricavandone una serie di informazioni tra le quali
c'è anche il codice
ASCII
del tasto; tutte queste informazioni vengono a loro volta
convertite in messaggi e inserite nella coda privata dell'applicazione destinataria.
Per l'invio dei messaggi appena decodificati da TranslateMessage si utilizza la procedura
DispatchMessage definita nella libreria USER32.LIB; il prototipo di questa
procedura è il seguente:
BOOL DispatchMessage(CONST MSG *lpMsg);
lpMsg è l'indirizzo della struttura di tipo MSG appena decodificata da
TranslateMessage; la procedura DispatchMessage provvede ad inviare materialmente
il messaggio alla window procedure della finestra destinataria. Il valore che
DispatchMessage restituisce in EAX coincide con il valore di ritorno della window
procedure.
A questo punto abbiamo a disposizione tutti gli elementi necessari per scrivere il codice relativo
al loop dei messaggi; prima di tutto dobbiamo definire un'istanza della struttura MSG. Come
al solito questa definizione può essere inserita sia nel blocco dati che nello stack; nel nostro
esempio definiamo l'istanza msg nello stack scrivendo:
LOCAL msg :MSG
In questo modo msg assume l'aspetto di una variabile locale di WinMain; a questo
punto possiamo scrivere il codice mostrato in Figura 5.4 per quanto riguarda il message loop (in versione MASM).
Come si può notare, se il programma termina regolarmente, WinMain a sua volta termina con
il valore msg.wParam in EAX; questo valore viene quindi passato a ExitProcess
che lo restituisce al SO. Se invece si è verificato un errore (causato ad esempio dal
fallimento di RegisterClassEx), l'esecuzione salta all'etichetta exitWinMain con
EAX=0.
5.7 La Window Procedure
Non appena il nostro programma viene caricato in memoria per l'esecuzione, entra in azione
il message loop che "pompa" continuamente messaggi dalla coda di attesa, li decodifica
e li smista alle window procedure delle finestre destinatarie. Nel caso del nostro esempio,
DispatchMessage invia i messaggi alla procedura WndProc associata alla main
window; il SO conosce la posizione in memoria di WndProc in quanto gli abbiamo
passato questa informazione attraverso il campo lpfnWndProc della struttura wc.
Siamo liberi di assegnare qualsiasi nome ad una window procedure; la lista dei parametri però
deve rispettare rigorosamente il seguente prototipo:
LRESULT NomeWinProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
hWnd è l'handle della finestra destinataria.
uMsg, wParam e lParam equivalgono ai campi message, wParam e
lParam della struttura MSG.
Una window procedure termina restituendo in EAX un codice che rappresenta il risultato
dell'elaborazione del messaggio; generalmente questo codice viene totalmente ignorato in
Win32.
Nel nostro esempio la window procedure viene chiamata WndProc; all'interno di WndProc
avviene l'elaborazione dei messaggi che intendiamo accettare. È fondamentale che tutti i messaggi
che non intendiamo accettare vengano rispediti al mittente; a tale proposito ci dobbiamo servire
della window procedure predefinita di Win32, chiamata DefWindowProc e definita nella
libreria USER32.LIB. Questa procedura naturalmente ha lo stesso prototipo mostrato in
precedenza; DefWindowProc riceve tutti i messaggi che abbiamo rifiutato ed effettua su di
essi una serie di elaborazioni predefinite. In base a tutte queste considerazioni, possiamo
definire il seguente scheletro in linguaggio C della procedura WndProc:
In sostanza, se rifiutiamo un messaggio, lo passiamo a DefWindowProc e poi usciamo da
WndProc con il valore di ritorno restituito in EAX dalla stessa
DefWindowProc; se invece accettiamo il messaggio, lo elaboriamo e usciamo da WndProc
con EAX che convenzionalmente ha valore zero. Come è stato detto in precedenza, questo
valore di ritorno rimane inutilizzato.
Nel caso del nostro esempio, si nota che la procedura WndProc gestisce solamente i due
messaggi WM_CLOSE e WM_DESTROY che sono fondamentali per la corretta terminazione
di un'applicazione; il messaggio WM_CLOSE viene generato da Win32 ogni volta che
si cerca di chiudere una finestra. Questo evento si verifica quando premiamo il bottone
Chiudi all'estremità destra della barra del titolo, quando selezioniamo Chiudi
dal menu di sistema della finestra, quando eseguiamo un doppio click sull'icona di sistema
della finestra (icona piccola), etc; in tutti questi casi la procedura WndProc si vede
arrivare il codice WM_CLOSE nel parametro uMsg. Nel nostro esempio intercettiamo
il messaggio WM_CLOSE e apriamo una MessageBox per chiedere conferma all'utente;
rispetto agli esempi del precedente capitolo, la MessageBox usata dall'applicazione
MAINWIN presenta alcune caratteristiche interessanti. Prima di tutto osserviamo che
questa volta la MessageBox viene generata dalla main window del nostro programma; questo
significa che la MessageBox è figlia della main window e quindi deve ricevere nel primo
parametro l'handle hWnd della madre e cioè della main window. Un'altro aspetto interessante
riguarda la presenza di due bottoni nella MessageBox; si tratta dei due bottoni YES
e NO che possiamo attivare con il codice MB_YESNO. Se si preme YES la
MessageBox restituisce in EAX il codice IDYES; se si preme NO la
MessageBox restituisce in EAX il codice IDNO.
Se premiamo NO saltiamo all'etichetta exitWndProc, usciamo da WndProc con
EAX=0 e l'esecuzione della nostra applicazione continua; se premiamo YES, stiamo
confermando la chiusura dell'applicazione. In questo caso dobbiamo seguire rigorosamente un
procedimento ben preciso che deve garantire una corretta chiusura dell'applicazione; in
particolare, è fondamentale che WndProc dopo aver "risposto" a WM_CLOSE passi
questo messaggio alla DefWindowProc. In questo modo anche DefWindowProc può
rispondere a WM_CLOSE eseguendo una serie di operazioni predefinite che precedono la
chiusura della finestra; al termine di queste operazioni, DefWindowProc spedisce il
messaggio WM_DESTROY per richiedere la "distruzione" fisica della finestra. A questo
punto la nostra WndProc deve intercettare WM_DESTROY e in risposta a questo
messaggio deve chiamare la procedura PostQuitMessage.
La procedura PostQuitMessage viene definita nella libreria USER32.LIB; il prototipo
di questa procedura è il seguente:
VOID PostQuitMessage(int nExitCode);
nExitCode deve contenere l'exit code con il quale vogliamo uscire dalla nostra
applicazione; generalmente i programmatori passano NULL in questo parametro, anche se
in questo modo l'exit code può essere confuso con un codice di errore (zero). La procedura
PostQuitMessage non restituisce nessun valore di ritorno (void), ma prima di
terminare inserisce nella coda di attesa il messaggio WM_QUIT; alla successiva iterazione
del message loop, la procedura GetMessage riempie nuovamente la struttura msg
inserendo il codice WM_QUIT nel campo message e il codice nExitCode nel
campo wParam. In presenza del messaggio WM_QUIT la procedura GetMessage
termina con valore di ritorno EAX=0; come già sappiamo questa è proprio la condizione di
uscita dal loop dei messaggi.
Subito dopo essere usciti dal loop, carichiamo in EAX il codice contenuto in
msg.wParam (cioè nExitCode) e quindi usciamo da WinMain; a questo punto
viene chiamata ExitProcess che termina l'applicazione con l'exit code contenuto
in EAX.
È importante sottolineare il fatto che se non seguiamo il procedimento descritto prima per
la gestione di WM_CLOSE può verificarsi una terminazione anomala del nostro programma;
in particolare, può capitare che la main window scompaia dallo schermo lasciando però vari
"pezzi" sparpagliati in memoria. Per verificare questa eventualità, possiamo premere la sequenza
di tasti [Ctrl][Alt][Canc] (o [Ctrl][Alt][Del]) per visualizzare il Task
Manager di Win32, cioè la finestra che mostra la lista delle applicazioni in
esecuzione; se il nostro programma è terminato in modo anomalo, pur essendo scomparso dallo
schermo potrebbe essere ancora presente in questa lista.
5.8 Esecuzione di MAINWIN.EXE
Eseguendo MAINWIN.EXE compare sullo schermo una classica finestra di Win32 dotata
di sfondo standard, bordi tridimensionali, menu di sistema, bottoni Riduci a icona,
Ingrandisci/Ripristina e Chiudi, barra del titolo, etc; ogni volta che si cerca di
chiudere questa finestra, compare una MessageBox che ci chiede di confermare la nostra
scelta.
È importante che il programmatore faccia degli esperimenti provando a modificare alcuni codici
passati alla struttura WNDCLASSEX o alla procedura CreateWindowEx; nel caso della
struttura WNDCLASSEX si può provare a modificare i campi hIcon, hCursor e
hbrBackground inserendo i codici predefiniti presenti nell'include file windows.inc
o nel Win32 Programmer's Reference. Nel caso della procedura CreateWindowEx si
può provare a modificare i parametri dwExStyle, dwStyle, x, y,
nWidth e nHeight; anche in questo caso, si consiglia di consultare il Win32
Programmer's Reference per avere maggiori informazioni su questi parametri.
5.9 Note importanti sulla generazione dell'eseguibile MAINWIN.EXE
Se stiamo utilizzando MASM32, la generazione di MAINWIN.EXE avviene in modo
estremamente semplice sia dalla linea di comando che dall'interno di QEDITOR.EXE; in
particolare, dall'interno di QEDITOR.EXE ci basta selezionare il menu Project + Build
All. Se non ci sono errori possiamo eseguire MAINWIN.EXE selezionando il menu
Project + Run Program.
Tutto si svolge in modo molto semplice grazie al fatto che MASM32 offre il pieno supporto
per lo sviluppo di applicazioni in Assembly per Win32; in particolare questo
assembler fornisce l'SDK completo di Win32, compresi gli include files associati alle
varie librerie.
5.10 Note importanti sulle versioni ASCII e UNICODE delle procedure di Win32
Nell'esempio presentato in questo capitolo e in tutti gli esempi dei capitoli successivi vengono
utilizzati gli include files associati all'SDK di Win32; in questi include files
sono presenti naturalmente i prototipi delle varie procedure dell'SDK. Come già sappiamo,
tutte le procedure che gestiscono stringhe sono presenti sia in versione
ASCII
che in versione UNICODE; le procedure in versione
ASCII
hanno nomi che terminano con la lettera A (MessageBoxA), mentre le procedure in versione UNICODE hanno nomi
che terminano con la lettera W (MessageBoxW). Se si consulta ad esempio il file
USER32.INC si nota che tutti i prototipi delle procedure in versione
ASCII,
vengono ridichiarati nel seguente modo:
In sostanza, grazie a queste ridichiarazioni, ogni volta che omettiamo la lettera A finale, viene chiamata la versione
ASCII
della procedura; in questo modo si evita il fastidio di dover specificare ogni volta
la lettera finale. A partire dagli esempi del prossimo capitolo utilizzeremo quindi i prototipi
delle procedure
ASCII
privi della A finale.
Codice sorgente per MASM:
Esempi capitolo 5 MASM