V minulém dílu jsme si řekli, jakým způsobem v Javě vytvoříme jednoduchou grafickou aplikaci. Dnes si ukážeme několik dalších komponent a ukážeme si, jakým způsobem jim lze přiřazovat chování. Hlavně si však předvedeme několik předdefinovaných správců rozmístění (layout managerů), které nám umožní skládat jednotlivé komponenty do smysluplných celků.

Přiřazování akcí komponentám

Každé uživatelské rozhraní je postaveno na interakci uživatel-aplikace. Uživatel klikne na tlačítko, aplikace zobrazí dialogové okno, uživatel vyplní data a odklepne okno. Otázkou však zůstává, jak tento proces implementovat.

Naivní řešení by bylo, aby se objekt dialogu nejprve periodicky ptal tlačítka, jestli už bylo zmáčknuto, a až by se dozvěděl, že ano, tak by se zobrazil. Pak by se periodicky ptal formuláře, jestli již byl vyplněn a odklepnut, a v okamžiku kdy ano, tak by se dialog opět zrušil. Toto řešení by však s sebou neslo řadu nevýhod – tou největší je stanovení intervalu, ve kterém se mají komponenty dotazovat na stav. Pokud bychom jej stanovili malý (třeba jednou za sekundu), tak by uživatel pozoroval velké prodlevy. Pokud bychom jej stanovili příliš malý (tisíckrát za vteřinu), tak by tyto dotazy spolehlivě zahltily celou aplikaci.

Návrhový vzor observer

Řešením této zapeklité situace je návrhový vzor observer, který říká, že pozorovatelné komponenty musí fungovat jako registr pozorovatelů. V okamžiku provedení uživatelské akce pak pozorovaná komponenta upozorní všechny registrované odběratele (ve Swingu se pozorovateli říká listener (posluchač)).

Pokud tento vzor vztáhneme na náš příklad s dialogovým oknem, tak se okno nejprve zaregistruje u tlačítka. Tlačítko v okamžiku kliku upozorní okno a toto se zobrazí, aniž by se muselo jakýmkoliv způsobem periodicky dotazovat na stav tlačítka.

Příklad v praxi

Ukažme si výše zmíněný příklad v praxi. Při jeho implementaci využijeme kromě již známé komponenty JFrame také komponentu JButton (tlačítko), JOptionPane (pomocná třída pro tvorbu dialogových oken) a konečně interface ActionListener, pomocí něhož implementujeme akci zobrazení dialogu.

Pro jednoduchost bude dialog obsahovat pouze zprávu obsahující text uvedený na zmáčknutém tlačítku.


    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        //Asynchronne pridej do fronty udalosti zadost o vykresleni GUI
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                createAndShowGUI();
            }
        });
    }
    
    /**
     * Vytvor a zobraz uzivatelske rozhrani
     */
    public static void createAndShowGUI(){
        JFrame frame = new JFrame("JButton frame"); //okno
        
        final JButton button = new JButton("I am a button"); //tlacitku s nazvem
        
        button.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                JOptionPane.showMessageDialog(button, "Button title: " + button.getText(), "Message", JOptionPane.INFORMATION_MESSAGE);
            }
        });
        
        frame.add(button);
        
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); //Pri zmacknuti krizku zavri celou aplikaci (tj. pozabijej vsechna vlakna)       
        frame.pack(); //prizpusob velikost okna
        frame.setLocation(100, 100); //levy horni roh bude na souradnici [100, 100]
        
        frame.setVisible(true); //zobraz okno
    }
Akce na tlačítku
Akce na tlačítku

V metodě main asynchronně zavoláme metodu createAndShowGUI. Připomeňme si, že Swing není vláknově bezpečný a veškeré akce týkající se rozhraní musí být uskutečněny v event dispatching threadu (což zajišťuje právě výše uvedený kód).

V samotné metodě createAndShowGUI vytvoříme uživatelské rozhraní. Nejprve vytvoříme nové okno – JFrame. Poté do něj vložíme nové tlačítko, kterému napřed přiřadíme text při jeho konstrukci a akci voláním addActionListener. Na závěr pak provedeme povinné operace – nastavíme, že po kliknutí na křížek okna dojde k uzavření celé aplikace, přizpůsobíme velikost okna obsaženým komponentám (tlačítku), posuneme okno na pozici [100, 100] a samotné okno zobrazíme.

