DirectX/Creare la finestra

Indice del libro

DirectX ha bisogno di un posto in cui disegnare. Generalmente si crea una finestra e, al posto di Buttons e TextBox, la si fa disegnare da Direct3D. È anche possibile generare immagini in Direct3D il cui risultato viene mostrato in una ImageBox di un programma che utilizza normalmente i Controls di Windows, ma questo non verrà trattato qui.

In questo capitolo verrà spiegato come creare una finestra in ambiente Win32.

main modifica

Dimenticate il caro vecchio main. Quando si programma Win32 si usa WinMain. Un entry point di esempio è:

#include <Windows.h>
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR cmdLine, int cmdShow)
{
    return 0;
}

Argomenti:

  • cmdLine è la command line del programma (come se aveste preso l'intero argv in una sola stringa).
  • cmdShow indica se il programma deve essere caricato normalmente, massimizzato o ridotto ad icona. È a scelta del programmatore se voler rispettare questo codice o meno. Noi lo ignoreremo.
  • hInstance è un HANDLE, ossia un codice che identifica l'applicazione (una specie di Process ID - pid)
  • hPrevInstance è sempre NULL. Una volta conteneva l'HANDLE di un'altra istanza della stessa applicazione. Cioè se questa applicazione era eseguita due volte, la seconda conteneva in hPrevInstance il valore hInstance della prima, così da poter comunicare o bloccarne una seconda esecuzione. Oggi non è più usato. Per rilevare se l'applicazione è in uso più volte si può approfondire qui.

Il tipo LPSTR di cmdLine sta per puntatore a CHAR, dove CHAR è char. Scoprirete che la libreria WIN32 è piena di typedef e di #define per la creazione di tipi personalizzati. Consiglio di far confidenza con:

CONST const
CHAR char
WCHAR w_char
VOID void
INT int
UINT unsigned int
LPSTR CHAR *
LPCSTR CONST CHAR *
LPWSTR W_CHAR *
LPCWSTR CONST WCHAR *
PUINT UINT *
LPVOID CONST *
LPCVOID CONST VOID *

Nonostante i nomi siano abbastanza chiari è bene conoscerli in quanto il loro uso in questo ambiente è continuo. Da notare come quasi tutti i puntatori inizino con LP o solo con P. Una volta (quando l'uso del DOS era ancora esteso) esisteva la distinzione tra LongPointer (o FAR Pointer) e Pointer (o NEAR Pointer). Su Windows non fa alcuna differenza usare puntatore P o LP, anche se gli LP sono di gran lunga preferiti.

In ambiente Windows è preferibile usare wchar al posto di char. Allo stesso modo è meglio LPCWSTR al posto di LPCSTR, e al posto di std::string, std::wstring. Questo perché internamente Win32 supporta Unicode in formato UTF-16. È quindi possibile visualizzare caratteri di ogni tipo da ogni parte del mondo (con rarissime eccezioni: usereste mai le rune antiche per il titolo della finestra?). Dato che Win32 ci da questa possibilità utilizzerò l'Unicode ovunque possibile. Se non l'aveste ancora fatto, nelle proprietà del progetto in Visual Studio potete specificare che intendete utilizzare Unicode.

Prepariamo la finestra modifica

Creare una finestra in Win32 è un processo un pochino laborioso. Innanzitutto va creata una struttura che indica le caratteristiche della finestra. Dopodiché tramite una funzione, questa struttura verrà tradotta in un HWND, un HANDLE a finestra. Questo HWND è un identificatore, che indica univocamente la nostra finestra tra tutte le finestre presenti nel sistema. La finestra è stata creata ma è ancora nascosta. Con un'altra funzione la renderemo visibile.

Creazione della struttura modifica

La struttura da creare è WNDCLASSEX. In questa struttura (detta classe) è contenuto quasi tutto il necessario a generare una finestra.

struct WNDCLASSEX
{
    UINT      cbSize;
    UINT      style;
    WNDPROC   lpfnWndProc;
    int       cbClsExtra;
    int       cbWndExtra;
    HINSTANCE hInstance;
    HICON     hIcon;
    HCURSOR   hCursor;
    HBRUSH    hbrBackground;
    LPCTSTR   lpszMenuName;
    LPCTSTR   lpszClassName;
    HICON     hIconSm;
};
  • cbSize deve essere impostato a sizeof(WNDCLASSEX). Questo permette in futuro l'espansione della struttura stessa, mantenendo retrocompatibilità.
  • cbClsExtra indica quanti byte devono essere allocati oltre a quelli della classe stessa. Questi byte possono essere usati per inserire dati personalizzati. Oppure la si imposta a 0.
  • cbWndExtra campo simile al precedente. Quanti byte da allocare oltre quelli della finestra.
  • hInstance va impostato al valore dell'argomento hInstance di WinMain.
  • hIcon è un HANDLE ad icona (HICON). Questa icona verrà visualizzata normalmente nella TaskBar e nella barra del titolo.
  • hCursor è un HANDLE a cursore (HCURSOR). Questo cursore sarà il cursore che verrà mostrato quando il mouse passa sulla finestra.
  • hbrBackground è un HANDLE a brush. Una brush è un metodo di riempimento: un colore, sfumatura, immagine ripetuta, ecc. Questo campo indica con quale brush verrà riempito lo sfondo.
  • lpfnWndProc la ritroveremo nel prossimo modulo. Se proprio volete provare, impostatelo a NULL per ora.
  • lpszMenuName è una stringa che identifica una barra dei menu. Noi non useremo menu, quindi basta impostarla a NULL.
  • lpszClassName è una stringa che identifica questa classe. Impostatela a qualsiasi valore vogliate, ma fate in modo che sia unico all'interno del programma (uno per ogni finestra). Un esempio potrebbe essere "DirectXTutorialWin1".
  • hIconSm è un altro HICON. Questo campo è stato aggiunto in seguito ed indica l'icona da mostrare quando la dimensione raggiunge i 16x16 pixel. In questo modo l'icona sulla TaskBar di Windows 7 sarà hIcon, mentre quella sulla barra del titolo sarà hIconSm. Potete anche impostare il campo a NULL e hIcon verrà usata in tutti i casi.
  • style imposta alcuni parametri secondari della finestra. Generalmente useremo CS_HREDRAW | CS_VREDRAW. Questa impostazione chiede a Windows di ridisegnare la finestra in caso di modifiche a altezza e larghezza.

Riempiamo ora questa struttura:

WNDCLASSEX wc;
ZeroMemory(&wc, sizeof(WNDCLASSEX));
wc.cbSize = sizeof(WNDCLASSEX);
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = NULL; // Verrà spiegato in seguito
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_APPLICATION));
wc.hCursor = LoadCursor(hInstance, IDC_ARROW);
wc.hbrBackground = NULL;
wc.lpszMenuName = NULL;
wc.lpszClassName = L"DxTutorial1";
wc.hIconSm = NULL;

