DirectX (9.)


V této lekci se seznámíme se třídou CInput, která zapouzdřuje práci s objekty DirectInput. Postupně tuto třídu rozvineme do rozsáhlé struktury a vyzkoušíme si práci s knihovnou DLL, protože celý modul DirectInput oddělíme do samostatné knihovny. Něco málo o knihovnách jsem zmínil minule a dnes to rozšíříme. Výsledkem tak bude samostatná knihovna DLL zapouzdřující funkčnost DirectInput.

9.1. Vytvoření nového projektu

Asi nejlepší bude, když nový projekt knihovny vložíte do již rozdělaného projektu DirectDraw. To proto, abychom mohli všechno hned testovat. Jak se to udělá jsem zmínil v minulé lekci o VC++, ale pro ty, kteří článek nečetli nebo se k nim nedostal Chip, to zopakuji.

Za prvé vytvoříme prázdný projekt Blank Workspace třeba se jménem DirectX. Z menu File vyberte položku New. Po zobrazený dialog vyplňte nějak takto:

V místě, které jste zadali v políčku Location se vytvoří adresář. Do tohoto adresáře kompletně zkopírujte projekt z předchozích lekcí o DirectDraw. Důležité je, aby DirectDraw zůstalo v samostatném podadresáři pod DirectX. Po té z vývojového prostředí přidáme tento projekt jako již existující. Z menu Project z volte poslední položku Insert Project into Workspace a v dialogu vyberte soubor DirectDraw.dsp v podadresáři DirectDraw. Všimněte si změny v ClassView.

Novou knihovnu Input nemusíte nutně spojovat s projektem DirectDraw, ale můžete si vytvořit zcela jinou aplikaci. Knihovna Input.dll bude nezávislá (alespoň prozatím). Nyní tedy vložme knihovnu:

Z menu File vyberte položku New. Zobrazený dialog vyplňte nějak takto:

Zde je důležité abyste projekt přidali do již otevřené Workspace. Projekt knihovny se opět vytvořil v samostatném adresáři Input. Dále nastavte rozšířenou knihovnu DLL, aby se nám lépe pracovalo s exporty a případně se zdroji:

Nyní si všimněte, že do okna ClassView přibyla položka Input. Dále trochu upravíme vlastnosti obou projektů, aby se například kompilovaly ve správném pořadí a do stejného adresáře Release a Debug celé Workspace.

Z menu Project vyberte položku Settings. Nastavte na kartě Link společný adresář pro výstupní soubory (EXE a DLL). Toto nastavení musíte udělat zvlášť pro oba projekty a navíc pro verze Release a Debug (čili 4x).

Dále nastavíme závislosti. Musíme zaručit že projekt Input se bude kompilovat před DirectDraw a že Input bude přilinkován k DirectDraw. I to jsme si ukazovali minule. Z menu Project vyberte položku Dependencies a dialog nastavte takto:

Vidíte závislost projektu DirectDraw na projektu Input.

To je k nastavení projektů zatím vše. Za chvilku ještě přidáme některé dynamické knihovny potřebné pro samotné DirectInput jako tomu bylo u DirectDraw. Před kompilací se ujistěte, že máte DirectDraw projekt nastavený jako výchozí (nebo aktivní). Zkuste si projekt zkompilovat a uvidíte, že nejprve se sestaví projekt Input a teprve pak DirectDraw.

9.2. Třída CInput

Konečně se dostáváme k programování. Jak už bylo řečeno, jádrem knihovny bude třída CInput. Tuto třídu lze exportovat dvěma způsoby. Za prvé můžete exportovat celou třídu. Klient, který knihovnu chce použít pak musí vytvořit objekt typu CInput a teprve pak volat jednotlivé metody. Druhý způsob je podle mě z uživatelského hlediska snazší, ale z vaší strany vyžaduje pár řádků navíc. V samotné knihovně vytvoříme globální objekt CInput a poté budeme exportovat pouze globální funkce, které uvnitř pouze volají metody globálního objektu. Tento způsob také zaručí, že uživatel nevytvoří více objektů CInput, což je nežádoucí.

