Jak vyzrát na GUI 3.díl - Tlačítka, menu, atd. - Builder.cz - Informacni server o programovani

Odběr fotomagazínu

Fotografický magazín "iZIN IDIF" každý týden ve Vašem e-mailu.
Co nového ve světě fotografie!

 

Zadejte Vaši e-mailovou adresu:

Kamarád fotí rád?

Přihlas ho k odběru fotomagazínu!

 

Zadejte e-mailovou adresu kamaráda:



C/C++

Jak vyzrát na GUI 3.díl - Tlačítka, menu, atd.

GUI

23. září 2002, 00.00 | Pokračování seriálu o programování grafického uživatelského rozhraní. Dnes nás čeká naprogramování základního objektu GUI - tlačítka a další, neméně důležité věci.

{Objekt C_BUTTON}

Po delší (opravdu delší) pauze jsem opět zde s dalším dílem seriálu Jak vyzrát na GUI. Všem, kteří jste četli mé předchozí články a netrpělivě jste čekali na další díl se velmi omlouvám za zpoždění. Ale teď už k věci. Pokud se nepletu, v minulém díle jsem vám slíbil, že se dnes budeme zabývat tlačítky, meny, atd. V tuto chvíli již máme představu o podobě skriptu GUI a nadefinovali jsme si základní objekt C_GUI. Nemáme ovšem nadefinováný ostatní objekty jako C_BUTTON, C_LABEL, C_MENU a přístupové funkce.

Objekt C_BUTTON
Tento objekt bude asi pro spoustu programátorů důležitý. Bez ostatních jako např. obrázek, popisek se obejdeme, ale já si myslím, že pokud chcete něco odkliknout (hlášku napíšete snadno), je to bez tlačítka dosti obtížné :-) Začněme tedy tímto objektem a hned jeho definicí. Jako každý objekt našeho GUI bude odvozen od předka C_GUI_OBJECT. Jeho definici naleznete v předchozím článku.

class C_BUTTON : public C_GUI_OBJECT
{
public:
   char caption[GUI_CAPTION_LEN_MAX];  // text na tlacitku

   int Draw(void);  //vykreslujici fce. (byla jiz nadefinovana v objektu C_GUI_OBJECT, ale pouze jako virtualni
   int Exec_Command(void);  //fce. ktera spusti fci. nadefinovanou ve skriptu po stisku tlacitka
   int Test(int mouse_x,int mouse_y,int left_but);  //fce. ktera otestuje mys a objekt
};

V podstatě se nám budou těla fci.(pouze s menšími změnami) opakovat i u ostatních objektu. Aby jsme si zjednodušili práci s kreslením rámečků (v podstatě jsou u každého objetu), nadefinujeme si jednoduchou fci., které řekneme kam má rámeček vykreslit a jakou má mít barvu. Jelikož toto GUI není vyhrazeno pouze pro OpenGL, ale i pro ostatní knihovny, bude k přepsání vykreslovací fce. stačit úprava fce. kreslící rámečky a vypisující text. To značně ulehčí přepisování a zvětší přehlednost, oproti způsobu, kde by každý objekt obsahoval vlastní kus kódu zabývající se vykreslováním.

void Draw_Rect(int x,int y,int width,int height,T_COLOR_RGBA color,int mode,int enable_blending = TRUE)
{
  int minus = 0;

  if(mode == GUI_LINE)  //pokud chceme kreslit cary
  {
    minus = 1;   //toto je pouze kvuli OpenGL (zmensime sirku o 1, protoze kresli okraj polygonu
    glPolygonMode(GL_FRONT_AND_BACK,GL_LINE); //nastavime vykreslovani polygonu na cary
  }

  if(color[3] > 0.0f)  // pokud je pruhlednost vetsi nez 0
  {
    if(color[3] < 1.0f && enable_blending == TRUE) engine.renderer.Enable_Blending(GUI_BLEND); else engine.renderer.Disable_Blending(); //a mensi nez 1 nebo pokud pruheldnost vyzadujeme (enable_blending), zapneme pruhlednost, pokud jedna z techto podminek neplati, pruheldnost vypneme
    glDisable(GL_ALPHA_TEST);  //vypneme ALPHA-TESTING (OpenGL - klicovani bitmap)

    glColor4fv(color); //nastavime barvu

    glDisable(GL_TEXTURE_2D);  // vypneme texturovani (OpenGL)
    glRecti(x,engine.canvas.Convert_Y(y),x + width - minus,engine.canvas.Convert_Y(y + height)); //vykreslime ramecek
  }

  if(mode == GUI_LINE) glPolygonMode(GL_FRONT_AND_BACK,GL_FILL);  //pokud jsme nastavili vykreslovani polygonu na cary, meli bychom to vratit zpet
};

