DirectX (16.)

Po měsíci tu máme další lekci kurzu DirectX. A čím se dnes budeme zabývat?  Nejprve si shrneme, co jsme udělali v minulé lekci tj. začali jsme implementovat knihovnu Engine.dll. V této lekci budeme pokračovat a přidáme další třídy CGItem, CGButton atd. Touto lekcí také zakončíme náš příklad a vlastně i DirectDraw.

16.1. Knihovna Engine.dll

Jak jsem zmínil v úvodu, v minulé lekci jsme začali utvářet knihovnu Engine.dll, která bude obsahovat systém našeho menu. Dnes ji zcela dokončíme, i když bude samozřejmě zcela na Vás, jak budete pokračovat (zda-li budete pokračovat). Naším dnešním úkolem tedy bude přidat následujících pět tříd: CGItem, CGButton, CGLabel, CGPage a CGMenu. CGItem je rodičovská třída pro třídy CGButton a CGLabel. CGMenu obsahuje pole prvků CGPage a tato třída obsahuje pole prvků CGItem.

16.1.1 CGItem

Začneme od nejnižší vrstvy tj. od třídy CGItem. Tato třída je základní třídou pro všechny budoucí prvky menu. Instanci této třídy nikdy nemusíte vytvářet (je to zcela zbytečné, jedná se totiž o zcela obecný prvek který nic neumí). Jako základní třída je velice jednoduchá. Podívejme se na deklaraci třídy (tentokrát vynechám tabulku, protože uvedené třídy jsou mnohem jednodušší):

class AFX_EXT_CLASS CGItem : public CSprite
{
   
// atributes
protected:
    DWORD m_dwID;
    BOOL m_CanFocus;

public:
    // callback func.
    MENUPROC ProcessFunc;

public:
    // visual aspect
    virtual void Enable() {}
    virtual void Disable() {}

    // drawing
    virtual void UpdateItem();
    virtual HRESULT ProcessItem(CGPage* _Page, void* _Data, UINT _Action);

    // return func.
    void SetID(DWORD dwID) {m_dwID = dwID;}
    DWORD GetID() {return m_dwID;}

    // focus func.
    BOOL CanBeFocused() {return m_CanFocus;}
    void SetCBFocused(BOOL _Value) {m_CanFocus = _Value;}

public:
    CGItem();
    virtual ~CGItem();
};

Vidíte, že třída obsahuje spoustu virtuálních metod. Většina těchto metod implicitně nedělá nic a jsou určeny k přetížení z potomka. Obsahuje pouze tři atributy a to sice ID objektu, které musí být unikátní vůči ostatním objektům na jedné stránce. Druhý atribut říká, zda-li prvek může dostat fokus. Například tlačítko fokus mít může zatímco statický text nikoli.  Poslední atribut je ukazatel na obslužnou funkci menu. Tuto funkci musíte definovat takto:

typedef HRESULT (CALLBACK* MENUPROC)(VOID*, VOID*, UINT);

Tento zápis definuje obecnou funkci MENUPROC se třemi parametry. Tato funkce se volá pokud nastane nějaká akce s prvkem (například stisknutí tlačítka apod.). Blíže si ji probereme později (ve skutečnosti se volá, i když se s prvkem neděje nic).

Přejděme rovnou k implementaci třídy, která je velmi krátká:

void CGItem::UpdateItem()
{
    UpdateSprite();
}

HRESULT CGItem::ProcessItem(CGPage* _Page, void* _Data, UINT _Action)
{
    return ProcessFunc((void*)_Page, (void*)this, _Action);
}

Metoda UpdateItem() obnoví sprite tj. zavolá metodu UpdateSprite(), kterou jsme psali minule. Tato metoda jde po odvození upravit. Další metody třídy jsou inline metody typu set/get a nebudu je zde podrobněji rozebírat.

Za povšimnutí snad stojí volání funkce ProcessFunc(), což je metoda typu MENUPROC. Má tři parametry: ukazatel na stránku, ukazatel na vlastní prvek a akci prvku. O akcích prvků si povíme dále. Ukazatele musíme přetypovat na void*, což je vidět z deklarace MENUPROC.

16.1.2. CGButton

První potomek třídy CGItem je třída CGButton, která rovněž není nikterak složitá. Představuje tlačítko, které může mít čtyři stavy: normální, s fokusem, stisknuté a nepřístupné. Příkald jak mohou vypadat čtyři stavy pro tlačítko vidíte na obrázku:

Stav normální (tlačítko v klidu):

Stav s fokusem (nad tlačítkem je kurzor):

Zamáčknuté tlačítko:

A nepřístupné tlačítko:

Není podmínkou, aby každé tlačítko mělo všechny čtyři stavy. Podívejme se na deklaraci:

#define BS_NORMAL    0
#define BS_FOCUS     1
#define BS_PRESS     2
#define BS_DISABLE   3

class AFX_EXT_CLASS CGButton : public CGItem
{
private:
    CSpriteState m_NormalState;
    CSpriteState m_FocusState;
    CSpriteState m_PressState;
    CSpriteState m_DisState;

public:
    //creation
    HRESULT CreateButton(int x, int y, LPCSTR szNormal, LPCSTR szFocus = NULL, LPCSTR szPress = NULL, LPCSTR szDis = NULL);

    //update
    virtual void UpdateItem();
    virtual HRESULT ProcessItem(CGPage* _Page, void* _Data, UINT _Action);

    //visual
    virtual void Enable() {SetState(BS_NORMAL);}
    virtual void Disable() {SetState(BS_DISABLE);}

public:
    CGButton();
    virtual ~CGButton();

};

