Kurz DirectX (8.)
V této lekci zcela zakončíme komponentu DirectDraw a s chutí se pustíme do
další neméně zajímavé části a to DirectInput. DirectInput zajišťuje
programátorovi přímý přístup k vstupním zařízením PC. Můžou to být úplně ty
nejzákladnější zařízení jako je klávesnice a myš nebo ty pokročilejší jako
je joystick nebo volant (a samozřejmě mnoho dalších zařízení).
8.1 Rozloučení s DirectDraw
I když se s DirectDraw loučíme, tak vězte, že je toho ještě hodně, co je potřeba
se naučit. I po dnešní lekci vám třeba některé věci nebudou zcela jasné nebo vás
bude zajímat nějaký konkrétní problém, který tu můžeme spolu vyřešit. Stačí
napsat email.
8.1.1 Ztrácení povrchů
Fakt, že povrch se může ztratit jsem Vám zprvu zatajil, ale v dnešní lekci se
dozvíte, že se taková věc může stát a naučíte se, jak naložit se ztraceným
povrchem.
Slovo ztracený z ní dost divně. Vlastně si asi nedokážete představit, že
by se kousek paměti ztratil. Máte pravdu! Povrch se vlastně neztratí, jen se
dealokuje místo v paměti a vy musíte tuto paměť znovu alokovat. Ukazatel na povrch je i
po "ztracení" platný, takže můžete zavolat členskou funkci
Restore(), která realokuje paměť, ale už nenahraje do povrchu původní
bitmapu, to musíte udělat zcela sami (pokud je to třeba).
Kdy ke ztrácení vlastně dochází?
Je to například tehdy, když změníte rozlišení během chodu vašeho programu.
Například, přepnete-li se do Windows pomocí kláves Alt-Tab (nebo jinak),
ztratí se Front i Back buffer. Měli byste zastavit flipovací smyčku a
spustit ji znovu až tehdy, když se uživatel vrátí zpět do programu. Při návratu
se rozlišení přepne zpět a právě v teto chvíli (dříve než spustíte smyčku),
musíte volat funkce Restore() všech povrchů které
se ztratili.
Funkce Blt() a Flip() vracejí hodnotu
DDERR_SURFACELOST, když povrch je ztracený a
je potřeba volat metodu Restore(). Takže byste asi
měli testovat právě tuto návratovou hodnotu.
Jak již bylo řečeno, funkce Restore() pouze
realokuje paměť pro povrch, ale už nenahraje původní bitmapu. Pokud voláte někde
funkci Blt(), budete potřebovat testovat hodnotu
DDERR_SURFACELOST a případně povrch obnovit a
naplnit původní bitmapou. V případě, že voláte funkci Flip(),
tak tam stačí pouze obnovit hlavní povrchy (viz dále).
Zpráva WM_ACTIVE
Abyste odhalili, kdy aplikace ztrácí fokus a přichází o výhradní práva na
fullscreen, musíte odchytnout zprávu WM_ACTIVATE.
Tu dostane okno ve chvílí, kdy je buď aktivováno nebo deaktivováno. Zpráva s
sebou nese dva parametry (jako každá zpráva) typu WPARAM a
LPARAM, což jsou 32-bitové hodnoty (jako
DWORD). Dolních 16-bitů (low word) parametru
WPARAM nám říká, zda-li okno bylo aktivováno
nebo deaktivováno (případně jakým způsobem). Může nabývat těchto hodnot:
Hodnota |
Popis |
WA_ACTIVE |
Tato hodnota nám říká, že okno bylo aktivováno
například pomocí klávesnice nebo funkcí
SetActiveWidows(). |
WA_CLICKACTIVE |
Tato hodnota pouze upřesňuje, že okno bylo aktivováno
tlačítkem myší. |
WA_INACTIVE |
Okno bylo deaktivováno. |
Ostatní hodnoty nás nezajímají, ale jen pro
úplnost si je stejně uvedeme.
Horních 16-bitů (high word) parametru WPARAM (druhá polovina
WPARAM) nám říká, zda-li je okno minimalizované či
nikoliv. Druhý parametr LPARAM je handle
na okno, ale závisí na hodnotě WPARAM. Pokud je
dolních 16-bitů parametru WPARAM rovno
WA_INACTIVE, pak je to handle okna,
které je deaktivováno (tzn. na naše okno). Pokud je hodnota
WA_ACTIVE nebo WA_CLICKACTIVE,
pak handle patří oknu, které bylo
deaktivováno, aby naše okno mohlo být aktivováno (tj. předchozí aktivní okno).
Poznámka: Horních a dolních 16-bitů z typu
DWORD dostaneme pomocí maker
HIWORD() a LOWORD().
Příklad
Nyní již začneme upravovat náš příklad a dovedeme ho tak k úplně "dokonalosti".
Za prvé bychom měli zastavit flipovací smyčku, takže musíme odchytit zprávu
WM_ACTIVE. Máme definovanou proceduru okna
MainWndProc(), ve které to celé provedeme. Konkrétně do příkazu
switch vložíme ke zprávě WM_SETCURSOR
zprávu WM_ACTIVE a do těla větve napíšeme
následující kód:
case WM_ACTIVATE:
low = LOWORD(wParam);
// Start
flipping loop if app is activated
if(low == WA_ACTIVE || low == WA_CLICKACTIVE) {
theApp.GetControl()->Activate(TRUE);
}
// Stop
flipping loop if app is deactivated
if(low == WA_INACTIVE) {
theApp.GetControl()->Activate(FALSE);
}
break;
Jak vidíte používáme novou metodu třídy
CControl Activate(), která jako parametr
přijímá hodnotu BOOL a nastavuje atribut
m_bReady. Pro úplnost jsem do třídy doplnil i
metodu pro opačný směr, která vrací BOOL, zda-li je smyčka aktivní či nikoliv. Dále
si všimněte makra LOWORD(), které vrací dolních
16-bitů hodnoty wParam (tj. parametr
WPARAM). Jinak zápis je velmi jednoduchý: pokud je
okno aktivováno (jakýmkoliv způsobem), pak je smyčka spuštěna, v opačném případě
je smyčka zastavena.
Nyní se podíváme do funkce Present()
třídy CDisplay.
HRESULT CDisplay::Present()
{
HRESULT hr;
if( NULL == m_pddsFrontBuffer && NULL == m_pddsBackBuffer
)
return E_POINTER;
while( 1 )
{
if( m_bWindowed )
hr = m_pddsFrontBuffer->Blt(
&m_rcWindow, m_pddsBackBuffer, NULL, DDBLT_WAIT, NULL );
else
hr = m_pddsFrontBuffer->Flip(
NULL, 0 );
if( hr == DDERR_SURFACELOST )
{
m_pddsFrontBuffer->Restore();
// Funkce Restore() pro front buffer
m_pddsBackBuffer->Restore();
// Funkce Restore() pro back buffer
}
if( hr != DDERR_WASSTILLDRAWING )
return hr;
}
}
Zde nic upravovat nebudeme, ale všimněte si funkce
Restore() pro oba hlavní povrchy. Jsou volány pokud
Flip() vrací DDERR_SURFACESLOST,
jak bylo zmíněno výše.
Nyní ovšem musíme takto upravit všechny místa, kde voláme
funkci Blt(), protože ta také vrací
DDERR_SURFACELOST. V našem jednoduchém příkladě je
to jen na dvou místech a to ve třídě CSprite a při
překreslování pozadí ve třídě CControl.
CSprite
Zde musíte přidat nový atribut, protože je nutné si pamatovat jméno bitmapy
odkud se vytváří povrch spritu. Zvolil jsem typ CString,
ale může to být jednoduchý řetězec char. Tento
atribut inicializujeme při vytváření spritu ve funkci
CreateStaticSprite() a kupodivu do něj zkopírujeme cestu bitmapy, ze
které vytváří povrch. A proč potřebujeme tento řetězec? U hlavních povrchů jsme
pouze realokovali paměť, ale u těchto off-screen povrchů je navíc potřeba
obnovit původní obsah a právě k tomu bude sloužit tento řetězec. Dále musíte ve
funkci DrawSprite() testovat návratovou hodnotu
funkce Blt() na DDERR_SURFACELOST
a v tomto případě obnovit povrch spritu a nahrát do něj znovu bitmapu. Upravená
část funkce vypadá takto:
//
// Draw sprite at specified location
dwRet = m_pDisplay->ColorKeyBlt(m_ptPos.x, m_ptPos.y, m_psurSpriteSurface->GetDDrawSurface(),
&rcSrc);
if(dwRet != DD_OK) {
if(dwRet == DDERR_SURFACELOST) {
m_psurSpriteSurface->GetDDrawSurface()->Restore();
m_psurSpriteSurface->DrawBitmap((TCHAR*)(LPCSTR)m_csBitmap,
0, 0);
}
else {
TRACE("Cannot render sprite due %d\n",
dwRet);
}
}
Všimněte si nové části v podmínce s
DDERR_SURFACELOST. Za prvé voláme metodu Restore()
povrchu a za druhé voláme funkci DrawBitmap(),
která je členem třídy CSurface. Tato metoda
zkopíruje obsah bitmapy ze souboru do povrchu a je přímo určena pro tyto účely.
A zde je to vše!
CControl a pozadí
Za druhé musíme trochu upravit třídu CControl. Opět
přidejte nový atribut, řetězec, v němž bude uložena cesta k bitmapě pozadí (je
to stejný princip jako u předchozí třídy). A pak upravte funkci
UpdateFrame() tam, kde překreslujete pozadí takto:
dwRet = m_theDisplay.Blt(0, 0, m_surBackground,
rcBackground);
if(dwRet != DD_OK) {
if(dwRet == DDERR_SURFACELOST) {
m_surBackground->GetDDrawSurface()->Restore();
m_surBackground->DrawBitmap((TCHAR*)(LPCSTR)m_csBackground,
0, 0);
}
}
Opět je zde vidět stejný postup: nejdříve obnovíme povrch
funkcí Restore() a poté zkopírujeme bitmapu
vesmíru.
A to je v této části vše. Nyní si zkuste program spustit.
Poté se v průběhu přepněte do Windows (například přes Alt-Tab). Mělo
by se správně přepnout rozlišení (do původního rozlišení plochy) a aplikace má zůstat minimalizovaná
dole na liště.
Pak se přepněte zpět (například klepněte na tlačítko na liště) a program by se
měl celý obnovit a vše pokračovat dál, jako by se nechumelilo.
Příklad si můžete stáhnout v sekci Downloads jako
DirectDraw.exe.
8.1.2. Uvolňování objektů DirectDraw
V úplně poslední části o DirectDraw se budeme zabývat tím posledním, co musíte
udělat při ukončení aplikace pracující na bázi DirectDraw. Vytvořili jste přece
nějaké objekty a ty se musí zrušit. DirectDraw a celé DirectX používá
COM (Component Object Model), takže vlastně nevlastníte ukazatel přímo na
objekt DirectDraw, ale pouze ukazatel na jeho rozhraní, pomocí kterého s objektem
pracujete.
U COMu platí, že samotný objekt se zruší sám, pokud neexistují již
žádné odkazy (reference) na jeho rozhraní (interface). Tím, že vytvoříte objekt DirectDraw pomocí funkce DirectDrawCreateEx()
získáte právě odkaz na jeho rozhraní. Objekt COMu si to zapamatuje,
protože si interně počítá reference (inkrementuje nebo dekrementuje počet
referencí). Pokud toto počítadlo dosáhne hodnoty 0, objekt se sám zruší. Jde
pouze o to, uvolnit všechny rozhraní, který na ten který objekt máte. V našem
případě je to jediné rozhraní IDirectDraw7.
Rozhraní se uvolňují pomocí funkce Release(). Po
zavolání této metody se sníží počet referencí na objekt. Metoda vrací aktuální
počet referencí.
Poznámka: Všimněte si, že nevytváříme žádný objekt
CDirectDraw7 (ten za nás vytvoří COM). Také si všimněte jakým
způsobem získáváme ukazatel. Nikde žádný operátor new!
Co musíme uvolňovat?
Obecně musíme uvolnit všechny získané rozhraní. Určitě to bude objekt
DirectDraw. Pokud vytváříte paletu nebo clipper, pak oba tyto
objekty musíte uvolnit (my používáme třídu CDisplay,
která vše dělá za nás, stačí se podívat do funkce
DestroyObject())
(této funkci si ještě všimněte navrácení cooperative
levelu na hodnotu normal). Nutné je ovšem uvolňovat i odkazy na
veškeré povrchy! Opět pokud používáte objekt CSurface,
nemusíte se o nic starat, protože v destruktoru najdete uvolnění vnitřního
povrchu.
Poznámka: U složitějších objektů, kde je potřeba uvolňovat nějaké objekty
i při chybě programu a násilnému ukončení, je dobré si vytvořit funkci
Clean(), která se bude volat nejen z destruktoru,
ale právě i při násilném ukončení. V této funkci pak uvolňujte nejen rozhraní
COM, ale i dynamicky vytvořené objekty, vlákna apod.
8.2. Úvod do DirectInput
Tím, že jste se rozloučili s DirectDraw zdaleka neznamená, že jste se rozloučili
s DirectX. DirectDraw v dnešním DirectX vlastně už ani neexistuje a je plně
nahrazeno Direct3D. Další zajímavou a důležitou částí DirectX je DirectInput.
Již v úvodu jsem nastínil, čím se zabývá. Seznamování nám půjde rychleji,
protože princip je podobný jako u DirectDraw.
Pokud budete programovat nějakou multimediální aplikaci (například hru), můžete
použit buď standardní zprávy Windows WM_KEYDOWN, WM_KEYUP,
WM_RBUTTONDOWN, WM_RBUTTONUP atd. nebo můžete použít komponentu
DirectInput. Můžete si zkusit chytat klávesovou zprávu a třeba pohybovat spritem
podle šipek. Zjistíte ale následující problém. Systém neposílá zprávy
WM_KEYDOWN úplně pravidelně (to proto, že těch
zpráv posílá víc a vaše zpráva si holt musí někdy počkat). Tato nepravidelnost
se projevuje trhaným pohybem spritu. Můžeme si pomoci tak, že pokud okno zachytí
WM_KEYDOWN nastaví nějaký flag a "nepustí ho",
dokud nepřijde opačná zpráva WM_KEYUP. Zdá se, že
problém s trhavostí je vyřešen, ale nyní si zkuste stisknout pět kláves
najednou. Musíte mít pět flagů pro každé tlačítko (rovnou si vytvořte pole pro
102 kláves). Sami jistě uznáte, že pokud stisknete více tlačítek, tak začne být
ve zprávách pěkný guláš.
Pro takové případy tu máme DirectInput. I když se bez polí neobejdete (budou
dokonce dvě), přesto tím, že DirectInput není založen na zprávách Window, se
nemůže stát, že by nějaké tlačítko bylo ignorováno, kvůli jinému, které bylo
stisknuto o 5 ms dřív.
Ještě větší výhody najdete u používání myši. Zkuste používat myš v DirectDraw
bez použití DirectInput a budete velice nešťastní. Na každém počítači dělá myš
něco jiného. Na prvním nemusí být vidět vůbec, na druhém zase bude blikat a na
třetím bude vidět, ale zase bude potrhávat. I tento problém řeší DirectInput
beze zbytku. Můžete si udělat animované kurzory, libovolně veliké kurzory a vše
bude chodit velmi pěkně (o tlačítkách ani nemluvě).
Navíc DirectInput můžete používat i u naprosto běžné "oknové aplikace". Pro
zajímavost vám přiložím příklad, který využívá DirectInput a přitom se jedná o
okno. Tento příklad také najdete u SDK 8.0 (8.1). Stáhnout si ho můžete v
sekci Downloads jako Scrawl.
Poznámka: Všimněte si, že uvádím verzi 8.1do závorek za 8.0. To proto, že
my budeme využívat rozhraní 8, ale máte nainstalované SDK verze 8.1. Rozdíly
mezi 8.0 a 8.1 jsou v DirectInput minimální (přístup k nim samozřejmě máte).
Seznam novinek naleznete v nápovědě. Navíc někdo z vás může mít nainstalované
SDK 8.0 a v tomto případě mu to nebude vadit.
8.2.1. Objekt DirectInput
Základem všeho je objekt DirectInput (že už jste to někde slyšeli?). Tento
objekt se vytvoří velice podobně jako objet DirectDraw. Jistě jste si všimli, že
rozhraní v DirectDraw měly verzi 7 (IDirectDraw7, IDirectDrawSurface7).
To souvisí s tím, co jsem psal na začátku. Verze DX 8.0 již nemá DirectDraw
samostatně a tudíž nevznikly ani nové rozhraní (rozhraní IDirectDraw8
neexistuje, i když to vypadá pěkně). DirectInput ovšem verzi 8.0 má,
takže konečně budeme plně vyžívat nainstalované SDK 8.0 (8.1), konkrétně rozhraní
objektu DirectInput IDirectInput8.
8.2.2. Objekty zařízení
V DirectInput pracujeme s tzv. zařízeními (devices), které představují třeba myš
nebo klávesnici. K objektu zařízení budeme přistupovat pomocí rozhraní IDirectInputDevice8. Předpokládejme, že budeme
chtít ovládat klávesnici a myš. To jsou dvě zařízení, pro každé bude vytvořen
zvlášť objekt typu zařízení. Už jistě přemýšlíte, jak bude vypadat nová třída
CInput.
O konkrétním řešení si však povíme až v příští lekci.
Těším se příště nashledanou.
Jiří Formánek
© 2001 Vogel Publishing,
design by ET NETERA
|