ZeroMemory è la nostra prima API Win32. Non fa nient'altro che azzerare la memoria di una data struttura. Infatti:

#define ZeroMemory(Destination, Length) memset(Destination, 0, Length);

LoadIcon e LoadCursor vengono usate per generare HANDLE ad icona e cursore partendo dal nome del file. In questo caso però anziché usare il nome del file abbiamo usato delle costanti che rispondono ad icone e cursori predefiniti. IDI_APPLICATION è l'icona di programma generico. IDC_ARROW è la freccia. Un altro cursore predefinito è IDC_WAIT (la clessidra fino a Windows XP, il cerchio blu di caricamento da Windows Vista in poi). Ultima particolarità: la L davanti a "DxTutorial1". Quella L indica al compilatore di codificare la stringa in UTF-16 e non come semplice stringa di char.

A questo punto dobbiamo dare questi dati a Windows. Windows infatti terrà nella sua memoria interna questa struttura a cui noi ci riferiremo solo tramite il valore contenuto in lpszClassName, o tramite il valore restituito da questa chiamata. Per dare in pasto questa struttura a Windows:

ATOM res = RegisterClassEx(&wc);
if (!res)
{
    printf("Impossibile registrare la classe.\n");
    return -1;
}

ATOM è un valore a 16 bit che identifica internamente a Windows il valore di lpszClassName. Come se fosse un indice in un array di stringhe che contengono ClassNames. A questo punto usare il valore dato da RegisterClassEx o il valore lpszClassName è equivalente. A noi basta sapere però che se il suo valore è 0, c'è stato un errore e che non si può andare avanti finché non l'avrete corretto.

Creare la finestra modifica

Creiamo ora la finestra usando la classe appena registrata. La funzione da usare è:

HWND WINAPI CreateWindow(LPCTSTR lpClassName, LPCTSTR lpWindowName, DWORD dwStyle, int x, int y, int nWidth, int nHeight, HWND hWndParent, HMENU hMenu, HINSTANCE hInstance, LPVOID lpParam);