Nejprve definujeme čtveřici ID pro jednotlivé stavy tlačítka. Třída obsahuje pouze objekty čtyř stavů popsaných výše. Poté obsahuje metodu, pomocí které definujeme pozici a bitmapy tlačítka. Hodnotou IP_CENTER zajistíme, že tlačítko bude umístěno uprostřed obrazovky (ať už horizontálně nebo vertikálně). Musíme definovat nejméně první bitmapu pro stav, kdy tlačítko je v normálním stavu.

void CGButton::CreateButton(int x, int y, LPCSTR szNormal, LPCSTR szFocus, LPCSTR szPress, LPCSTR szDis)
{
    //
    // Create and defines segment for normal state of button - clear button
    m_NormalState.CreateState(BS_NORMAL, szNormal);
    AddState(&m_NormalState);
    //
    // Create and defines segment for focused state of button - focused button
    if(szFocus) {
        m_FocusState.CreateState(BS_FOCUS, szFocus);
        AddState(&m_FocusState);
    }
 
   //
    // Create and defines segment for pressed state of button - pressed button
 
  if(szPress) {
        m_PressState.CreateState(BS_PRESS, szPress);
        AddState(&m_PressState);
    }

    //
    // Create and defines segment for disabled state of button - disabled button
  
 if(szDis) {
        m_DisState.CreateState(BS_DISABLE, szDis);
        AddState(&m_DisState);
    }
    //
    // Center position of button
    if(m_pCurState) {
        if(x == IP_CENTER) {
            x = disGetResolution().cx / 2 - m_pCurState->GetSourceSurface()->Width() / 2;
        }
        if(y == IP_CENTER) {
            y = disGetResolution().cy / 2 - m_pCurState->GetSourceSurface()->Height() / 2;
        }
    }
    SetPosition(CPoint(x, y));
}

V této metodě postupně definujeme sprite-stavy pro jednotlivé tlačítko-stavy. Dále musíme nastavit pozici tlačítka. Pokud uživatel chce mít tlačítko  uprostřed obrazovky, musí polohu definovat pomocí konstanty IP_CENTER. Pak se spočítá skutečná poloha podle vztahu: rozlišení / 2 - šířka (výška) tlačítka / 2.

Metodou UpdateItem() tlačítko vykreslíme a zajistíme, aby nemohlo dostat fokus pokud je ve stavu NEPŘÍSTUPNÉ.

void CGButton::UpdateItem()
{
    if(GetState()->GetID() == BS_DISABLE) {
        SetCBFocused(FALSE);
    }
    else {
        SetCBFocused(TRUE);
    }

    CGItem::UpdateItem();
}

Musíme volat metodu rodičovské třídy, aby se tlačítko vůbec vykreslilo.

Nakonec tu máme metodu ProcessItem(), která je volána pro každé tlačítko na stránce v každém cyklu aplikace.

HRESULT CGButton::ProcessItem(CGPage* _Page, void* _Data, UINT _Action)
{
    if(GetState()->GetID() != BS_DISABLE) {

        switch(_Action) {
        case IA_MOUSEMOVE:
            if(GetState()->GetID() != BS_PRESS) {
                SetState(BS_FOCUS);
            }
            break;
        case IA_MOUSECLICK_UP:
            if((*((UINT*)_Data)) == LEFT_MOUSE_BUTTON) {
                SetState(BS_FOCUS);
            }
            break;
        case IA_MOUSECLICK_DOWN:
            if((*((UINT*)_Data)) == LEFT_MOUSE_BUTTON) {
                SetState(BS_PRESS);
            }
        break;
        case IA_KEYPRESS:
            //none handling
            break;
        case IA_NONE:
            SetState(BS_NORMAL);
            break;
        }
    }

    return CGItem::ProcessItem(_Page, (void*)_Data, _Action);
}

Pokud je tlačítko ve stavu NEPŘÍSTUPNÉ, neděje se nic. Dále u tlačítka rozlišujeme pět různých akcí. Pokud uživatel najede na tlačítko kurzorem, nastaví se fokus (IA_MOUSEMOVE). Pokud uživatel stiskne tlačítko, nastaví se stav STISKNUTO (IA_MOUSECLICK_DOWN), když následně tlačítko pustí, nastaví se opět tlačítko s fokusem (IA_MOUSECLICK_UP). Pokud se neděje nic, nastaví se stav normální (IA_NONE). Nakonec se zavolá metoda rodičovské třídy, která volá obslužnou funkci menu, kde pracujeme s chováním vlastního menu. Existuje ještě poslední akce a to je stisk klávesy, která ovšem pro tlačítko nemá smysl (IA_KEYPRESS).

Tímto způsobem nastavíme implicitní chování každého tlačítka - po najetí kurzorem nad tlačítko se nastaví fokus, po stisku se tlačítko promáčkne atd. Proto je důležité, aby každé tlačítko mělo definováno stavy: normální, s fokusem a stisknuté. Stav "nepřístupné tlačítko" je takový nadstandard:-)

16.1.3. CGLabel

A je tu další potomek třídy CGItem - CGLabel. Tato třída představuje statický text doplňující tlačítka na stránce. Tento prvek je snad ještě jednodušší:

#define LABEL_STATE 0

class AFX_EXT_CLASS CGLabel : public CGItem
{
    CSpriteState m_LabelState;
public:
    void CreateLabel(int x, int y, CString _BMPFile);

public:
    CGLabel();
    virtual ~CGLabel();

};

Protože každý sprite musí mít alespoň jeden stav, i zde musíme definovat stav LABEL_STATE. Třída obsahuje pouze jednu metodu pro definici textu - jeho polohu a zdrojovou bitmapu.

