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