Esempio: simple_window

Creare una finestra non è esattamente semplice. O meglio è semplice una volta che si sa cosa si sta facendo ma ci sono alcune cose da tenere in considerazione per far apparire una finestra sullo schermo.
Personalmente ho sempre preferito fare prima le cose e poi impararle dopo… per questo motivo ecco il codice per creare una semplice finestra; ne sarà spiegato brevemente in seguito il significato.

#include <windows.h>

const char g_szClassName[] = "myWindowClass";

// Step 4: La procedura per la Finestra
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch(msg)
    {
        case WM_CLOSE:
            DestroyWindow(hwnd);
        break;
        case WM_DESTROY:
            PostQuitMessage(0);
        break;
        default:
            return DefWindowProc(hwnd, msg, wParam, lParam);
    }
    return 0;
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
    LPSTR lpCmdLine, int nCmdShow)
{
    WNDCLASSEX wc;
    HWND hwnd;
    MSG Msg;

    //Step 1: Registrazione della Window Class
    wc.cbSize        = sizeof(WNDCLASSEX);
    wc.style         = 0;
    wc.lpfnWndProc   = WndProc;
    wc.cbClsExtra    = 0;
    wc.cbWndExtra    = 0;
    wc.hInstance     = hInstance;
    wc.hIcon         = LoadIcon(NULL, IDI_APPLICATION);
    wc.hCursor       = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
    wc.lpszMenuName  = NULL;
    wc.lpszClassName = g_szClassName;
    wc.hIconSm       = LoadIcon(NULL, IDI_APPLICATION);

    if(!RegisterClassEx(&wc))
    {
        MessageBox(NULL, "Registrazione della Finestra Fallita!", "Errore!",
            MB_ICONEXCLAMATION | MB_OK);
        return 0;
    }

    // Step 2: Creazione della finestra
    hwnd = CreateWindowEx(
        WS_EX_CLIENTEDGE,
        g_szClassName,
        "Titolo della mia finestra",
        WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT, 240, 120,
        NULL, NULL, hInstance, NULL);

    if(hwnd == NULL)
    {
        MessageBox(NULL, "Creazione della Finestra Fallita!", "Errore!",
            MB_ICONEXCLAMATION | MB_OK);
        return 0;
    }

    ShowWindow(hwnd, nCmdShow);
    UpdateWindow(hwnd);
    // Step 3: Il ciclo dei messaggi
    while(GetMessage(&Msg, NULL, 0, 0) > 0)
    {
        TranslateMessage(&Msg);
        DispatchMessage(&Msg);
    }
    return Msg.wParam;
}

Per i contenuti del codice questo è il più semplice programma windows che possa essere scritto per creare e visualizzare una finestra funzionale, mere 70 linee di codice più o meno. Se il primo esempio è stato compilato correttamente anche questo programma dovrebbe compilare senza problemi.

Step 1: Registrazione della Window Class

Una Window Class mantiene le informazioni sulla finestra, incluso il tipo, la procedura che controlla la finestra stessa, le icone piccole e grandi, il colore di sfondo. In questo modo si può registrare una classe e creare tante finestre basate su di essa, senza specificare gli stessi attributi più di una volta. La maggior parte degli attributi specificati possono essere modificati singolarmente agendo sulla specifica finestra. Una Window Class non ha nulla a che vedere con le classi in C++.

const char g_szClassName[] = "myWindowClass";

La variabile qui sopra conserva il nome della Window Class, la useremo presto per registrare la nostra Window Class nel sistema.

    WNDCLASSEX wc;

    wc.cbSize        = sizeof(WNDCLASSEX);
    wc.style         = 0;
    wc.lpfnWndProc   = WndProc;
    wc.cbClsExtra    = 0;
    wc.cbWndExtra    = 0;
    wc.hInstance     = hInstance;
    wc.hIcon         = LoadIcon(NULL, IDI_APPLICATION);
    wc.hCursor       = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
    wc.lpszMenuName  = NULL;
    wc.lpszClassName = g_szClassName;
    wc.hIconSm       = LoadIcon(NULL, IDI_APPLICATION);

    if(!RegisterClassEx(&wc))
    {
        MessageBox(NULL, "Registrazione della Finestra Fallita!", "Errore!",
            MB_ICONEXCLAMATION | MB_OK);
        return 0;
    }

