DirectX (10.)


V dnešní jubilejní lekci budeme pokračovat v implementaci nové třídy CInput, kterou jsme rozdělali v minulé lekci. Výsledkem dnešní lekce, by měla být fungující myš i klávesnice, ale bohužel zatím nemáme co ovládat. V několika příštích lekcích však hodlám sestavit větší projekt, který bude využívat znalostí, které máte. Bude se jednat o trochu složitější systém grafického menu, ale více se o tom dovíte až příště.

10.1. Funkce ProcessInput() a UpdateCursor()

Obě tyto funkce jsou společné tím, že je budeme volat z funkce UpdateFrame() v našem hlavním programu. Problém je ale v tom, že funkce ProcessInput() musíte volat dříve, než obnovíte grafiku. Kdybyste ale obnovili grafiku dříve než kurzor, byl by kurzor skryt za pozadím a to nechceme. Funkce ProcessInput() stahuje data z klávesnice a myší, případně posune souřadnice kurzoru. Na tyto nové souřadnice se posléze nakreslí kurzor z funkce UpdateCursor().

Abychom mohli tyto dvě hlavní funkce implementovat, musíme přidat pár nových proměnných a metod, které s těmito funkcemi spolupracují.

Za prvé to bude inline funkce MoveCursor(), která je vskutku primitivní:

void MoveCursor(int dx, int dy) {m_ptCursor += CPoint(dx, dy);}

Vidíte, že musíme přidat atribut m_ptCursor. To je objekt typu CPoint (pokud nemáte rádi MFC, můžete požít strukturu POINT) a obsahuje aktuální absolutní souřadnice kurzoru na monitoru. Hodnoty x-ové souřadnice se pohybují od 0 do hodnoty rozlišení v x-ovém směru. Ve vertikálním směru je to obdobné. Nyní už víte, proč jsme si ukládali aktuální rozlišení.

Funkce ProcessInput() vypadá následovně:

HRESULT dwRet = 1;
if(m_bInit) {
  
//
   // Get state of keyboard and save it to internal structure (array)

   m_lpDIDKeyboard->GetDeviceState(sizeof(m_arKeyboard), &m_arKeyboard);
  
//
   // Update cursor if is showed

   if(m_bShowCursor) {
     
//
      // Get relative position of cursor.
      // Save state of mouse.

      m_lpDIDMouse->GetDeviceState(sizeof(m_MouseState) , &m_MouseState);
    
 //
      // Add or substract position of cursor

      MoveCursor(m_MouseState.lX, m_MouseState.lY);
    
 //
      // Bounds cursor on the screen

      if(m_ptCursor.x < 0) m_ptCursor.x = 0;
      if(m_ptCursor.x > m_csResolution.cx) m_ptCursor.x = m_csResolution.cx;
      if(m_ptCursor.y < 0) m_ptCursor.y = 0;
      if(m_ptCursor.y > m_csResolution.cy) m_ptCursor.y = m_csResolution.cy;
   }
}
return dwRet;

Přibyly ještě další proměnné. Proměnná m_bShowCursor nám říká, zda-li je kurzor vidět či nikoliv. Pokud ne, je zbytečné stahovat data z myši a vůbec pracovat s kurzorem.

První co v této funkci uděláme je, že stáhneme data z klávesnice. Funkce GetDeviceState() naplní pole o 256 byte prvcích. Pole m_arKeyboard je tedy další atribut. V tomto poli je uložena informace o stavu všech kláves.

To samé provedeme s myší (pokud je kurzor zobrazen). Za prvé stáhneme data z myši tentokrát do struktury DIMOUSESTATE, která obsahuje vše co potřebuje: přírůstky pozice kurzoru a stavy tlačítek. V dalším kroku přičteme relativní přírůstky kurzoru k absolutní pozici - to provádí výše definovaná funkce MoveCursor(). Všimněte si, že používáme atributy lX a lY struktury DIMOUSESTATE.

Struktura DIMOUSESTATE obsahuje následující atributy:

LONG lX;
LONG
lY;
LONG
lZ;
BYTE
rgbButtons[4];

lX a lY jsou přírůstky ve směru osy x a y. lZ je přírůstek otočení kolečka myši (pokud myš nemá kolečko, je tato hodnota rovna 0). Poslední pole rgbButtons[] obsahuje stavy čtyř tlačítek. V poli je na prvním místě levé tlačítko, pak pravé a na dalších dvou indexech jsou další tlačítka, pokud myš nějaké má. Pokud je požadované tlačítko stisknuto, je v poli nenulová hodnota jinak 0.

V posledním kroku funkce, provedeme tzv. clipping kurzoru. Musíme zabránit tomu, aby kurzor mohl vyjet mimo obrazovku. Konečně využijeme rozlišení, které jsme si uložili. A to je vše.

Dále si rozeberme funkci UpdateCursor(). I k této funkci budeme potřebovat jednu funkce navíc. Bude to SetCursor(), která nastaví handle kursoru, který se bude vykreslovat na pozici určené atributem m_ptCursor. Opět se jedná o jednoduchou inline funkci:

HRESULT SetCursor(HICON hCursor) {m_hCursor = hCursor; return 0;}

Pouze přiřadíme nový handle.

Samotná funkce UpdateCursor() bude také celkem primitivní:

HRESULT CInput::UpdateCursor(HDC hDC)
{
    HRESULT dwRet = 1;
    if(m_bInit && m_bShowCursor && m_hCursor) {

        dwRet = DrawIcon(hDC, m_ptCursor.x, m_ptCursor.y, m_hCursor);
        if(dwRet == 0) {
            dwRet = GetLastError();
            TRACE("Nastala chyba pri vykresleni kurzoru: %d\n", dwRet);
        }
    }
    return dwRet;
}

Nejdříve otestujeme správnost handlu m_hCursor a zobrazíme kurzor jen když má být skutečně zobrazen (m_bShowCursor). Jako druhý a poslední krok zavoláme funkci DrawIcon(), která vykreslí požadovaný kurzor na správných souřadnicích. Možná si teď právě myslíte, proč nevyužit k vykreslení kurzoru DirectDraw. Bylo by správné to tak udělat, ale v současné době máme kód DirectDraw v modulu .exe souboru a ten využívá knihovny Input.dll. Tudíž Input.dll nemůže být závislá na .exe souboru, kde je potřebné DirectDraw. Později projekt upravíme tak, že i DirectDraw bude v samostatné knihovně a poté budeme kurzor vykreslovat pomocí DirectDraw.

10.2. Exportování funkcí

Nyní jsme dospěli do stavu, kdy náš systém DirectInput má něco dělat a potřebovali bychom to vyzkoušet. To znamená, že potřebuje volat funkce jako je ProcessInput() a UpdateCursor(). Toto je dá provést několika způsoby (o těchto způsobech jsem psal v minulých lekcích). Vytvořme tedy globální objekt CInput:

CInput  g_theInput;

Tento řádek napište na začátek souboru input1.cpp. Nyní do hlavičkového souboru input1.h doplníme seznam exportovaných globálních funkcí:

INPUT_API HRESULT inpCreateDirectInputSystem(HINSTANCE hInst, HWND hWnd, CSize csResolution);
INPUT_API HRESULT inpProcessInput();
INPUT_API HRESULT inpUpdateCursor(HDC hDC);
INPUT_API HRESULT inpSetCursor(HICON hCursor);

Konstanta INPUT_API je definována na dvou místech různě. V implementační souboru input1.cpp definujte konstantu takto:

#define   INPUT_API __declspec(dllexport)

Tuto definici musíte provést před vložením hlavičkového souboru input1.h. Za druhé připište následují řádky na začátek souboru input1.h:

#ifndef INPUT_API
     #define INPUT_API __declspec( dllimport )
#endif // INPUT_API