Pokud budeme chtít vykreslit například modrou plochu o rozměrech 100x100 na pozici [20,20] se žlutým rámečkem, budeme postupovat asi takto :

{
  ...
  ...
  T_COLOR_RGBA modra = {0,0,1,1}; //pruhlednost je 0% = 100% viditelnosti = 1
  T_COLOR_RGBA zluta = {1,1,0,1};

  Draw_Rect(20,20,100,100,modra,GUI_FILL);  //plocha, parametr enable_blend se implicitni nastavi na TRUE, ale pruhlednost barvy je 0, takze se pruhlednost nezaplne
  Draw_Rect(20,20,100,100,modra,GUI_LINE); //ramecek
  ...
  ...
};

Tak toto by byl nutný zaklad, ale stále nám schází jednotlivé metody. Mohli by jsme začít například funckí / metodou pro vykreslování C_BUTTON::Draw() Pokud si vzpomínáte na minulý článek, na konci jsme probírali barevná schémata nastavovaná ve skriptu, pro každé menu zvlášť. Pro nás teď budou podstatné barvy, které začínají na #line_ (#line_color, #line_move_color, #line_down_color), stejné je to u #button_ a #text_. Co vlastně znamenají jednotlivé přípony?

color - standartní barva, která se používá při vykreslování
move_color -  barva, která se použije při pohybu myší nad objektem (highlight efekt)
down_color -  barva při stisku levého myšítka nad objektem

Toto schéma se bude stále opakovat i u ostatních objektů, ale pojďme se již podívat na samotnou vykreslovací fci.

int C_BUTTON::Draw(void)
{
  C_MENU * menu;  //toto je matersky objekt (ono menu, ktere bylo jiz ve skriptu)
  int tex_x,tex_y;  // pozice textu [x,y]
  int flag = GUI_NORMAL; //vykreslovaci parametr

  if(used == FALSE) return FCE_ERROR;  //pokud je polozka nepouzita, neni co vykreslovat
  if(gui.object_array[owner_ID] == NULL) return FCE_ERROR;  //pokud nema matersky objekt, plati to same
  menu = gui.Menu(owner_ID);  // naplnime pointer menu materskym objektem

  glLoadIdentity();  //OpenGL (vymazeme transformacni matici)

  if(mouse_state == GUI_MOUSE_DOWN) flag = GUI_DOWN; // pokud je tlacitko stisknute, vybereme patricnou barvu
  if(mouse_state == GUI_MOUSE_MOVE) flag = GUI_HIGHLIGHTED; //pokud je mys nad tlacitkem,  ---| |---


  //OpenGL (nastaveni parametru pro blending / pruhlednost - mix barev)
  glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);

  //vykreslime barevne pozadi tlacitka
  Draw_Rect(x,y,width,height - 1,menu->button_color[flag],GUI_FILL);

  // vykreslime ramecek okolo tlacitka
  Draw_Rect(x,y,width,height,menu->line_color[flag],GUI_LINE);

  //vypiseme napis na tlacitku
  glEnable(GL_TEXTURE_2D);  // OpenGL (zapneme texturovani, texturu mame jiz nabindovanou)
  glBlendFunc(GL_SRC_ALPHA,GL_ONE);   //OpenGL (nastaveni parametru pro blending / pruhlednost - additivni blending)

  tex_x = x + width / 2 - strlen(caption) * engine.canvas.Get_Char_Width(font_size) / 2 - (4 * font_size); //pokusime se vycentrovat napis na stred tlacitka
  tex_y = y + height / 2 - engine.canvas.Get_Char_Height(font_size) / 2;

  OutTextXY(tex_x,tex_y,caption,menu->text_color[flag],font_size);  //vypiseme napis

  return FCE_OK;
};