Questo è il codice che usiamo in WinMain() per registrare la nostra Window Class. In sintesi andiamo a riempire i membri della struttura WNDCLASSEX e chiamiamo RegisterClassEx().

Seguono i membri della struttura e il loro significato:

  • cbSize La grandezza della struttura.
  • style Stili della Classe (CS_*), da non confondere con gli stili delle finestre (WS_*). Questo normalmente può essere settato a 0.
  • lpfnWndProc Puntatore alla procedura che processerà i messaggi passati alla Classe.
  • cbClsExtra Quantità di memoria extra da allocare per la classe. In genere 0.
  • cbWndExtra Quantità di memoria allocata per i dati extra da passare alle finestre create sulla base di questa classe. In genere 0.
  • hInstance Handle all’istanza dell’applicazione (quella che abbiamo ricevuto nel primo parametro di WinMain()).
  • hIcon Icona grande(generalmente 32x32) da visualizzare quando l’utente preme Alt+Tab.
  • hCursor Cursore visualizzato quando il mouse passa sulla nostra finestra.
  • hbrBackground Il Brush di Background per settare il colore di sfondo della nostra finestra.
  • lpszMenuName Nome di una risorsa di menù da usare per le finestre create su questa classe.
  • lpszClassName Nome della classe.
  • hIconSm Icona piccola (generalmente 16x16) da visualizzare nella taskbar e nell’angolo in alto a sinistra della finestra.

Non preoccupatevi se tutto questo vi crea ancora confusione. Le parti importanti saranno riprese e spiegate più approfonditamente in seguito. Un’altra cosa da ricordare è di evitare di imparare a memoria queste strutture. Raramente (mai) io memorizzo le strutture, o i parametri delle funzioni, è una perdita di risorse e soprattutto di tempo. Se si conosce il nome della funzione da chiamare, è questione di pochi secondi trovare l’esatto numero e la sequenza dei parametri da passargli tramite ai file della guida. Se non avete file di guida, procurateveli perchè sarete persi senza. Eventualmente imparate i parametri delle funzioni che usate di più.

A questo punto chiamiamo RegisterClassEx() e ne controlliamo l’esito, se fallisce visualizziamo un messaggio in cui comunichiamo l’errore e usciamo dal programma uscendo dalla funzione WinMain().

Step 2: Creazione della Finestra

Dopo aver registrato la classe, possiamo crearci una finestra con essa. Sarebbe il caso che vi studiaste tutti i parametri per CreateWindowEx() (come dovrebbe farsi SEMPRE, ogni volta che viene usata una nuova chiamata API) per poter passare le opzioni giuste per poter fare esattamente quello che desiderate. Comunque ve la spiegherò brevemente io.

    HWND hwnd;

    hwnd = CreateWindowEx(
        WS_EX_CLIENTEDGE,
        g_szClassName,
        "Titolo della mia finestra",
        WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT, 240, 120,
        NULL, NULL, hInstance, NULL);

Il primo parametro(WS_EX_CLIENTEDGE) è un stile esteso, in questo caso l’ho settato in modo da ottenere un bordo interno intorno a tutta la finestra. Settatelo a 0 se volete vedere la differenza. Potete modificare anche gli altri parametri per vedere cosa fanno

Dopodiché abbiamo il nome della classe (g_szClassName), questo parametro dice al sistema che tipo di finestra creare. Siccome vogliamo creare una finestra utilizzando la classe appena registrata, mettiamo il nome di questa classe. Dopo di questo inseriamo il nome della finestra o il titolo, cioè il testo che sarà visualizzato nel titolo ovvero la barra del titolo (Title bar) della nostra finestra.