Implementace akce je velmi jednoduchá. Pouze zavoláme metodu na pomocné třídě JOptionPane. Tato pak zajistí zobrazení modálního okna s příslušným titulkem, textem, informačním piktogramem a tlačítkem na odklepnutí dialogu (více o této třídě se lze dočíst zde). Veškeré operace v listenerech jsou prováděny event dispatching threadem – můžeme zde proto bez obav provádět změny v uživatelském rozhraní.

Dědění vlastních komponent

Jedním ze základních způsobů, jak vytvořit vlastní komponentu, je ji oddědit od nějaké již existující. Pokud bychom htěli naše tlačítko použít vícekrát, tak bychom jej implementovali pomocí veřejné třídy dědící z JButton:

/**
 * Tlacitko zobrazujici pri akci dialog se svym popiskem (textem)
 * @author Pavel Micka
 */
public class TitleJButton extends JButton{
    public TitleJButton(String text) {
        super(text);
        addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                JOptionPane.showMessageDialog(TitleJButton.this, "Button title: " + TitleJButton.this.getText(), "Message", JOptionPane.INFORMATION_MESSAGE);
            }
        });
    }       
}

V tomto případě si vystačíme s překrytím konstruktoru. Nejprve zavoláme konstruktor předka a předáme mu text, který má být vypsán na tlačítku – konstruktor předka bychom v případě Swingu měli volat (téměř) vždy, protože v předkovi obvykle dochází k inicializaci grafického kontextu a datového modelu. Poté přidáme akci, která se provede po kliknutí na tlačítko. Zde si všimněme konstrukce TitleJButton.this, která značí, že se odvoláváme na this kontext obalujícího objektu typu TitleJButton. Prosté volání this by nás odkázalo na kontext objektu listeneru.

JPanel

Všem třídám, které dědí z třídy Container (dokumetace), lze přiřadit layout manager (správce rozmístění). Správce rozmístění zajistí, že přidané podkomponenty budou vyskládány určitým způsobem (vizte dále).

Typickým příkladem kontejneru je JPanel (dokumentace), jehož využíváme primárně pro tvorbu složených komponent. JPanel sám o sobě nemá žádnou vizuální reprezentaci, stejně tak nepřidává žádné chování.

Správci rozmístění

FlowLayout

Nejjednodušším rozmisťovačem je FlowLayout, který umišťuje komponenty do řádku jednu za druhou v pořadí, v němž byly do přidány do daného kontejneru. V případě, že se komponenty nevejdou do jednoho řádku, tak zbylé přetečou do řádku nového.

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        //Asynchronne pridej do fronty udalosti zadost o vykresleni GUI
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                createAndShowGUI();
            }
        });
    }
    
    /**
     * Vytvor a zobraz uzivatelske rozhrani
     */
    public static void createAndShowGUI(){
        JFrame frame = new JFrame("Layout demonstration"); //okno      
        
        frame.add(createLayoutPanel());
        
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); //Pri zmacknuti krizku zavri celou aplikaci (tj. pozabijej vsechna vlakna)       
        frame.pack(); //prizpusob velikost okna
        frame.setLocation(100, 100); //pravy horni roh bude na souradnici [100, 100]
        
        frame.setVisible(true); //zobraz okno
    }
    
    /**
     * Vytvor panel s danym rozmistovacem
     * @return panel
     */
    public static JPanel createLayoutPanel(){
        JPanel panel = new JPanel();
        
        FlowLayout l = new FlowLayout(FlowLayout.CENTER); //zarovnano na stred
        l.setHgap(50); //horizontalni mezera mezi obsazenymi komponentami v pixelech
        
        panel.setLayout(l); 
        
        panel.add(new JButton("First"));
        panel.add(new JButton("Second"));
        panel.add(new JButton("Third"));
                        
        return panel;
    }

Ve výše uvedeném kódu je pro nás zajímavá pouze metoda createLayoutPanel, jež vytváří JPanel s příslušným layoutem a několika obsaženými komponentami (tlačítky). V dalších příkladech správců rozmístění si již budeme uvádět pouze ji.

FlowLayout
FlowLayout

BorderLayout

