DirectX: Labirynt 3D od podstaw

15.03.2010 - Adam Błaszkiewicz
TrudnośćTrudność

Tworzenie okna w Windows API

Funkcje Windows API występują w dwóch wersjach: ASCII i Unicode. Jeśli chcemy, aby nasz program był w stanie wyświetlić znaki Unicode, czyli na przykład polskie litery, musimy włączyć w ustawieniach projektu odpowiednią opcję - Use Unicode Character Set w polu Character Set. Domyślnie opcja ta jest aktywna. Zależnie od tego ustawienia, funkcje Windows API są mapowane na odpowiednią wersję - na przykład MessageBox jest mapowany na MessageBoxA (ANSI - ASCII; a żeby być ścisłym, ASCII rozszerzone o dodatkowy, ósmy bit) lub MessageBoxW (wide - Unicode). Aby pisany przez nas kod był uniwersalny, tj. działał niezależnie od ustawienia Unicode lub nie, Microsoft udostępnia nam plik nagłówkowy tchar.h, w którym zdefiniowane są między innymi następujące makra, których mapowanie jest zależne od ustawienia Unicode lub nie:

- TCHAR - char lub wchar_t 

- LPTSTR - LPSTR (char *) lub LPWSTR (wchar_t *)

- LPCTSTR - LPCSTR (const char *) lub LPCWSTR (const wchar_t *)

- _T("żółć") lub TEXT("żółć") - "żółć" lub L"żółć" (czyli const wchar_t *)

W naszym programie będziemy korzystać z tych makr, więc pierwszym krokiem będzie dołączenie do naszego programu pliku nagłówkowego tchar.h. Przepiszmy więc poniższą linijkę kodu do naszego pliku main.cpp.

1
#include <tchar.h>

Poza tym, musimy dołączyć też następujący plik nagłówkowy (główny plik nagłówkowy Windows API):

1
#include <windows.h>

Pisanie programów pod Windows wymaga użycia głównej funkcji programu WinMain, zamiast main. Funkcja ta wyjątkowo nie jest mapowana w zależności od Unicode zgodnie z konwencją. Mianowicie WinMain to wersja ANSI, wWinMain to wersja Unicode, a _tWinMain to uniwersalne makro z tchar.h. Tak więc nasza funkcja będzie wyglądała następująco:

1
2
3
int WINAPI _tWinMain( HINSTANCE hInstance, HINSTANCE, LPTSTR, INT )
{
}

Makro WINAPI powoduje odpowiednie wywołanie funkcji - funkcje Windows API same czyszczą po sobie stos - usuwają z niego przekazane im argumenty, natomiast w C++ domyślnie stos jest czyszczony w miejscu wywołania funkcji. Nasza funkcja WinMain także powinna zachowywać się zgodnie z tą konwencją, ponieważ Windows będzie tego oczekiwał, gdy ją wywoła.

Aby utworzyć okno, musimy wywołać funkcję CreateWindow, a następnie, aby wyświetlić okno na ekranie, ShowWindow. Wcześniej jednak musimy utworzyć klasę okna, której użyjemy w CreateWindow. Zdefiniujmy więc strukturę klasy okna i przekażmy ją Windowsowi poprzez funkcję RegisterClassEx:

1
2
3
4
5
6
7
8
9
10
WNDCLASSEX wc;
ZeroMemory( &wc, sizeof( WNDCLASSEX ) );
wc.cbSize = sizeof( WNDCLASSEX );
wc.style = CS_CLASSDC;
wc.lpfnWndProc = WndProc;
wc.hInstance = hInstance;
wc.hCursor = LoadCursor( NULL, IDC_ARROW );
wc.lpszClassName = _T( "LabiryntWndClass" );
 
RegisterClassEx( &wc );

Wersja z komentarzami:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Struktura klasy okna:
WNDCLASSEX wc;
 
// Zerujemy wszystkie pola struktury, aby potem ustawić
// tylko te, które będą nam potrzebne:
ZeroMemory( &wc, sizeof( WNDCLASSEX ) );
 
// Tu musimy podać rozmiar struktury:
wc.cbSize = sizeof( WNDCLASSEX );
 
