Szkielet aplikacji

Transkrypt

Szkielet aplikacji
Grafika komputerowa – Szkielet aplikacji
1.1 Szkielet aplikacji
Poniżej przedstawiony jest szkielet aplikacji, na którym oparte są wszystkie programy
pisane w ramach ćwiczeń. Główna klasa okna programu może być wyprowadzona z klasy
TForm, tak jak to jest zaprezentowane poniżej. Wytłuszczeniem zaznaczone są prywatne
składowe klasy, potrzebne do zainicjowania DirectDraw. Typy danych: LPDIRECTDRAW,
DDSURFACEDESC, LPDIRECTDRAWSURFACE, DDSCAPS zdefiniowane są w pliku
nagłówkowym ddraw.h.
class TForm1 : public TForm
{
__published:
// IDE-managed Components
void __fastcall FormCreate(TObject *Sender);
void __fastcall FormDestroy(TObject *Sender);
void __fastcall FormKeyDown(TObject *Sender, WORD
&Key,
TShiftState Shift);
private: // User declarations
LPDIRECTDRAW
lpDD;
DDSURFACEDESC
ddsd;
LPDIRECTDRAWSURFACE
FrontBuffer;
LPDIRECTDRAWSURFACE
BackBuffer;
DDSCAPS
ddscaps;
bool InitDDraw();
public:
// User declarations
__fastcall TForm1(TComponent* Owner);
};
1.2 Obiekty COM
Budowa bibliotek DirectX oparta jest bezpośrednio na obiektach COM (Component
Object Model), stanowiących rozszerzenie pojęcia klas abstrakcyjnych języka C++. Obiekt
COM z punktu widzenia aplikacji jest „czarną skrzynką” wykonującą ściśle określone operacje. Główna różnica między obiektami COM, a klasami języka C++ polega na sposobie wywoływania metod, w przypadku języka C++ wystarczy utworzyć egzemplarz danej klasy i
bezpośrednio wywoływać jej funkcję. Natomiast publiczne metody obiektów COM zgrupowane są w jeden lub kilka interfejsów, a sam obiekt jest w zasadzie strukturą przechowującą
wskaźniki do funkcji z nim powiązanych. Także w celu skorzystania z określonej metody,
musimy utworzyć obiekt COM i uzyskać wskaźnik do odpowiedniego interfejsu. Olbrzymią
zaleta obiektów COM jest możliwość ich użycia z poziomu języków nie zorientowanych
obiektowo. Wszystkie interfejsy DirectX wyprowadzane są z interfejsu IUnknown, posiadającego jedynie trzy funkcje AddRef(), QueryInterface( REFIID riid, LPVOID* ppvObj ) oraz
Release(). Głównymi zadaniami metod tego interfejsu jest określanie liczby odwołań aplikacji
do danego obiektu COM. W przypadku języka C++ każda aplikacja tworzy niezależnie egzemplarze obiektów np. przy pomocy operatora new, w chwili gdy przestaje korzystać z danego obiektu wywołuje operator delete i usuwa obiekt z pamięci. Z obiektami COM jest zupełnie inaczej, gdyż w pamięci znajduje się pojedynczy egzemplarz danego obiektu COM, a
każda z aplikacji chcących z obiektu COM skorzystać otrzymuje wskaźnik do niego. Za każ-
-1-
Grafika komputerowa – Szkielet aplikacji
dym razem, gdy aplikacja tworzy wybrany obiekt COM, tak naprawdę wywoływana jest
funkcja AddRef(), zwiększająca ilość odwołań do znajdującego się w pamięci obiektu i aplikacji zwracany jest wskaźnik. Po skończonym działaniu aplikacja powinna wywoływać metodę Release() zmniejszającą o 1 liczbę odwołań do obiektu, w momencie osiągnięcia przez
liczbę odwołań wartości 0, obiekt może być usunięty z pamięci.
1.3 Tworzenie obiektu DirectDraw
Utworzenie obiektu DirectDraw polega na wywołaniu funkcji DirectDrawCreate, która ma następującą postać:
HRESULT WINAPI DirectDrawCreate( GUID FAR *lpGUID,
*lplpDD,
IUnknown FAR *pUnkOuter);
LPDIRECTDRAW FAR
Parametr lpGUID określa wskaźnik na identyfikator karty, w przypadku podania wartości
NULL wybrana zostaje podstawowa karta graficzna. W *lplpDD podaje się adres wskaźnika
LPDIRECTDRAW, który zostanie ustawiony na interfejs IDirectDraw. Ostatni parametr powinien mieć wartość NULL. W przypadku poprawnego utworzenia obiektu DirectDraw funkcja zwróci wartość DD_OK.
Jeżeli utworzymy obiekt DirectDraw możemy przystąpić do ustawienia rodzaju
współpracy (tryb okienkowy lub pełnoekranowy) urządzenia (DirectDraw) z naszą aplikacją.
Czynności tej dokonujemy za pomocą funkcji.
HRESULT SetCooperativeLevel( HWND hwnd,
DWORD dwFlags
);
Pierwszym parametrem jaki musimy podać tej funkcji jest uchwyt naszego okna. Drugi parametr określa rodzaj współpracy, aplikacja może działać w trybie okienkowym lub pełnoekranowym, możemy zabronić systemowi reakcji na kombinację klawiszy Ctrl+Alt+Del. W
związku z tym, że flag jest dość dużo opiszę jedynie dwie, których będziemy używać.
DDSCL_EXCLUSIVE oznacza, że nasza aplikacja ma prawo do wyłączności ekranu. Flaga
ta musi być używana z DDSCL_FULLSCREEN, która wymusza działanie aplikacji w trybie
pełno ekranowym.
Kolejną czynnością jest ustawienie odpowiedniego trybu graficznego, w którym pracować będzie nasz program. Rozdzielczość ekranu ustawiamy za pomocą funkcji
HRESULT SetDisplayMode(DWORD dwWidth, DWORD dwHeight, DWORD dwBPP );
Kolejne parametry tej funkcji to po prostu szerokość, wysokość i liczba bitów na piksel trybu
graficznego , jaki chcemy uzyskać. Prawidłowo napisane aplikacje korzystające z DirectX
powinny oferować użytkownikowi zarówno możliwość karty graficznej (lub trybu jej pracy
np. emulacja) jak i trybu graficznego. Jednak dla naszych potrzeb wystarczające będzie podanie parametrów jednego z trybów, które są obsługiwane przez posiadaną kartę graficzną. W
związku z tym, że duża część kart nie obsługuje trybu 24-bitowego (mają tryby 32-bitowe) w
ramach zajęć używać będziemy trybów 8 i 16- bitowych.
Wspomniane tryby graficzne wymagają szerszego omówienia. Pierwszy z nich jest
trybem indeksowym, czyli takim, w którym piksel w pamięci komputera reprezentowany jest
bezpośrednio przez indeks określający numer koloru w palecie barw. Jak łatwo obliczyć w
trybie 8-bitowym dostępne mamy 256 kolorów 24-bitowych. W związku z tym, że paletę kolorów przypisuje się bezpośrednio do powierzchni sposób jej tworzenia przedstawiony będzie
poniżej. W drugim z trybów piksel opisywany jest bezpośrednio wartościami kolorów RGB, i
-2-
Grafika komputerowa – Szkielet aplikacji
tak na składową czerwoną oraz niebieską przeznaczono 5 bitów, natomiast na zieloną 6 bitów.
1.4 Powierzchnie
Powierzchnie (obiekt DirectDrawSurface) są obiektami reprezentującymi liniowe obszary pamięci graficznej, lub systemowej. Podstawowym zadaniem tych obiektów jest przechowywanie danych reprezentujących obraz, widziany na ekranie monitora. Powierzchnie
mogą być również używane jako różnego typu bufory pomocnicze, służące np. jako źródło
operacji kopiujących grafikę do innych powierzchni lub realizować zadanie z-bufora.
Chcąc wyświetlać jakąkolwiek grafikę za pomocą DirectDraw musimy utworzyć przy najmniej jedną powierzchnię (podstawową). Dokonać możemy tego za pomocą funkcji
HRESULT IDirectDraw::CreateSurface(LPDDSURFACEDESC lpDDSurfaceDesc,
LPDIRECTDRAWSURFACE FAR *lplpDDSurface, IUnknown FAR *pUnkOuter);
Pierwszym parametrem tej funkcji jest struktura typu DDSURFACEDESC, opisująca właściwości tworzonej powierzchni. W związku z tym, że struktura ta posiada dużą liczbę pól
omówione zostaną tylko niezbędne. Bardzo istotnym polem jest dwSize, w zmiennej tej należy podać rozmiar struktury (przykład użycia powierzchni znajduje się poniżej). W polu
dwFlags podajemy kombinację flag informującą DirectDraw, które z pól całej struktury należy wziąć pod uwagę, w naszym przypadku należy podać DDSD_CAPS| DDSD_BACKBUFFERCOUNT, kombinacja ta określa, że należy prawidłowo zdefiniować pola dwBackBufferCount i ddsCaps. Pierwsze z wymienionych pól określa liczbę tylnych buforów powierzchni, natomiast drugie jest strukturą typu DDSCAPS, określającą właściwości samej powierzchni. W naszych programach będziemy podawać
ddsCaps.dwCaps = DDSCAPS_COMPLEX | DDSCAPS_FLIP | DDSCAPS_PRIMARYSURFACE |DDSCAPS_VIDEOMEMORY;
DDSCAPS_COMPLEX określa, że wraz z powierzchnią podstawową tworzone są tylne bufory. Flaga ta dodatkowo powoduje, że wszystkie powierzchnie mogą być usunięte tylko poprzez usuniecie powierzchni podstawowej. DDSCAPS_FLIP oznacza, że powierzchnia będzie przełączana z tylnymi buforami. DDSCAPS_PRIMARSURFACE określa, że tworzona
powierzchnia jest podstawową. DDSCAPS_VIDEOMEMORY flaga ta wymusza przydzielenie pamięci na powierzchnie w przestrzeni karty graficznej. Zmienna lplpDDSurface powinna zawierać adres wskaźnika na interfejs IDirectDrawSurface. Ostatni parametr powinien
mieć wartość NULL.
Po utworzeniu podstawowej powierzchni możemy uzyskać dostęp do tylnego bufora,
utworzonego wraz z nią. Można tego dokonać za pomocą funkcji
HRESULT IDirectDrawSurface::GetAttachedSurface( LPDDSCAPS lpDDSCaps,
RECTDRAWSURFACE3 FAR *lplpDDAttachedSurface );
LPDI-
Pierwszy z parametrów jest adresem struktury DDSCAPS, opisującym właściwości powierzchni, którą chcemy uzyskać. W naszym wypadku pole dwCaps tej struktury powinno
mieć wartość DDSCAPS_BACKBUFFER. Ostatni parametr powinien zawierać adres
wskaźnika na interfejs IDirectDrawSurface.
-3-
Grafika komputerowa – Szkielet aplikacji
1.5 Palety kolorów
Paletę kolorów w DirectX reprezentuje interfejs IDirectDrawPalette. Dostęp do tego
interfejsu można uzyskać za pomocą funkcji
HRESULT IDirectDraw::CreatePalette( DWORD dwFlags, LPPALETTEENTRY lpColorTable, LPDIRECTDRAWPALETTE FAR *lplpDDPalette,
IUnknown FAR *pUnkOuter);
Pierwszym parametrem jest flaga określająca typ palety, w naszym przypadku (dla trybu 8bitowego) należy podać DDPCAPS_8BIT. Kolejny parametr jest adresem do tablicy elemantów PALETTEENTRY, opisujących kolor w postaci RGB. Kolejny argument powinien zawierać adres wskaźnika na interfejs IDirectDrawPalette. Ostatni parametr powinien mieć
wartość NULL.
Utworzoną w ten sposób paletę możemy skojarzyć z wybraną powierzchnią za pomocą funkcji
HRESULT IDirectDrawSurface::SetPalette(LPDIRECTDRAWPALETTE lpDDPalette );
Jedynym argumentem tej funkcji jest adres wskaźnika na interfejs IdirectDrawPalette.
Poniżej zamieszczony jest kod funkcji inicjującej obiekt DirectDraw oraz tworzącej podstawową oraz tylną powierzchnię.
HRESULT
LPDIRECTDRAW
DDSURFACEDESC
HWND hWnd;
LPDIRECTDRAWSURFACE
LPDIRECTDRAWSURFACE
DDSCAPS
ddrval;
lpDD;
ddsd;
FrontBuffer=NULL;
BackBuffer=NULL;
ddscaps;
...
bool TForm1::InitDDraw()
{
ddrval = DirectDrawCreate( NULL, &lpDD, NULL );
if (ddrval!=DD_OK) return false;
ddrval=lpDD->SetCooperativeLevel(hWnd,DDSCL_EXCLUSIVE| DDSCL_FULLSCREEN );
if (ddrval!=DD_OK) return false;
ddrval = lpDD->SetDisplayMode( 640, 480, 16 );
if (ddrval!=DD_OK) return false;
ZeroMemory(&ddsd, sizeof(ddsd));//zawsze należy wyzerować wszystkie pola
ddsd.dwSize = sizeof(ddsd);
//ustawiamy rozmiary struktury
ddsd.dwFlags =
DDSD_CAPS |
DDSD_BACKBUFFERCOUNT;
ddsd.ddsCaps.dwCaps = DDSCAPS_COMPLEX |
DDSCAPS_FLIP |
DDSCAPS_PRIMARYSURFACE |
DDSCAPS_VIDEOMEMORY;
ddsd.dwBackBufferCount = 1;
ddrval = lpDD->CreateSurface( &ddsd, &FrontBuffer, NULL );
if (ddrval!=DD_OK) return false;
ddscaps.dwCaps = DDSCAPS_BACKBUFFER;
-4-
Grafika komputerowa – Szkielet aplikacji
ddrval = FrontBuffer->GetAttachedSurface( &ddscaps, &BackBuffer );
return true;
}
1.6 Dostęp bezpośredni do zawartości powierzchni oraz rysowanie na powierzchni
Najprostszą metodą rysowania obrazu na powierzchniach jest użycie standardowych
funkcji GDI. Zasadnicza różnica w stosunku do rysowania w zwykłym oknie Windows polega na pobieraniu uchwytu kontekstu urządzenia. W zwykłej aplikacji wystarczy pobrać
wspomniany uchwyt raz (np. po utworzeniu okna) i stosując zmienną globalną odwoływać się
do niej za każdym razem, gdy chcemy użyć funkcji GDI. W przypadku powierzchni takie
podejście jest niedopuszczalne, gdyż powierzchnia zostaje zablokowana po pobraniu uchwytu
kontekstu i żadne operacje na niej nie będą mogły być przeprowadzone do czasu zwolnienia
uchwytu. Funkcja pobierająca uchwyt jest bardzo podobna do standardowej funkcji API i ma
następującą składnie: HRESULT IDirectDrawSurface::GetDC(HDC FAR *lphDC );
Jedynym parametrem tej metody jest adres zmiennej, w której ma zostać zwrócony uchwyt.
Do zwalniania kontekstu powierzchni używa się następującej funkcji HRESULT IDirectDrawSurface::ReleaseDC(HDC hDC ); Poniżej znajduje się przykład wydrukowania napisu na powierzchni
LPDIRECTDRAWSURFACE
FrontBuffer;
HDC
hDC;
...
FrontBuffer->GetDC(&hDC);
//pobieramy
SetBkColor(hDC,0);
//ustawiamy
SetTextColor(hDC,0xffffff); //ustawiamy
TextOut(hDC, 0,0,”Tekst”,5); //drukujemy
FrontBuffer->ReleaseDC(hDC); //zwalniamy
...
uchwyt
kolor tła
kolor tekstu
napis
uchwyt
Inną i znacznie ciekawszą metodą rysowania grafiki jest bezpośrednie odwoływanie się do
obszarów pamięci powierzchni. Jednak aby móc odwołać się do jakiegokolwiek obszaru pamięci należy znać jego adres. W przypadku powierzchni adres uzyskuje się po wywołaniu
następującej funkcji Lock() o następującej składni:
HRESULT IdirectDrawSurface::Lock( LPRECT lpDestRect, LPDDSURFACEDESC
lpDDSurfaceDesc, DWORD dwFlags, HANDLE hEvent);
Pierwszym argumentem jest wskaźnik na strukturę RECT opisującą prostokątny obszar, który ma być zablokowany. Jeżeli podamy NULL blokowana jest cała powierzchnia. Kolejnym
parametrem jest wskaźnik na strukturę DDSURFACEDESC, w której zostaną zwrócone informacje dotyczące powierzchni (między innymi adres pamięci). Struktura ta ma szereg pól
jednak dla naszych potrzeb istotne są dwa. Pierwsze z nich to lpSurface w przypadku poprawnego wywołania funkcji pole to będzie adresem pamięci powierzchni. Drugie z pól to
lPitch i określa ono odstęp między początkami dwóch kolejnych linii wyrażony w bajtach.
Należy o tym pamiętać, gdyż często zdarza się, że w trybie graficznym 640x480x8 pojedyncza linia ekranu ma więcej niż 640 bajtów. Wynika to bezpośrednio z mechanizmów zarządzania pamięcią. Kolejny parametr określa sposób blokowania powierzchni dla naszych potrzeb należy podać wartość DDLOCK_WAIT, co oznacza, że powierzchnia nie zostanie zablokowana jeżeli jakiekolwiek operacje graficzne są na niej w danej chwili wykonywane.
-5-
Grafika komputerowa – Szkielet aplikacji
Ostatni parametr powinien zawierać wartość NULL. Po zakończeniu operacji na powierzchni
należy ją odblokować za pomocą funkcji HRESULT IDirectDrawSurface::Unlock(LPVOID
lpSurfaceData ); Jedynym parametrem tej funkcji jest adres obszaru blokowanego. Parametr ten może mieć wartość NULL, jeżeli blokowana była cała powierzchnia. Poniżej
znajduje się fragment kodu prezentujący dostęp do powierzchni i przykład postawienia punktu o współrzędnych (56,124).
DDSURFACEDESC
ddsd;
LPDIRECTDRAWSURFACE
pdds ;
...
if((pdds ->Lock(NULL, &ddsd, DDLOCK_WAIT, NULL)) != DD_OK)
return FALSE;
BYTE *screen=(BYTE*)ddsd.lpSurface;
DWORD pitch=ddsd.lPitch;
screen[124*pitch+56]=255;
pdds->Unlock(NULL);
...
1.7 Konfiguracja środowiska programowania
W celu skompilowania szkieletowego programu należy dodać do projektu bibliotekę
ddraw.lib. W tym celu należy po utworzeniu projektu (z wszystkimi niezbędnymi plikami)
wybrać z menu „View” pole „Projekt Manager”. Po wykonaniu opisanej czynności powinno
ukazać się okno takie, jak na rysunku poniżej
Rys 1. Konfiguracja bibliotek linkowanlych dla Borland Builder C++ 5.0.
-6-
Grafika komputerowa – Szkielet aplikacji
We wspomnianym oknie należy na nazwie naszego programu (np. „projekt.exe”) kliknąć
prawym przyciskiem i z menu kontekstowego wybrać opcję „Add…” Następnie należy wskazać na plik z biblioteką, którą chcemy dodać. Standartowo plik ddraw.lib mieści się w katalogu ..\Lib w podkatalogu PSDK. Po prawidłowo wykonanej konfiguracji w głównym pliku
projektu powinna pojawić się następująca linia
USELIB("..\Lib\PSDK\ ddraw.lib ");
-7-