V této lekci nakousneme poněkud rozsáhlejší příklad využívající znalosti DirectDraw a DirectInput, které máte. V minulé lekci jsme dokončili třídu CInput, kterou i v tomto příkladu využijeme. Naopak zcela předěláme systém DirectDraw tak, aby skutečně něco uměl a nejen kreslil primitivní obrázky po obrazovce. Dále budeme využívat některé další knihovny, abychom nemuseli všechno psát znovu.
Protože struktura programu bude radikálně odlišná od předchozích jednoduchých struktur, bude asi nejlepší vytvořit zcela nový projekt. Vytvořte tedy Blank Workspace, do kterého postupně budeme přidávat tyto knihovny:
Input.dll
Display.dll
Engine.dll
Common.dll
Zip.dll
a samozřejmě nesmíme zapomenout na program:
Game.exe
Knihovny Zip.dll a Common.dll psát nebudeme. Tyto dvě knihovny Vám dám k dispozici a budeme z nich využívat jen některé funkce.
Na následujícím obrázku vidíte závislosti jednotlivých knihoven:
Zřejmě na vrcholu bude program, který využije všechny knihovny pod sebou. Šipka vyjadřuje směr vkládání, to znamená, že třeba knihovna Engine.dll je vkládá do Game.exe a to znamená, že Game.exe je závislý na Engine.dll. Další zajímavost, která Vás možná překvapí je, že Input.dll je závislá na Display.dll. Minule jsem se zmiňoval o tom, že kurzor budeme později vykreslovat pomocí DirectDraw, z toho je zřejmé, že Input.dll musí být závislá na Display.dll.
Projekt začneme stavět od spodu. Za prvé si řekneme, jak napojíme knihovny Common.dll a Zip.dll do našeho projektu. Budete totiž mít k dispozici pouze tyto soubory:
common.h
common.lib
common.dll
Soubor .lib obsahuje reference na exportované funkce z .dll a tento soubor využijeme v projektu. Jistě si pamatujete, jak jsme vkládali soubory dinput8.lib nebo ddraw.lib. Stejným způsobem vložíme common.lib. V hlavičkovém souboru pak naleznete deklarace exportovaných funkcí, abyste je vůbec mohli použít ve vašem kódu. O knihovnu Zip.dll se vlastně nemusíme vůbec starat, protože tu interně využívá common.dll a z ostatních knihoven nebudeme využívat žádnou její funkci.
Tato knihovna obsahuje několik funkcí, které nám trochu usnadní život. Funkce jsou rozděleny následovně:
diagnostické funkce
funkce pro práci s datovým souborem
funkce pro práci s konfiguračním souborem setup.ini
obecnější funkce pro práci z adresáři a soubory
Deklarace těchto funkcí naleznete v souboru common.h. Budeme zejména používat makra DXTRACE a DXERR, které fungují podobně jako makro TRACE s tím rozdílem, že zapisují řetězec do souboru log.txt a navíc makro DXERR dokáže rozluštit chybový kód chyby. Dále použijeme funkce stgGetFile3() k nahrávání bitmap z datového souboru, který otevřeme funkcí stgOpenDataFile3(). Ostatní funkce použijeme příležitostně, jak se nám to bude hodit.
Nyní tedy udělejme první krok a vytvořme Blank WorkSpace. Vývojové prostředí zatím vytvoří adresář se souborem .dsw. Do tohoto adresáře (nazvěme ho například Game), vytvořte podadresář Common, do kterého nakopírujte soubory common.h a strings.h. Dále vytvořte adresáře Debug a Release a do obou nakopírujte soubory common.lib, common.dll a zip.dll. Pokud budete chtít použít knihovnu common.dll, musíte vložit hlavičkový soubor do vašeho projektu příkazem include:
#include "..\Common\Common.h"
a pak v menu Project-Settings nastavit něco takového:
Tím jste překladači a linkeru řekli, že chcete přilinkovat knihovnu Common.dll do vašeho projektu. Rovněž si všimněte, že výstupní soubor (v tomto případě Testing.exe) je vytvářen v adresáři Release celého projektu (tedy v tom adresáři, který jsme před chvilkou vytvořili), jinak by program knihovnu nenašel.
Vložme nyní do projektu projekt aplikace, na kterém budeme testovat funkce z ostatních knihoven. Z této aplikace budeme postupně tvořit program Game.exe, o kterém byla řeč výše. Tentokrát tuto aplikaci vytvoříme jinak. Neuděláme čistý projekt MFC, ale předěláme Win32 API projekt na projekt Win32 API s podporou MFC. Takže budeme moci využívat některé výhody MFC a zároveň se nemusíme držet tříd CMainFrame apod. Projekt se také snáze upravuje.
Vytvořte tedy projekt Win32 Application, nezapomeňte zaškrtnout volbu Add to current project. V AppWizardovi zvolte druhou volbu A simple Win32 Application. Nyní v Settings projektu nastavte na první kartě, že chcete používat MFC ve sdílených DLL (rovnou v tomto dialogu nastavte, že se soubor Game.exe má vytvářet v adresáři Debug resp. Release celého projektu). Nakonec stačí do souboru stdafx.h připsat následující dva řádky:
#include <afxwin.h> // MFC core and
standard components
#include <afxext.h> // MFC extensions
Vymazali jsme řádek: #include <windows.h>
Nyní přidáme pár funkcí, které jsou nutné pro běh Win32 aplikace. Jsou to WinInit() a MainWndProc(). První z těchto funkcí vlastně není ani nutná, protože zde vytváříme okno aplikace a tento kód můžeme napsat rovnou do připravené funkce WinMain(). Naopak MainWndProc() potřeba bude určitě. Jedná se o tzv. proceduru okna, která je volána pokud oknu přijde zpráva od systému. Dále vytvořme globální handle na naše okno. Tento handle budeme právě inicializovat ve funkci WinMain(). Kdo zná programování s API, může následující řádky prakticky přeskočit, důležité jsou pouze atributy okna.
Vytvořme tělo funkce WinInit():
HRESULT WinInit(HINSTANCE hInstance)
{
DWORD dwRet;
WNDCLASSEX wc;
//
// Register the Window Class
wc.cbSize =
sizeof(WNDCLASSEX);
wc.lpszClassName = "GameWin";
wc.lpfnWndProc = MainWndProc;
wc.style = CS_VREDRAW|CS_HREDRAW;
wc.hInstance = hInstance;
wc.hIcon =
LoadIcon(hInstance,"IDI_PROGRAM_ICON");
wc.hCursor = NULL;
wc.hbrBackground = (HBRUSH) (COLOR_WINDOW
+ 1);
wc.lpszMenuName = NULL;
wc.hIconSm = LoadIcon(hInstance, "IDI_PROGRAM_ICON");
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
//
// Register window class
if(RegisterClassEx(&wc)
== 0 ) {
dwRet =
GetLastError();
DXERR("Cannot
register the window class due", dwRet);
return
dwRet;
}
//
// Create new main window
g_hWnd = CreateWindow("GameWin",
"Game",
WS_OVERLAPPEDWINDOW, 0, 0, 0,
0, NULL, NULL, hInstance, NULL);
if(!g_hWnd)
{
dwRet =
GetLastError();
DXERR("Cannot
create the window due", dwRet);
return
dwRet;
}
ShowWindow(g_hWnd, SW_SHOW);
UpdateWindow(g_hWnd);
dwRet = ERROR_SUCCESS;
return
dwRet;
}
Nyní si vysvětlíme, jak funkce pracuje. V prvním kroku musíme zaregistrovat tzv. třídu okna. Naplníme strukturu WNDCLASSEX a poté zavoláme funkci RegisterClassEx(). Pak teprve můžeme vytvořit okno jistých vlastností tak, jak jsme ho zaregistrovali. Do struktury WNDCLASSEX postupně plníme velikost struktury v bytech, jméno třídy, které pak využijeme při tvorbě okna, ukazatel na proceduru okna (v našem případě MainWndProc), dále styl, instanci aplikace, handle ikony, handle kurzoru, handle štětce pozadí, jméno zdroje menu, handle malé ikony. Aby se nahrála správná ikonka okna, musíte mít vložený příslušný zdroj do projektu. Ikona ale není tak důležitá, protože stejné budeme používat fullscreen. V dalším kroku vytvoříme a zobrazíme okno. Funkce CreateWindow() má několik parametrů: první je řetězec třídy okna, kterou jsme před chvilkou zaregistrovali, další je titulek okna, pak styl, pozice, šířka a výška, handle okna rodiče, handle menu, handle na instanci aplikace a poslední parametr představuje dodatečné informace předané oknu. Funkce ShowWindow() a UpdateWindow() okno zobrazí a to je celé.
Funkce MainWndProc() je deklarována následovně:
LRESULT CALLBACK MainWndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
Jedná se o tzv. callback funkci. Předáte její adresu prostřednictvím registrace třídy okna systému a ten pak volá tuto funkci jako reakci na zprávy poslané tomuto oknu. Tělo vypadá zatím takto jednoduše:
LRESULT CALLBACK
MainWndProc( HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam )
{
return DefWindowProc(hWnd, msg, wParam, lParam);
}
Postupně ale budeme přidávat vlastní obsluhy některých zpráv. Funkce DefWindowProc() pošle zprávu zpět systému a ten ji zpracuje standardně.
Funkci WinInit() budeme volat hned na prvním místě z funkce WinMain():
HRESULT dwRet;
dwRet = WinInit(hInstance);
if(dwRet != ERROR_SUCCESS) {
DXTRACE("Nemohu vytvorit okno aplikace.");
return dwRet;
}
Všimněte si použití maker DXTRACE a DXERR. DXERR má dva povinné parametry, z nichž druhý je kód chyby, kterou se funkce diagTrace() pokusí rozluštit. Naopak DXTRACE má povinný parametr řetězce a pak libovolný počet dalších parametrů například jako funkce printf(). Pokud máte v souboru setup.ini nastavenu hodnotu TraceToFile na 1, budou se hlášky rovněž zapisovat i do souboru log.txt.
Teď , když aplikaci spustíte, vytvoří se okno a aplikace skončí. Je to tím, že jsme nespustili smyčku zpráv. Do funkce WinMain() dopište následující kód:
int APIENTRY WinMain(HINSTANCE
hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
HRESULT dwRet;
MSG msg;
dwRet = WinInit(hInstance);
if(dwRet != ERROR_SUCCESS) {
DXTRACE("Nemohu vytvorit okno
aplikace.");
return dwRet;
}
// Run message
loop
while( TRUE )
{
// Look for messages, if none are found
then
// update the state and display it
if( PeekMessage( &msg, NULL, 0, 0, PM_NOREMOVE
) )
{
if( 0 == GetMessage(&msg,
NULL, 0, 0 ) )
{
// WM_QUIT was posted, so exit
return
(int)msg.wParam;
}
// Translate and dispatch the message
TranslateMessage( &msg );
DispatchMessage( &msg );
}
else
{
UpdateFrame();
}
}
return 0;
}
Připsaná část pracuje stejně jako funkce Run() v našem předchozím projektu, vlastně je prakticky stejná. Všimněte se další globální funkce UpdateFrame(), tuto funkci musíte deklarovat na začátku souboru game.cpp a definovat někde dole. I u předchozích funkcí nezapomínejte na funkční prototypy.
Nyní již aplikace neskončí a okno zůstane zobrazené v rohu. Problém je ten, že po zavření okna zůstane aplikace viset ve smyčce zpráv, kterou nikdo neukončil. Ta se ukončí zprávou WM_QUIT a tu posílá funkce PostQuitMessage(). Upravte tedy obslužnou funkci následovně:
LRESULT CALLBACK
MainWndProc( HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam )
{
switch(msg) {
case WM_DESTROY:
// Cleanup and close the app
PostQuitMessage( 0 );
return 0L;
}
return
DefWindowProc(hWnd, msg, wParam, lParam);
}
Příkazem switch rozdělíme obsluhu jednotlivých zpráv na jednotlivé větve. Při zavření okna, dostane okno zprávu WM_DESTROY. Reakcí na tuto zprávu bude celé ukončení aplikace. Velice důležitý je příkaz return 0L, protože zabrání v provedení funkce DefWindowProc() - zprávu jsme přece obsloužili a obsluha systému tedy není potřeba. Nyní už bude aplikace fungovat očekávaným způsobem i při ukončení.
Funkce UpdateFrame() bude mít naprosto stejnou úlohu jako v předcházejícím projektu. Příště až vytvoříme knihovnu Display.dll, připíšeme do této funkce volní UpdateBackground() a Present(). Z toho tedy vyplývá, co budeme dělat příště. Pokusíme se postavit kompletně knihovnu Display.dll a potom k ní napojíme knihovnu Input.dll s upravenou třídou CInput tak, aby používala DirectDraw na vykreslování kurzoru.
Těším se příště nashledanou.