Vala 5: GTK+ Kontejnery

V předchozí kapitole jsem otevřel grafickou knihovnu GTK+. Napsali jsme první aplikaci, která něco dělala. Dnes budeme pokračovat, tentokrát kontejnery. Ukážeme si tři nejzákladnější a řekneme si něco o tzv. balení widgetů v GTK+.

13.3.2014 13:00 | Ondřej Tůma | přečteno 14247×

V GTK+, jak jsem psal v minulém dílu, existují kontejnery. Tyto kontejnery vizuálně obsahují další prvky - widgety, třeba další kontejnery. Tím úplně nejvyšším kontejnerem je okno. (Ve skutečnosti to je obrazovka, ale to až někdy jindy.) V něm je další kontejner a v něm další a tak dál, až hierarchie končí u prvku, který je poslední a už další prvek neobsahuje, bez ohledu na to, zda je kontejner či nikoliv. Celá tahle stromová struktura je nakonec vykreslena a zobrazena uživateli.


Kontejner, který obsahuje, tedy obaluje, další prvky se nazývá rodič a prvek který je uvnitř nějakého prvku, tedy je obalen, se nazývá dítě. Toto názvosloví může být matoucí, zejména pokud se bavíme v kontextu objektového programování. Dále proto pokud budu mluvit o objektech, budu používat vazbu rodič – potomek, a pokud o aplikační struktuře, tak vazbu rodič – dítě. Tato struktura a především vazba, je v GTK+ vhodně využita. Jednak se tak můžete přes metodu get_parent () dostat k rodiči, jednak můžete díky metodě show_all () zobrazit všechny děti, pokud nejsou označené jako schované, ale především, destrukce rodiče, díky počítání referencí ničí i děti. Nemusíte se tak starat o úklid a to je hrozně fajn ;)


objektová hierarchie třídy Gtk.Window
Obrázek: objektová hierarchie třídy Gtk.Window.

Packaging model

Některé kontejnery mohou mít jedno dítě, některé dvě, jiné takřka neomezeně. Každý kontejner se svým dítětem může dál vizuálně pracovat. Způsob jak se to děje se nastavuje na dvou místech, a do začátku je nejjednodušší to prostě zkoušet.


Kontejner může nastavit rámeček (border). Rámeček je mezera mezi jím jako obalem a jeho dětmi. Tato mezera se udává v pixelech a je nastavitelná jako jeden údaj pro všechny strany.


Některé kontejnery mohou pro své děti nastavit rozestupy (spacing) a to horizontálně nebo vertikálně. Jde o mezery, které jsou vytvořeny mezi jeho dětmi. Dále mohou nastavit homogennost. To je vlastnost, která pokud je zapnutá, nutí všechny své děti zobrazovat stejnou velikostí. To znamená že v případě mřížky, budou všechny sloupce stejně široké, resp. všechny řádky stejně vysoké.


I dětem lze nastavit nějaké chování uvnitř kontejneru. Je to vodorovné a horizontální zarovnání (align) . To může nabývat hodnot: FILL, START, END, CENTER a BASELINE. Fill způsobí roztažení do celého rodiče. To znamená že například tlačítko bude stejně velké jako jeho rodič. V opačném případě bude tak velký, jak sám potřebuje a je zarovnaný na začátek, konec, nebo na střed pro danou polohu. Tedy horizontálně nebo vertikálně.


Dále lze nastavit tzv. roztahování (expand). To se občas chová podobně jako zarovnání, s tím rozdílem že postupuje vůči svému rodiči. Pokud je nastaveno, požaduje od svého rodiče více prostoru.


V poslední řadě lze nastavit okraje každého prvku v pixelech (margin). Tento okraj není součástí prvku samotného. Okraj, na rozdíl od rámečku, lze nastavit pro každou stranu zvlášť a udává se též v pixelech. Výsledná mezera je pak součet všech hodnot.


Všechny tyto možnosti, zejména pokud jsou správně nastaveny, což implicitně jsou, vedou k tomu, že jsou velikosti prvků přizpůsobovány rozměrům svého rodiče. Díky tomu se tak často u GTK aplikací nesetkáte s tím, že okno má zakázáno měnit velikost. Ono je k tomu totiž velmi málo důvodů.


packaging model
Obrázek: packaging model

Gtk.Box

Jedním z kontejnerů do začátku je Gtk.Box. Box je jednoduchý kontejner, který zobrazuje děti v jednom směru. Tedy horizontálně nebo vertikálně. Do verze 3.1 včetně, bylo běžné používat ještě VBox pro vertikální Box a HBox pro Box horizontální. Toto třídy jsou ale již označeny za zastaralé (deprecated).