Přidat třídu do projektu již umíme. Z menu Insert zvolte položku New class.

TIP: Abyste přidali třídu do projektu Input musí být tento projekt aktivní. Abyste nemuseli přepínat mezi projekty, stačí využít kontextové menu v ClassView. Kliknete-li pravým tlačítkem na projekt Input, můžete přímo vybrat volbu New class.

Následující dialog vyplňte přibližně takto:

Právě v tuto chvíli budeme potřebovat knihovny DirectInput. Jsou dvě: dinput8.dll a dxguid.dll. V menu Project volte položku Settings. Opět na kartě Link přidejte do políčka Objects/library modules jména knihoven importů těchto dvou knihoven: dinput8.lib a dxguid.lib. Nastavení proveďte i pro Release. Dále je potřeba vložit hlavičkový soubor dinput.h nejlépe do souboru stdafx.h projektu Input (nejlépe se k němu dostanete pře FileView)  Nyní máme zajištěno, že pokud použijeme některé objekty a funkce DirectInput, bude je kompilátor i linker znát.

Následující tabulky popisují třídu CInput:
 
Členské proměnné
Typ proměnné Název proměnné Popis
BOOL m_bInit TRUE pokud je systém inicializovaný jinak FALSE. To jest ochrana proti vícenásobném volání inicializační metody a proti neoprávněnému volání ostatních metod.
LPDIRECTINPUT8 m_lpDI Ukazatel na objekt DirectInput. Tento objekt vytvoříme v inicializační metodě.
LPDIRECTINPUTDEVICE8 m_lpDIDKeyboard Ukazatel na objekt klávesnice. Používá se ke stažení informací o aktuálním stavu klávesnice (naplní pole jednotlivých kláves, ze kterého je pak možno určit, která klávesa je stisknuta a která nikoliv).
LPDIRECTINPUTDEVICE8 m_lpDIDMouse Ukazatel na objekt myši. Používá se ke stažení informací o aktuálním stavu myši (poloze a tlačítkách).
BYTE m_arKeyboard[256] V tomto poli se právě ukládá stav jednotlivých kláves.
DIMOUSESTATE m_MouseState Struktura obsahující aktuální stav myši (informace o tlačítkách, pohybu apod.)
BOOL m_IsHandled[256] Do tohoto pole se ukládá informace o tom, zda-li je klávesa stisknuta poprvé, či jestli je držena.
BOOL m_bIsMouseHandled[2] Obdobné pole jako předchozí, pouze pro dvě tlačítka myši.
CPoint m_ptCursor Aktuální souřadnice kurzoru.
CSize m_szRes Hodnoty x a y popisují aktuální rozlišení. Tuto hodnotu budeme inicializovat v inicializační metodě a slouží nám k tomu, abychom kurzor udrželi ve správných mezích.
HICON m_hCursor Handle na aktuální kurzor.
int m_iMouseSensitivity Sensitivita myšky čili citlivost.
BOOL m_bShowCursor Informace o tom, zda-li má být kurzor vidět (TRUE) či nikoliv (FALSE).

 

Členské funkce - metody