void CGLabel::CreateLabel(int x, int y, CString _BMPFile)
{
    // Create one state for each label
    m_LabelState.CreateState(LABEL_STATE, _BMPFile);
    // remeber this state
    AddState(&m_LabelState);
    // set position of label
    if(m_pCurState) {
        if(x == IP_CENTER) {
            x = disGetResolution().cx / 2 - m_pCurState->GetSourceSurface()->Width() / 2;
        }
        if(y == IP_CENTER) {
            y = disGetResolution().cy / 2 - m_pCurState->GetSourceSurface()->Height() / 2;
        }
    }
    SetPosition(CPoint(x, y));
}

Nejprve vytvoříme stav pro objekt sprite. Využijeme k tomu konstantu LABEL_STATE a cestu k bitmapě _BMPFile. Dále inicializujeme polohu úplně stejným způsobem jako u tlačítka.

Řekli jsme si, že tento prvek nemůže dostat fokus:

CGLabel::CGLabel()
{
    SetCBFocused(FALSE);
}

Nyní přejedeme ke složitější části, ke třídám CGPage a CGMenu.

16.1.4. CGPage

Tato třída představuje stránku menu, která obsahuje pole prvků. Tyto prvky jsou reprezentovány třídou CGItem respektive jejími potomky CGButton a CGLabel. Každá stránka musí mít unikátní ID vůči objektu CGMenu tzn. vůči celé aplikaci (objekt menu je pouze jeden). U této třídy si opět zavedeme tabulku, protože obsahuje více členů:

Atributy

Typ Jméno Popis
DWORD m_dwID Unikátní ID stránky
CPtrArray m_Items Pole prvků, které jsou umístěny na stránce *
MENUPROC ProcessFunc Ukazatel na obslužnou funkci menu
Metody
Návratová hodnota Jméno a parametry Popis
void CreateItem(DWORD, CGItem*) Metoda přidá další prvek na stránku tj. uloží ukazatel na tento prvek do pole. Přitom kontroluje zda-li není na stránce prvek se stejným ID.
void ReleasePage() Metoda pro uvolnění alokované paměti apod.
CPtrArray* GetItems() Vrací ukazatel na pole prvků.
void TestMouseMove(CPoint) Zjistí, zda-li je kurzor myši nad nějakým prvkem a volá metodu ProcessItem() všech vložených prvků.
void TestMouseClick(CPoint, UINT, UINT) Tato metoda se volá pokud uživatel stiskne tlačítko myši. Po té se zjistí, jestli je kurzor nad některým prvkem a volá se metoda ProcessItem().
void UpdatePage() Metoda slouží k vykreslení stránky. Postupně projde všechny vložené prvky a zavolá metodu UpdateItem().
void ResetPage() Tuto metodu je nutno volat při přepínání stránek. Nastavuje u všech prvků normální stav - pokud není prvek ve stavu nepřístupný.
void SetID(DWORD) Nastavuje ID stránky.
DWORD GetID() Vrací ID stránky.

Poznámka:
* Pokud nechcete používat pole CPtrArray knihovny MFC, můžete například použít lineární spojový seznam. Zde odkazuji na Kurz C++ pro ty, co nevědí, jak takový seznam vytvořit. V dnešní a příští lekci se totiž dovíte jak na to.

Podrobněji si metody rozebereme při jejich implementaci. Nejdříve je třeba definovat některé symbolické konstanty.

Tyto konstanty představují akce prvku na stránce. Slouží k upozornění prvku, co se s nimi vlastně děje:

#define IA_MOUSEMOVE              0
#define IA_MOUSECLICK_DOWN        1
#define IA_MOUSECLICK_UP          2
#define IA_KEYPRESS               3
#define IA_NONE                   4

Při volání metody TestMouseClick(), musíme určit, které tlačítko bylo stisknuto a zda-li bylo právě stisknuto nebo puštěno:

#define LEFT_MOUSE_BUTTON  0
#define RIGHT_MOUSE_BUTTON 1


#define BA_UP   10
#define BA_DOWN 20


Pomocná konstanta, kterou určíme polohu prvku (viz. výše):

#define IP_CENTER -1

Nyní se vrhněme na deklaraci samotné třídy:

class AFX_EXT_CLASS CGPage
{
    //atributes
private:
    DWORD m_dwID;
    CPtrArray m_Items;

public:
    //creating items
    void CreateItem(DWORD dwID, CGItem* _NewItem);
    void ReleasePage();
    CPtrArray* GetItems() {return &m_Items;}

    //general
    void TestMouseMove(CPoint _Cursor);
    void TestMouseClick(CPoint _Cursor, UINT _MouseButton, UINT _ButtonAction);

    //drawing
    void UpdatePage();
    void ResetPage();
    //
    // Get ID of page
    void SetID(DWORD dwID) {m_dwID = dwID;}
    DWORD GetID() {return m_dwID;}

public:
    //callback func.
    MENUPROC ProcessFunc;

public:
    CGPage();
    ~CGPage();

};

Zde není nic neobvyklého. Parametry metod si rozebereme podrobněji za chvilku. Třída musí být exportována z knihovny, protože to vyžaduje způsob, jakým vytváříme strom menu (mimochodem to platí i pro třídy prvků).

Začněme třídu postupně implementovat. Metoda CreateItem() slouží k připojení objektu libovolného prvku na stránku:

void CGPage::CreateItem(DWORD dwID, CGItem* _NewItem)
{
   
//
    // Check item pointer

    if(_NewItem == NULL) {
        DXTHROW("Pointer to page is NULL.");
    }
    CGItem* pItem;
   
// Check if the item with ID is not already on the page
    for(int i = 0; i < m_Items.GetSize(); i++) {
        pItem = (CGItem*) m_Items[i];
        if(pItem->GetID() == dwID) {
            DXTHROW("Item is already on the page.");
        }
    }
   
// Set some atributes
    _NewItem->SetID(dwID);
    _NewItem->ProcessFunc = ProcessFunc;
   
// Add item pointer
    m_Items.Add(_NewItem);
   
// Show item pointer
    DXTRACE("Creating item. ID: %d\tPointer: 0x%X", dwID, int(_NewItem));
}

Nejprve otestujeme vstupní parametry. Pokud zvenku uživatel pošle místo platného ukazatele na prvek NULL, metoda vyhodí výjimku. Pokud se uživatel pokusí vložit dva prvky se stejným ID na jednu stránku, metoda opět vyhodí výjimku. Pokud tyto vstupní parametry proběhnou v pořádku, nastaví se ID nového prvku a ukazatel na obslužnou funkci menu. Nakonec se ukazatel prvku uloží do pole.

Poznámka: Ještě se zmíním o chytání výjimek (o nich se podrobně dovíte v příští lekci Kurzu C++). V našem případě vyhazujeme řetězec, který zachytíte následovně:

try {
    VolaniFunkceKteraVyahazujeVyjimku()
}
catch(LPCSTR str) {
    DXTRACE(str);
}


 

Po skončení aplikace musíme pole prvků uvolnit. To zařídí metoda ReleasePage(), která je volána z destruktoru stránky:

void CGPage::ReleasePage()
{
    CGItem *pItem;
    DXTRACE("Page has %d item(s).", m_Items.GetSize());
    for(int i = 0; i < m_Items.GetSize(); i++) {
        pItem = (CGItem*) m_Items[i];
        DXTRACE("Deleting item. ID: %d\tPointer: 0x%X", pItem->GetID(), int(pItem));
        SAFE_DELETE(pItem);
    }
   
//
    // Remove items from array

    m_Items.RemoveAll();
}

Projdeme celé pole a smažeme (dealokujeme)  všechny prvky, které alokuje uživatel - z toho plyne, že prvky musí být alokovány dynamicky. Konkrétní způsob, jak budeme prvky a stránky menu vytvářet, si ukážeme na závěr této lekce.

Dále tu máme dvojici metod, které mění stavy prvků podle stavu myši. Za prvé podle polohy kurzoru a za druhé podle stavu tlačítek.

void CGPage::TestMouseMove(CPoint _Cursor)
{
    CGItem * pItem = NULL;
    CRect rcDestin;
    for(int i = 0; i < m_Items.GetSize(); i++) {
        pItem = (CGItem*) m_Items[i];

        pItem->GetDestin(&rcDestin);
        if(pItem && rcDestin.PtInRect(_Cursor)) {

            pItem->ProcessItem(this, 0, IA_MOUSEMOVE);
        }
        else {
            pItem->ProcessItem(this, 0, IA_NONE);
        }
    }
}

První z těchto metod testuje právě pohyb myši a pokud se nějaký prvek ocitne pod kurzorem, volá metodu ProcessItem() s parametrem IA_MOUSEMOVE. Tak prvek pozná, že má nastavit fokus. Pro ostatní prvky musíme volat metodu s parametrem IA_NONE, protože se s nimi neděje nic.

Dále budeme testovat stisk tlačítka. To už bude o trochu složitější, protože musíme rozlišit jaké tlačítko bylo stisknuto a zda-li bylo právě stisknuto nebo puštěno:

void CGPage::TestMouseClick(CPoint _Cursor, UINT _MouseButton, UINT _ButtonAction)
{
    CGItem * pItem = NULL;
    CRect rcDestin;
    for(int i = 0; i < m_Items.GetSize(); i++) {
        pItem = (CGItem*) m_Items[i];

        pItem->GetDestin(&rcDestin);
        if(rcDestin.PtInRect(_Cursor)) {

            if(_ButtonAction == BA_UP) {
                pItem->ProcessItem(this, (void*)&_MouseButton, IA_MOUSECLICK_UP);
            }
            if(_ButtonAction == BA_DOWN) {
                pItem->ProcessItem(this, (void*)&_MouseButton, IA_MOUSECLICK_DOWN);
            }
        }
        else {
            pItem->ProcessItem(this, (void*)&_MouseButton, IA_NONE);
        }
    }
}

K tomu slouží dva poslední parametry metody. Opět procházíme všechny prvky a testujeme, zda-li není prvek pod kurzorem. Pokud ano, pošleme tlačítku zprávu, že bylo stisknuto. Zde ovšem musíme rozlišit pravé a levé tlačítko - _MouseButton může nabývat dvou hodnot: LEFT_MOUSE_BUTTON nebo RIGHT_MOUSE_BUTTON. Parametr _ButtonAction říká, zda-li tlačítko bylo stisknuto nebo puštěno. Podle toho prvku pošleme buď akci IA_MOUSECLICK_DOWN nebo UP. To je vše.

Stránka se musí v každém cyklu obnovit - vykreslit. To zařídí metoda UdpatePage():

void CGPage::UpdatePage()
{
    CGItem *pItem;
    for(int i = 0; i < m_Items.GetSize(); i++) {
        pItem = (CGItem*) m_Items[i];
        pItem->UpdateItem();
    }
}

Jednoduše projde všechny prvky na stránce a volá metody UpdateItem().

Nakonec tu máme metodu, kterou jsem již umínil výše. Jedná se o metodu ResetPage(), která nastaví všem aktivním (to znamená ne nepřístupným) prvkům základní stav s ID = 0. Je to třeba udělat před tím, než změníme aktuální stránku menu.