// Styl klasy okna:
wc.style = CS_CLASSDC;
 
// Wskaźnik do funkcji przetwarzającej komunikaty okna,
// którą za chwilę zdefiniujemy:
wc.lpfnWndProc = WndProc;
 
// Uchwyt instancji naszego programu,
// który dostajemy jako pierwszy argument WinMain:
wc.hInstance = hInstance;
 
// Uchwyt do kursora używanego nad naszym oknem
// IDC_ARROW to zwykła strzałka
wc.hCursor = LoadCursor( NULL, IDC_ARROW );
        
// Identyfikator klasy okna:
wc.lpszClassName = _T( "LabiryntWndClass" );
 
// Rejestracja naszej klasy okna:
RegisterClassEx( &wc );

Musimy pamiętać, aby po zniszczeniu okna, na końcu programu, wyrejestrować naszą klasę okna - zrobimy to potem.

Następnie, gdy nasza klasa okna jest już zarejestrowana, możemy utworzyć okno i wyświetlić je:

1
2
3
4
5
6
7
8
9
10
11
HWND hWnd = CreateWindow(
    _T( "LabiryntWndClass" ),
    _T( "Trójwymiarowy labirynt ;)" ),
    WS_SYSMENU | WS_MINIMIZEBOX,
    CW_USEDEFAULT, CW_USEDEFAULT,
    800, 600
    NULL, NULL,
    hInstance,  
    NULL );
 
ShowWindow( hWnd, SW_SHOWDEFAULT );

Znaczenia poszczególnych argumentów:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
HWND hWnd = CreateWindow(
 
    // Identyfikator klasy okna, którą stworzyliśmy:
    _T( "LabiryntWndClass" ),
 
    //  Ten tekst wyświetli nam się na pasku tytułu:
    _T( "Trójwymiarowy labirynt ; )" ),
 
    // Styl naszego okna -
    // - możliwość maksymalizowania, minimalizowania, zamykania,
    // obecność ikony na pasku tytułu, itp.
    // WS_SYSMENU | WS_MINIMIZEBOX,
 
    // Pozycja x,y - mówimy Windowsowi, żeby wybrał domyślną:
    CW_USEDEFAULT, CW_USEDEFAULT,
 
    // Szerokość i wysokość:
    800, 600
    NULL, NULL,
 
    // uchwyt instancji naszego programu,
    // który otrzymaliśmy jako argument WinMain:
    hInstance,  
    NULL );
        
// Okno jest już utworzone w pamięci komputera,
// wystarczy je wyświetlić na ekranie:
ShowWindow( hWnd, SW_SHOWDEFAULT );

Teraz nadszedł czas na główną pętlę naszego programu. W tym miejscu program zapętli się, dopóki okno nie zostanie zamknięte. Wtedy zniszczymy jego klasę i zakończymy program.

1
2
3
4
5
6
7
8
9
MSG msg;
while( GetMessage( &msg, NULL, 0, 0 ) )
{
    TranslateMessage( &msg );
    DispatchMessage( &msg );
}       
 
UnregisterClass( _T("LabiryntWndClass"), hInstance );
return msg.wParam;

Komunikacja z Windowsem opiera się na komunikatach. Typem komunikatu jest MSG. Funkcja GetMessage pobiera następny w kolejce komunikat od systemu Windows. Jeśli komunikatem tym jest WM_QUIT, funkcja dodatkowo zwraca 0, aby umożliwić zapisanie pętli komunikatów w prosty sposób, w przedstawionej wyżej formie. Po otrzymaniu komunikatu innego niż WM_QUIT (wtedy nie wychodzimy z pętli while), wywołujemy na nim funkcję TranslateMessage i DispatchMessage, co powoduje przekazanie przetłumaczonego komunikatu do funkcji obsługi komunikatów naszego okna, którą zaimplementujemy za chwilę. Windows wymaga, aby funkcja WinMain zwracała wartość wParam komunikatu WM_QUIT (czyli tego ostatniego) więc ją zwracamy.