Il parametro passato come WS_OVERLAPPEDWINDOW è lo stile della Finestra (Window Style). Ce ne sono pochi e dovreste vederli tutti per capire e sperimentare cosa fanno. Lo vedremo più tardi.

I successivi quattro parametri (CW_USEDEFAULT, CW_USEDEFAULT, 320, 240) sono le coordinate X e Y per il lato in alto a sinistra della finestra e la larghezza e l’altezza della finestra stessa. Ho impostato i valori di X e Y a CW_USEDEFAULT per lasciar scegliere al sistema operativo dove posizionare la finestra sullo schermo. Ricordate che un valore di 0 assegnato ad X posizionerà la finestra sulla parte sinistra dello schermo e un valore di 0 assegnato ad Y posizionerà la finestra sulla parte superiore dello schermo. L’unità di misura sono i pixel ovvero la più piccola unità che uno schermo può visualizzare ad una determinata risoluzione.

Successivamente (NULL, NULL, g_hInst, NULL) abbiamo l’handle alla Finestra Padre principale, l’handle del menu, l’handle dell’istanza dell’applicazione e infine il puntatore ai dati opzionali per la creazione della finestra. In windows, le finestre sullo schermo sono posizionate ordinate in una gerarchia di finestre padre e figlio. Quando vedete un bottone in una finestra, il bottone è il Figlio contenuto nella finestra che è il Padre. In questo esempio l’handle del Padre e’ NULL perche’ non abbiamo nessun padre, quindi la finestra è la principale (Top Level Window). Anche il menù e’ NULL per ora perchè ancora non ne abbiamo uno. L’handle dell’istanza viene settato pari al valore passato come primo parametro di WinMain(). I dati di creazione (che non saranno mai usati in questo tutorial) possono essere usati per specificare dati addizionali alla finestra che verrà creata, sono anch’essi NULL.

Se a questo punto vi state chiedendo cos’è questo magico NULL, sappiate che è semplicemente definito come 0 (zero). Attualmente in C è definito come ((void*)0), perchè viene inteso per l’uso con i puntatori. Per questo motivo potrete ricevere warning se usate NULL con i valori interi (int) a seconda del compilatore e i settaggi del livello di warning; potete scegliere di ignorare questi warning oppure usare 0 al loro posto.

Il primo motivo per cui non si conosce cosa c’è di sbagliato in un programma che apparentemente non funziona è quasi sempre perchè non si controlla il valore di ritorno di una funzione per vedere se la chiamata è fallita o meno. CreateWindow() fallirà in un determinato punto se non avete esperienza, semplicemente perchè mentre si programma si possono fare facilmente tantissimi errori. Finché non imparerete come identificare velocemente questi errori, datevi una possibilità per immaginare dove le cose iniziano a prendere una piega sbagliata. Controllate sempre il valore restituito da una funzione!

    if(hwnd == NULL)
    {
        MessageBox(NULL, "Creazione della Finestra Fallita!", "Errore!",
            MB_ICONEXCLAMATION | MB_OK);
        return 0;
    }

Dopo aver creato la finestra e controllato di avere un handle valido, possiamo visualizzare la finestra usando l’ultimo parametro passato a WinMain() dopodichè aggiornarla per essere sicuri che sia stata ridisegnata correttamente sullo schermo.

   ShowWindow(hwnd, nCmdShow);
   UpdateWindow(hwnd);

Il parametro nCmdShow è opzionale, è possibile infatti passare semplicemente SW_SHOWNORMAL tutte le volte senza avere problemi. Ad ogni modo passando il parametro passato a WinMain() potete dare la possibilità alla persona che sta facendo girare il vostro programma di specificare se desiderano o meno che esso sia subito visibile, massimizzato, minimizzato ecc… (E’ possibile specificare queste opzioni andando sulle proprietà dei collegamenti di windows).

Step 3: Il ciclo dei messaggi

Questo è il cuore dell’intero programma, praticamente tutto quello che fa il programma passa da questo punto di controllo.

    while(GetMessage(&Msg, NULL, 0, 0) > 0)
    {
        TranslateMessage(&Msg);
        DispatchMessage(&Msg);
    }
    return Msg.wParam;