BorderLayout zarovnává komponenty do světových stran a na střed (sever, jih, východ, západ, střed). BorderLayout je jedním z nejpoužitelnějších správců rozmístění, jelikož téměř každá aplikace má nějakou hlavičku (např. menu umístěné na severu), obsah umístěný na středu a patičku na jihu. Na východě a západě se pak často nachází nějaký typ kontextové navigace (obvykle nějaká stromová komponenta).

Všechny umístěné komponenty jsou vždy roztaženy tak, aby zabraly veškeré dostupné místo.

    public static JPanel createLayoutPanel() {
        JPanel panel = new JPanel();

        BorderLayout l = new BorderLayout();

        panel.setLayout(l);

        panel.add(new JButton("North"), BorderLayout.NORTH);
        panel.add(new JButton("South"), BorderLayout.SOUTH);
        panel.add(new JButton("East"), BorderLayout.EAST);
        panel.add(new JButton("West"), BorderLayout.WEST);
        panel.add(new JButton("Center"), BorderLayout.CENTER);

        return panel;
    }
BorderLayout
BorderLayout
BoxLayout
BoxLayout

BoxLayout

BoxLayout umísťuje komponenty buď do jednoho řádku nebo sloupce podle nastavení, které předáme jeho konstruktoru. Na rozdíl od FlowLayoutu komponenty nikdy nepřetečou do dalšího řádku (sloupce), ale jsou vždy zmenšeny nebo oříznuty tak, aby se vešly do daného prostoru.

Pro tvorbu BoxLayoutu také můžeme použít třídu Box, která nám umožní jednak vytvářet své instance (Box je kontejner, který implicitně obsahuje BoxLayout jako správce rozmístění) a za druhé pak s jeho pomocí můžeme vytvářet neviditelné komponenty tyčí (strut) a tmelu (glue).

Horizontální a vertikální tyče jsou komponenty, které mají buď nulovou výšku, nebo nulovou šířku a zajišťují, že se předchozí a následující komponenty nepřiblíží více, než je délka tyče. Komponenta tmelu se pak snaží zabrat veškerý přebytečný prostor – slouží jako výplň.

    public static JPanel createLayoutPanel() {
        JPanel panel = new JPanel();
        panel.setLayout(new BorderLayout());
        
        Box left = Box.createVerticalBox();
        Box right = Box.createVerticalBox();
        
        panel.add(left, BorderLayout.WEST);
        panel.add(right, BorderLayout.CENTER);
        
        left.add(new JButton("L1"));
        left.add(Box.createVerticalStrut(10));
        left.add(new JButton("L2"));
        left.add(Box.createVerticalStrut(30));
        left.add(new JButton("L3"));
        
        right.add(new JButton("R1"));
        right.add(Box.createVerticalGlue());
        right.add(new JButton("R2"));
        
        left.setBorder(BorderFactory.createTitledBorder("Left"));
        right.setBorder(BorderFactory.createTitledBorder("Right"));
        
        return panel;
    }
GridLayout
GridLayout

Tento příklad nám zároveň ukazuje, že je možné (a žádoucí) kombinovat více správců rozmístění. Zde pro rozmístění na nejvyšší úrovní používáme BorderLayout, který zajistí roztažení komponent na maximální možné rozměry (při zvětšování má vždy přednost komponenta na středu). V levém panelu pak umísťujeme do boxu tři tlačítka a ponecháváme mezi nimi mezery 10 a 30 pixelů. V pravém sloupci pak umísťujeme dvě tlačítka a dáváme mezi ně tmel, čímž zajistíme, že mezi nimi bude situován veškerý volný prostor – tj. jedno tlačítko bude nahoře a druhé dole. Nakonec oběma boxům přidáme ohraničení s popiskem.

GridLayout

GridLayout, jak již název napovídá, umisťuje komponenty do pravoúhlé mřížky. Konstruktoru můžeme předat 2 parametry – počet komponent na šířku a na výšku – z nichž může maximálně jeden nulový. Nulový argument znamená, že se v daném rozměru bude mřížka automaticky natahovat.

    public static JPanel createLayoutPanel() {
        JPanel panel = new JPanel();
        panel.setLayout(new GridLayout(0, 2)); //nedefinovany pocet radku, 2 sloupce
        
        panel.add(new JButton("First"));
        panel.add(new JButton("Second"));
        panel.add(new JButton("Third"));
        panel.add(new JButton("Fourth"));
        panel.add(new JButton("Fifth"));
              
        return panel;
    }
