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