GetMessage() prende un messaggio dalla coda dei messaggi dell’applicazione. Ogni volta che l’utente muove il mouse, scrive con la tastiera, clicca sul menù, o qualsiasi altra cosa, viene generato un messaggio dal sistema e messo nella coda dei messaggi per il vostro programma. Chiamando GetMessage() non fate altreo che richiedere il successivo messaggio disponibile per rimuoverlo dalla coda e processarlo. Se non ci sono messaggi GetMessage() Blocca. Se non siete familiari con i termini vuol dire che di ferma ed aspetta finché non arriva un messaggio, dopodiché lo passa al programma.

TranslateMessage() esegue un’elaborazione supplementare sugli eventi della tastiera come generare messaggi WM_CHAR per farli andare insieme ai messaggi WM_KEYDOWN. Infine DispatchMessage() invia il messaggio alla finestra per la quale il messaggio è stato generato; la finestra può essere la nostra finestra principale o un’altra, un controllo, e in alcuni casi una finestra che è stata creata all’interno della scena dal sistema o da un altro programma. Comunque non dovete preoccuparvi di tutto questo perchè ci pensa il sistema a indirizzare il messaggio alla finestra giusta.

Step 4: La procedura per la finestra

Se il ciclo dei messaggi è il cuore del programma la procedura per la finestra ne è il cervello. Questa è la funzione dove vengono processati tutti i messaggi inviati alla finestra.

LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch(msg)
    {
        case WM_CLOSE:
            DestroyWindow(hwnd);
        break;
        case WM_DESTROY:
            PostQuitMessage(0);
        break;
        default:
            return DefWindowProc(hwnd, msg, wParam, lParam);
    }
    return 0;
}

La procedura viene chiamata per ogni messaggio, il parametro HWND è l’handle della finestra, quella alla quale vengono applicati i messaggi. Questo è importante perche’ potete avere due o più finestre con la stessa classe che usano la stessa procedura (WndProc()). La differenza è che il parametro hwnd sarà diverso a seconda di quale finestra riceve il messaggio. Per esempio quando riceviamo il messaggio WM_CLOSE distruggiamo la finestra, se noi usiamo l’handle ricevuto come primo parametro possiamo processare correttamente il messaggio ed applicarlo alla finestra per la quale esso è stato generato.

WM_CLOSE viene inviato quando l’utente preme il tasto di chiusura x o si preme Alt-F4. Per default la finestra viene distrutta, è comunque preferibile processarla esplicitamente, dato che è un punto perfetto per eseguire controlli, liberare memoria, chiedere all’utente se si vuole salvare i files ecc… prima di uscire dal programma.

Quando chiamiamo DestroyWindow() il sistema invia il messaggio WM_DESTROY alla finestra che sta per essere distrutta, in questo caso la nostra finestra, dopodichè distrugge tutte le finestre Figlie rimanenti prima di eliminare definitivamente la finestra dal sistema. Dato che questa è l’unica finestra nel nostro programma possiamo uscire tranquillamente e chiamare PostQuitMessage(). Questo comando posta WM_QUIT al ciclo dei comandi. Non riceveremo mai questo messaggio perchè provoca il ritorno di un valore FALSE da GetMessage() , e come vedrete nel nostro codice del message loop, quando questo accade, smettiamo di processare i messaggi e ritorniamo il codice di uscita, il wParam di WM_QUIT corrisponde al valore passato a PostQuitMessage(). Il codice di uscita è necessario solamente se il programma è progettato per essere chiamato da un altro programma esterno e si vuole ritornare un valore specifico per condizionare l’andamento del programma chiamante.

Step 5: Non c’è nessuno Step 5

Wow. Questo è tutto! Se ancora non mi sono spiegato molto chiaramente fermatevi un attimo a riflettere e state tranquilli perchè possibilmente le cose diventeranno un attimo più chiare non appena inizieremo a scrivere qualcosa di più concreto.