Jedyna rzecz, której nam teraz brakuje, to funkcja przetwarzająca komunikaty związane z naszym oknem, której nazwę (a właściwie wskaźnik) wpisaliśmy do struktury okna. Poprzez wywołanie  DispatchMessage w powyższym kodzie, Windows przekaże komunikat do funkcji obsługi komunikatów odpowiedniego okna - czyli tego naszego.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
LRESULT WINAPI WndProc( HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam )
{
    switch( msg )
    {
        case WM_DESTROY:
            PostQuitMessage( 0 );
            return 0;
 
        case WM_PAINT:
            RECT rect;
            PAINTSTRUCT ps;
            HDC hdc = BeginPaint(hWnd, &ps);
            GetClientRect(hWnd, &rect);
            FillRect(hdc, &rect, (HBRUSH) (COLOR_WINDOW));
            DrawText(
                hdc,
                _T("Hej! : D"),
                -1, &rect,
                DT_CENTER);
            EndPaint(hWnd, &ps);
            return 0;
    }
 
    return DefWindowProc( hWnd, msg, wParam, lParam );
}

Typ zwracany przez funkcję przetwarzającą komunikaty okna to LRESULT (parę makr dalej ukrywa się tu int, a dokładnie long). Jeśli funkcja powiedzie się, powinniśmy zwrócić 0. Jako argument dostajemy od systemu uchwyt (HWND) do naszego okna, identyfikator (UINT - unsigned int) komunikatu oraz jego dwa parametry: WPARAM i LPARAM. Każdy komunikat może przekazywać w tych argumentach jakieś dodatkowe informacje. W systemie 32-bitowym oba typy mają po 32 bity.

W powyższym kodzie obsługujemy komunikaty WM_DESTROY oraz WM_PAINT. Ten pierwszy informuje nasz program, że okno jest niszczone, a drugi nakazuje odmalowanie obszaru okna.

Po otrzymaniu komunikatu WM_DESTROY, nie chcemy, aby program nadal działał w tle, więc umieszczamy w pętli komunikatów WM_QUIT za pomocą PostQuitMessage. Komunikat ten zostanie odebrany przez GetMessage w naszej funkcji WinMain (pamiętamy, że wtedy GetMessage zwróci 0 i program wyjdzie z pętli while). Jako argument przekazujemy wartość, jaką chcemy, aby WinMain zwrócił. Zostanie ona umieszczona w polu wParam komunikatu WM_QUIT - dlatego w WinMain zwracamy właśnie tę wartość.

Komunikat WM_PAINT jest wysyłany przez system, gdy całe okno lub jego część wymaga odmalowania. Dzieje się tak na przykład wtedy, gdy zminimalizujemy okno i z powrotem zmaksymalizujemy lub gdy zasłonimy częściowo okno innym oknem, po czym odsłonimy jego część. Odmalowane fragmenty okna trzeba zgłosić systemowi. W przeciwnym wypadku komunikat WM_PAINT będzie wysyłany w nieskończoność (w powyższym kodzie robią to za nas funkcje FillRect i DrawText, które odpowiednio zamalowują całe okno wybranym kolorem i wypisują tekst).

Jeśli w zdefiniowanej przez nas funkcji WndProc otrzymamy komunikat, którego nie chcemy przetwarzać, powinniśmy wywołać na nim funkcję DefWindowProc, która przetwarza komunikat w sposób domyślny. Każdy komunikat powinien zostać odpowiednio przetworzony. Na przykład wspomniany WM_PAINT powinien oznaczyć obszar okna jako odmalowany, aby komunikat ten nie był wysyłany w nieskończoność. Tym właśnie zajęłaby się funkcja DefWindowProc, gdybyśmy nie przetwarzali WM_PAINT na własną rękę.

Możemy już skompilować nasz program. Spróbujmy zmienić tekst "Hej!" lub tytuł okna na jakiś inny. Możemy też dodać do funkcji DrawText w ostatnim argumencie DT_SINGLELINE | DT_VCENTER, co spowoduje wyśrodkowanie tekstu w pionie.

Kod źródłowy do tego miejsca można pobrać z ramki po lewej.

4.666665
Twoja ocena: Brak Ocena: 4.7 (6 ocen)

Copyright © 2008-2010 Wrocławski Portal Informatyczny

design: rafalpolito.com