Win32 Assembly

Capitolo 5: La Main Window.

Nel precedente capitolo sono state presentate alcune piccolissime applicazioni per Win32 come ad esempio PRIMO.ASM; l'estrema semplicita' di un programma come PRIMO.ASM e' 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 complessita' del programma e' 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 piu' generale, quando si realizza una 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 e' stato gia' spiegato nei precedenti capitoli, ogni applicazione standard per Win32 e' 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) e' 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 Win32 Assembly 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:

1) Inizializzazione della main window.
2) Registrazione della main window.
3) Attivazione della main window.
4) Gestione della main window.

La fase piu' importante e' 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 cioe' 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 puo' essere dotata di una o piu' finestre; una di esse rappresenta la finestra principale (main window), mentre le altre svolgono il ruolo di finestre secondarie (o finestre figlie). 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 cioe' le procedure che vengono utilizzate dal SO per inviare indirettamente messaggi ad una applicazione, vengono chiamate procedure di callback. Il programmatore Assembly e' libero di assegnare qualsiasi nome ad una window procedure; tradizionalmente la window procedure associata alla main window viene chiamata WndProc.

Nota importante.
I manuali di programmazione per win32, raccomandano che tutte le procedure di callback preservino il contenuto originario dei registri EBX, ESI e EDI; quindi, se una procedura di callback deve utilizzare questi registri, deve prima salvare il loro contenuto originario nello stack, in modo da poter procedere successivamente alla fase di ripristino.

Cominciamo quindi l'analisi dettagliata delle quattro fasi che ci portano a realizzare una applicazione standard per Win32; prima di tutto procediamo al download del codice sorgente relativo all'esempio mostrato in questo capitolo:
Download di tutti gli esempi del capitolo (versione MASM)
Download di tutti gli esempi del capitolo (versione TASM)
Il file in formato ZIP deve essere scompattato come al solito nella cartella di lavoro win32asm; a questo punto lanciamo il nostro editor preferito e apriamo il file MAINWIN.ASM che contiene il programma di esempio che stiamo per analizzare. Si puo' notare che il codice sorgente ha dimensioni ben piu' 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 puo' essere riciclato e impiegato per scrivere altre applicazioni.

5.2 La procedura WinMain.

Analizzando il listato di MAINWIN.ASM 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 cioe' 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 e' scritto chiaramente in Assembly. All'interno di C0.ASM troviamo l'entry point del programma, chiamato STARTX piu' 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 conterra' 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 e' terminata, dal modulo C0.ASM viene chiamata la procedura main; il prototipo C di questa procedura e':
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 e' 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 e' obbligato a seguire il procedimento appena descritto; in Assembly siamo liberi di seguire lo stile che piu' si adatta ai nostri gusti. Naturalmente pero' siamo tenuti a rispettare le regole che definiscono la struttura di un'applicazione Win32 standard; questa struttura puo' essere schematizzata attraverso tre blocchi fondamentali:

1) Il primo bocco contiene la cosiddetta fase di startup dell'applicazione; in questa fase dobbiamo reperire in particolare alcune informazioni molto importanti.
2) Il secondo blocco contiene le quattro fasi che portano alla creazione della main window del programma; come gia' sappiamo queste quattro fasi consistono nell'inizializzazione, registrazione, attivazione e gestione della main window.
3) 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, e' 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 piu' semplice la verifica e la manutenzione del codice sorgente; proprio per questo motivo, tutti gli esempi presentati nella sezione Win32 Assembly, 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 e' perfettamente equivalente a STDCALL; infatti nel file windows.inc e' presente una dichiarazione del tipo:
WINAPI equ STDCALL
Come si puo' notare, la procedura WinMain termina restituendo un valore di tipo int (intero con segno a 32 bit); questo valore e' l'exit code che verra' restituito al SO dopo la terminazione della nostra applicazione. Come e' 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 piu' avanti.
Passiamo ora all'analisi dettagliata dei parametri richiesti da WinMain.

hInst e' l'handle che identifica l'istanza corrente della nostra applicazione, e cioe' il codice numerico che il SO assegna alla nostra applicazione al momento del caricamento in memoria per la fase di esecuzione; come e' 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 Gb.

hPrevInst e' l'handle che identifica eventuali altre istanze della nostra applicazione gia' in fase di esecuzione; come sappiamo, questa informazione e' importante solo in ambiente Win16. In Win32 il concetto di istanze multiple di una 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 e' 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 e' un puntatore FAR ad una stringa C contenente gli eventuali parametri passati alla nostra applicazione dalla command line; come gia' 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 cioe' un dato di tipo DWORD; il parametro lpCmdLine non viene quasi mai utilizzato in quanto e' rarissimo che un'applicazione Win32 riceva l'input dalla linea di comando.

nCmdShow e' un codice numerico a 32 bit che ci permette di specificare lo stato iniziale della main window della nostra applicazione; i codici piu' 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 (cioe' 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 gia' 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 e':
LPTSTR GetCommandLine(void);
Come si puo' 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 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 seguente struttura (in versione TASM):
call     GetModuleHandleA, NULL     ; trova l'handle del modulo MAINWIN.EXE
mov      hInstance, eax             ; e lo salva in hInstance

call     GetCommandLineA            ; trova l'indirizzo della command line
mov      commandLine, eax           ; e lo salva in commandLine

call     WinMain, hInstance, NULL, commandLine, SW_SHOWNORMAL

call     ExitProcess, eax           ; exit code restituito da WinMain
Come si puo' notare, grazie all'uso di WinMain il codice appare estrememente chiaro e ordinato; i tre blocchi evidenti sono:

a) acquisizione di hInstance e commandLine;
b) chiamata di WinMain;
c) 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 e' 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 (o win32.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:
WNDCLASSEX  STRUC
  
   cbSize            UINT     ?        ; dimensione struttura in byte
   style             UINT     ?        ; stile della classe finestra
   lpfnWndProc       WNDPROC  ?        ; puntatore alla window procedure 
   cbClsExtra        LONG     ?        ; extra byte per la classe finestra
   cbWndExtra        LONG     ?        ; extra byte per la finestra
   hInstance         HANDLE   ?        ; handle dell'applicazione
   hIcon             HICON    ?        ; handle dell'icona;
   hCursor           HCURSOR  ?        ; handle del cursore del mouse 
   hbrBackground     HBRUSH   ?        ; colore di sfondo 
   lpszMenuName      LPCTSTR  ?        ; puntatore alla risorsa menu 
   lpszClassName     LPCTSTR  ?        ; puntatore alla stringa di classe
   hIconSm           HICON    ?        ; handle dell'icona piccola

WNDCLASSEX  ENDS
Prima di tutto nel nostro programma definiamo un'istanza wc di questa struttura; questa istanza puo' 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 e' una DWORD destinata a contenere la dimensione in byte della struttura; per inizializzare questo campo possiamo utilizzare gli operatori SIZE del TASM e SIZEOF del MASM. Con il TASM possiamo scrivere:
mov wc.cbSize, SIZE WNDCLASSEX
Con il MASM possiamo scrivere:
mov wc.cbSize, SIZEOF WNDCLASSEX

style e' 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 e' l'indirizzo della window procedure associata alla nostra finestra; come e' 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 piu' avanti; il tipo di dato WNDPROC non e' 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) verra' 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) verra' 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 gia' 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 e' 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:
MOV32 MACRO Destinazione, Sorgente

   push     dword ptr Sorgente
   pop      dword ptr Destinazione
   
ENDM
A questo punto possiamo scrivere:
MOV32 wc.hInstance, hInstance
La macro MOV32 potrebbe essere scritta anche come:
MOV32 MACRO Destinazione, Sorgente

   mov      eax, Sorgente
   mov      Destinazione, eax
   