No, asi jste si všimli, že je tu dost neznámých věcí. Vezmeme to hezky po pořádku. O třídě C_MENU jsem se zmínil v minulém článku, její definici si uvedeme níže. Takže zatím, prosím, nezoufejte, vše bude vysvětleno. Někdo by se mohl pozastavit nad aktivováním texturování (glEnable(GL_TEXTURE_2D);) bez předešlého "nabindování" textury (glBindTexture();). Toto se děje při zavolání metody objekty C_GUI::Draw(); (celkové vykreslení GUI). My totiž předpokládáme, že se textura v průběhu vykreslování GUI nezmění. A co funkce OutTextXY();? Pokud si vzpomínáte na funkce OutText(); a MoveText(); Nebudete mít s touto funkcí problémy. Jsou to totiž ony dvě funkce v jedné, s tím rozdílem, že se automaticky neodřádkovává text. A kde jsme vzali proměnnou mouse_state? Ta se naplní ve funkci C_BUTTON::Test();

int C_BUTTON::Test(int mouse_x,int mouse_y,int left_but)  // left_but je promena do ktere se uklada aktualni stav leveho mysitka (nahore, dole)
{
  C_MENU * menu; //matersky objekt
  menu = gui.Menu(owner_ID);  // naplnime ukazatel (je to stejne jako u fce. Draw();

  if(menu->visible != TRUE || used != TRUE) return FCE_ERROR_NEG;  // pokud neni menu viditelne, nebo pokud objekt neni aktivni / pouzit, neni co testovat a oznamime, ze doslo k chybe (testovani nepouziteho / neviditelneho objektu

  if(mouse_x >= x && mouse_x <= x + width && mouse_y >= y && mouse_y <= y + height)  //pokud lezi kursor mysi nad objektem
  {
    if(left_but == GUI_BUTTON_UP && gui.focus == this)  //pokud je tlacitko uvolnene / nahore a jako aktivni / zamereny / posledni vybrany objekt byl vybrany tento (focus == this), znamena to, ze jsme v minulem kroku tlacitko stiskli a ted uvolnili --> musime spustit fci. tlacitka
    {
      mouse_state = GUI_LEFT_MOUSE; //oznamime, ze leve tlacitko bylo stisknuto a uvolneno
      return FCE_OK;  // vse probehlo v poradku, tak to take oznamime
    }

    if(left_but == GUI_BUTTON_DOWN && (gui.focus == NULL || gui.focus == this)) // pokud je ale tlacitko dole / stisknute A neni zadny vybrany objekt (focus == NULL) nebo je vybrany tento objekt, ale stale tlacitko drzime (focus == this), oznamime, ze je tlacitko stisknute (zvolime barevne schema) a nastavime focus na this (focus = this), pro pripad, ze tlacitko nebylo jeste stisknuto (focus == NULL)
    {
      mouse_state = GUI_MOUSE_DOWN;
      gui.focus = this;
      return FCE_OK;
    }

//zde je uz jasne, ze zadne tlacitko stisknute neni, ale kursor je nad objektem, tak vybereme barevne schema pro nasviceni (highlight - move_color)
    {
      mouse_state = GUI_MOUSE_MOVE;
      return FCE_OK;
    }
  };

  mouse_state = GUI_NOTHING;  // kursor mysi neni nad objektem, tak neni co resit :-)
  return FCE_OK;
};

To je testovací funkce. Ostatní budou v podstatě podobné, až na menší změny. Poslední funkcí z tohoto objektu, kterou jsem ještě nezmínil je C_BUTTON::Exec_Command(); Tato funkce pouze spustí příslušnou funkci přiřazenou ve skriptu. Jedinou věcí, na kterou by jsme si měli dát pozor je : Tato funkce se volá až po otestování a vykreslení všech objektů a před vymazáním ukazatele na aktivní / vybraný objekt (focus). Umístíme ji tedy do fce. C_GUI::Draw(); Ale to až dál.