Veřejné funkce - funkce které budou exportovány z knihovny
Návratová hodnota Název funkce a parametrů Popis metody i parametrů
HRESULT CreateDirectInputSystem(HINSTANCE hInst, HWND hWnd, CSize csResolution) Inicializuje objekt DirectInput, objekt klávesnice a objekt myši. Nastavuje chování aplikace vůči svému okolí a ukládá si rozlišení.
HRESULT RestoreDevices() Pokud si pamatujete na obnovování ztracených povrchů, pak tato metoda dělá něco podobného, ale pro myš a klávesnici.
BOOL IsRButtonDown(BOOL _Handling = TRUE) Vrací TRUE pokud je stisknuté pravé tlačítko myši.
BOOL IsLButtonDown(BOOL _Handling = TRUE) Vrací TRUE pokud je stisknuté levé tlačítko myši.
BOOL IsRButtonUp() Vrací TRUE pokud není stisknuté pravé tlačítko myši.
BOOL IsLButtonUp() Vrací TRUE pokud není stisknuté levé tlačítko myši.
BOOL IsKeyDown(int Key, BOOL bHandle) Vrací TRUE pokud je stisknutá požadovaná klávesa (Key).
void ProcessInput() Stahuje data z klávesnice a myši. Posouvá kurzor myšky.
void UpdateCursor(HDC hdc) Vykresluje kurzor na obrazovku. Požaduje HDC (handle kontext zařízení) okna nebo povrchu DirectDraw.
CPoint GetCursor() Vrací aktuální pozici kurzoru.
void ShowCursor(BOOL bShow) Pokud je parametr bShow, kurzor se zviditelní jinak zůstane skrytý.
HRESULT LoadCursor(HICON hCursor) Nastavuje aktuální zobrazovaný kurzor.
Soukromé funkce
Návratová hodnota Název funkce a parametrů Popis metody i parametrů
void Clean() Provádí čistící práce při ukončení aplikace nebo při násilném ukončení v důsledku chyby.
HRESULT MoveCursor(int dx, int dy) Pouze přičítá přírůstky x-ové a y-ové souřadnice kurzoru k aktuální pozici kurzoru. Přírůstky jsou vytaženy ze stavové struktury myši.

Ve třídě CDisplay byla funkce CreateFullscreenDisplay(). My si podobnou funkci vytvoříme i naší třídě. Třída bude mít následující deklaraci:

HRESULT CreateDirectInputSystem(HINSTANCE hInst, HWND hWnd, CSize csResolution);

Předáváme ji tři parametry, které později využijeme při vytváření objektu DirectInput:
 
HINSTANCE hInst handle instance aplikace
HWND hWnd handle okna aplikace
CSize csResolution struktura naplněná informací o rozlišení

Přidejte tuto funkci do třídy. Máme dva způsoby, jak přidat novou metodu. Buď opět využijeme kontextového menu, ale nyní přímo na třídě, nebo to uděláme ručně. Minule jsem vám taky poradil, abyste si vytvářeli funkce Clean() u složitějších tříd. Tuto funkci s výhodou využijeme i v naší nové třídě.

Nyní doplníme kód funkce CreateDirectInputSystem():

HRESULT CInput::CreateDirectInputSystem(HINSTANCE hInst, HWND hWnd, CSize csResolution)
{
   HRESULT dwRet = 1;
   //
   // You cannot call this function twice
   if(!m_bInit) {
      //
      // Creation of direct input object
     dwRet = DirectInput8Create(hInst, DIRECTINPUT_VERSION, IID_IDirectInput8, (VOID**)&m_lpDI, NULL);
     if(dwRet != DI_OK) {
        TRACE("Cannot create direct input object %d\n", dwRet);
        Clean();
        return dwRet;
     }
     //
     // Create keyboard object
     dwRet = m_lpDI->CreateDevice(GUID_SysKeyboard, &m_lpDIDKeyboard, NULL);
     if(dwRet != DI_OK) {
         TRACE("Cannot create keyboard object %d\n", dwRet);
         Clean();
         return dwRet;
     }
     //
     // Create mouse object
     dwRet = m_lpDI->CreateDevice(GUID_SysMouse, &m_lpDIDMouse, NULL);
     if(dwRet != DI_OK) {
         TRACE("Cannot create mouse object %d\n", dwRet);
         Clean();
         return dwRet;
     }
     //
     // Set data format for both devices
     dwRet = m_lpDIDMouse->SetDataFormat(&c_dfDIMouse);
     if(dwRet != DI_OK) {
        TRACE("Cannot set data format for mouse %d\n", dwRet);
        Clean();
        return dwRet;
     }
     dwRet = m_lpDIDKeyboard->SetDataFormat(&c_dfDIKeyboard);
     if(dwRet != DI_OK) {
         TRACE("Cannot set data format for keyboard %d\n", dwRet);
         Clean();
         return dwRet;
     }
     //
     // Set cooperative level for both devices
     dwRet = m_lpDIDKeyboard->SetCooperativeLevel(hWnd, DISCL_FOREGROUND|DISCL_NONEXCLUSIVE);
     if(dwRet != DI_OK) {
         TRACE("Cannot set cooperative level for keyboard %d\n", dwRet);
         Clean();
         return dwRet;
     }
     dwRet = m_lpDIDMouse->SetCooperativeLevel(hWnd, DISCL_NONEXCLUSIVE|DISCL_FOREGROUND);
     if(dwRet != DI_OK) {
         DXTRACE6("Cannot set cooperative level for mouse %d\n", dwRet);
         Clean();
         return dwRet;
     }
     //
     // Acquire mouse and keyboard
     dwRet = m_lpDIDKeyboard->Acquire();
     if(dwRet != DI_OK) {
         DXTRACE6("Cannot acquire keyboard %d\n", dwRet);
         Clean();
         return dwRet;
     }
     dwRet = m_lpDIDMouse->Acquire();
     if(dwRet != DI_OK) {
         TRACE("Cannot acquire mouse %d\n", dwRet);
         Clean();
         return dwRet;
     }
     // Inicializace nekterych vnitrnich atributu
     m_csResolution = csResolution;
     m_bInit = TRUE;
   }
   return dwRet;
}