void CGPage::ResetPage()
{
    CGItem * pItem = NULL;
    for(int i = 0; i < m_Items.GetSize(); i++) {
        pItem = (CGItem*) m_Items[i];
        if(pItem->GetState()->GetID() != BS_DISABLE) {
            pItem->SetState(0);
        }
    }
}

Projdeme všechny prvky na stránce a pokud jsou aktivní, nastavíme stav 0. To můžeme udělat, protože tlačítko musí mít nastaven alespoň normální stav 0 a objekt label má rovněž stav s ID = 0.

16.1.5. CGMenu

Konečně tu máme poslední třídu CGMenu, která je v mnoha ohledech podobná třídě CGPage, jen nepracuje s prvky, ale se stránkami tj. s objekty CGPage.

Atributy

Typ Jméno Popis
BOOL m_bInit -
CPtrArray m_Pages Pole objektů CGPage. Zde jsou uloženy všechny stránky menu. Přepínat je lze pomocí metody SetCurrentPage().
CGPage* m_pCurrentPage Ukazatel na aktuální stránku, tato stránka se vykresluje.
MENUPROC ProcessFunc Ukazatel na obslužnou funkci menu. Tento ukazatel se nastavuje pomocí metody InitMenu() a je pak předáván všem stránkám a posléze u prvkům.
Metody
Návratová hodnota Jméno a parametry Popis
void CreatePage(DWORD, CGPage*) Přidá novou stránku do systému menu. Ověří také ID stránky. V menu samozřejmě nesmí být dvě stránky se stejný ID.
CGPage* GetPage(DWORD) Vrací ukazatel na stránku podle ID stránky. Pokud stránka v menu není, vrací NULL.
HRESULT InitMenu(MENUPROC) Inicializační metoda pro objekt CGMenu. Nastavuje obslužnou funkci.
HRESULT DefineProcessFunc(MENUPROC) Tato metoda slouží k nastavení a otestování obslužné metody, která musí vracet 0, když se pošle NULL v prvním parametru.
void SetCurrentPage(DWORD) Nastaví viditelnou stránku podle ID.
CGPage* GetCurrentPage() Vrací ukazatel na viditelnou stránku. Může být i NULL.
void ReleaseMenu() Uvolní alokovanou paměť pro stránky a prvky.
void UpdateMenu() Volá metodu UpdatePage() aktuální stránky.
void TestMouseMove(CPoint) Volá metodu TestMouseMove() aktuální stránky, pokud nějaká je.
void TestMouseClick(CPoint, UINT, UINT) Volá metodu TestMouseClick() aktuální stránky.

Podívejme se na deklaraci třídy:

class CGMenu
{
    //private atributes
private:
    BOOL m_bInit;
    CPtrArray m_Pages;
    CGPage *m_pCurrentPage;

public:
    //callback func.
    MENUPROC ProcessFunc;