Tyto řádky zařídí to, že pokud se pokusíme vložit tento hlavičkový soubor mimo knihovnu input.dll, nadefinuje se makro COMMON_API pro import výše uvedených funkcí. Naopak v modulu knihovny je konstanta  definována pro export funkcí.

Od teď, pokud chcete použít funkce s prefixem inp, musíte pouze  vložit hlavičkový soubor input1.h.

Zbývá jen nadefinovat exportované funkce. Definice bude velmi jednoduchá. Stačí využít globálního objektu k volaní jednotlivých metod:

//
// Exportovane funkce
HRESULT inpCreateDirectInputSystem(HINSTANCE hInst, HWND hWnd, CSize csResolution)
{
    return g_theInput.CreateDirectInputSystem(hInst, hWnd, csResolution);
}

HRESULT inpProcessInput()
{
    return g_theInput.ProcessInput();
}

HRESULT inpUpdateCursor(HDC hDC)
{
    return g_theInput.UpdateCursor(hDC);
}

HRESULT inpSetCursor(HICON hCursor)
{
    return g_theInput.SetCursor(hCursor);
}

 

Dále upravíme druhý projekt DirectDraw tak, abychom konečně mohli zavolat nové funkce. Do souboru control.cpp vložte hlavičkový soubor input1.h tímto způsobem:

#include "..\Input\Input1.h"

Uvědomte si, že soubor input1.h leží úplně v jiném adresáři. Od této chvíle můžete využít globální funkce, které jsme před chvilkou nadefinovali. Náš systém DirectDraw není moc vhodný pro vkládání DirectInput, ale pro začátek nám to bude stačit.

Do funkce InitDD() přidejte následující řádky:

dwResult = inpCreateDirectInputSystem(AfxGetInstanceHandle(), hWnd, CSize(RES_X, RES_Y));
if(dwResult != ERROR_SUCCESS) {
    TRACE("Nemohu inicializovat DirectInput protoze %d.\n", dwResult);
    return dwResult;
}

inpSetCursor(LoadIcon(AfxGetInstanceHandle(), MAKEINTRESOURCE(IDI_DD)));

Zaprvé je nutno zavolat funkci, která zinicializuje DirectInput. Funkce AfxGetInstanceHalndle() vrací handle instance, což je přesně to, co potřebujeme. Dále posíláme handle okna a nakonec strukturu CSize s rozlišením obrazovky. V druhém kroku nastavíme kurzor tzn., že nahrajeme požadovaný kurzor do paměti a nastavíme handle funkcí inpSetCursor(). Kurzor přidejte do zdrojů projektu DirectDraw do složky Icons.

Dále funkci UpdateFrame() upravte takto:

void CControl::UpdateFrame()
{
   DWORD dwRet;
   if(m_bReady) {

      inpProcessInput();
      //...
      CRect rcBackground;
      rcBackground.SetRect(0,0,RES_X, RES_Y);

      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);
          }
      }
      //...
      //
      // Bound moving sprite in the screen
      CPoint ptPos = m_sprShow.GetPosition();
      int iVelX = m_sprShow.GetVelX();
      if(ptPos.x >= RES_X-150 || ptPos.x <= 0) {
         m_sprShow.SetVelX(-iVelX);
      }
      //
      // Update first sprite
      m_sprShow.MoveSprite();
      m_sprShow.DrawSprite();
      //...
      // Update second sprite
      m_sprAnimShow.ChangeAnimationPhase();
      m_sprAnimShow.DrawSprite();
 
      HDC hdc;
      m_theDisplay.GetBackBuffer()->GetDC(&hdc);
        inpUpdateCursor(hdc);
      m_theDisplay.GetBackBuffer()->ReleaseDC(hdc);

      //...
      m_theDisplay.Present();
   }
}