int C_BUTTON::Exec_Command(void)
{
  if(gui.focus == this && mouse_state == GUI_LEFT_MOUSE)  //toto je tu jen pro jistotu, zda je vybrany objekt skutecne ten pravy a zda na nem bylo stisknute leve mysitko
  {
    gui.focus = NULL;  // vymazeme focus (uz neni potreba a prestava platit v momente uvolneni tlacitka)
    if(function != NULL) function(0,0,0);  //zavolame fci. s parametry 0,0,0 (to nema zadny duvod, proste se mi ta cisla libila, stejne je zatim nepouzijeme)
  };

  return FCE_OK;
};

A jak vypadá praxe? První obrázek je tlačítko v klidu, na druhém je zvýrazněné a na třetím je při stisknutém levém myšítku. Grafická úprava a vykreslení je pak už na každém. Pokud chce někdo obrázek na pozadí, stačí upravit fci. Draw_Rect(). :-)

[-more-]{Mateřský objekt C_MENU} Mateřský objekt C_MENU
O tomto objektu jsem se zmiňoval již vícekrát a konečně je zde. Na začátek musím říci, že to opravdu nebude žádná věda, protože nám poslouží jako jakýsi prostor pro informace. Jedinou věcí, proč ho vlastně potřebujeme je roztřídění jednotlivých tlačítek do menu a určení barev platných pro každé menu zvlášť. A to je v podstatě vše. Teď už je vám jasné, že to žádna věda není, definice objektu vypadá takto :

class C_MENU : public C_GUI_OBJECT
{
public:
  T_COLOR_RGBA line_color[3];  // barva car (normalni, nasvicena, stisknuta)
  T_COLOR_RGBA button_color[3];  //barva pozadi tlacitek
  T_COLOR_RGBA text_color[3];  // barva textu
  T_COLOR_RGBA scrbar_color[1]; // barva pozadi scrollbaru (ale to az dale)
  T_COLOR_RGBA edit_color[3]; //normal, highlighted, selected // barva pozadi editboxu ( ---| |--- )
  T_COLOR_RGBA edit_font_color[3]; //normal, highlighted, selected // barva textu v editboxu a listboxu

  int Draw(void) {return FCE_OK;}; // nic nevykreslujeme
  int Test(int mouse_x,int mouse_y,int left_but) {return FCE_OK;};  // nic netestujeme

  int Hide() {visible = FALSE; return FCE_OK;};  // funkce pro schovani MENU, promenna visible je soucast predka C_GUI_OBJECT
  int Show(){visible = TRUE; return FCE_OK;}; //funkce pro zviditelneni MENU
};

Pokud jste pozorně četli minulý článek, bude vám jasné, proč se i tento objekt odvozuje od C_GUI_OBJECT. Pokud ne, tak vám to vysvětlím. Ve třídě C_GUI je obsaženo pole ukazatelů na tento objekt. Pokud my nějaký objekt od něj odvodíme, v tomto případě C_MENU, můžeme tento odvozený objekt zařadit do pole, ale můžeme přistupovat pouze k funkcím a proměnným, které zdědil (Draw(), Test()). Ty jsou totiž shodné jak pro předka tak i pro potomka. Pokud by jsme na poziti 0 uložili objekt C_MENU. Můžeme k němu přistupovat asi takto :

gui.object_array[0]->Draw();
gui.object_array[0]->Test();
...

Pokud bychom ale chtěli přistoupit k funkcím objektu C_MENU, musíme položku 0 přetypovat na typ (C_MENU*)

(C_MENU*)(gui.object_array[0])->Show();
...

Tento zápis je poněkud kostrbatý a občas jsou s ním i problémy. Proto přidáme do objektu C_GUI tyto funkce :

class C_GUI
{
public :
  ...
  ...
  C_BUTTON* Button(int ID);  // vrati ukazatel na tlacitko podle jeho indexu v poli
  C_BUTTON* Button(char name[]); // vrati ukazatel na tlacitko podle jeho jmena

  C_MENU* Menu(int ID);  // vrati ukazatel na menu
  C_MENU* Menu(char name[]);
  ...
  ...
};

Podrobně si rozepíšeme funkce pro C_BUTTON, ostatní je už pouhá analogie, jak pro C_MENU, tak i pro popisky, obrázky, atd.