    //public fuc.
public:
    CGPage* GetCurrentPage() {if(m_pCurrentPage) { return m_pCurrentPage;}
    void SetCurrentPage(DWORD dwID);
    //init menu
    HRESULT InitMenu(MENUPROC _ProcessFunc);
    HRESULT DefineProcessFunc(MENUPROC _ProcessFunc);
    void ReleaseMenu();

    //pages
    void    CreatePage(DWORD dwID, CGPage *_NewPage);
    CGPage* GetPage(DWORD dwID);

    //general
    void UpdateMenu();
    void TestMouseMove(CPoint _Cursor);
    void TestMouseClick(CPoint _Cursor, UINT _MouseButton, UINT _ButtonAction);

public:
    CGMenu();
    ~CGMenu();
};

Tuto třídu nemusíme exportovat, protože s ní nebudeme pracovat přímo, ale prostřednictvím exportovaných funkcí. Tyto funkce definujeme za chviličku.

Nejprve musíme zavolat metodu InitMenu():

HRESULT CGMenu::InitMenu(MENUPROC _ProcessFunc)
{
    DXTRACE("Initializing menu...");
    //
    // Menu is initilized
    m_bInit = TRUE;
    // Callback menu function is OK.
    return DefineProcessFunc(_ProcessFunc);
}

Tato metoda musí otestovat správnost obslužné funkce pro menu. K tomu slouží metoda DefineProcessFunc():

HRESULT CGMenu::DefineProcessFunc(MENUPROC _ProcessFunc)
{
   
//
    // Save pointer to call back function of menu

    ProcessFunc = _ProcessFunc;
   
// Check function
    if(ProcessFunc((void*) NULL, 0, 0) != ERROR_SUCCESS) {
        DXTHROW("Menu process function is not valid. Must return ERROR_SUCCESS(0).");
    }
    return ERROR_SUCCESS;
}

Pokud obslužná funkce vrátí něco jiného než hodnotu ERROR_SUCCESS, metoda vyhodí výjimku. Pokud jako první nezavoláte metody InitMenu(), všechny ostatní metody menu vám budou vyhazovat výjimky.

Následující metoda přidá novou stránku do menu:

void CGMenu::CreatePage(DWORD dwID, CGPage *_NewPage)
{
    //
    // Check initialization of menu
 
   if(!m_bInit) {
        DXTHROW("Menu is not initialzed. Call menInitMenu() first.");
    }
    // Check input parametr
    if(_NewPage == NULL) {
        DXTHROW("NULL page passed.");
    }
    // Check if the page is not already in the page array
    CGPage *pRet;
    for(int i = 0; i < m_Pages.GetSize(); i++) {
        pRet = (CGPage*) m_Pages[i];
        if(pRet->GetID() == dwID) {
            DXTHROW("Page is already in the menu system.");
        }
    }
 
   //
    // Set some atrributes of the page
 
   _NewPage->SetID(dwID);
    _NewPage->ProcessFunc = ProcessFunc;
    //
    // Add page
    m_Pages.Add(_NewPage);
    //
    // Show adrress fo the new page
 
   DXTRACE("Creating page. ID: %d\tPointer: 0x%X", dwID, int(_NewPage));
 
   //
    // Set current page if is not page is set
 
  if(m_pCurrentPage == NULL) {
        m_pCurrentPage = _NewPage;
    }
}

Prvním parametrem určíme ID nové stránky které musí být unikátní, jinak metoda vyhodí výjimku. Druhý parametr je ukazatel na vlastní stránku. Tento ukazatel musíme inicializovat předem, jinak metoda opět vyhodí výjimku. Poté do objektu stránky uložíme její ID a ukazatel na obslužnou funkci menu. Dále uložíme ukazatel na stránku. Na závěr ještě zinicializujeme ukazatel na aktuální stránku, pokud je NULL tj. při prvním vložení, abychom nemuseli explicitně volat metodu SetCurrentPage().

Metoda GetPage() vybere z pole požadovanou stránku podle ID a vrátí její ukazatel. Pokud se stránka v menu nenachází, vrací NULL.

CGPage* CGMenu::GetPage(DWORD dwID)
{
 
  //
    // Check initialization of menu
 
  if(!m_bInit) {
        DXTHROW("Menu is not initialzed. Call menInitMenu() first.");
    }
    CGPage *pRet = NULL;
   
//
    // Find page in the array and return pointer at it

    for(int i = 0; i < m_Pages.GetSize(); i++) {
        pRet = (CGPage*) m_Pages[i];
        // Check page's IDs
        if(pRet->GetID() == dwID) {
            break;
        }
    }
    return pRet;
}

Zde není co řešit, podobný kód jsme psali již několikrát.

Metoda UpdateMenu() slouží k obnovení aktuální stránky. Stačí tedy zavolat metodu UpdatePage() pro aktuální viditelnou stránku:

void CGMenu::UpdateMenu()
{
 
   //
    // Check initialization of menu
 
  if(!m_bInit) {
        DXTHROW("Menu is not initialzed. Call menInitMenu() first.");
    }
  
 //
    // If some page is selected, update this page

    if(m_pCurrentPage) {
        m_pCurrentPage->UpdatePage();
    }
}

Za povšimnutí snad jen stojí kontrola inicializace menu (toho si ostatně všimnete u všech metodu CGMenu).

Následuje dvojice velice jednoduchých metod, které pouze volají tytéž metody pro aktuální stránku:

void CGMenu::TestMouseMove(CPoint _Cursor)
{
 
  //
    // Check initialization of menu

    if(!m_bInit) {
        DXTHROW("Menu is not initialzed. Call menInitMenu() first.");
    }
    if(m_pCurrentPage) {
        m_pCurrentPage->TestMouseMove(_Cursor);
    }
}

void CGMenu::TestMouseClick(CPoint _Cursor, UINT _MouseButton, UINT _ButtonAction)
{
   
//
    // Check initialization of menu

    if(!m_bInit) {
        DXTHROW("Menu is not initialzed. Call menInitMenu() first.");
    }
    if(m_pCurrentPage) {
        m_pCurrentPage->TestMouseClick(_Cursor, _MouseButton, _ButtonAction);
    }
}

O co si tyto metody v principu starají, jsem popsal o něco výše.

Poslední metoda je velice důležitá, protože nám dovoluje změnit aktuální stránku:

void CGMenu::SetCurrentPage(DWORD dwID)
{
 
  //
    // Check initialization of menu

    if(!m_bInit) {
        DXTHROW("Menu is not initialzed. Call menInitMenu() first.");
    }
    CGPage *pRet = NULL;
  
 //
    // Set normal states for all items

    m_pCurrentPage->ResetPage();
   
// Find page in the array
    for(int i = 0; i < m_Pages.GetSize(); i++) {
        pRet = (CGPage*) m_Pages[i];
       
// Check IDs
        if(pRet->GetID() == dwID && !(pRet == m_pCurrentPage)) {
           
// Set current page
            m_pCurrentPage = pRet;
            return;
        }
    }
    DXTRACE("Specified page is not defined");
}

Nejprve vyhledáme v poli požadovanou stránku podle ID (pokud stránka neexistuje, metoda neudělá kromě vypsání hlášky nic). Pokud ovšem stránku najde resetuje aktuální stránku a poté změní ukazatel na novou stranu menu. Tato strana se vykreslí v dalším cyklu aplikace.

16.1.6. Export funkcí

Jak jsem se již zmínil, objekt CGMenu vytvoříme přímo v knihovně a pracovat s ním budeme pomocí exportovaných funkcí. Seznam exportovaných funkcí je následující:

MENU_API HRESULT menInitMenu(MENUPROC _ProcessFunc);
MENU_API void    menUpdateMenu();
MENU_API void    menTestMouseMove(CPoint _Cursor);
MENU_API void    menTestMouseClick(CPoint _Cursor, UINT _MouseButton, UINT _ButtonAction);
MENU_API void    menCreatePage(DWORD dwID, CGPage *_NewPage);
MENU_API void    menSetVisiblePage(int PageID);
MENU_API void    menReleaseMenu();

Přičemž výtaz MENU_API musíte definovat dvěma rozlišnými způsoby. V hlavičkovém souboru menu takto:

#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000

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

#include "gpage.h"

class CGMenu

A v implementačním souboru takto:

#include "stdafx.h"

#define MENU_API __declspec(dllexport)

#include "..\Common\Common.h"
#include "GMenu.h"

Tento zápis zařídí, že funkce budou exportovány z knihovny a importovány do projektu Game. Takový způsob exportu funkcí jsme již použili v minulých projektech.

16.2. Projekt Game

Na závěr lekce ještě drobně upravíme projekt Game, aby bylo vidět něco z toho, o co jsme se celou dobu pokoušeli. Zkrátka vybudujeme jednoduchý strom menu.

Za prvé musíme vložit soubor GMenu.h do hlavního implementační ho souboru Game.cpp:

#include "..\Engine\GMenu.h"

Dále nadefinujeme ID pro stránky a prvky menu:

#define PAGE_MAIN 0
#define PAGE_EXIT 1
#define PAGE_OPTIONS 2

#define BUTTON_STARTGAME 0
#define BUTTON_OPTIONS 1
#define BUTTON_CREDITS 2
#define BUTTON_EXIT 3
#define LABEL_ROCKET7 4
#define BUTTON_YES 5
#define BUTTON_NO 6
#define LABEL_AREYOUSURE 7
#define BUTTON_DONE 8
#define LABEL_CONTROLS 9


Vidíte, že budeme mít tři stránky - hlavní, odchozí a nastavení.

Ve funkci WinMain() musíme zinicializovat menu. K tomuto účelu vytvoříme funkci InitMenu(), kterou si popíšeme dále. Do WinMain() připište modrý řádek:

        DXERR("Cannot open data file due", dwRet);
        return dwRet;
    }

    disInit(g_hWnd, DDFLG_CLIPPER|DDFLG_FULLSCREEN);
    inpCreateDirectInputSystem(hInstance, g_hWnd, disGetResolution());
    disDefineBackground(_S_BACKGROUND, 0);

    InitMenu();
    // Run message loop
    while( TRUE )
    {
        // Look for messages, if none are found then
        // update the state and display it

 

A jak tedy bude vypadat InitMenu():

Nejprve voláme funkci menInitMenu(), jak jsme si popisovali před chvilkou:

HRESULT dwRet;
dwRet = menInitMenu(ProcessMenu);
if(dwRet != ERROR_SUCCESS) {
    DXERR("Cannot init menu due ", dwRet);
    return;
}

Dále vytvoříme první hlavní stránku a prvky na ní:

CGPage * p_pMain = new CGPage;
menCreatePage(PAGE_MAIN, p_pMain);

CGLabel *l_pLabel1 = new CGLabel;
l_pLabel1->CreateLabel(IP_CENTER, 130, "\\Graphics\\Menu\\Labels\\rocket7.bmp");
p_pMain->CreateItem(LABEL_ROCKET7, l_pLabel1);

CGButton *b_pNewGame = new CGButton;
b_pNewGame->CreateButton(IP_CENTER, 230, "\\Graphics\\Menu\\Buttons\\startgame_clear.bmp", "\\Graphics\\Menu\\Buttons\\startgame_focus.bmp", "\\Graphics\\Menu\\Buttons\\startgame_press.bmp", "\\Graphics\\Menu\\Buttons\\startgame_dis.bmp");
p_pMain->CreateItem(BUTTON_STARTGAME, b_pNewGame);
b_pNewGame->Disable();

CGButton *b_pOptions = new CGButton;
b_pOptions->CreateButton(IP_CENTER, 280, "\\Graphics\\Menu\\Buttons\\options_clear.bmp", "\\Graphics\\Menu\\Buttons\\options_focus.bmp","\\Graphics\\Menu\\Buttons\\options_press.bmp");
p_pMain->CreateItem(BUTTON_OPTIONS, b_pOptions);

CGButton *b_pCredits = new CGButton;
b_pCredits->CreateButton(IP_CENTER, 330, "\\Graphics\\Menu\\Buttons\\credits_clear.bmp", "\\Graphics\\Menu\\Buttons\\credits_focus.bmp", "\\Graphics\\Menu\\Buttons\\credits_press.bmp", "\\Graphics\\Menu\\Buttons\\credits_dis.bmp");
p_pMain->CreateItem(BUTTON_CREDITS, b_pCredits);
b_pCredits->Disable();

CGButton *b_pExit = new CGButton;
b_pExit->CreateButton(IP_CENTER, 380, "\\Graphics\\Menu\\Buttons\\exitgame_clear.bmp", "\\Graphics\\Menu\\Buttons\\exitgame_focus.bmp", "\\Graphics\\Menu\\Buttons\\exitgame_press.bmp");
p_pMain->CreateItem(BUTTON_EXIT, b_pExit);



Pak tu máme odchozí stránku:

CGPage * p_pExit = new CGPage;
menCreatePage(PAGE_EXIT, p_pExit);

CGLabel *l_pLabel2 = new CGLabel;
l_pLabel2->CreateLabel(IP_CENTER, 130, "\\Graphics\\Menu\\Labels\\areyousure1.bmp");
p_pExit->CreateItem(LABEL_AREYOUSURE, l_pLabel2);

CGButton *b_pYes = new CGButton;
b_pYes->CreateButton(IP_CENTER, 230, "\\Graphics\\Menu\\Buttons\\yes_clear.bmp", "\\Graphics\\Menu\\Buttons\\yes_focus.bmp", "\\Graphics\\Menu\\Buttons\\yes_press.bmp");
p_pExit->CreateItem(BUTTON_YES, b_pYes);

CGButton *b_pNo = new CGButton;
b_pNo->CreateButton(IP_CENTER, 280, "\\Graphics\\Menu\\Buttons\\no_clear.bmp", "\\Graphics\\Menu\\Buttons\\no_focus.bmp", "\\Graphics\\Menu\\Buttons\\no_press.bmp");
p_pExit->CreateItem(BUTTON_NO, b_pNo);

 

A nakonec stránku s nastavením:

CGPage * p_pOptions = new CGPage;
menCreatePage(PAGE_OPTIONS, p_pOptions);

CGButton *b_pDone = new CGButton;
b_pDone->CreateButton(IP_CENTER, 450, "\\Graphics\\Menu\\Buttons\\done_clear.bmp", "\\Graphics\\Menu\\Buttons\\done_focus.bmp", "\\Graphics\\Menu\\Buttons\\done_press.bmp");
p_pOptions->CreateItem(BUTTON_DONE, b_pDone);

CGLabel *l_pLabel3 = new CGLabel;
l_pLabel3->CreateLabel(IP_CENTER, 80, "\\Graphics\\Menu\\Labels\\controls.bmp");
p_pOptions->CreateItem(LABEL_CONTROLS, l_pLabel3);

Princip funkce je velice jednoduchý a hlavně se pořád opakuje. Vždy musíme alokovat místo pro novou stránku nebo prvek. Pak voláme inicializační metody ať už tlačítka nebo textu (u stránky nemusíme nastavovat nic). Nakonec musíme uložit ukazatel na stránku nebo prvek. To provádí metody CreatePage() nebo CreateItem(), kde zároveň určíme ID objektu. Vertikální souřadnice určuji absolutně, to znamená, že při jiném rozlišení může dojít k menší kolizi.

Další funkce, kterou musíme vytvořit, je obslužná funkce pro menu. Tu musíme deklarovat takto:

HRESULT CALLBACK ProcessMenu(void *_Page, void *_Item, UINT _Action);
 

A naše definice vypadá takto:

HRESULT CALLBACK ProcessMenu(void *_Page, void *_Item, UINT _Action)
{
    CGPage* Page = (CGPage*) _Page;
    CGItem* Item = (CGItem*) _Item;

    if(!Page) {
        return ERROR_SUCCESS;
    }

    if(Item->GetState()->GetID() != BS_DISABLE) {
        switch(Page->GetID()) {
        case PAGE_MAIN:
                case BUTTON_OPTIONS:
                    if(_Action == IA_MOUSECLICK_UP) {
                        menSetVisiblePage(PAGE_OPTIONS);
                    }
                    break;
                case BUTTON_EXIT: // EXIT GAME
                    if(_Action == IA_MOUSECLICK_UP) {
                        menSetVisiblePage(PAGE_EXIT);
                    }
                    break;
                }
                break;
        case PAGE_EXIT:
            switch(Item->GetID()) {
                case BUTTON_YES:
                    if(_Action == IA_MOUSECLICK_UP) {
                        PostMessage(g_hWnd, WM_CLOSE, 0, 0);
                    }
                    break;
                case BUTTON_NO:
                    if(_Action == IA_MOUSECLICK_UP) {
                        menSetVisiblePage(PAGE_MAIN);
                    }
                    break;
             }
             break;
        case PAGE_OPTIONS:
            switch(Item->GetID()) {
                case BUTTON_DONE:
                    if(_Action == IA_MOUSECLICK_UP) {
                        menSetVisiblePage(PAGE_MAIN);
                    }
                    break;
            }
            break;
        }
    }
    return 0;
}