Na začátku zavoláme funkci inpProcessInput() a na konci před prohozením bufferů inpUpdateCursor(). Po zkompilování a spuštění vskutku uvidíte kurzor (poznámka: myš musí mít nastavený cooperative level DISCL_EXCLUSIVE, jinak bude dělat neočekávané věci). Používám funkce pro získání a uvolnění kontextu zařízení našeho zadního bufferu (což je v tuto chvíli nejlepší řešení, i když jsou funkce pomalé). Pokud bychom použili kontext zařízení okna, kurzor by problikával. Nezapomeňte kontext zařízení uvolnit.

Ještě jsme zapomněli na jednu funkci. Je to funkce ShowCursor(), která podle parametru buď zobrazí či skryje kurzor. Jistě tušíte, že funkce bude velmi krátká. Zkrátka nastaví svým jediným parametrem atribut třídy m_bShowCursor. I tuto funkci exportujte z knihovny:

input1.h:

void ShowCursor(BOOL bShow = TRUE) {m_bShowCursor = bShow;}

INPUT_API void inpShowCursor(BOOL bShow = TRUE);
 

input1.cpp:

void inpShowCursor(BOOL bShow)
{
    g_theInput.ShowCursor(bShow);
}

Zvolil jsem parametr bShow implicitní, jelikož primární úkol funkce ShowCursor() je zobrazovat kurzor.

10.3. Detekce stisku klávesy a tlačítka myši

Budeme pokračovat v implementaci zbylých funkcí, které zajišťují detekci stisku ať klávesy na klávesnici nebo tlačítka myši.

Jsou to funkce:

IsKeyDown()
IsLButtonDown()
IsLButtonUp()
IsRButtonDown()
IsRButtonUp()

Jistě jste si všimli, že některé tyto funkce mají zvláštní parametr _Handling. Konečně se k tomuto problému dostáváme. Musíte se totiž starat o to, zda-li stisk klávesy nebo tlačítka byl již obsloužen nebo na toto obsloužení teprve čeká. Pokud je tlačítko dole, ale bylo již obslouženo, funkce vrací FALSE a nikoliv TRUE jak by se dalo čekat. To samé platí u klávesnice. Ve výsledku to způsobí, že na jeden stisk tlačítka myši se provede požadovaná operace pouze jednou a ne 100x (tolikrát by se totiž zopakovala za jedno stisknutí, představte si, že stisk testuje v každé smyčce ve funkci UpdateFrame()). Tento jev je samozřejmě někdy nežádoucí např. když chcete pohybovat grafickým objektem po monitoru, tak by to asi moc nešlo, protože by se objekt posunul vždy o kousek a při dalším stisku opět o kousek atd. Proto je obsluha volitelná.

Abychom mohli obsloužit jak klávesnici tak myš, musíme mít další pole, kde bude nastaveno zda-li byla klávesa obsloužena či nikoliv (TRUE nebo FALSE).

Funkce IsRButtonDown():

//
// Checking left mouse button
if(m_MouseState.rgbButtons[MOUSEBUTTON_RIGHT]) { // 1
   //
   // If handling is not set, set it
   if(_Handling) { // 2
      //
      // User press mouse button...handling activated
      if(!m_IsHandled[MOUSEBUTTON_RIGHT]) {  // 3
         m_IsHandled[MOUSEBUTTON_RIGHT] = TRUE; // 4
         return TRUE; // 5
      }
      else {
         //
         // Return false because handling is already set.
         return FALSE; // 6
      }
   }
   else {
      //
      // User does not want hadling, simply return TRUE
      return TRUE; // 7
   }
}
else {
   //
   // Reset handling because mouse button is up
   m_IsHandled[MOUSEBUTTON_RIGHT] = FALSE; // 8
   //
   // Button is up so return FALSE
   return FALSE;
}


Kód je na první pohled složitý, ale není to zase tak hrozné. Všimněte si, jak jednoduchá by to byla funkce, kdybychom se nemuseli zabývat obsluhou. Abychom porozuměli zbytku funkce, musíme přidat další atributy do naší třídy. Především to bude pole m_IsHandled, které má pouze dva prvky, protože potřebujeme obsloužit dvě tlačítka myší. Navíc je potřeba definovat nějaké symbolické konstanty:

#define MOUSEBUTTON_LEFT      0
#define MOUSEBUTTON_RIGHT     1

Definuji je rovnou pro obě tlačítka, protože je zřejmé, že obě funkce budou totožné právě až na tyto konstanty. Nyní se konečně dostaneme k principu funkce. První podmínka (1) zjišťuje zda-li je pravé resp. levé tlačítku stisknuté. Testujeme hodnotu v poli rgbButtons, o kterém již byla řeč. Pokud není žádné tlačítko stisknuté (8), musíme vynulovat obsluhu, abychom zaznamenali nový stisk. V případě, že tlačítko stisknuté je, musíme zjistit, zda-li chce uživatel použít obsluhu (2). Pokud ne (7), jednoduše vrátíme TRUE. Problém nastává, když uživatel požaduje obsluhu. Zase tak velký problém to není. Zkrátka se podíváme do obslužného pole (3) a zjistíme, jestli už tlačítko bylo obslouženo či nikoliv. Pokud ano, vracíme FALSE (6) a pokud ne vracíme TRUE (5), ale navíc je potřeba nastavit (4), že tlačítko bylo právě obslouženo.

Funkce IsRButtonUp():

return (m_MouseState.rgbButtons[MOUSEBUTTON_LEFT]) ? FALSE : TRUE;

Tato funkce je proti svému opaku velmi jednoduchá. Prostě jen vracíme TRUE, pokud je tlačítko nahoře a FALSE pokud je stisknuté. Opět jsou funkce pro pravé a levé tlačítko identické.

Zbývá metoda IsKeyDown():

//
// Check validity of input param
if(Key >= 256) {
    return FALSE;
}
//
// Checking specified key
if(KEYDOWN(m_arKeyboard, Key)) {
    if(bHandle) {
       //
       // User press key button...handling activated
       if(!m_IsHandled[Key]) {
           m_IsHandled[Key] = TRUE;
           return TRUE;
       }
       else {
          //
          // Return false because handling is already set.
          return FALSE;
       }
    }
    //
    // User does not want handling, simply return TRUE
    else {
        return TRUE;
    }
}
//
// Key is up, reset handling, return FALSE
else {
    m_IsHandled[Key] = FALSE;
    return FALSE;

}

Funkce je v principu naprosto stejná jako u myši. Musíme ovšem definovat makro KEYDOWN:

#define KEYDOWN(name, key) (name[key] & 0x80)

Makro zjistí hodnotu v obslužném poli klávesnice, které má tentokrát 256 prvků (pro každou klávesu jeden). Na začátku je test vstupního parametru, který pouze hlídá, aby uživatel nepřesáhl meze pole. Zbytek funkce je naprosto analogický.

Všech 5 uvedených funkcí budeme exportovat:

input1.h:

INPUT_API BOOL inpIsRButtonDown(BOOL _Handling = TRUE);
INPUT_API BOOL inpIsLButtonDown(BOOL _Handling = TRUE);
INPUT_API BOOL inpIsRButtonUp();
INPUT_API BOOL inpIsLButtonUp();
INPUT_API BOOL inpIsKeyDown(int Key, BOOL bHandle);

input1.cpp:

BOOL inpIsRButtonDown(BOOL _Handling)
{
    return g_theInput.IsRButtonDown(_Handling);
}

BOOL inpIsLButtonDown(BOOL _Handling)
{
    return g_theInput.IsLButtonDown(_Handling);
}

BOOL inpIsRButtonUp()
{
    return g_theInput.IsRButtonUp();
}

BOOL inpIsLButtonUp()
{
    return g_theInput.IsLButtonUp();
}

BOOL inpIsKeyDown(int Key, BOOL bHandle)
{
    return g_theInput.IsKeyDown(Key, bHandle);
}

10.4. Funkce RestoreDevices()

A je tu úplně poslední funkce! Je to  funkce, která obnoví přístup k zařízením, když aplikace ztratí fokus (vlastně, když ho opětovně dostane). Funkce bude velmi jednoduchá:

HRESULT CInput::RestoreDevices()
{
    DWORD dwRet = 1;
// INPUT IS NOT INIT
    if(m_bInit) {
        dwRet = m_lpDIDKeyboard->Acquire();
        if(dwRet != DI_OK) {
            TRACE("Cannot reacquire the keyboard due %d\n", dwRet);
            return dwRet;
        }
        dwRet = m_lpDIDMouse->Acquire();
        if(dwRet != DI_OK) {
            TRACE("Cannot reacquire the mouse due\n", dwRet);
            return dwRet;
        }
    }
    return dwRet;
}

Prostě zavoláme funkci Acquire() pro každé zařízení, které máme. I tuto funkci exportujte. To je vše k naší třídě.

Nyní ještě malinko upravíme projekt DirectDraw, abychom vyzkoušeli detekční funkce.

Vložte hlavičkový soubor input1.h rovněž do souboru directdraw.cpp a pak funkci MainWndProc() upravte takto:

LRESULT CALLBACK MainWndProc( HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam )
{
    WORD low;

    switch(msg) {
    case WM_SETCURSOR:
        SetCursor(NULL);
        break;
    case WM_ACTIVATE:
        low = LOWORD(wParam);
        // Start flipping loop if app is activated
        if(low == WA_ACTIVE || low == WA_CLICKACTIVE) {
           theApp.GetControl()->Activate(TRUE);

           inpRestoreDevices();
        }
        // Stop flipping loop if app is deactivated
        if(low == WA_INACTIVE) {
           theApp.GetControl()->Activate(FALSE);
        }
        break;
    }

    return DefWindowProc(hWnd, msg, wParam, lParam);
}

Tato úprava zajistí správnou funkčnost zařízení i pro ztracení a opětném navrácení fokusu okna.

Na úplný závěr ještě drobně upravte funkci UpdateFrame() takto:

void CControl::UpdateFrame()
{
   DWORD dwRet;
   if(m_bReady) {

      inpProcessInput();
      //...
      CRect rcBackground;
      rcBackground.SetRect(0,0,RES_X, RES_Y);

      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);
          }
      }
      //...
      //
      // Bound moving sprite in the screen
      CPoint ptPos = m_sprShow.GetPosition();
      int iVelX = m_sprShow.GetVelX();
      if(ptPos.x >= RES_X-150 || ptPos.x <= 0
|| inpIsKeyDown(DIK_SPACE, TRUE)) {
         m_sprShow.SetVelX(-iVelX);
      }
      //
      // Update first sprite
      m_sprShow.MoveSprite();
      m_sprShow.DrawSprite();
      //...
      // Update second sprite
      m_sprAnimShow.ChangeAnimationPhase();
      m_sprAnimShow.DrawSprite();
 
      HDC hdc;
      m_theDisplay.GetBackBuffer()->GetDC(&hdc);
        inpUpdateCursor(hdc);
      m_theDisplay.GetBackBuffer()->ReleaseDC(hdc);

      //...
      m_theDisplay.Present();
   }
}

Všimněte si, že naše funkce inpIsKeyDown() přijímá parametr DIK_SPACE. To je konstanta, která je unikátní pro každou klávesu. Úplný seznam těchto konstant najdete na této stránce. Vyzkoušejte si, co by program dělal, kdybyste nepoužili obsluhu (funkce bude naprosto nepoužitelná). Po této úpravě změní raketka směr po každém stisku mezerníku.

Dnešní kód si samozřejmě můžete stáhnout jako obvykle v sekci Downloads.

10.5. Závěr

Tímto dílem jsme úplně dokončili kurz DirectInput, který samozřejmě nebyl zcela úplný a podrobný, ale základy jsme zvládli. Jak jsem naznačil minule i na začátku dnešní lekce, hodlám příště začít větší příklad, kde kompletně využijeme třídy CInput avšak mohutně upravíme DirectDraw.

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

Jiří Formánek