public class Application: Gtk.Window {          // potomek třídy Gtk.Window
    private bool hpos;
    private bool vpos;

    public Application () {
        hpos = true;
        vpos = true;
        destroy.connect (Gtk.main_quit);        // na signál destroy zavěsím ukončení gtk

                                                // první vertikální box
        var vbox = new Gtk.Box (Gtk.Orientation.VERTICAL, 5);
        add (vbox);                             // okno přidává prvek metodou add

                                                // druhý horizontální
        var hbox = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 2);
        vbox.pack_start (hbox);                 // pack_start přidá dítě na první volnou pozici od začátku

        var konec = new Gtk.Button.with_label ("Konec");
        konec.clicked.connect (Gtk.main_quit);  // na talčítko zavěsíme ukončení gtk
        hbox.pack_start (konec);

        var haha1 = new Gtk.Button.with_label ("Ha ha :)");
        haha1.clicked.connect (swap1);          // na tlačítko zavěsíme metodu swap1
        hbox.pack_start (haha1);

        var haha2 = new Gtk.Button.with_label ("Ha ha :)");
        haha2.clicked.connect (swap2);          // na tlačítko zavěsíme metodu swap1
        vbox.pack_start (haha2);
    }

    // do proměnné w bude uloženo tlačíko, na kterém událost vznikla
    private void swap1 (Gtk.Widget w) {
        hpos = !hpos;
        var hbox = w.get_parent () as Gtk.Box;  // přetypování na Gtk.Box
        hbox.reorder_child (w, (int) hpos);     // přehození dětí
    }

    private void swap2 (Gtk.Widget w) {
        vpos = !vpos;
        var hbox = w.get_parent () as Gtk.Box;
        hbox.reorder_child (w, (int) vpos);
    }

    public static int main (string args[]) {
        Gtk.init (ref args);
        var app = new Application();
        app.show_all ();                        // metodou show_all zobrazíme všechny děti v okně
        Gtk.main ();
        return 0;
    }
}
Soubor: box.vala
$~ valac box.vala --pkg=gtk+-3.0

V minulém díle tohoto seriálu jsem Vám slíbil jiný přístup k hlavnímu objektu. Tady je. Třída Application, jejíž instanci vytvářím ve statické metodě main je potomek třídy Gtk.Window. Tomu nic nebrání, ba naopak. Snadno si tak mohu dovolit přístup k proměněným instance v metodách swap1 a swap2.


Kód by měl být celkem jasný, proberu tak ty nejzajímavější části. Definování třídy, včetně klíčových slov public nebo private. Ty se ostatně chovají tak, jak by jste očekávali. V konstruktoru a v metodách nepoužívám klíčové slovo this. V podstatě stejně jako v C++ mě k tomu nic nenutí. Pokud bych ale potřeboval pracovat ještě s nějakou lokální proměnou, pak bych this použil pro upřesnění, že jde o proměnou instance. Je ale dobré jej používat, protože je hned zřejmé o jakou proměnou jde.


Jak jsem již psal, není třeba nějak zvlášť držet prvek v paměti, pokud je přidán do kontejneru. Proto nemá objekt proměnou vbox ani hbox, místo toho používám jen lokální proměnou. Pravda je, že jí pak musím získat v metodách swap1 a swap2 přes rodiče prvku, na který bylo kliknuto. Pro studijní účely je to ale vhodnější.


Pokud se podíváte do dokumentace k signálu pro jazyk C , zjistíte, že do signálu jsou posílány dvě informace. První je widget, v jazyku Vala tedy objekt třídy Gtk.Widget a druhý je pointer na uživatelská data. Ten je nám skrytý, protože Vala ho využívá pro pointer na instanci objektu, ze které byla metoda na událost navěšena. V našem případě to je tedy objekt app. To znamená, že do metody se tímto způsobem přenese this.


Pokud budete srovnávat zavěšení metody a anonymní metody (lambda funkce) na signál, zjistíte, že anonymní funkce nevyžaduje typ a dokonce ani plný počet vstupních parametrů, ale naše metoda ho mít musí. To je snadné vysvětlit. Zatímco námi definovaná metoda, např. swap1 může být volána odkudkoli, a vlastně nemusí být ani na žádný signál napojena, musíme ji definovat čistě a správně. Anonymní metoda je však vytvářená signálu na míru, a typy nebo chybějící parametry obstará Vala za nás.


V metodách swap1 a swap2 provádím dynamické přetypování objektu. Toto je celkem častá operace, především v metodách, volaných z nějakého signálu, právě proto, že na vstupu metody je pouze Gtk.Widget. Pokud bych chtěl s tímto objektem pracovat jako s jeho skutečnou třídou, musel bych si ho přetypovat. Bez přetypování mám k dispozici jen takové metody, které jsou dostupné třídě Gtk.Widget, což get_parent () je. Touto metodou získám kontejner, ve kterém se tlačítko nachází. Na kontejneru ale potřebuji volat už metodu dostupnou jen kontejneru, proto si návratový objekt z metody get_parent () musím přetypovat na Gtk.Box, neboť get_parent () také vrací jen Gtk.Widget. V obou metodách tak malým trikem změním pozici tlačítka z 1 na 0 a zpět.

Ve statické metodě main tradičně inicializuji GTK+, vytvořím objekt app, což je vlastně okno, zobrazím okno a rekurzivně všechny jeho děti a pustím GTK+.


Gtk.Grid (Gtk.Table)

V prvním dnešním příkladu jsme si ukázali, jak se dá používat nejběžnější kontejner. Velmi často Vám ale Box nestačí. Můžete tak sice sestrojit libovolně vypadají formuláře, je to ale krkolomné plýtvání prostředky. Aby ne, takový formulář má často podobu tabulky, a tak musíte do každého z řádků vertikálního boxu přidávat box horizontální. Na formuláře a ne jen na ně, se nám výborně hodí Gtk.Grid. Je to tabulkový kontejner, do kterého jen stačí přidávat prvky. Gtk.Grid je v GTK+ novým kontejnerem, a je náhradou Gtk.Table, jenž je od verze 3.4 označen jako zastaralý.


public class Application: Gtk.Window {
    public Application () {
        destroy.connect (Gtk.main_quit);

        var grid = new Gtk.Grid ();
        add (grid);

        var konec = new Gtk.Button.with_label ("Konec");
        konec.clicked.connect (Gtk.main_quit);
        // první sloupec, první řádek, jeden sloupec široký, jeden řádek vysoký
        grid.attach (konec, 0, 0, 1, 1);

        var nic1 = new Gtk.Button.with_label ("A nic");
        // durhá sloupec, druhý řádek, jeden sloupec široký, jeden řádek vysoký
        grid.attach (nic1, 1, 1, 1, 1);

        // třetí sloupec, třetí řádek, jeden sloupec široký, jeden řádek vysoký
        var nic2 = new Gtk.Button.with_label ("A nic");
        grid.attach (nic2, 2, 2, 1, 1);

        // první sloupec, čtvrtý řádek, tři sloupece široký, jeden řádek vysoký
        var nic3 = new Gtk.Button.with_label ("A nic");
        grid.attach (nic3, 0, 3, 3, 1);
    }

    public static int main (string args[]) {
        Gtk.init (ref args);
        var app = new Application();
        app.show_all ();
        Gtk.main ();
        return 0;
    }
}
Soubor: grid.vala
$~ valac grid.vala --pkg=gtk+-3.0

Příklad jsem výrazně zjednodušil ve prospěch kontejneru Gtk.Grid. Grid je velmi jednoduchý a typické použití je prostě vložit prvek na pozici v mřížce. Vedle pozice je ještě pro grid důležitá informace, jak velký prvek je, tedy jak zasahuje do ostatních buněk. Může se rozkládat jak přes více řádků, tak přes více sloupců. Grid pak stejně jako Box pracuje homogenně vůči svému rodiči, což lze ověřit změnou velikosti okna.


Gtk.Fixed

Pokud z nějakého důvodu potřebujete prvek usadit na konkrétním místě, nechcete jej posouvat se změnou velikostí rodiče, nebo prostě máte rádi absolutní umístění, máte jedinečnou příležitost vyzkoušet Gtk.Fixed. Je to velmi jednoduchý kontejner, který umí umístit prvek na nějaké místo a přesunout jej na jiné.


public class Application: Gtk.Window {
    private Gtk.Fixed fixed;
    private int x;
    private int y;