Za prvé si přetypujeme ukazatele na stránku a prvek, abychom s nimi mohli rovnou pracovat jako s objekty CGPage a CGItem. Dále musíme vrátit ERROR_SUCCESS pokud je ukazatel na stránku NULL (to je určeno pro testování funkce). Rozdělíme si funkci na bloky-stránky a každý tento blok ještě rozdělíme na podbloky-prvky (tlačítka). O prvek se budeme starat jen tehdy, pokud není v nepřístupném stavu.

Na úplný závěr ještě lehce modifikujeme metodu UpdateFrame():

void UpdateFrame()
{
    disUpdateBackground();

    inpProcessInput();

    // Pri stisknuti klavesy Esc ukoncime aplikaci
    if(inpIsKeyDown(DIK_ESCAPE, FALSE)) {
        PostMessage(g_hWnd, WM_DESTROY, 0, 0);
    }
    menTestMouseMove(inpGetCursor());
    if(inpIsLButtonDown()) {
        menTestMouseClick(inpGetCursor(), LEFT_MOUSE_BUTTON, BA_DOWN);
    }
    if(inpIsLButtonUp()) {
        menTestMouseClick(inpGetCursor(), LEFT_MOUSE_BUTTON, BA_UP);
    }

    menUpdateMenu();
 
   inpUpdateCursor();

    disPresent();
}

Zde musíme za prvé testovat pohyb myši a také stisk levého tlačítka. Samozřejmě také musíme menu vykreslit pomocí funkce menUpdateMenu().

16.3. Závěr

Tak a jsme u konce našeho mega-příkladu. Aplikace je tvořena tak, aby šla libovolně rozšířit nejen o další prvky menu, ale i o další možnosti.

Spuštěná aplikace by mohla vypadat nějak takto:

Grafika je použitá z mého předešlého projektu a najdete ji v datovém souboru na CD. Grafiku vytvořil Michal Burišin.

V příští lekci bych chtěl přidat ještě jednu knihovnu Audio.dll. Pomocí této knihovny zakomponujeme do našeho příkladu zvuk a hudbu. Knihovna využívá komponentu DirectMusic.

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

Jiří Formánek