C_BUTTON * C_GUI::Button(int ID)
{
  if(ID >= 0 && ID < GUI_OBJECT_MAX)  // otestujeme zda index lezi v mezich pole
  {
   if(object_array[ID] != NULL) // pokud je na dannem miste v poli ulozen nejaky ukazatel na objekt, pokracujeme
   if(object_array[ID]->used == TRUE && object_array[ID]->type == GUI_BUTTON) return((C_BUTTON*)object_array[ID]);  // pokud je objekt pouzit a pokud sedi jeho ID, vratime na nej  ukazatel
  };

  fprintf(stderr,"ERROR: Trying to use non-existing button [%d]\n",ID);  // jinak vypisme chybu
  return &err_button;  // a vratime ukazatel na chybovy objekt (viz. nize)
};

C_BUTTON * C_GUI::Button(char name[])
{
  int i;

  for(i = 0; i < GUI_OBJECT_MAX; i++)  // v podstate je to to same, akorat projedeme pole objektu a porovname jmena, jakmile najdeme shodu, mame vyhrano
  {
   if(gui.object_array[i] != NULL)
   if(strcmp(name,object_array[i]->name) == 0) return((C_BUTTON*)object_array[i]);
  };

  fprintf(stderr,"ERROR: Trying to use non-existing button [%s]\n",name);
  return &err_button;
};

Zapomněl jsem připomenout jednu důležitou věc. Pokud budeme takto ukládat do pole zděděné objekty a přistupovat k nim pomocí přetypování, může se stát, že se jedno hezkého dne spleteme a omylem přetypujeme objekt A na objekt B a program spadne. Proto jsem do C_GUI_OBJECT přidal proměnnou type, do které uložíme ID objektu (viz. minulý článek->definice). Pak pouze zkontrolujeme, jestli vracime ID == GUI_BUTTON, atd. A co chybový objekt? Co to je? Ano, zni to možná trochu divně. Představte si tuto situaci :

{
  C_BUTTON * tlacitko;

  tlacitko = gui.Button(50);
  strcpy(tlacitko->caption,"POKUS");
};

My tedy chceme získat ukazatel na tlačítko, které je uloženo na 50té pozici v našem poli. A potom změnit popisek tlačítka. Vše bude v pořádku až do chvíle, kdy buď bude položka 50 patřit jinému objektu (fce. by tedy měla vrátit NULL) nebo 50 bude mimo rozsah pole nebo položka nebude aktivní / použita. Ve všech třech případech by fce. měla vrátit onen NULL a při dalším kroku by program nejspíše zkolaboval. Zde se nabízejí dvě možnosti jak tento problém ošetřit :

{
  C_BUTTON * tlacitko;

  tlacitko = gui.Button(50);

  if(tlacitko != NULL)
  {
    strcpy(tlacitko->caption,"POKUS");
  }
};

To by šlo, ale já jsem člověk od přírody líný, tak jsem tu udělal asi takto. V unitě pro GUI jsem si nadefinoval pro každý objekt tzv. err_object, který je tam pouze pro případ podobné situace. Pokud k ni dojde, fce. nevrátí NULL, ale ukazatel na tento err_object a já s ním můžu vyvádět psí kusy a program se nezhroutí (snad).

C_BUTTON err_button;
C_MENU err_menu;

{
  C_BUTTON * tlacitko;

  tlacitko = gui.Button(50);
  strcpy(tlacitko->caption,"POKUS");  //pokud nastane onen problem, zmenim jmeno u chyboveho objektu a nic se nedeje. Pouze do stderr.txt to ulozi varovani
};

A je to! Máme tu základní objekty, které jsou již skoro schopny práce. Zbývá nám nahrávání skriptů a obhospodařování GUI. Je to poněkud obsáhlejší, tak vás pro dnešek nechám oddychnout a těším se na vás příště.

Obsah seriálu (více o seriálu):

Tématické zařazení:

 » Rubriky  » C/C++  

 

 

 

Nejčtenější články
Nejlépe hodnocené články

 

Přihlášení k mému účtu

Uživatelské jméno:

Heslo: