Assembly Windows 32bit con MASM
Capitolo 6: La gestione del testo
In questo capitolo vengono analizzati i principali strumenti che win32 ci mette a
disposizione per l'output delle stringhe di testo sullo schermo; per verificare in pratica i
vari concetti esposti nel capitolo, viene utilizzato un apposito programma denominato
FONTWIN.ASM mostrato in Figura 6.1.
Analizzando il contenuto del file FONTWIN.ASM mostrato in Figura 6.1, si può subito notare che, come era stato
anticipato nel precedente capitolo, è stato riciclato praticamente il 100% del codice sorgente
presente nel file MAINWIN.ASM; questa caratteristica dei programmi per Windows,
viene largamente sfruttata dai programmatori che vogliono risparmiare parecchio tempo nella
scrittura del codice sorgente. Supponiamo ad esempio di avere a disposizione il codice sorgente
di MAINWIN.ASM e di voler scrivere un nuovo programma chiamato FONTWIN.ASM; invece
di ricominciare da zero, conviene aprire con un editor il file MAINWIN.ASM, salvarlo
sul disco con il nuovo nome FONTWIN.ASM e apportare in seguito tutte le necessarie
modifiche.
Cominciamo allora con l'analizzare proprio le principali modifiche apportate al file
FONTWIN.ASM; prima di tutto, notiamo che le procedure dell'SDK che gestiscono
stringhe
ASCII,
vengono chiamate senza la A finale (LoadIcon,
LoadCursor, MessageBox, etc). Come è stato detto nel precedente capitolo, nei
vari include files dell'SDK, tutte le procedure che gestiscono stringhe
ASCII
hanno nomi che vengono ridichiarati senza la A finale, come ad esempio:
In questo modo, se dimentichiamo di scrivere la A finale, viene chiamata la versione
ASCII
della procedura; da questo capitolo in poi, la A finale di questi nomi verrà
sempre omessa.
Il blocco dati inizializzati (_DATA) e il blocco dati non inizializzati (_BSS) di
FONTWIN.ASM, assumono l'aspetto mostrato in Figura 6.2.
Come si può notare, per le variabili principali come className, winTitle, etc, si
è proceduto semplicemente a sostituire la stringa MainWin con la stringa FontWin;
tutte le altre variabili definite nel blocco _DATA e nel blocco dati non inizializzati
_BSS, vengono descritte nel seguito del
capitolo.
Passiamo ora alle modifiche apportate nella fase di inizializzazione della main window; come
già sappiamo, questa fase consiste nel riempimento di una struttura di tipo WNDCLASSEX.
Per il campo hIcon (handle dell'icona), viene utilizzata l'icona predefinita individuata
dal codice IDI_INFORMATION; questa icona è la stessa che viene mostrata in una
MessageBox con il codice MB_ICONINFORMATION.
Per il campo hCursor (handle del cursore del mouse), viene utilizzato il cursore predefinito
individuato dal codice IDC_HAND; in questo modo, il cursore del mouse assume la forma di
una mano.
Indubbiamente, le modifiche più interessanti riguardano il campo hbrBackground che come
sappiamo, ci permette di assegnare il colore di sfondo alla finestra che stiamo inizializzando;
nel precedente capitolo abbiamo visto che questo campo è una DWORD che contiene un codice
di tipo HBRUSH. Questo codice rappresenta l'handle del pennello (brush = spazzola, pennello)
che verrà utilizzato per colorare lo sfondo; se non abbiamo esigenze particolari, possiamo
utilizzare uno dei tanti colori predefiniti di Windows. Nel precedente capitolo ad esempio,
abbiamo utilizzato il colore predefinito COLOR_WINDOW+1 che rappresenta il colore standard
per lo sfondo delle finestre; questo colore di sfondo viene assegnato da Windows a tutte
le finestre che non fanno richiesta di colori personalizzati. Per modificare il colore per lo
sfondo standard, bisogna cliccare sul desktop con il pulsante destro del mouse e selezionare
Proprietà; nella finestra che compare bisogna poi selezionare Aspetto.
Nel caso dell'applicazione FONTWIN, viene utilizzato un colore personalizzato per lo sfondo
della main window; prima di analizzare il procedimento da seguire, è necessario illustrare il
metodo che viene impiegato da Windows per la gestione dei colori.
Come si sa dalla fisica ottica, è possibile ottenere un qualsiasi colore a partire dai tre colori
rosso, verde e blu; per questo motivo, questi tre colori vengono definiti
colori fondamentali. Per ottenere un determinato colore, bisogna miscelare tra loro i tre
colori fondamentali dopo aver scelto opportunamente la tonalità di rosso, la tonalità di verde e
la tonalità di blu; nel mondo del computer, i colori ottenuti in questo modo vengono chiamati
colori RGB. La sigla RGB significa Red Green Blue (rosso, verde blu); si
usa anche la definizione di terna RGB.
In Windows le terne RGB vengono gestite attraverso il tipo di dato COLORREF;
un valore di tipo COLORREF è una DWORD che assume la struttura mostrata dalla
Figura 6.3.
Come possiamo notare, i bit da 0 a 7 contengono la tonalità di rosso, i bit da
8 a 15 contengono la tonalità di verde, i bit da 16 a 23
contengono la tonalità di blu, mentre i bit da 24 a 31 contengono il valore del
"canale alfa" (Alpha Channel); il canale alfa viene utilizzato dai programmi di grafica
per gestire l'effetto trasparenza delle immagini. Nel caso dei colori di sfondo delle finestre,
il canale alfa non viene utilizzato, e quindi i bit da 24 a 31 di un COLORREF
dovrebbero valere sempre zero; ci restano quindi a disposizione 24 bit (da 0 a
23) per definire numerosissimi colori personalizzati. Osserviamo che per definire la
tonalità di ciascuno dei tre colori fondamentali, abbiamo a disposizione 8 bit; con
8 bit possiamo rappresentare 28=256 valori differenti. In totale quindi
con 3*8=24 bit possiamo rappresentare:
28 * 28 * 28 = 28+8+8 = 224 = 16777216 colori = 16M colori
Naturalmente, sarà possibile visualizzare tutti i 16M di colori solo se abbiamo a
disposizione una scheda video che gestisce 16M colori; in questo caso, tecnicamente si
parla di schede video a 24 bit o anche di colori a 24 bit. Se oltre ai 16M di
colori vogliamo gestire anche il canale alfa, dobbiamo disporre ovviamente di una scheda video a
32 bit; se il nostro computer è dotato di scheda video con caratteristiche inferiori
(64K colori, 32K colori, etc), allora i colori non disponibili verranno
"approssimati" da Windows. In pratica, se selezioniamo un colore non supportato dalla
nostra scheda video, Windows ci fornisce il colore più somigliante a quello richiesto; è
chiaro quindi che tanto maggiore è il numero di colori supportato dalla nostra scheda video,
tanto migliore sarà la qualità delle immagini gestite attraverso Windows.
Per creare facilmente un dato di tipo COLORREF, possiamo scrivere ad esempio la seguente
macro:
Questa macro non fa altro che disporre nell'ordine giusto i tre valori intRed,
intGreen e intBlue; alla fine il registro EAX contiene un dato in formato
COLORREF.
Osserviamo che:
Una volta chiariti questi aspetti, vediamo come si deve procedere per assegnare ad esempio il
colore (0, 150, 150) allo sfondo della main window dell'applicazione FONTWIN; il
problema da affrontare riguarda il fatto che il campo hbrBackground della struttura
WNDCLASSEX richiede un HBRUSH e non un COLORREF. Per risolvere questo problema
ci viene incontro la procedura CreateSolidBrush definita nella libreria GDI32; il
prototipo di questa procedura è:
HBRUSH CreateSolidBrush(COLORREF crColor);
In pratica questa procedura riceve in input un argomento crColor di tipo COLORREF e
lo converte in un HBRUSH che ci consente di "dipingere" con il colore richiesto; il codice
di tipo HBRUSH come al solito viene restituito in EAX. In base a queste
considerazioni possiamo scrivere (versione MASM):
A questo punto possiamo divertirci a colorare lo sfondo della finestra con tutti i colori
disponibili; naturalmente, il colore di sfondo va ad interessare solamente la client area della
finestra, mentre le altre parti (bordo, barra del titolo, etc) vengono colorate da Windows
con i colori di sistema.
A titolo di curiosità possiamo fare degli esperimenti anche con la procedura
CreateHatchBrush definita sempre nella libreria GDI32; il prototipo di questa
procedura è:
HBRUSH CreateHatchBrush(int fnStyle, COLORREF crColor);
Questa procedura converte un COLORREF in un HBRUSH che ha la caratteristica di
essere non un pennello "solido" (solid brush), ma un pennello "retinato" (hatch brush); il
parametro fnStyle è una DWORD che codifica appunto lo stile di retinatura. Per
conoscere i vari stili disponibili possiamo consultare il Win32 Programmer's Reference o
l'include file windows.inc (i vari codici iniziano per HS =
hatch style). Per ottenere ad esempio una retinatura incrociata a 45 gradi, possiamo
scrivere:
invoke CreateHatchBrush, HS_DIAGCROSS, eax
(con EAX che deve contenere come al solito un COLORREF); nei capitoli successivi
vengono illustrate anche altre procedure che permettono di creare sfondi più sofisticati.
Esaminiamo ora le modifiche apportate nella fase di creazione della main window; questa fase come
sappiamo, viene gestita attraverso la procedura CreateWindowEx. Rispetto al caso di
MAINWIN.ASM, le uniche modifiche apportate riguardano i parametri x, y,
nWidth e nHeight che specificano le coordinate iniziali del vertice in alto a
sinistra della main window e le dimensioni orizzontale e verticale della main window stessa; come
si può notare, vengono utilizzati i valori x=10, y=10, nWidth=600 e
nHeight=550. Nel precedente capitolo, per questi parametri abbiamo utilizzato la costante
simbolica CW_USEDEFAULT che lascia la scelta a Windows; se non si hanno esigenze
particolari, conviene sempre utilizzare CW_USEDEFAULT che permette a Windows di
stabilire i valori più opportuni in base alla risoluzione grafica dello schermo.
Come è stato spiegato nel precedente capitolo, se la procedura CreateWindowEx termina con
successo, ci restituisce in EAX l'handle della finestra appena creata; inoltre, prima di
terminare, CreateWindowEx invia alla window procedure il messaggio WM_CREATE.
L'applicazione FONTWIN intercetta questo messaggio per effettuare determinate
inizializzazioni; tutti i dettagli relativi a questo aspetto vengono illustrati più avanti.
6.1 Modifiche apportate alla Window Procedure
Nel precedente capitolo abbiamo visto che il cuore di un'applicazione Windows è
rappresentato sicuramente dalla procedura di finestra (window procedure); ogni finestra
di un programma è dotata di un'apposita window procedure. Attraverso la window procedure,
Windows invia i messaggi alla relativa finestra; il nostro compito all'interno della
window procedure, è quello di decidere quali messaggi accettare e quali invece rifiutare
restituendoli al mittente. Restituire un messaggio al mittente significa passarlo alla procedura
DefWindowProc che è la window procedure predefinita di Windows; all'interno di
DefWindowProc vengono effettuate tutte le elaborazioni predefinite relative ai vari
messaggi in arrivo. Abbiamo anche visto che esistono particolari messaggi che dopo essere stati
intercettati ed elaborati dalla nostra window procedure, devono essere passati ugualmente a
DefWindowProc; questo è ad esempio il caso dei due messaggi WM_CREATE e
WM_CLOSE.
Per applicazioni relativamente semplici, tutto il codice per l'elaborazione dei vari messaggi può
essere inserito all'interno della window procedure; nel caso dell'esempio MAINWIN.ASM è
stata seguita proprio questa strada. Quando però l'applicazione che stiamo scrivendo comincia ad
assumere una certa complessità, conviene decisamente cambiare tattica scrivendo una apposita
procedura per ogni messaggio che intendiamo elaborare; questo è proprio il metodo seguito
nell'esempio FONTWIN.ASM. Per chiarire la situazione, vediamo in Figura 6.4 l'aspetto assunto
dalla window procedure WndProc dell'applicazione FONTWIN (versione MASM).
Come si può notare, l'applicazione FONTWIN elabora i 4 messaggi WM_CREATE,
WM_PAINT, WM_CLOSE e WM_DESTROY; osserviamo anche che come è stato detto in
precedenza, i due messaggi WM_CREATE e WM_CLOSE dopo essere stati intercettati
ed elaborati da WndProc, vengono passati anche a DefWindowProc attraverso un salto
all'etichetta messaggio_restituito. Per motivi di stile e di chiarezza, nella sequenza dei
messaggi intercettati dalla window procedure, conviene mettere per primo il messaggio
WM_CREATE; per gli stessi motivi, i messaggi WM_CLOSE e WM_DESTROY dovrebbero
occupare rispettivamente la penultima e l'ultima posizione (vedi Figura 6.4).
Ciascuno dei messaggi intercettati viene elaborato in un'apposita procedura; come accade per
WndProc, possiamo scegliere liberamente i nomi di queste procedure, mentre la lista dei
parametri viene imposta obbligatoriamente da Windows. La Microsoft consiglia di
utilizzare nomi del tipo Cls_OnMessage, dove Cls sta per class name (nome
della classe finestra); al posto di Cls bisogna quindi scrivere il nome della classe
finestra, mentre al posto di Message si deve scrivere il nome del messaggio. In questo
modo, nel caso dell'applicazione FONTWIN, si ottengono nomi del tipo:
FontWin_OnCreate, FontWin_OnClose, FontWin_OnDestroy, etc; in tutti gli esempi
presentati nella sezione Assembly Windows 32bit, viene seguito proprio questo procedimento (vedi
Figura 6.4).
Il problema fondamentale da affrontare, consiste nel reperire informazioni relative al tipo dei
parametri e degli eventuali valori di ritorno di queste procedure; il consiglio migliore che si
può dare è quello di procurarsi una copia dell'header file windowsx.h, distribuito
insieme ai compilatori C/C++ per Windows. Questo header file è stato introdotto
dalla Microsoft ai tempi di Windows95; al suo interno sono presenti i cosiddetti
message crackers. Si tratta di una serie di macro attraverso le quali tutto il codice
mostrato in Figura 6.4, nel caso di un programma C/C++ può essere semplificato come si vede
in Figura 6.5.
Come si può notare, la macro HANDLE_MSG ha tre parametri che rappresentano l'handle
della finestra (hWnd), il messaggio da elaborare, e l'indirizzo della funzione che
elabora il messaggio; ricordiamo che in C/C++ il nome di una funzione rappresenta
l'indirizzo della funzione stessa. All'interno dell'header file windowsx.h è presente
la dichiarazione:
Nel caso ad esempio del messaggio WM_CREATE, attraverso l'operatore di preprocessor
## del C, avviene il concatenamento delle due stringhe HANDLE_ e
WM_CREATE in modo da ottenere HANDLE_WM_CREATE; l'espansione della macro produce
quindi:
case WM_CREATE: return HANDLE_WM_CREATE((hwnd), (wParam), (lParam), (fn))
In sostanza, la macro HANDLE_MSG chiama a sua volta la macro HANDLE_WM_CREATE.
Cercando all'interno del file windowsx.h, ci imbattiamo nella dichiarazione di questa
seconda macro che assume la seguente struttura:
L'espansione di questa macro porta alla chiamata:
FontWin_OnCreate(hwnd, lParam);
Dal commento presente prima della dichiarazione di HANDLE_WM_CREATE, possiamo ricavare
tutte le informazioni relative ai parametri e ai valori di ritorno della procedura
Cls_OnCreate; attraverso ulteriori ricerche possiamo trovare i prototipi di tutte le
altre procedure che elaborano i vari messaggi. Analizziamo in particolare i prototipi delle
procedure relative ai quattro messaggi intercettati da WndProc (Figura 6.4); osserviamo che
queste procedure vengono utilizzate solo dalla window procedure, per cui i loro prototipi sono
stati dichiarati prima della definizione di WndProc.
6.2 Gestione del messaggio WM_CREATE
La procedura FontWin_OnCreate che elabora il messaggio WM_CREATE, ha il seguente
prototipo:
BOOL FontWin_OnCreate(HWND hwnd, LPCREATESTRUCT lpCreateStruct);
Il parametro hwnd rappresenta l'handle della finestra per la quale è stata richiesta la
creazione, mentre il parametro lpCreateStruct è l'indirizzo di una struttura di tipo
CREATESTRUCT contenente una serie di informazioni addizionali per la creazione della
finestra; nel precedente capitolo abbiamo visto che se vogliamo ricorrere a questa struttura,
dobbiamo specificare il suo indirizzo attraverso il parametro lpParam di
CreateWindowEx. In fase di creazione della finestra, CreateWindowEx spedisce un
messaggio WM_CREATE; se decidiamo di intercettare questo messaggio, possiamo reperire
l'indirizzo della struttura CREATESTRUCT attraverso il parametro lpParam di
WndProc (Figura 6.4). Nel caso dell'applicazione FONTWIN, questa struttura non viene
utilizzata, per cui la procedura FontWin_OnCreate viene chiamata con il secondo argomento
che vale NULL; la struttura CREATESTRUCT verrà descritta al momento opportuno in
un prossimo capitolo.
L'invio del messaggio WM_CREATE da parte della procedura CreateWindowEx assume una
notevole importanza; infatti, intercettando questo messaggio possiamo effettuare una serie di
inizializzazioni prima che la finestra compaia sullo schermo. Utilizzando la terminologia dei
linguaggi ad oggetti come il C++, possiamo dire che la procedura che elabora
WM_CREATE rappresenta il costruttore della classe finestra che stiamo creando.
Una osservazione importante riguarda il fatto che il messaggio WM_CREATE viene inviato
dall'interno della procedura CreateWindowEx; questo significa che ancora non abbiamo a
disposizione il valore di ritorno di CreateWindowEx (handle della finestra) da memorizzare
nella variabile globale hWindow definita nel segmento _BSS. La procedura
FontWin_OnCreate deve utilizzare quindi l'argomento hWnd inviato a WndProc
da CreateWindowEx; in sostanza, all'interno di FontWin_OnCreate, la variabile
hWindow non ha ancora nessun significato e non deve quindi essere usata.
Le vecchie versioni di Windows, richiedevano che la procedura Cls_OnCreate dovesse
terminare restituendo in EAX il valore zero per indicare il successo della fase di
inizializzazione; nelle attuali versioni di win32 questo valore di ritorno viene ignorato,
per cui possiamo simbolicamente caricare in EAX il valore TRUE.
6.3 Gestione del messaggio WM_CLOSE
Il messaggio WM_CLOSE viene elaborato dalla procedura FontWin_OnClose; questa
procedura ha il seguente prototipo:
void FontWin_OnClose(HWND hwnd);
Il messaggio WM_CLOSE viene inviato dal SO ogni volta che cerchiamo di chiudere
una finestra; molte applicazioni hanno la necessità di intercettare questo messaggio per evitare
situazioni spiacevoli. Pensiamo ad esempio al caso di un elaboratore di testi (wordprocessor)
che contiene informazioni non ancora salvate sul disco; se chiudiamo l'applicazione senza
intercettare il messaggio WM_CLOSE, tutte le informazioni non salvate vengono perse senza
che l'utente abbia la possibilità di intervenire. Le situazioni di questo genere possono essere
evitate intercettando ed elaborando il messaggio WM_CLOSE; l'elaborazione di questo
messaggio consiste in genere nella visualizzazione di un messaggio di avvertimento che fornisce
all'utente la possibilità di confermare o meno la richiesta di chiusura dell'applicazione. Nel
caso in cui la chiusura venga confermata, è importantissimo passare il messaggio WM_CLOSE
anche alla window procedure predefinita DefWindowProc; in questo modo il SO può
effettuare le necessarie deinizializzazioni e può inviare infine il messaggio WM_DESTROY
che autorizza la terminazione dell'applicazione.
Come si nota dal prototipo, la procedura Cls_OnClose richiede un argomento di tipo
HWND che rappresenta l'handle della finestra per la quale è stata richiesta la chiusura;
la procedura Cls_OnClose termina senza nessun valore di ritorno (void).
6.4 Gestione del messaggio WM_DESTROY
Il messaggio WM_DESTROY viene elaborato dalla procedura FontWin_OnDestroy; questa
procedura ha il seguente prototipo:
void FontWin_OnDestroy(HWND hwnd);
Il messaggio WM_DESTROY viene inviato dal SO per autorizzare la chiusura di una
finestra; in sostanza, con il messaggio WM_DESTROY il SO ci sta informando del
fatto che le varie deinizializzazioni predefinite sono state portate a termine con successo, per
cui la chiusura della finestra può avvenire in modo corretto. Intercettando questo messaggio,
possiamo effettuare anche noi le nostre deinizializzazioni; questa fase consiste in genere nella
restituzione al SO delle varie risorse utilizzate dalla finestra che vogliamo chiudere.
Come già sappiamo, win32 e in particolare Windows2000 e WindowsXP, sono
in grado di effettuare automaticamente questo lavoro; in ogni caso, per questioni di chiarezza e
di stile, è meglio svolgere questa fase in modo esplicito.
Dalle considerazioni appena esposte, risulta evidente il ruolo svolto dalle due procedure
Cls_OnCreate e Cls_OnDestroy; così come Cls_OnCreate rappresenta il
costruttore della classe finestra che vogliamo creare, allo stesso modo Cls_OnDestroy
rappresenta il distruttore della classe finestra che vogliamo "distruggere".
Prima di terminare, Cls_OnDestroy deve chiamare la procedura PostQuitMessage; come
già sappiamo, questa procedura inserisce il messaggio WM_QUIT nella coda dei messaggi
della finestra che intendiamo chiudere. Nel precedente capitolo abbiamo anche visto che il
parametro nExitCode richiesto da PostQuitMessage, rappresenta il codice di uscita
(exit code) della nostra applicazione; all'interno del loop dei messaggi, la successiva chiamata
di GetMessage determina l'estrazione del messaggio WM_QUIT. In questo caso, la
procedura GetMessage restituisce zero in EAX, determinando in questo modo la
condizione di uscita dal loop dei messaggi; il contenuto di nExitCode può essere reperito
nel campo wParam della struttura di tipo MSG passata a GetMessage.
Come si nota dal prototipo, la procedura Cls_OnDestroy richiede un argomento di tipo
HWND che rappresenta l'handle della finestra per la quale è stata richiesta la chiusura;
la procedura Cls_OnDestroy termina senza nessun valore di ritorno (void).
6.5 Gestione del messaggio WM_PAINT
Il messaggio WM_PAINT viene elaborato dalla procedura FontWin_OnPaint; questa
procedura ha il seguente prototipo:
void FontWin_OnPaint(HWND hwnd);
In Windows il messaggio WM_PAINT assume una importanza fondamentale; questo messaggio
viene inviato automaticamente dal SO ogni volta che la client area di una finestra ha
bisogno di un aggiornamento. Questa situazione si verifica ad esempio quando modifichiamo le
dimensioni orizzontale e verticale di una finestra, quando ripristiniamo una finestra dopo averla
ridotta ad icona, quando una finestra viene portata in primo piano dopo essere stata parzialmente
coperta da altre finestre, etc; in tutti questi casi, si dice tecnicamente che il contenuto della
finestra si "sporca" e deve quindi essere ripristinato. Per poter procedere a questa fase di
ripristino, dobbiamo chiaramente intercettare il messaggio WM_PAINT; l'elaborazione del
messaggio WM_PAINT ci offre l'opportunità di conoscere alcune importanti caratteristiche
di Windows.
In Windows, prima di poter visualizzare qualsiasi cosa nella client area di una finestra,
dobbiamo richiedere al SO un cosiddetto Device Context; per capire il significato
del Device Context possiamo servirci della Figura 6.6.
Supponiamo che il blocco Device (dispositivo), rappresenti la scheda video del nostro computer; in questo caso, il blocco
Device Driver (pilota di dispositivo) rappresenta il software sviluppato per Windows
dal produttore della scheda video. Attraverso questo software, Windows può accedere
direttamente all'hardware della scheda video sfruttandone tutte le caratteristiche; il Device
Driver permette quindi a Windows di selezionare le varie modalità video (CGA,
EGA, VGA, SVGA, etc), di selezionare il numero di colori da utilizzare, di
visualizzare materialmente sullo schermo l'output del nostro programma, e così via. In ambiente
DOS, un programma che vuole inviare il suo output allo schermo deve gestire in proprio tutti
questi dettagli hardware; in ambiente Windows invece, le applicazioni delegano tutto questo
lavoro al Device Context (contesto di dispositivo). Possiamo dire quindi che il Device
Context rappresenta un'interfaccia tra le applicazioni Windows e il Device
Driver di un dispositivo di output che può essere lo schermo, la stampante, il plotter, etc;
in questo modo si ottiene una enorme semplificazione nella scrittura delle applicazioni. L'aspetto
negativo è rappresentato dal fatto che tutti questi passaggi, portano ad una inevitabile
diminuzione delle prestazioni generali dell'interfaccia grafica di Windows; d'altra parte,
in assenza di questi accorgimenti, una applicazione Windows sarebbe libera di mostrare il
suo output in qualunque punto dello schermo, anche al di fuori della client area.
In definitiva, se decidiamo di intercettare il messaggio WM_PAINT, prima di "scrivere"
qualsiasi cosa sulla client area della nostra finestra dobbiamo procurarci un Device
Context; a tale proposito, Windows ci mette a disposizione diverse procedure come
BeginPaint e GetDC. È fondamentale che in relazione al messaggio WM_PAINT
venga utilizzata esclusivamente la procedura BeginPaint; questa procedura infatti è
stata concepita esplicitamente per questo particolare messaggio. La procedura BeginPaint
è definita nella libreria USER32; il suo prototipo è il seguente:
HDC BeginPaint(HWND hwnd, LPPAINTSTRUCT lpPaint);
Il parametro hwnd rappresenta come al solito l'handle della finestra nella quale vogliamo
scrivere; il parametro lpPaint rappresenta una DWORD contenente l'indirizzo di
una struttura di tipo PAINTSTRUCT i cui campi verranno riempiti da BeginPaint.
Nell'include file windows.inc, troviamo la seguente dichiarazione della
struttura PAINTSTRUCT:
Il campo hdc è una DWORD contenente un codice numerico di tipo HDC (handle
to device context); questo campo viene quindi riempito da BeginPaint con l'handle del
device context che utilizzeremo per disegnare.
Il campo fErase è un valore booleano che indica se deve essere aggiornato anche lo
sfondo della finestra; se fErase vale TRUE, verrà aggiornato anche lo sfondo.
Il campo rcPaint è una struttura di tipo RECT (rettangolo) che indica il rettangolo
della client area interessato all'aggiornamento; in questo modo Windows ha la possibilità
di migliorare le prestazioni del programma in quanto viene aggiornata solo la porzione della
finestra che è stata sporcata.
La struttura RECT viene dichiarata nel file windows.inc nel
seguente modo:
In pratica, left e top rappresentano il vertice in alto a sinistra del rettangolo;
right e bottom rappresentano invece il vertice in basso a destra del rettangolo.
I campi fRestore, fIncUpdate e rgbReserved della struttura PAINTSTRUCT
sono riservati a Windows e non devono essere assolutamente modificati dall'utente.
La procedura BeginPaint termina restituendo in EAX il device context che avevamo
richiesto; se la richiesta fallisce, il registro EAX contiene il valore NULL. Una
volta che abbiamo ottenuto il nostro device context, siamo pronti per disegnare nella client
area della finestra da aggiornare; in sostanza, elaborare il messaggio WM_PAINT significa
dire a Windows in cosa consiste l'output che il nostro programma visualizza nella client
area della sua finestra.
Per indicare a Windows che l'elaborazione del messaggio WM_PAINT è terminata,
dobbiamo utilizzare la procedura EndPaint definita anch'essa nella libreria USER32;
il prototipo di questa procedura è il seguente:
BOOL EndPaint(HWND hwnd, CONST LPPAINTSTRUCT lpPaint);
Il parametro hwnd indica l'handle della finestra appena aggiornata; il parametro
lpPaint è una DWORD contenente l'indirizzo della struttura di tipo PAINTSTRUCT
precedentemente riempita da BeginPaint. La procedura EndPaint, termina sempre con
un valore non nullo in EAX; questo valore può essere tranquillamente ignorato.
Per elaborare il messaggio WM_PAINT utilizziamo quindi la procedura FontWin_OnPaint
che, come si nota dal prototipo, richiede come argomento l'handle della finestra da aggiornare,
e termina senza nessun valore di ritorno (void); dalle considerazioni appena esposte,
possiamo definire il seguente scheletro di una generica procedura Cls_OnPaint (versione
MASM):
Dopo aver illustrato le caratteristiche generali delle procedure che elaborano i quattro messaggi
intercettati dalla window procedure di FONTWIN, possiamo passare all'analisi delle due
principali procedure che vengono utilizzate in Windows per l'output di stringhe di testo
sullo schermo.
6.6 La procedura TextOut
La prima procedura che andiamo ad esaminare viene chiamata TextOut; questa procedura che
viene definita nella libreria GDI32, presenta il seguente prototipo C:
BOOL TextOut(HDC hdc, int nXStart, int nYStart, LPCTSTR lpString, int cbString);
Il parametro hdc rappresenta l'handle del device context che vogliamo usare per disegnare.
Il parametro nXStart è una DWORD che rappresenta l'ascissa del punto della client
area dal quale inizia l'output.
Il parametro nYStart è una DWORD che rappresenta l'ordinata del punto della client
area dal quale inizia l'output.
Il parametro lpString è una DWORD che rappresenta l'indirizzo di una stringa
ASCII,
non necessariamente terminata da uno zero; questa è ovviamente la stringa che verrà stampata sullo schermo.
Il parametro cbString è una DWORD che rappresenta il numero di byte (caratteri)
che formano la stringa.
Se TextOut termina con successo, restituisce un valore non nullo in EAX; se invece
TextOut fallisce, restituisce zero in EAX.
Supponiamo ad esempio di voler stampare la stringa strOut1 definita nel blocco _DATA
mostrato in Figura 6.2; approfittando del fatto che questa stringa viene definita staticamente,
possiamo determinare facilmente la sua lunghezza con il solito metodo del location counter.
A questo punto possiamo visualizzare la stringa strOut1 scrivendo:
invoke TextOut, paintDC, 10, 10, offset strOut1, LEN_STROUT1
In questo modo, la stringa strOut1 viene stampata a partire dal punto (10, 10)
della client area; le coordinate sono espresse in pixel e sono riferite al vertice in alto a
sinistra della client area. È necessario ricordare che il sistema di riferimento cartesiano
dello schermo del computer, ha l'origine nel vertice in alto a sinistra; le ascisse crescono
da sinistra verso destra, mentre le ordinate crescono dall'alto verso il basso (in sostanza, l'asse
y punta verso il basso).
Analizzando gli effetti prodotti da questa istruzione, ci accorgiamo che l'area della finestra
interessata dall'output della stringa, ha subito una modifica dello sfondo; quest'area, prende
il nome di sfondo della stringa. In particolare, si osserva che Windows stampa le
stringhe utilizzando un colore predefinito per il testo, chiamato foreground color (colore
di primo piano); analogamente, lo sfondo della stringa viene riempito con un colore predefinito
chiamato background color (colore di sfondo). Generalmente, in assenza di indicazioni da
parte del programmatore, Windows utilizza il bianco per lo sfondo, e il nero per il
primo piano; naturalmente Windows ci mette a disposizione tutti gli strumenti necessari per
modificare sia il colore di sfondo, sia il colore di primo piano.
Per modificare il colore di sfondo possiamo utilizzare la procedura SetBkColor; questa
procedura viene definita nella libreria GDI32, ed ha il seguente prototipo:
COLORREF SetBkColor(HDC hdc, COLORREF crColor);
Il parametro hdc rappresenta l'handle del device context che stiamo usando per scrivere.
Il parametro crColor rappresenta il colore in formato COLORREF che vogliamo usare
per lo sfondo della stringa da visualizzare.
Se SetBkColor termina con successo, restituisce in EAX il precedente colore di sfondo
in formato COLORREF; se SetBkColor fallisce, restituisce in EAX la costante
simbolica CLR_INVALID.
Volendo definire ad esempio uno sfondo nero per l'output, possiamo scrivere:
Per modificare il colore di primo piano, possiamo utilizzare la procedura SetTextColor.
Questa procedura viene definita nella libreria GDI32, ed ha il seguente prototipo:
COLORREF SetTextColor(HDC hdc, COLORREF crColor);
Il significato dei parametri è identico al caso di SetBkColor.
Se SetTextColor termina con successo, restituisce in EAX il precedente colore di
primo piano in formato COLORREF; se SetTextColor fallisce, restituisce in EAX
la costante simbolica CLR_INVALID.
Volendo usare ad esempio un colore rosso intenso per il testo, possiamo scrivere:
Windows ci permette anche di definire con precisione la modalità di modifica dello sfondo
dell'area interessata dall'output; a tale proposito possiamo servirci della procedura
SetBkMode. Questa procedura viene definita nella libreria GDI32, e presenta il
seguente prototipo:
int SetBkMode(HDC hdc, int iBkMode);
Il parametro hdc è il solito handle del device context che stiamo utilizzando per scrivere
nella client area della nostra finestra.
Il parametro iBkMode è una DWORD che codifica la modalità di modifica dello sfondo;
le due modalità più utilizzate sono espresse dalle due costanti simboliche OPAQUE e
TRANSPARENT. La modalità OPAQUE indica che allo sfondo verrà assegnato il colore
predefinito di Windows o il colore che eventualmente abbiamo scelto con SetBkColor;
la modalità TRANSPARENT indica che l'output non provocherà nessuna modifica dello sfondo
(output trasparente).
Se SetBkMode termina con successo, restituisce in EAX il codice della precedente
modalità di modifica dello sfondo; se SetBkMode fallisce, restituisce in EAX il
valore zero.
Osserviamo che utilizzando SetBkMode con la costante TRANSPARENT, non abbiamo la
necessità di preoccuparci del colore di sfondo dell'output; in definitiva, se vogliamo stampare
la stringa strOut1 con il colore (0, 250, 0) e senza alterare lo sfondo, possiamo
scrivere (versione MASM):
6.7 La procedura DrawText
Se abbiamo bisogno di una gestione più sofisticata dell'output, possiamo servirci della procedura
DrawText; questa procedura viene definita nella libreria USER32 e presenta il
seguente prototipo:
int DrawText(HDC hdc, LPCTSTR lpString, int nCount, LPRECT lpRect, UINT uFormat);
Il parametro hdc è l'handle del device context che vogliamo utilizzare per l'output.
Il parametro lpString è una DWORD contenente l'indirizzo di una stringa
ASCII,
non necessariamente terminata da uno zero; questa è la stringa che verrà stampata sullo schermo da DrawText.
Il parametro nCount è una DWORD che indica la lunghezza in byte della stringa
da stampare; se lpString contiene l'indirizzo di una stringa C, e vogliamo delegare
a DrawText il calcolo della lunghezza della stringa, dobbiamo passare nCount = -1.
Il parametro lpRect è una DWORD contenente l'indirizzo di una struttura di tipo
RECT; la stringa da stampare, verrà formattata all'interno di questo rettangolo.
Il parametro uFlags, è una DWORD contenente una serie di flags che indicano il
tipo di formattazione che DrawText applicherà alla stringa; i vari flags possono essere
combinati tra loro con l'operatore OR dell'Assembly.
Per conoscere i numerosi flags disponibili, conviene consultare il Win32 Programmer's
Reference; nell'esempio FONTWIN, viene utilizzata la combinazione di flags:
DT_SINGLELINE OR DT_CENTER OR DT_VCENTER
In base a questa combinazione, all'interno dell'area rettangolare passata a DrawText
attraverso lpRect, verrà stampata una stringa su una sola linea (DT_SINGLELINE);
questa stringa verrà inoltre centrata orizzontalmente (DT_CENTER) e verticalmente
(DT_VCENTER) all'interno del rettangolo.
Supponiamo ad esempio di voler stampare la stringa strOut3 definita nel blocco _DATA
di Figura 6.2; come si può notare, si tratta di una stringa C (zero terminated string).
Supponiamo inoltre di voler stampare questa stringa esattamente al centro della client area della
main window dell'applicazione FONTWIN; per individuare il rettangolo che rappresenta
l'intera client area, possiamo utilizzare la procedura GetClientRect. Questa procedura
viene definita nella libreria USER32 e presenta il seguente prototipo:
BOOL GetClientRect(HWND hwnd, LPRECT lpRect);
Il parametro hwnd rappresenta l'handle della finestra di cui vogliamo determinare le
coordinate della client area.
Il parametro lpRect rappresenta una DWORD contenente l'indirizzo di una struttura
di tipo RECT che verrà riempita da GetClientRect con le informazioni richieste;
considerando il fatto che le coordinate sono riferite al vertice in alto a sinistra della client
area (0, 0), si otterrà ovviamente left=0, top=0, mentre right e
bottom conterranno rispettivamente la larghezza e l'altezza della client area.
La prima cosa da fare consiste quindi nel definire una struttura di tipo RECT chiamata
ad esempio paintRect; a questo punto, se vogliamo stampare la stringa con il colore
(150, 0, 0), possiamo scrivere (versione MASM):
Come possiamo notare, per quanto riguarda il colore di sfondo, il colore di primo piano e la
modalità di modifica dello sfondo, valgono tutte le considerazioni già svolte per TextOut;
in sostanza, anche con DrawText possiamo utilizzare le procedure SetBkMode,
SetBkColor e SetTextColor.
Se DrawText termina con successo, restituisce in EAX l'altezza del testo appena
stampato; in caso di fallimento DrawText restituisce in EAX il valore zero.
6.8 I font di Windows
Abbiamo visto quindi che in assenza di indicazioni da parte del programmatore, le due procedure
TextOut e DrawText utilizzano valori predefiniti per il colore di sfondo, per il
colore di primo piano e per la modalità di modifica dello sfondo dell'output; un'altro aspetto
importante, è dato dal fatto che queste due procedure stampano il loro output utilizzando un
tipo di carattere avente uno stile grafico predefinito. L'aspetto grafico (estetico) di un
carattere prende il nome di font; Windows dispone di un numerosissimo insieme di
font utilizzabili per conferire alle stringhe un aspetto grafico di elevata qualità.
Se non selezioniamo nessun font particolare, Windows utilizza un font predefinito
chiamato font di sistema; generalmente il font predefinito è un tipo di carattere a
spaziatura fissa simile a quello utilizzato dal DOS. Se ci serve invece un font più
elegante, dobbiamo richiederlo a Windows attraverso l'invio di una serie di opportune
informazioni.
Il primo passo da compiere consiste nel definire tutte le caratteristiche grafiche del font
che vogliamo selezionare; a tale proposito dobbiamo riempire i campi di una struttura di tipo
LOGFONT. Nel file windows.inc troviamo la seguente dichiarazione
di questa struttura:
Il campo lfHeight indica l'altezza del font.
Il campo lfWidth indica la larghezza media del font; si tratta di un valore medio in quanto
esistono caratteri (come la 'm') che sono molto più larghi di altri (come la 'i').
Specificando il valore zero in questo campo, lasciamo che la larghezza più adatta venga calcolata
da Windows.
Il campo lfEscapement indica l'angolo che la stringa deve formare rispetto alla linea
orizzontale che rappresenta la base dello schermo; questo angolo è espresso in gradi e deve
essere moltiplicato per 10. Per richiedere ad esempio un'inclinazione del testo di
45 gradi, dobbiamo caricare nel campo lfEscapement il valore 450.
Il campo lfOrientation indica l'angolo che ogni carattere della stringa deve formare
rispetto alla linea orizzontale che rappresenta la base dello schermo; anche questo valore deve
essere moltiplicato per 10. Attualmente questa caratteristica non è ancora supportata
da Windows.
Il campo lfWeight indica il peso del font, cioè la sua consistenza; il peso deve essere
un valore compreso tra 0 e 1000 e può variare solo a salti di 100 unità.
Il valore 400 viene indicato come normal (testo normale); il valore 700
viene indicato come bold (testo in grassetto).
Il campo lfItalic è un valore booleano che indica se il testo deve essere stampato in
stile italic (inclinato verso destra); se questo campo vale TRUE (non-zero), verrà attivato
lo stile italic.
Il campo lfUnderline è un valore booleano che indica se il testo deve essere sottolineato;
se questo campo vale TRUE (non-zero), il testo verrà sottolineato.
Il campo lfStrikeOut è un valore booleano che indica se il testo deve essere sbarrato;
se questo campo vale TRUE (non-zero), il testo verrà sbarrato.
Il campo lfCharSet indica la nazionalità del set di caratteri da utilizzare; generalmente
questo campo vale zero per indicare il set di caratteri ANSI (si può usare anche la
costante simbolica ANSI_CHARSET).
Il campo lfOutPrecision indica con quale precisione Windows rispetterà le
richieste del programmatore relative ai campi lfHeight, lfWidth,
lfEscapement e lfOrientation; il valore di questo campo indica anche a Windows
a quale famiglia deve appartenere il font di riserva da utilizzare se il font che abbiamo richiesto
non è disponibile. La famiglia di appartenenza del font viene specificata dal campo
lfPitchAndFamily; per obbligare ad esempio Windows ad usare solo font True
Type (ad alta qualità), dobbiamo specificare nel campo lfOutPrecision il valore
rappresentato dalla costante simbolica OUT_TT_ONLY_PRECIS.
Il campo lfClipPrecision indica con quale precisione Windows "taglierà" la porzione
di testo che sconfina dalla client area o che viene coperta da un'altra finestra; il taglio delle
porzioni di finestra non visibili, viene chiamato tecnicamente clipping. Generalmente per
questo campo si può utilizzare la costante simbolica CLIP_DEFAULT_PRECIS.
Il campo lfQuality indica la qualità generale dell'output; possiamo utilizzare ad esempio
le costanti simboliche DEFAULT_QUALITY, DRAFT_QUALITY, PROOF_QUALITY, etc.
Il campo lfPitchAndFamily indica il tipo e la famiglia di appartenenza del font che
vogliamo selezionare; come è stato detto in precedenza, Windows utilizza queste
informazioni per poter reperire eventuali sostituti del font che abbiamo richiesto. I primi
4 bit di questo campo indicano il tipo di font (fisso o variabile); i successivi bit
indicano la famiglia di appartenenza (modern, roman, swiss, etc). Come al solito, per combinare
tra loro il tipo e la famiglia di un font, si può utilizzare l'operatore OR
dell'Assembly; volendo utilizzare ad esempio un font di tipo variabile della famiglia
roman, dobbiamo caricare nel campo lfPitchAndFamily il valore:
VARIABLE_PITCH OR FF_ROMAN
Il campo lfFaceName deve contenere il nome utilizzato da Windows per identificare il
font da selezionare; tra i vari nomi si possono citare ad esempio Times New Roman,
Arial, Courier New, etc (la distinzione tra maiuscole e minuscole non è importante).
Come si può notare, questo campo è un vettore formato da LF_FACESIZE byte (TCHAR =
BYTE). La costante LF_FACESIZE vale 32, e questo significa che il nome del font
non può superare i 32 byte di lunghezza; in questi 32 byte deve trovare posto
anche lo zero finale in quanto il nome di un font deve essere espresso sotto forma di stringa
C.
Per conoscere l'enorme numero di costanti simboliche utilizzabili con questi campi, si consiglia
di consultare il Win32 Programmer's Reference o l'include file windows.inc; è
importante che il programmatore utilizzi queste costanti simboliche per
sperimentare in pratica l'effetto che esse producono.
Una volta che abbiamo riempito i vari campi della struttura di tipo LOGFONT, dobbiamo
procedere con la creazione del font avente le caratteristiche richieste; a tale proposito, viene
utilizzata la procedura CreateFontIndirect. Questa procedura viene definita nella libreria
GDI32, e presenta il seguente prototipo:
HFONT CreateFontIndirect(CONST LPLOGFONT lplf);
Il parametro lplf è una DWORD contenente l'indirizzo di una struttura di tipo
LOGFONT; naturalmente questa struttura deve essere stata già riempita con tutte le
informazioni descritte in precedenza.
Se CreateFontIndirect termina con successo, restituisce in EAX l'handle del font
(tipo HFONT) appena installato; se CreateFontIndirect fallisce, restituisce in
EAX il valore NULL.
Un concetto molto importante in Windows è rappresentato dal fatto che i vari oggetti
grafici come i font, le icone, i cursori, le bitmap, etc, non sono disponibili in numero
illimitato; non bisogna dimenticare inoltre che in un SO multitasking come Windows,
ci possono essere più programmi contemporaneamente in esecuzione, ciascuno dei quali può aver
bisogno di un certo numero di oggetti grafici. Tutto ciò significa che le applicazioni
Windows devono comportarsi in modo responsabile, evitando di sprecare inutilmente queste
importanti risorse; in sostanza, ogni applicazione deve richiedere un determinato oggetto grafico
nel momento in cui ne ha effettivamente bisogno, e deve poi restituirlo quando non gli serve più.
Vediamo allora come ci si deve comportare nel caso della risorsa di tipo font; prima di tutto
definiamo una variabile globale destinata a contenere l'handle di un font. In Figura 6.2 notiamo
appunto la presenza di una variabile chiamata fontHandle; questa variabile viene
inizializzata con il valore zero per indicare che inizialmente non contiene nessun handle. Ogni
volta che vogliamo utilizzare un nuovo font, dobbiamo restituire a Windows l'eventuale
handle del font precedentemente utilizzato; a tale proposito possiamo servirci della procedura
DeleteObject che provvede a liberare tutte le risorse associate ad un determinato handle.
Questa procedura viene definita nella libreria GDI32, e presenta il seguente prototipo:
BOOL DeleteObject(HGDIOBJ hObject);
Il parametro hObject rappresenta l'handle di una risorsa grafica che può essere un font,
una bitmap, un pennello, etc; la procedura DeleteObject restituisce in EAX un valore
non nullo quando termina con successo.
Definiamo poi nel segmento dati non inizializzati (_BSS) una struttura di tipo
LOGFONT, che nel caso del nostro esempio viene chiamata fontStruct (Figura 6.2);
questa struttura naturalmente deve essere riempita con le necessarie informazioni. A questo punto,
per creare un nuovo font avente le caratteristiche che abbiamo richiesto, possiamo scrivere
(versione MASM):
In questo modo otteniamo nella variabile fontHandle, l'handle del nuovo font che abbiamo
appena creato; prima di poter utilizzare fisicamente questo font, dobbiamo selezionarlo attraverso
la procedura SelectObject. Il compito di SelectObject è quello di dire al nostro
device context che il font corrente verrà sostituito da un nuovo font; la procedura
SelectObject viene definita nella libreria GDI32, e presenta il seguente prototipo:
HGDIOBJ SelectObject(HDC hdc, HGDIOBJ hgdiobj);
Il parametro hdc è l'handle del device context che vogliamo utilizzare per l'output.
Il parametro hgdiobj è l'handle dell'oggetto che vogliamo selezionare; nel nostro caso
si tratta dell'handle di un font (fontHandle).
Se la procedura SelectObject fallisce, termina restituendo in EAX il valore
NULL; se la procedura SelectObject termina con successo, restituisce in EAX
l'handle dell'oggetto appena sostituito (nel nostro caso si tratta dell'handle del precedente
font). È fondamentale che l'handle restituito in EAX da SelectObject, venga
salvato da qualche parte; infatti, quando abbiamo finito il nostro lavoro, dobbiamo chiamare
nuovamente SelectObject per ripristinare il font precedente.
Supponiamo ad esempio di voler utilizzare TextOut per visualizzare la stringa strOut1
con il nuovo font che abbiamo creato; prima di tutto definiamo una variabile locale chiamata ad
esempio oldFont e destinata a contenere l'handle del font corrente che dovremo poi
ripristinare. A questo punto possiamo scrivere (versione MASM):
Come si può notare, prima di tutto selezioniamo il nuovo font con SelectObject; l'handle
del vecchio font, restituitoci da SelectObject in EAX, viene salvato nella
variabile locale oldFont. In seguito viene chiamata TextOut che stampa la stringa
strOut1 con il nuovo font; al termine di questa fase, procediamo a ripristinare il vecchio
font attraverso una nuova chiamata a SelectObject.
6.9 Implementazione di FONTWIN.ASM
Dopo aver illustrato gli aspetti teorici relativi alla gestione del testo in Windows,
possiamo passare all'implementazione pratica di questi concetti nell'applicazione FONTWIN;
il programma può essere suddiviso in tre blocchi fondamentali che gestiscono le fasi di
inizializzazione, invio dell'output alla client area e deinizializzazione.
È chiaro che il luogo più adatto nel quale inserire la fase di inizializzazione, è rappresentato
dalla procedura FontWin_OnCreate che viene chiamata da WndProc in risposta al
messaggio WM_CREATE; in particolare, all'interno di questa procedura possiamo inserire
anche l'inizializzazione della struttura di tipo LOGFONT. Nel blocco dati non inizializzati
_BSS, definiamo quindi la struttura fontStruct; bisogna tenere presente che non
è obbligatorio riempire tutti i campi di questa struttura (come ad esempio lfWidth). È
importante però che tutti i campi non inizializzati, vengano riempiti con degli zeri; in questo
modo Windows può inizializzare questi campi con dei valori predefiniti. Il procedimento
più sicuro consiste allora nell'inizializzare tutta la struttura fontStruct riempiendo
tutti i suoi campi con degli zeri; a tale proposito possiamo scrivere un'apposita procedura
MemSet che assume il seguente aspetto:
Il parametro ptrMem rappresenta l'indirizzo del blocco di memoria da inizializzare; il
parametro fillValue rappresenta il valore di inizializzazione, mentre il parametro
fillSize indica la dimensione in byte del blocco di memoria da riempire. Come si può
notare, MemSet utilizza l'istruzione STOSB; in modalità protetta a 32
bit, questa istruzione utilizza AL come operando sorgente, e [EDI] come operando
destinazione. MemSet preserva il contenuto originale di EDI in quanto, come è stato
detto in un precedente capitolo, in win32, le procedure di callback devono sempre
preservare il contenuto dei registri EBX, ESI e EDI; osserviamo che (come
viene mostrato in seguito), MemSet viene chiamata da FontWin_OnCreate che a sua
volta viene chiamata da WndProc che è una procedura di callback. I parametri di
MemSet sono tutti di tipo DWORD perché, come già sappiamo, in win32 le
istruzioni PUSH e POP devono lavorare esclusivamente con operandi a 32 bit;
naturalmente, nel caso del parametro fillValue viene sfruttato solo il suo byte meno
significativo. La procedura MemSet può essere utilizzata per inizializzare qualunque blocco
di memoria; è consigliabile inizializzare con MemSet tutte le strutture utilizzate da una
applicazione (come ad esempio la struttura di tipo WNDCLASSEX).
Una volta definiti questi aspetti, possiamo procedere con l'implementazione della procedura
FontWin_OnCreate; l'aspetto assunto da questa procedura è il seguente (versione
MASM):
Come è stato detto in questo capitolo, il campo lfFaceName della struttura
fontStruct, è un vettore di 32 byte che deve contenere sotto forma di stringa
C, il nome del font che vogliamo creare; nel caso del nostro esempio, viene definita
un'apposita variabile fontName che punta alla stringa:
'Times New Roman', 0
(Figura 6.2). Questa stringa C deve essere copiata nel vettore lfFaceName; a tale proposito,
possiamo scriverci un'apposita procedura StrCpy che assume il seguente aspetto:
Il parametro strFrom rappresenta l'indirizzo della stringa sorgente; il parametro
strTo rappresenta l'indirizzo della stringa destinazione. Ricordiamo che in modalità
protetta a 32 bit, qualunque registro generale può essere utilizzato come registro
puntatore; il test di fine stringa viene effettuato in fondo al loop in modo che venga copiato
in strTo anche lo zero di fine stringa. È chiaro che il vettore destinazione deve essere
in grado di contenere il vettore sorgente; nel nostro caso, la stringa C che rappresenta
il nome del font non deve superare i 32 caratteri (compreso lo zero finale).
Dopo aver inizializzato fontStruct, la procedura FontWin_OnCreate chiama una
MessageBox per informare l'utente che le inizializzazioni sono state effettuate; questa
MessageBox viene mostrata prima della creazione della main window di FONTWIN, e
quindi deve utilizzare l'handle hwnd passato da CreateWindowEx a WndProc che
a sua volta lo passa a FontWin_OnCreate. La procedura FontWin_OnCreate termina
restituendo in EAX il valore simbolico TRUE, che generalmente viene ignorato da
Windows.
Terminata l'elaborazione del messaggio WM_CREATE, la main window di FONTWIN (se
tutto procede bene) viene creata da CreateWindowEx, visualizzata da ShowWindow e
aggiornata da UpdateWindow; in particolare, la procedura UpdateWindow invia il
messaggio WM_PAINT che viene intercettato ed elaborato dal nostro programma.
L'elaborazione del messaggio WM_PAINT si svolge all'interno della procedura
FontWin_OnPaint; osservando il sorgente dell'applicazione FONTWIN, si nota che
questa procedura definisce le seguenti variabili locali:
Queste variabili vengono dichiarate localmente in quanto sono necessarie solamente all'interno
di FontWin_OnPaint; come è stato spiegato in precedenza, il primo passo che questa
procedura deve compiere consiste nel procurarsi un device context che verrà utilizzato per
scrivere nella client area. Il codice necessario è il seguente:
In possesso di paintDC, la procedura FontWin_OnPaint può stampare la prima stringa
strOut1 con il seguente codice:
Come si può notare, viene selezionata con SetBkMode la modalità trasparente che non
altera lo sfondo dell'output; questa modalità vale per tutto l'output prodotto da
FontWin_OnPaint. Siccome non abbiamo ancora selezionato nessun font particolare,
TextOut utilizza il font di sistema di Windows.
Il prossimo passo compiuto da FontWin_OnPaint consiste nel selezionare un font
personalizzato che verrà usato per stampare 8 volte la stringa strOut2 con 8
valori differenti per il campo lfEscapement.
È perfettamente inutile limitarsi a modificare solo questo campo; la modifica infatti non avrà
effetto finché non verrà creato un nuovo font con CreateFontIndirect. A tale proposito,
possiamo scrivere la seguente procedura:
Il procedimento seguito da SetFont è molto importante; prima di tutto viene usato il
parametro angolazione per modificare il campo lfEscapement. Subito dopo viene
verificato il contenuto di fontHandle; se fontHandle è diverso da zero, vuol dire
che "punta" ad una risorsa font che dobbiamo restituire a Windows attraverso
DeleteObject. Terminata questa fase, possiamo chiamare CreateFontIndirect; infine,
l'handle del nuovo font, restituito da questa procedura, viene caricato in fontHandle.
A questo punto, all'interno di FontWin_OnPaint, possiamo scrivere il loop che stampa
8 volte la stringa strOut2:
All'interno del loop vengono compiute due operazioni molto importanti; prima di chiamare
TextOut, viene selezionato il nuovo font attraverso SelectObject. L'handle del
vecchio font restituito da SelectObject viene memorizzato in oldFont; al termine
di TextOut, viene chiamata nuovamente SelectObject per riselezionare il vecchio
font.
Subito dopo il loop, FontWin_OnPaint chiama DrawText per visualizzare la stringa
strOut3 al centro della client area; il codice è il seguente:
Siccome strOut3 è una stringa C, passiamo l'argomento -1 per delegare a
DrawText il calcolo della lunghezza della stringa; l'ultima fase svolta da
FontWin_OnCreate consiste nel segnalare a Windows la fine delle operazioni di
disegno:
invoke EndPaint, hwnd, paintStruct
Ogni volta che la main window di FONTWIN viene sporcata, il SO invia il messaggio
WM_PAINT; intercettando questo messaggio abbiamo quindi la possibilità di ripristinare
costantemente il contenuto della nostra finestra.
Non appena si prova a chiudere la main window di FONTWIN, il SO invia il messaggio
WM_CLOSE; ovviamente, nel caso di FONTWIN, chiudere la main window significa
chiudere l'intera applicazione. Il messaggio WM_CLOSE viene intercettato da WndProc
ed elaborato da FontWin_OnClose; il codice di questa procedura è il seguente:
Come si può notare, FontWin_OnClose si limita a chiedere all'utente la conferma della
richiesta di chiusura della main window; se la richiesta viene confermata, il messaggio
WM_CLOSE viene passato a DefWindowProc che provvede ad eseguire una serie di
deinizializzazioni predefinite inviando poi il messaggio WM_DESTROY. Naturalmente, la
window procedure WndProc di FONTWIN intercetta questo messaggio e lo passa per
l'elaborazione alla procedura FontWin_OnDestroy; dal punto di vista dei linguaggi di
programmazione ad oggetti, questa procedura rappresenta il distruttore della classe finestra
FontWin. All'interno di FontWin_OnDestroy effettuiamo quindi tutte le necessarie
deinizializzazioni; il codice di questa procedura è il seguente:
Chiamando DeleteObject con l'argomento fontHandle, permettiamo a Windows
di liberare la risorsa font associata a questo handle; in questo modo, questa risorsa viene
messa a disposizione di altre applicazioni. Naturalmente, FontWin_OnDestroy prima di
terminare, chiama PostQuitMessage che provvede ad inviare alla nostra applicazione il
messaggio WM_QUIT; questo messaggio come sappiamo determina l'uscita dal loop dei messaggi
e la terminazione del nostro programma.
Codice sorgente per MASM:
Esempi capitolo 6 MASM