ENDM
Questo metodo pero' e' sconsigliabile in quanto il registro EAX (o qualsiasi altro registro generale) potrebbe essere gia' 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 e':
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):
invoke   LoadIconA, NULL, IDI_WINLOGO
mov      wc.hIcon, eax
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 e':
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):
invoke   LoadCursorA, NULL, IDC_ARROW
mov      wc.hCursor, eax
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 e' 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 e' 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 e' 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'estremita' 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 rimpicciolita 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 e' 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 puo' 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 e' 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 TASM; se wc e' stata definita nel segmento dati, allora questa variabile e' dotata di un offset ben preciso che puo' essere calcolato dall'assembler in fase di assemblaggio del programma. In questo caso possiamo scrivere:
call RegisterClassExA, offset wc
Se invece wc e' stata definita nello stack, allora l'assembler ovviamente non puo' conoscere in anticipo l'offset di questa variabile; ricordiamoci infatti che, come e' stato spiegato nel Capitolo 24 della sezione Assembly Base, lo stack e' un'area di memoria gestita dinamicamente. Le variabili locali (cosi' come gli argomenti) di una procedura, vengono create nello stack in fase di chiamata della procedura, e distrutte al termine della procedura stessa; puo' capitare che ad ogni chiamata queste variabili vengano ricreate nello stack sempre ad offset differenti. In questo caso, al posto dell'operatore OFFSET dobbiamo utilizzare l'istruzione LEA (load effective address) che e' in grado di calcolare un offset in fase di esecuzione del programma (run time); in definitiva, se wc e' stata creata nello stack, dobbiamo scrivere:
lea      eax, wc
call     RegisterClassExA, eax
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 puo' portare alla scrittura di programmi contenenti bugs molto pericolosi e difficili da scovare.
Per quanto riguarda il MASM, valgono ovviamente le stesse considerazioni; questo assembler pero' ci mette a disposizione l'operatore ADDR che utilizzato in combinazione con la direttiva INVOKE permette la scrittura di codice piu' 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
Indipendentemente dagli aspetti formali, e' ovvio che alla fine sia TASM che MASM produrranno un codice macchina equivalente; questo codice macchina corrisponde alla sequenza di istruzioni:
lea      eax, wc
push     eax
call     RegisterClassExA
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):
invoke   RegisterClassExA, ADDR wc  ; richiesta di registrazione
test     eax, eax                   ; errore ?
jz       exitWinMain                ; termina con eax = 0
In caso di errore la procedura WinMain termina con EAX=0; in una eventualita' del genere sarebbe opportuno informare l'utente con un'apposita MessageBox. Il lettore puo' provare per esercizio ad inserire il codice necessario per la gestione degli errori; questo codice puo' essere inserito prima della chiamata di ExitProcess.

5.5 Attivazione della Main Window.

Se la fase di registrazione e' terminata con successo, possiamo passare alla fase di attivazione della main window; questa fase si svolge fondamentalmente in tre passi:

1) Creazione della finestra.
2) Visualizzazione della finestra.
3) 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 e' il seguente:
HWND CreateWindowEx(
   DWORD dwExStyle,        /* extended window style */
   LPCTSTR lpClassName,    /* pointer to registered class name */
   LPCTSTR lpWindowName,   /* pointer to window name */
   DWORD dwStyle,          /* window style */
   int x,                  /* horizontal position of window */
   int y,                  /* vertical position of window */
   int nWidth,             /* window width */
   int nHeight,            /* window height */
   HWND hWndParent,        /* handle to parent or owner window */
   HMENU hMenu,            /* handle to menu, or child-window identifier */
   HINSTANCE hInstance,    /* handle to application instance */
   LPVOID lpParam          /* pointer to window-creation data */
);
Tutti i parametri richiesti da questa procedura sono di tipo DWORD; analizziamo in dettaglio il loro significato.

dwExStyle e' un codice numerico che rappresenta lo stile esteso della finestra; modificando questo codice e' 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 e' 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 (o win32.inc).

lpClassName e' l'indirizzo di una stringa C contenente il nome assegnato alla window class; si tratta dello stesso parametro gia' utilizzato per inizializzare la struttura wc.

lpWindowName e' l'indirizzo di una stringa C che verra' visualizzata nella barra del titolo della finestra; nel nostro esempio utilizziamo la stringa winTitle.

dwStyle e' un codice numerico che rappresenta lo stile ordinario della finestra; nel nostro esempio utilizziamo lo stile WS_OVERLAPPEDWINDOW che e' 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 e' 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 e' 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 e' la larghezza iniziale della finestra; nel nostro esempio utilizziamo il codice predefinito CW_USEDEFAULT che lascia la scelta di nWidth al SO.

nHeight e' l'altezza iniziale della finestra; nel nostro esempio utilizziamo il codice predefinito CW_USEDEFAULT che lascia la scelta di nHeight al SO.

hWndParent e' l'handle della finestra "madre" di cui e' "figlia" la nostra finestra; nel nostro caso stiamo creando proprio la finestra madre (main window), per cui questo parametro deve valere NULL.

hMenu e' 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 e' l'handle della nostra applicazione; questa informazione e' gia' stata descritta in precedenza.

lpParam e' 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 puo' 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 e' 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 e' il seguente:
BOOL ShowWindow(HWND hWnd, int nCmdShow);
Il parametro hWnd e' l'handle della finestra che in precedenza avevamo salvato in hWindow; il parametro nCmdShow individua lo stato iniziale della finestra ed e' stato gia' 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 e' il seguente:
BOOL UpdateWindow(HWND hWnd);
Il parametro hWnd e' il solito handle della finestra gia' 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 pero' 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 seguente struttura (versione TASM):
call     CreateWindowExA, WS_EX_OVERLAPPEDWINDOW, \
                          offset className, \
                          offset winTitle, \
                          WS_OVERLAPPEDWINDOW, \
                          CW_USEDEFAULT, \
                          CW_USEDEFAULT, \
                          CW_USEDEFAULT, \
                          CW_USEDEFAULT, \
                          NULL, \
                          NULL, \
                          hInstance, \
                          NULL

test     eax, eax                         ; errore ?
jz       exitWinMain                      ; termina con eax = 0
mov      hWindow, eax                     ; salva l'handle della finestra
call     ShowWindow, hWindow, nCmdShow    ; visualizza la finestra
call     UpdateWindow, hWindow            ; aggiorna l'area client
test     eax, eax                         ; errore ?
jz       exitWinMain                      ; termina con eax = 0
A questo punto se tutto e' filato liscio possiamo passare alla fase piu' importante del nostro programma, e cioe' 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 pricipale e' 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:
MSG      STRUC

   hwnd     HWND     ?     ; handle della finestra destinataria
   message  UINT     ?     ; codice messaggio
   wParam   WPARAM   ?     ; informazioni addizionali
   lParam   LPARAM   ?     ; informazioni addizionali
   time     DWORD    ?     ; ora esatta di invio del messaggio
   pt       POINT    < ? > ; posizione del cursore del mouse
   
MSG      ENDS
Analizziamo in dettaglio il significato dei vari campi che formano questa struttura.

hwnd e' l'handle della finestra destinataria del messaggio.

message e' il codice numerico a 32 bit che identifica il messaggio; nel file windows.inc e' 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 e' legata al codice presente nel campo messaggio.

time rappresenta l'ora esatta in cui il SO ha inviato il messaggio.

pt e' 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:
POINT    STRUC

   x     LONG     ?     ; ascissa
   y     LONG     ?     ; ordinata

POINT    ENDS
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:

1) Estrazione del prossimo messaggio dalla coda di attesa.
2) Decodifica del messaggio.
3) Invio del messaggio al destinatario.

A ciascuno di questi tre passi e' 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 e' il seguente:
BOOL GetMessage(LPMSG lpMsg, HWND hWnd, UINT wMsgFilterMin, UINT wMsgFilterMax);

lpMsg e' una DWORD contenente l'indirizzo di una struttura di tipo MSG; i campi che formano questa struttura vengono riempiti da GetMessage.

hWnd e' 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 e' fondamentale che il parametro hWnd valga NULL.

wMsgFilterMin e wMsgFilterMax rappresentano rispettivamente il codice piu' piccolo e il codice piu' 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 e' 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 e' 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'eventualita' estremamente improbabile, ma conoscendo Windows e' 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 e' il seguente:
BOOL TranslateMessage(CONST MSG *lpMsg);

lpMsg e' 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'e' 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 e' il seguente:
BOOL DispatchMessage(CONST MSG *lpMsg);

lpMsg e' 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 puo' essere inserita sia nel blocco dati che nello stack; nel nostro esempio definiamo l'istanza msg nello stack scrivendo:
LOCAL msg :MSG
In queto modo msg assume l'aspetto di una variabile locale di WinMain; a questo punto possiamo scrivere (in versione MASM):
MessageLoop:
   invoke   GetMessage, ADDR msg, NULL, 0, 0       ; estrazione prossimo messaggio
   test     eax, eax                               ; eax = 0 ? 
   jz       exitMessageLoop                        ; fine del loop (WM_QUIT)
   invoke   TranslateMessage, ADDR msg             ; decodifica messaggio
   invoke   DispatchMessage, ADDR msg              ; invio messaggio alla WinProc
   jmp      MessageLoop                            ; ripeti
exitMessageLoop:
   mov      eax, msg.wParam                        ; exit code = OK
exitWinMain:                                       ; exit code = 0 (errore)
Come si puo' 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 e' 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 pero' deve rispettare rigorosamente il seguente prototipo:
LRESULT NomeWinProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

