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-