Non lasciamoci spaventare dal numero di argomenti (c'è di peggio in Direct3D) e vediamo cosa inserire:

  • dwStyle contiene alcune flag che indicano un aspetto generale della finestra (bordo, barra del titolo, riduzione ad icona, massimizzazione, ancore di ridimensionamento). Per una finestra generica, con barra del titolo eccetera, impostiamolo a WS_OVERLAPPEDWINDOW. Per tutti gli stili possibili vedete qui.
  • hWndParent viene impostato, se la finestra è figlia di un'altra finestra, all'HANDLE della finestra madre.
  • lpClassName qui andrà messo il valore lpszClassName della classe oppure il valore res ritornato da RegisterClassEx (con un cast a LPCTSTR)
  • hInstance è il valore hInstance dell'argomento a WinMain.
  • hMenu è l'HANDLE di un eventuale menu.
  • lpParam per ora lo impostiamo a NULL, ma lo useremo probabilmente dopo.
  • lpWindowName qui invece inseriremo il titolo della finestra, quello che apparirà nella Title Bar.
  • width e height sono la dimensione in pixel dell'intera finestra (inclusa barra del titolo e bordi), ma non dell'area da utilizzare per disegnare (o inserire pulsanti). Quindi se vogliamo avere una zona per disegnare di 800x600, imposteremo questi valori a 840x680 ad esempio (non sono corretti: esiste una funzione specifica per trovare i valori adatti. Vedremo più avanti).
  • x ed y sono la posizione all'interno dello schermo della finestra. Per dare libertà a Windows di posizionarci dove meglio ritiene impostiamoli entrambi a CW_USEDEFAULT.

Quindi:

HWND windowHandle = CreateWindow(L"DxTutorial1", L"Titolo della finestra", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 800, 600, NULL, NULL, hInstance, NULL);
// In alternativa se preferite usare l'atom al posto della ClassName:
HWND windowHandle = CreateWindow((LPCTSTR) res, L"Titolo della finestra", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 800, 600, NULL, NULL, hInstance, NULL);

Mostrare la finestre modifica

A questo punto dopo aver creato la finestra non ci resta che mostrarla con:

ShowWindow(HWND hWnd, int nCmdShow);
  • hWnd è l'HANDLE alla finestra appena creata.
  • nCmdShow indica come la finestra deve essere mostrata (ridotta ad icona, massimizzata, ecc.). Normalmente qui andrebbe passato l'argomento a WinMain nCmdShow, ma noi abbiamo deciso di ignorare quel valore e passeremo SW_SHOWDEFAULT, che mostra una normalissima finestra davanti a voi.

Quindi:

ShowWindow(windowHandle, SW_SHOWDEFAULT);

Conclusione modifica

Abbiamo ora creato una bella finestra, ma che rifiuterà di mostrarsi a causa del campo lpfnWndProc che abbiamo lasciato a NULL, il che ci causa qualche frustrazione. Questa finestra non sa fare nulla. In più subito dopo ShowWindow, verrà il return 0, che termina il programma. Nel prossimo modulo vedremo come farla finalmente apparire e come farle rispondere a qualche comando di base (e a farla chiudere correttamente).

Ecco il codice completo di questo modulo:

#include <Windows.h>
#include <stdio.h>
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR cmdLine, int cmdShow)
{
    WNDCLASSEX wc;
    ZeroMemory(&wc, sizeof(WNDCLASSEX));
    wc.cbSize = sizeof(WNDCLASSEX);
    wc.style = CS_HREDRAW | CS_VREDRAW;
    wc.lpfnWndProc = NULL; // Verrà spiegato in seguito
    wc.cbClsExtra = 0;
    wc.cbWndExtra = 0;
    wc.hInstance = hInstance;
    wc.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_APPLICATION));
    wc.hCursor = LoadCursor(hInstance, IDC_ARROW);
    wc.hbrBackground = NULL;
    wc.lpszMenuName = NULL;
    wc.lpszClassName = L"DxTutorial1";
    wc.hIconSm = NULL;
    ATOM res = RegisterClassEx(&wc);
    if (!res)
    {
        printf("Impossibile registrare la classe.\n");
        return -1;
    }
    HWND windowHandle = CreateWindow(L"DxTutorial1", L"Titolo della finestra", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 800, 600, NULL, NULL, hInstance, NULL);
    ShowWindow(windowHandle, SW_SHOWDEFAULT);
    return 0;
}