GridBagLayout
GridBagLayout

GridBagLayout

Posledním layoutem, který si dnes představíme, je GridBagLayout. Jedná se o zobecnění GridLayoutu, ve němž může komponenta zabírat více buněk. GridBagLayout je ze všech uvedených správců nejsilnější a nejkomplexnější. Na druhou stranu je však mnohdy snazší (a přehlednější) použít složeninu několika jednodušších správců.

Pro každou komponentu umístěnou v GridBagLayoutu musíme vytvořit objekt typu GridBagConstraints, který specifikuje její pozici pomocí následujících vlastností:

  • gridx, gridy – Pozice buňky, do níž bude komponenta umístěna.
  • gridwidth, gridheight – Počet buňek na výšku a na šířku, které komponenta zabere.
  • weightx, weighty – Poměr, v jakém bude přebytečné místo rozděleno mezi komponenty.
  • ipadx, ipady – Určuje počet pixelů, o něž má být komponenta větší, než je její minimální velikost.
  • fill – Určuje, jakým způsobem má být komponenta roztažena, aby vyplnila prostor, který jí byl vymezen (roztáhnout horizontálně, vertikálně, v obou dimenzích).
  • insets – Specifikuje vnitřní okraje. Tj. kolik má být volného prostoru kolem komponenty.
    public static JPanel createLayoutPanel() {
        JPanel panel = new JPanel();
        panel.setLayout(new GridBagLayout()); 
        
        GridBagConstraints gc1 = new GridBagConstraints();
        gc1.gridx = 0;
        gc1.gridy = 0;
        gc1.anchor = GridBagConstraints.WEST; //bunka zarovnana vlevo
        gc1.fill = GridBagConstraints.VERTICAL; //komponenta natazena na vysku
              
        panel.add(new JButton("First"), gc1);
        
        GridBagConstraints gc2 = new GridBagConstraints();
        gc2.gridx = 0;
        gc2.gridy = 1;
        gc2.fill = GridBagConstraints.BOTH; //komponenta natazena do obou rozmeru
        
        panel.add(new JButton("Second"), gc2);

        GridBagConstraints gc3 = new GridBagConstraints();
        gc3.gridx = 2;
        gc3.gridy = 0;
        gc3.anchor = GridBagConstraints.CENTER; //bunka zarovnana na stred
        gc3.fill = GridBagConstraints.BOTH; //komponenta natazena do obou rozmeru
        gc3.gridheight = 3; //komponenta zabere na vysku 3 bunky
        gc3.insets = new Insets(10, 10, 10, 10); //vnitrni okraj bunky 10px na kazdou stranu
        gc3.weightx = 1; //komponenta zabere prebytecne misto na sirku
        panel.add(new JButton("Third"), gc3);
        
        
        GridBagConstraints gc4 = new GridBagConstraints();
        gc4.gridx = 0;
        gc4.gridy = 2;
        gc4.ipady = 10; //zvetsi komponentu o 10px na vysku
        gc4.anchor = GridBagConstraints.WEST; //bunka zarovnana vlevo
        gc4.fill = GridBagConstraints.BOTH; //komponenta natazena do obou rozmeru
              
        panel.add(new JButton("Fourth"), gc4);        
            
        return panel;
    }

Další možnosti

Rozmísťování komponent pomocí layout managerů je poměrně otravná a náročná činnost, které by však měl rozumět každý programátor desktopových aplikací. Existuje však ještě jedna možnost, jak jednodušeji dosáhnout lepšího cíle. Jsou jí různé vizuální klikací nástroje, které obsahuje téměř každé vývojové prostředí.

Tyto nástroje mohu doporučit pro rychlý vývoj aplikací a pro tvorbu složitých layoutů, které by napsat ručně bylo přinejmenším obtížné. Na druhou stranu je vhodné psát jednoduché aplikace a znovupoužitelné komponenty ručně, jelikož je výsledný kód mnohem čistější. Dále je třeba mít na paměti, že programátor by měl při použití každého generátoru vědět, co výsledný kód dělá a proč to dělá.

Literatura

  • HORTON, Ivor. Java 5. Praha : Neocortex spol s.r.o., 2005. 1443 s. ISBN 80-86330-12-5.







Doporučujeme