hWnd e' 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. E' 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:
LRESULT WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
   if (uMsg == WM_AAAA)          /* se il messaggio e' WM_AAAA */
      ......................     /* elabora il messaggio WM_AAAA */
   else if (uMsg == WM_BBBB)     /* se invece il messaggio e' WM_BBBB */
      ......................     /* elabora il messaggio WM_BBBB */
   ......................... 
      ......................
   else                          /* altrimenti chiama DefWindowProc */    
      return DefWindowProc(hWnd, uMsg, wParam, lParam);
      
   return 0;                     /* valore di ritorno di 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 e' 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'estremita' 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 e' figlia della main window e quindi deve ricevere nel primo parametro l'handle hWnd della madre e cioe' 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, e' fondamentale che WndProc dopo aver "risposto" a WM_CLOSE passi questo messaggio alla DefWindowProc. In questo modo anche DefWindowProc puo' 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 e' 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 puo' 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 gia' sappiamo questa e' prorpio la condizione di uscita dal loop dei messaggi.
Subito dopo essere usciti dal loop, carichiamo in EAX il codice contenuto in msg.wParam (cioe' nExitCode) e quindi usciamo da WinMain; a questo punto viene chiamata ExitProcess che termina l'applicazione con l'exit code contenuto in EAX.
E' importante sottolineare il fatto che se non seguiamo il procedimento descritto prima per la gestione di WM_CLOSE puo' verificarsi una terminazione anomala del nostro programma; in particolare, puo' capitare che la main window scompaia dallo schermo lasciando pero' vari "pezzi" sparpagliati in memoria. Per verificare questa eventualita', possiamo premere la sequenza di tasti [Ctrl][Alt][Canc] (o [Ctrl][Alt][Del]) per visualizzare il Task Manager di Win32, cioe' la finestra che mostra la lista delle applicazioni in esecuzione; se il nostro programma e' 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.
E' 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 puo' 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 puo' 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.

Se invece vogliamo usare TASM, dobbiamo procurarci la versione piu' aggiornata possibile dell'SDK in quanto quella fornita con TASM 5.x e' ormai obsoleta; per quanto riguarda gli include files possiamo utilizzare quelli del MASM32 con l'eccezione di windows.inc che usa una sintassi incompatibile con TASM. Come e' stato detto nei precedenti capitoli, il problema piu' grave e' legato al fatto che i nomi usati per i campi di una struttura sono considerati locali da MASM e globali da TASM; questo significa che con MASM possiamo riutilizzare questi nomi all'interno di altre strutture, mentre con TASM questo non e' possibile. Per questo motivo e' necessario procurarsi un apposito file windows.inc scritto espressamente per il TASM; alternativamente, con un po' di olio di gomito e' possibile modificare il file windows.inc di MASM adattandolo al TASM.
Le modifiche da apportare sono relative fondamentalmente ai nomi usati per i campi delle varie strutture predefinite di Win32; lo scopo di queste modifiche e' quello di eliminare la possibilita' che il nome di un campo di una struttura venga ridefinito altrove provocando un messaggio di errore di TASM. Vediamo ad esempio il caso del campo wParam della struttura MSG; questo nome entra in conflitto con il nome del terzo parametro utilizzato per la procedura WndProc.
Come si puo' facilmente intuire, si tratta di una situazione veramente assurda che costringe il programmatore a compiere veri e propri salti mortali; e' un vero peccato che la Borland non abbia ancora risolto questo problema visto che il TASM per qualita', efficienza e affidabilita' e' nettamente superiore al MASM.
Attraverso Internet e' possibile reperire numerose versioni del file windows.inc per il TASM; e' fondamentale procurarsi una versione che si attenga nel modo piu' fedele possibile ai nomi imposti dalla Microsoft per le varie dichiarazioni.
Per venire incontro ai numerosi estimatori del TASM, e' possibile scaricare da questo sito una versione di windows.inc curata da Ra.M. Software; il file si chiama win32.zip ed e' prelevabile dalla sezione Downloads. Questo file una volta scompattato diventa win32.inc e deve essere spostato nella cartella c:\tasm\include; il file win32.inc viene costantemente aggiornato sulla base delle esigenze legate ai programmi di esempio presentati nei vari capitoli della sezione Win32 Assembly. Tutti gli esempi per TASM presentati in questa sezione utilizzano l'include file win32.inc; le dichiarazioni presenti in win32.inc presuppongono che il programmatore disponga del TASM 5.x. Le versioni precedenti del TASM non supportano il modello di memoria FLAT per cui non e' possibile utilizzare neanche la sintassi avanzata per la chiamata delle procedure; in questo caso e' necessario programmare in puro Assembly.
Se si intende utilizzare win32.inc e' consigliabile aprire questo file con un editor per prendere confidenza in particolare con le modifiche apportate ai membri delle strutture WNDCLASSEX, MSG, etc; ad esempio, il membro wParam di MSG e' stato modificato in ms_wParam. E' chiaro che il programmatore e' libero di modificare il contenuto di win32.inc (o di qualsiasi versione di windows.inc) in base ai gusti personali; in particolare, e' importante che l'include file principale venga continuamente aggiornato con le informazioni piu' recenti reperite ad esempio attraverso Internet.
Se si decide invece di scaricare wintasm.zip dalla sezione Downloads, si ha a disposizione una delle tante versioni di windows.inc reperibile via Internet; se si vuole utilizzare questo file e' necessario inserire nei propri programmi la dichiarazione:
UNICODE = 0
Naturalmente questa dichiarazione deve precedere l'inclusione di windows.inc.


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 gia' 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:
MessageBoxA PROTO :DWORD, :DWORD, :DWORD, :DWORD
MessageBox  equ   < MessageBoxA >
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.