Předchozí kód možná vypadá poněkud složitě, ale ve skutečnosti je to všechno lehké. První podmínka je pouze z důvodu ochrany před vícenásobným volání metody. Zkrátka pokud se uživatel pokusí víckrát volat tuto i další metody, nic se nestane a je vrácena hodnota 1. Všechno to zajišťuje proměnná typu BOOL m_bInit. V prvním kroku vytváříme objekt DirectInput. K tomu slouží funkce DirectInput8Create() s pěti parametry:
 
HINSTANCE hinst Handle na instanci aplikace. Právě u tohoto parametru využijeme vstupní parametr funkce CreateDirectInputSystem()
DWORD dwVersion Požadovaná verze vytvářeného objektu. U tohoto parametru využijeme symbolické konstanty DIRECTINPUT_VERSION. Pokud bychom chtěli jinou verzi než 8.0, museli bychom tuto konstantu sami definovat ještě před vložením hlavičkového souboru input.h. Napsali bychom třeba #define DIRECTINPUT_VERSION 0x0700 pro verzi 7.0 apod. Pokud chceme použít verzi 8.0 (což chceme), je dobré explicitně konstantu definovat také, protože jinak ses nám bude ve výstupním okně objevovat varovná hláška.
 REFIID riidltf Unikátní identifikátor rozhraní, které po funkci chceme. Chceme rozhraní objektu DirectInput, takže posíláme hodnotu IID_IDirectInput8.
LPVOID* ppvOut Proměnná, do které se uloží adresa požadovaného rozhraní. Je to stejné jako u DirectDraw, posíláme ukazatel na ukazatel.
LPUNKNOWN punkOuter Tento parametr se využívá, pouze pokud chceme použít agregaci COM. My pošleme hodnotu NULL.

Funkce vrací hodnotu DI_OK (0) pokud je vše v pořádku. Všimněte si logiky odhalování chyb. Pokud by se něco pokazilo, ihned je volána metoda Clean(), která uvolní rozhraní a funkce je ukončena s tím, že nám zapíše kód chyby do výstupního okna. V DirectX existuje pár funkcí a maker, které jsou schopny odhalit o jakou chybu se jedná a vrací řetězec čitelný pro člověka.

V dalších krocích provádíme tytéž kroky pro myš a klávesnici. Pomocí funkce CreateDevice() vytvoříme objekt myši a objekt klávesnice. My si pouze ukládáme ukazatel na rozhraní těchto objektů a pokud je toto rozhraní uvolněno funkcí Release(), je i objekt myši nebo klávesnice uvolněn. Všimněte si také, že metoda CreateDevice() je členská funkce rozhraní objektu DirectInput.
CreateDevice() má tři parametry:
 