    public Application () {
        set_size_request (400, 400);            // nastavim velikost prvku (okna)
        set_resizable (false);                  // oknu se nedá měnit velikost
        destroy.connect (Gtk.main_quit);

        x = y = 150;
        fixed = new Gtk.Fixed ();               // kontejner fixed
        add (fixed);

        var konec = new Gtk.Button.with_label ("Konec");
        konec.clicked.connect (Gtk.main_quit);
        fixed.put (konec, 160, 180);            // do fixed se vkládá absolutně
                                                // hodnoty x a y jsou v pixelech

        var entity = new Entity ();
        entity.up.connect ((w) => {             // signál up
                    y = (y > 0) ? y-10 : 300;   // změní souřadnice y
                    fixed.move (w, x, y);       // a posune entitu na novou pozici
                });
        entity.down.connect ((w) => {
                    y = (y < 300) ? y+10 : 0;
                    fixed.move (w, x, y);
                });
        entity.left.connect ((w) => {
                    x = (x > 0) ? x-10 : 300;
                    fixed.move (w, x, y);
                });
        entity.right.connect ((w) => {
                    x = (x < 300) ? x+10 : 0;
                    fixed.move (w, x, y);
                });

        fixed.put (entity, x, y);       // prvky se v kontejneru překrývají dle pořadí
                                        // vložení, entity je poslední, tedy nejvýš
    }
Část souboru: fixed.vala

Kód z dnešní poslední ukázky již není do článku vložen celý, pro jeho kompilaci si jej prosím stáhněte. Poté můžete opět kompilovat standardním způsobem.


$~ valac fixed.vala --pkg=gtk+-3.0

Pokud nahlédnete do celého kódu, najdete tentokrát dva objekty. První Entity, je potomkem kontejneru Gtk.Grid. Proti svému rodiči navíc obsahuje čtyři tlačítka, vyjadřující 4 směry. Tyto tlačítka jsou svázány se čtyřmi veřejně dostupnými signály. To proto, abychom je v dalším objektu Application mohli použít. Samozřejmě bychom mohli použít přímo tlačítka, ale to by nebyla taková legrace.


Application je potomek okna, to už známe, obsahuje jediný kontejner Gtk.Fixed, to také známe, a do něj jsou umístěny dva objekty. Tlačítko pro ukončení a přes něj objekt entity, tedy 4 další tlačítka. Na každý signál objektu entity navážeme anonymní metodu, jenž na vstupu dostane samotný objekt entity. Každá metoda provede nový výpočet souřadnice a následně posune prvek na souřadnici novou. Souřadnice se udávají číselnou hodnotou v pixelech. Možná právě proto, se tento kontejner tak moc nepoužívá, přeci jen, velikost ostatních prvků v pixelech je závislá na velikosti fontů a vzhledu vůbec. Zbývá dodat, že Fixed tím pádem neumí nastavit homogennost nebo rozestupy.


Prvotní spuštění příkladu, by mělo vykreslit tlačítko konec co nejvíce ve středu okna a přes něj by měl být vykreslen objekt entity, samozřejmě v závislosti na Vašem vzhledu GTK+ aplikací. To proto, že Gtk.Fixed vykresluje děti tak, jak jsou přidávány metodou put.


Gtk.Paned, Gtk.Notebook a ty další

Gtk samozřejmě obsahuje i další kontejnery. Okna si probereme v některé z příštích kapitol. Další zajímavým kontejnerem je Gtk.Panned. To je kontejner pro dva prvky. Mezi těmito to prvky je horizontální nebo vertikální posuvný oddělovač. Znáte jej například z mail klientů, nápovědy atd.


Druhý zajímavý prvek, na který snad zbude v tomto seriálu místo je Gtk.Notebook. Překlady na tento prvek i jeho děti se různí. Rodiči tedy budu říkat notebook, dětem pak listy, což je typické například pro tabulkové procesory. Prvek mezi běžné uživatele rozšířili zejména prohlížeče v posledních pár letech, dětem notebooku říkají panely. Konec konců, je to asi jediný kontejner, jenž má pojmenované i své děti.


GTK+ však s kontejnery nekončí. Najdete zde ještě několik různě speciálních kontejnerů. Většinou však jde o rozhraní, nebo další speciální třídy, určené pro magické operace se svými dětmi. Ty nechám ke studii laskavému čtenáři přímo v dokumentaci.


Co nás čeká příště ?

Dnešní díl se byl z hlediska GTK+ podstatný, právě kvůli chování kontejnerů a jejich dětí. Proto, budeme moci v příštím díle přistoupit k další velké kapitole, k uživatelskému vstupu. Ten jsme sice nakousli v podobě tlačítek, ale v budoucnu by jsme si s nimi moc nevystačily. I nadále připomínám existenci diskuzního fóra pod článkem, jehož případné využití vítám.


Online verze článku: http://www.linuxsoft.cz/article.php?id_article=2000