Lezione 2 – Primi passi
Un programma "Win32 API" non dispone dei normali "servizi di sistema" cui un programma C può di solito fare riferimento (come, ad esempio, l'esistenza di file già aperti per standard input, standard output, e standard error); ne ha a disposizione, per contro, molti altri, appunto tutta la ricca interfaccia delle "Win32 API".
E’ possibile, comunque, continuare ad usare le funzioni della libreria Standard C (segnaleremo presto quelle poche che invece non si applicano), ma può essere che, così facendo, vi troviate ad avere un programma più grosso del necessario, poiché esso "re-implementa" (nelle librerie standard C che il vostro compilatore linka con esso) funzionalità già messe a disposizione dalle librerie di sistema di Windows, magari in modo molto simile. Ad esempio, per formattare un numero (intero) su di una stringa, potete, come sempre, scrivere:
sprintf(buffer, "%d", i);
ma, in Win32, potreste anche scrivere invece:
wsprintf(buffer, "%d", i);
con identico effetto e, se avete sempre usato la funzione di sistema wsprintf invece della sprintf dello standard C, il programma compilato e linkato sarà più piccolo, magari di vari KByte. wsprintf non supporta tutti i formati della sprintf dello standard C (ad esempio, mancano %f, %g, e %e, quindi tutta la possibilità di formattare numeri a virgola mobile), ma ne supporta alcuni altri, per tipi particolari, importanti in Windows, come le stringhe UNICODE.
Win32 utilizza molte macro e typedef (definite in windows.h, e/o nei file che esso include a sua volta) per definire i tipi esatti degli oggetti che tratta; ad esempio, viene regolarmente usata ULONG invece di "unsigned long", eccetera. Noi rispetteremo questa convenzione, anzitutto per chiarezza e congruenza con altre pubblicazioni su Windows, e poi per l'aiuto che essa può portare ad eventuali future "migrazioni". Ad esempio, è in corso di preparazione una versione a 64 bit di Windows; le applicazioni scritte interamente nel rispetto delle convenzioni Microsoft saranno senza dubbio più agevoli, dal punto di vista del "porting" a questa futura "Win64", di quelle scritte in termini di "veri" tipi C.
Un'altra convenzione Windows, che rispetteremo spesso (ma non sempre!), è quella della 'notazione ungherese', per cui i primi caratteri del nome di una variabile codificano il tipo della variabile stessa (ad esempio, lpszQualcosa per una variabile che è un puntatore ad una stringa di caratteri terminati da 0 binario, cioè normale stringa C). Il risultato è certo "brutto", ma può aiutare a chiarire quali tipi siano in gioco in una data operazione. Per altri dettagli sulla notazione ungherese, si veda ad esempio il testo originale di Charles Simonyi della Microsoft, che introdusse il concetto.
La funzione principale WinMain – Il punto di ingresso
La prima funzione standard che non potete utilizzare è int main(int argc, char* argv[]), cioè proprio quella che, in C standard, costituisce il punto d'ingresso del vostro stesso programma. Un programma Win32 inizia invece, nello stesso senso in cui uno C standard inizia da "main" ecc, dalla vostra funzione:
int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrevInst,
LPSTR lpCmdLine, int nCmdShow)
{
// inserire qui il vostro programma
return 0;
}
La macro WINAPI indica una funzione che adotta la convenzione di chiamata standard di Windows (originalmente tipica del Pascal, e diversa da quella abituale del C); dovremo usarla noi stessi quando definiremo funzioni che Windows chiama (come in questo caso), e Windows la usa regolarmente (la wsprintf suaccennata è una delle pochissime eccezioni). Esistono, secondo anche le versioni esatte di SDK, tante altre macro sinonime di questa, come PASCAL, STDCALL, CALLBACK, tutte con lo stesso effetto.
Il tipo HINSTANCE, e i due argomenti che lo usano, non hanno per ora importanza, e poca ne ha pure, sempre per ora, nCmdShow. Invece, l'argomento lpCmdLine è l'intera "linea di comando" passata al nostro programma, l'equivalente dell'intero array argv[] del main dello standard C; se vogliamo esaminare gli argomenti come parole separate, in Windows, dovremo "spezzare" noi stessi questa stringa e NON con una semplice strtok, perché essa non rispetta la convenzione relativa alle virgolette per "tenere assieme" un argomento, anche contenente spazi.
Il valore tornato dal nostro programma, come in Standard C, sarà un eventuale "codice d'errore", o 0 per indicare "OK".
La seconda importante funzione C che dovete disimparare è printf, per emettere output (e così pure altre come puts, ecc, e quelle di input-standard come getchar, gets, ecc). In Win32 non avete il file "standard output" (stdout) a disposizione, e neppure stdin, lo "standard input". Potete invece, volendo, continuare ad usare funzioni di input/output su file, come fopen, fprintf, fclose, eccetera; le API propongono altri modi di accedere ai file, ma l'approccio standard C, che pure funziona, in molti casi può risultare più comodo. Il modo più semplice di emettere un messaggio per l'utente è attraverso la prima API che vedremo, la MessageBox:
int MessageBox(HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType);
Il tipo HWND, "handle di window", è un tipo usato da Windows per permettere al nostro programma di identificare le finestre; di questi tipi HANDLE, ne esistono tanti nelle API di Windows. Spesso, dove è prevista un handle, si può passare zero per dire "nulla/nessuno/come da default"; qui, in particolare, possiamo passare zero come primo argomento e non preoccuparcene oltre per ora.
Handle, alla lettera, significa "maniglia"; come molti altri termini tecnici, però, è più normale lasciarlo, anche in un testo italiano, nell'originale inglese.
LPCTSTR, almeno per ora è const char*, cioè: una stringa standard C terminata da 0, tale che la funzione che la riceve come argomento "si impegna" ad usarla solo "in lettura", cioè senza modificarla.
MessageBox ha due argomenti-stringa in input: lpText è il messaggio che la finestra conterrà, mentre lpCaption sarà il suo titolo.
uType, infine (di tipo UINT, cioè in pratica unsigned int), è l'argomento che determina (come "maschera di bit") il tipo di finestra di messaggio da mostrare. Usando, come uType, la macro MB_OK, avremo la più semplice delle message box: senza icone, e con l'unico pulsante di "OK".
Ecco dunque il nostro programma "Hello world", per intero:
#include <windows.h>
int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrevInst, LPSTR lpCmdLine, int nCmdShow)
{
MessageBox(0, "Hello, World!", "Messaggio al mondo", MB_OK);
return 0;
}
MessageBox ha anche un valore di ritorno, che ci interessa nei casi in cui la finestra mostrata ha diverse combinazioni di pulsanti (codificate dal campo uType). Ad esempio, se noi passiamo come ultimo parametro MB_YESNO, la finestra avrà i due pulsanti "Yes" e "No" ("Si" e "No" in italiano); la funzione MessageBox tornerà IDYES se l'utente clicca su ‘Yes’, IDNO se l'utente clicca ‘No’. Questo può servire per fare operazioni di input molto rudimentali e immediate. In questi casi, il testo del messaggio sarà una "domanda", ed è abituale usare anche un'icona adeguata; ciò si ottiene usando per uType il valore MB_YESNO|MB_ICONQUESTION.
Le finestre di Dialogo
Le "finestre" sono oggetti astratti (cui normalmente, ma non sempre, corrisponde anche una rappresentazione su schermo) sulle quali si impernia gran parte del modello di programmazione Windows.
Ogni finestra ha tanti attributi, di svariati ordini, il più importante del quali è la classe della finestra. Nella programmazione Windows, spesso si definiscono classi speciali per le finestre del nostro programma.
Comunque, possiamo rimandare la discussione sulla definizione di nuove classi, perché il sistema ne fornisce parecchie, già definite, con le quali si possono già scrivere molti utili programmi Windows, e ci converrà dunque iniziare presentando queste classi già esistenti ed usabili.
Una delle più importanti categorie di finestre predefinite in Windows sono i dialoghi. I dialoghi possono essere creati dinamicamente dal nostro programma, ma è più abituale, e spesso comodo, definirne invece l'aspetto a priori, inserendo un "modello di dialogo" ("dialog template") come risorsa del nostro programma.
Le risorse di un programma vengono normalmente descritte in un file di testo chiamato NOME.RC, che un "resource compiler" provvede a tradurre in un vero e proprio "file di risose", NOME.RES; il linker, poi, o un programma ausiliario, inserisce questo .RES nel file eseguibile .EXE del programma (o, anche, in una libreria a caricamento dinamico, cioè un file di tipo .DLL).
I file .RC sono spesso preparati con l'ausilio di programmi speciali, gli "editor di risorse", ma essi possono anche benissimo essere scritti come file di testo con un qualsiasi editor di testo; da quest'ultimo punto di vista, RC è un linguaggio, con la propria sintassi, e la propria semantica, non un linguaggio di programmazione, ma un linguaggio, specializzato, di descrizione di dialoghi ed altre risorse Windows. Una semplice finestra di dialogo "vuota" può, ad esempio, essere descritta così, nel file di testo MIODIALOGO.RC:
#include <windows.h> MioDialogo DIALOG DISCARDABLE 0, 0, 186, 92 STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU CAPTION "Un Dialogo vuoto" FONT 8, "MS Sans Serif" BEGIN END
Qui, MioDialogo è l'identificatore (in questo caso, un identificatore-stringa) della risorsa di tipo dialogo; sulla stessa riga seguono la keyword DIALOG, gli attributi (qui, DISCARDABLE), e le coordinate di posizionamento della finestra: le prime due sono l’origine (x ed y), le seconde danno la dimensione della finestra di dialogo, in una unità di misura, detta dialog unit, che corrisponde a un diverso numero di pixel a seconda del font usato, proporzionalmente alle dimensioni medie dei caratteri del font.
La seconda riga descrive lo "stile" della finestra di dialogo; qui, definiamo una "classica" fienstra, con barra di titolo ("caption") e menu di sistema. Si tratta di "macro" (definite da un tipico #define del preprocessore C); è perché il resource-compiler conosca queste macro, dobbiamo includere il solito <windows.h>.
La terza riga indica il contenuto della barra di titolo; la quarta, il font da usare con dimensione e carattere.
Il tutto si conclude con una coppia di righe BEGIN ed END; non vi è nulla fra di loro, perché questa finestra è vuota, ma, normalmente, vi sarebbero elencati i "controlli" (pulsanti, caselle di edit,…), ciascuno con i propri vari attributi di stile, coordinate, dimensioni, ecc.
Per usare questa finestra di dialogo (che supponiamo di avere inserito come risorsa nel nostro EXE, col passaggio, come già descritto, attraverso la forma compilata .RES) il modo più semplice è usare la API DialogBoxParam:
int DialogBoxParam(HINSTANCE hInstance, LPCTSTR lpTemplateName, HWND hWndParent, DLGPROC lpDialogFunc, LPARAM dwInitParam);
Abbiamo già incontrato il tipo HINSTANCE e quello HWND, fortunatamente, anche in questo caso si può rimandare lo studio dei loro significati, e passare solo zero per i parametri di questi tipi. Così pure, abbiamo incontrato LPCTSTR (const char*, in pratica); come nome del template, parametro lpTemplateName, si passerà, appunto, la stringa che è il nome della risorsa template da usare.
Il tipo DLGPROC è un puntatore a funzione, e, nel relativo parametro, passeremo il puntatore alla "dialog procedure" il cui codice caratterizzerà il comportamento della nostra finestra di dialogo; LPARAM (praticamente, un "token" di 32 bit, che possono avere diversi significati a seconda dei contesti) identifica un nostro parametro che il sistema passerà alla nostra dialog procedure nella chiamata di inizializzazione della finestra (sarà dunque solo a codice scritto da noi stessi, che potrà eventualmente interessare il suo valore); per ora, si possono ignorare anch'essi, e passare 0 per ciascuno.
Abbiamo così il nostro secondo programma Windows inoltre, ripetiamo che nell'EXE dovranno essere state inserite le risorse corrispondenti al nostro .RC:
#include <windows.h>
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow)
{
DialogBoxParam(0,"MioDialogo",0,0,0);
return 0;
}
Eseguendo questo programma, apparirà la finestra di dialogo vuota prima disegnata. Noteremo, però, che, pur essendo esso dotata della normale "crocetta" in alto a destra, cliccando quest'ultima (che ci aspetteremmo dovrebbe chiudere la finestra), non succederà nulla! L'unico modo di terminare l'applicazione sarà l'uso del Task Manager di Win/NT, o, in Win95 o Win98, un CTRL-ALT-DEL seguito dall'"End Task" per questa nostra "applicazione". Perché, ci chiediamo...?
Messaggi
Con le finestre si comunica ricevendo e spedendo messaggi (da non confondersi con i "messaggi" mostrati all'utente da MessageBox!). Il sistema spedisce messaggi alle finestre a fronte di vari possibili eventi; il nostro codice, ricevendo questi messaggi e rispondendovi, determinerà il comportamento della nostra applicazione. Inoltre, noi stessi spediremo messaggi a varie finestre allo scopo di richiedere ad esse di compiere certe azioni, o anche per ottenere da esse delle informazioni.
Sul flusso dei messaggi si impernia ogni normale programma Windows, che dovrà anzi essere, di norma, strutturato allo scopo, "ad eventi" (in questo contesto, un evento è sinonimo di "messaggio").
Un messaggio, in certi contesti, esiste come singola struttura MSG:
typedef struct MSG
{
HWND hWnd; // finestra di destinazione
UINT message; // codice del messaggio
WPARAM wParam; // 1° parametro
LPARAM lParam; // 2° parametro
DWORD time; // istante di spedizione
POINT pt; // coordinate del mouse
} MSG;
I campi time e pt non sono normalmente usati; i campi wParam ed lParam sono arbitrari "token", di 32 bit l'uno, che contengono i parametri fondamentali del messaggio; hWnd, quando una finestra riceve un messaggio, sarà appunto la handle della finestra stessa.
Il codice message sarà normalmente una macro del tipo WM_QUALCOSA, per i normali "messaggi di Windows"; sono definiti centinaia di questi codici. Inoltre, si possono definire ulteriori codici di messaggio per i propri scopi, e le varie classi di finestre predefinite di Windows ("controlli comuni") lo fanno.
Più spesso, vedremo un messaggio in termini dei quattro valori hwnd, message, wParam, ed lParam, "spediti" o "ricevuti" separatamente, come argomenti di una funzione (API, o funzione da noi scritta).
Perché, dunque, il nostro dialogo non termina quando l'utente clicca sulla crocetta in altro a destra? Perché, a fronte di questo evento, il sistema non risponde in modo automatico chiudendo la finestra: quello che fa, invece, è spedire un messaggio WM_CLOSE alla finestra, chiedendole, in pratica, di chiudersi. Questo schema è molto più flessibile, permettendo al programma di controllare il proprio comportamento con precisione. Nel nostro esempio, noi non avevamo usato nessuna procedura che rispondesse ai messaggi; il WM_CLOSE, dunque, arriva, ma, poiché nessuno vi risponde, esso resta senza effetto.
Come gia detto, una finestra di dialogo risponde ai messaggi usando una dialog procedure, una funzione il cui indirizzo viene passato al sistema, alla creazione della finestra, nel parametro di tipo DLGPROC della API DialogBoxParam. La dialog procedure deve avere questa forma:
BOOL CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
// eventuali azioni
return FALSE;
}
Quando il sistema chiama la dialog procedure, essa riceve come argomenti l'HWND della finestra, il codice di messaggio in uMsg, i parametri in wParam ed lParam; deve normalmente tornare FALSE se non ha già interamente gestito il messaggio (e vuole che il sistema "completi" la gestione stessa), e TRUE se e solo se ha già interamente gestito il messaggio in proprio.
Per far sì che la nostra finestra di dialogo vuota si chiuda, come è normale che sia, al click dell'utente sulla crocetta in alto a destra, basta dunque avere una dialog procedure come, ad esempio:
BOOL CALLBACK unaDlgProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
if(uMsg==WM_CLOSE)
{
EndDialog(hwndDlg, 0);
return TRUE;
}
return FALSE;
}
L'API EndDialog chiude la finestra di dialogo (la cui HWND viene inserita come primo parametro) con un "codice di ritorno" pari al secondo parametro (qui usiamo zero, per indicare "tutto OK"); il valore passato come secondo parametro di EndDialog è anche tornato come risultato complessivo dalla chiamata DialogBoxParam. In questa dialog procedure, dunque, noi gestiamo il solo messaggio WM_CLOSE, cui rispondiamo con la terminazione della finestra di dialogo poi, restituiamo TRUE, per dire al sistema che abbiamo interamente gestito noi il messaggio stesso.
Con questa dialog procedure, e le stesse risorse di prima, il nostro programmino diventa dunque:
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow)
{
return DialogBoxParam(0,"MioDialogo", 0, unaDlgProc,0);
}
Per trasformare questo "scheletrico" programma in uno che faccia qualcosa che sia un minimo interessante, occorre, sostanzialmente:
- aggiungere finestre ("controlli") dentro la finestra di dialogo,
- rispondere a una maggiore varietà di eventi (messaggi).
Sarà dunque lungo questi due "assi" che ci muoveremo nel prossimo capitolo.











Hello