REFGUID rguid Unikátní identifikátor zařízení, pro které chceme vytvořit objekt. Pokud chceme pouze myš nebo klávesnici můžeme použít předdefinované hodnoty GUID_SysMouse nebo GUID_SysKeyboard. Pokud ovšem chcete jiná zařízení, musíte použít funkci EnumDevices(), abyste zjistili, co je právě připojeno k PC.
LPDIRECTINPUTDEVICE *lplpDirectInputDevice Proměnná, do které se uloží ukazatel našeho rozhraní. Opět je to stejné jako všude jinde: předáváme ukazatel na ukazatel.
LPUNKNOWN pUnkOuter Poslední parametr je stejný jako poslední parametr předchozí funkce. Pošleme NULL.

Nyní máme zinicializované všechny potřebná rozhraní a zbývá pár funkcí nutných pro správný chod. Především je to funkce SetDataFormat(), SetCooperativeLevel() a Acquire(). Všechny tyto metody voláme pro každé zařízení zvlášť.

Funkce SetDataFormat() pouze nastavuje, v jakém formátu budeme stahovat data z klávesnice nebo myšky. Funkce má jeden parametr, který představuje buď předdefinovaný nebo uživatelský formát stáhnutých dat. Předdefinované formáty jsou následující (v pravém sloupci jsou příslušné struktury, které budeme používat):
 
c_dfDIMouse DIMOUSESTATE
c_dfDIMouse2 DIMOUSESTATE2
c_dfDIKeyboard array of 256 bytes
c_dfDIJoystick DIJOYSTATE
c_dfDIJoystick2 DIJOYSTATE2

Například rozdíl mezi DIMOUSESTATE a DIMOUSESTATE2 je takový, že struktura DIMOUSESTATE2 obsahuje informace o osmi tlačítkách myši, zatímco struktura DIMOUSESTATE jen o čtyřech. Všimněte si také, že pro klávesnici máme již zmiňované pole bytů. Více se o těchto datových strukturách dovíte, až je budeme přímo používat.

Funkce SetCooperativeLeve() nastavuje chování našeho zařízení (objektu zařízení) vůči ostatním objektům téhož zařízení a vůči okolním aplikacím. Je to podobné jako u DirectDraw. Funkce má dokonce velmi podobné parametry:
 
HWND hWnd Handle na okno aplikace, to jest druhý parametr naší funkce  CreateDirectInputSystem().
DWORD dwFlags Zde je to o trochu složitější. Tento parametr se může skládat z více hodnot. Je pět hodnot: DISCL_BACKGROUND pro zajistí to, že přístup k zařízení může získat i okno, které není aktivní, čili je v pozadí. Pokud programujeme s DirectDraw, pravděpodobně budeme používat fullscreen a tam máme pouze jedno naše okno na popředí takže raději nastavíme druhou hodnotu DISCL_FOREGROUND. Hodnota DISCL_EXCLUSIVE zajišťuje exkluzivní přístup zařízení. Pokud získáme exkluzivní přístup, žádná jiná aplikace nemůže získat rovněž exkluzivní přístup. Naproti tomu stojí DISCL_NONEXCLUSIVE, která zajišťuje běžný přístup a který může získat více aplikací najednou. Abychom se vyhnuli konfliktům nastavíme hodnotu neexkluzivní. Poslední hodnota DISCL_NOWINKEY pouze zabraňuje použití klávesy s okénkem, pro vyskočení z aplikace.

Poslední metodou Acquire() získáme konečně přístup k datům zařízení. Pokud vyskočíme mimo z naší aplikace, je toto právo přístup zrušeno a po zpětném navrácení se musí opět zavolat funkce Acquire(), abychom opět získali přístup k zařízení (tuto činnost bude provádět funkce RestoreDevices()).

9.3. Závěr

A to je pro tuto lekci vše. Příště budeme pokračovat v implementaci třídy CInput a doufejme, že stačíme celý systém spustit. Do budoucna bych chtěl uvést trochu komplexnější příklad grafického menu, které pracuje s DirectDraw a s DirectInput. Budu rád za vaše dotazy a připomínky.


Těším se příště nashledanou.

Jiří Formánek