Java (26) - tvorba GUI

Při návrhu a implementaci grafického uživatelského rozhraní se lze vydat dvěma hlavními cestami: buď tvořit ručně (přímým psaním kódu), nebo využít grafické návrhové prostředí. Na obě se nyní podíváme, samozřejmě i včetně kombinace obou přístupů. Při tom se dostanou ke slovu některé více či méně zajímavé objektové třídy.

24.7.2006 09:00 | Lukáš Jelínek | přečteno 56070×

Okna, dialogy, rámy

Prvky grafického rozhraní obvykle neplavou "ve vzduchu" (tedy jen tak na ploše obrazovky), i když to v některých případech bývá. Mnohem častěji je umísťujeme do různých oken. Před návrhem GUI nějaké aplikace se vyplatí dobře znát vlastnosti základních druhů oken, se kterými se ve frameworku Swing pracuje.

Okno

Základním prvkem je "obyčejné" okno, reprezentované třídou JWindow. Nemá žádný rámeček, titulkovou lištu ani ovládací tlačítka. Přímo ho využijeme málokdy, uvádím ho hlavně proto, že má většinu vlastností, které souvisejí s vkládáním komponent GUI. Okno je přímo navázáno na nativní objekt (např. v X Window Systemu).

Každé okno obsahuje určité plochy, které mají svůj význam pro fungování okna a s každou z nich se pracuje specifickým způsobem:

Zdaleka nejčastěji pracujeme s obsahovou plochou. Buď použijeme tu, která je v okně již obsažena (ale pak předem neznáme její vlastnosti, resp. ani třídu), nebo nastavíme komponentu vlastní. Vhodnými kandidáty bývají třídy JPanel (pro okna pevné velikosti), JScrollPane, JSplitPane apod.

Někdy využijeme také nabídkovou lištu. Práce s ní je triviální, ještě o tom bude řeč.

Dialog

Smysl této komponenty (představované třídou JDialog) je jasný - používá se pro různé dialogy a pevná okna. Dialog má titulkovou lištu a ovládací tlačítka, může být modální. Podobně jako okno, i dialog je na obrazovce tvořen nativním grafickým objektem.

Rám

Pod tímto nepříliš výstižným termínem se skrývá okno s proměnnou velikostí. Opět je to nativní objekt okenního systému. Ve Swingu ho reprezentuje třída JFrame.

Vnitřní rám

Od předchozího se liší tím, že je to čistě objekt Swingu (není reprezentován nativně) a může existovat jen uvnitř jiné swingové komponenty. Typicky se používá pro dokumentová okna v MDI programech. Jedná se o třídu JInternalFrame.

Applet

Uvádím ho jen pro úplnost. Applet bývá umístěn na webové stránce a plní podobné úkoly jako běžné okno. Třída má název JApplet.

Sestavení GUI aplikace

Vytváříme-li GUI ručně (bez pomoci grafického návrháře ve vývojovém prostředí), je dobré si vše předem nakreslit na papír, nejlépe milimetrový. Takto si připravíme rozvržení aplikace a pak už jen implementujeme chování grafických komponent. Pokud nepotřebujeme pracovat s pevným rozmístěním komponent (a používáme správce rozložení, layout managery - bude o nich řeč v jednom z příštích dílů), je to ještě jednodušší a ruční práce je velice efektivní.

Můžeme přímo používat již existující třídy nebo si od nich vytvářet potomky - druhá možnost je lepší v případech, kdy potřebujeme nějak zásadněji změnit chování některé komponenty nebo tehdy, chceme-li nějakou upravenou komponentu používat opakovaně.

Příklad ruční tvorby

Pusťme se nyní do tvorby aplikace. Na příkladu bude nejlépe vidět, jak se dá s GUI pracovat a jak to celé funguje. Budeme vytvářet jednoduchý textový editor - bude umět založit nový text, otevřít existující soubor a uložit data do zvoleného souboru. Kdo by chtěl nějakou funkci navíc (např. udržování názvu souboru v programu, automatické ukládání, dotaz na zahození neuložených dat, automatické zalamování řádků a podobně), jistě snadno přijde na to, jak to udělat.

Třída editoru bude odvozena od třídy JFrame, o níž byla řeč výše. Lze to samozřejmě udělat i v samostatné třídě a do JFrame nesahat. Začněme tedy:

public class Editor extends JFrame implements ActionListener {
    
    private JScrollPane sp = null;
    private JTextArea ta = null;
    private JMenuBar mb = null;
    
    private String sep = null;
    

Deklarujeme členské proměnné hlavních komponent. Není to nutné, ale pro pozdější přístup se to hodí. Některé můžeme rovnou plně inicializovat, ovšem pro lepší orientaci to ponechám na později. Ještě upozorním na proměnnou sep, která bude obsahovat oddělovač řádků (brzy vysvětlím).

Chvíli bych se zdržel u třídy JTextArea. Protože je ve Swingu důkladně využita hierarchie tříd, projevuje se to i zde. Máme abstraktní třídu JTextComponent, která obsahuje základní funkcionalitu pro práci s textem. Neřeší však implementační detaily, zejména způsob komunikace s uživatelem. Umožňuje jak primitivní práci s textem (jako je to i v tomto příkladu), tak možnost použít dokumentový aparát Swingu s mnohem rozsáhlejšími možnosti (složitější editace, undo/redo, logické členění apod.). JTextArea je jednou z konkrétních implementací JTextComponent, další je např. třída JTextField (jednořádkové textové pole), která má sama o sobě ještě další potomky (např. JPasswordField pro zadávání hesel).

public Editor() {
    super();
    init();
}

Konstruktor je jednoduchý a volá nejprve konstruktor předka a potom inicializační metodu. Do té je vhodné umístit všechno, co se týká inicializace komponenty. Můžeme pak mít více konstruktorů a z každého tuto metodu volat.

public void init() {
    sep = System.getProperty("line.separator");
    
    ta = new JTextArea();
    Font f = Font.decode("Monospaced");
    if (f != null)
        ta.setFont(f);
    
    sp = new JScrollPane(ta,
        JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
        JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
    setContentPane(sp);
    

První část inicializační metody nejprve nastaví již zmíněný oddělovač řádků do podoby, jaká odpovídá platformě (tedy na GNU/Linuxu to bude "\n") - oddělovač budeme potřebovat při načítání souboru. Pak se vytvoří komponenta pro editaci textu. Protože je vhodnější pracovat s neproporcionálním písmem, zkusíme ho nastavit (pokud se to nepovede, zůstává původní písmo). A konečně poslední část kódu vytvoří plochu s posuvníky (budou zobrazovány jen v případě potřeby) a textovou oblast do ní vloží. Plocha se pak nastaví jako obsahová plocha rámu. Protože nezasahujeme do nastavení správců rozložení, uplatní se výchozí stav, který nám zajistí, že velikost textové oblasti bude odpovídat textu uvnitř.

    JMenu menu= new JMenu("Soubor");
    menu.setMnemonicv(KeyEvent.VK_S);
    
    JMenuItem mi = new JMenuItem("Nový", KeyEvent.VK_N);
    mi.setActionCommand("new");
    mi.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N, KeyEvent.CTRL_MASK));
    mi.addActionListener(this);
    menu.add(mi);
    
    mi = new JMenuItem("Otevřít...", KeyEvent.VK_O);
    vmi.setActionCommand("open");
    mi.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, KeyEvent.CTRL_MASK));
    mi.addActionListener(this);
    menu.add(mi);
    
    mi = new JMenuItem("Uložit...", KeyEvent.VK_U);
    mi.setActionCommand("save");
    mi.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, KeyEvent.CTRL_MASK));
    mi.addActionListener(this);
    menu.add(mi);
    
    mi = new JMenuItem("Konec", KeyEvent.VK_K);
    mi.setActionCommand("quit");
    mi.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Q, KeyEvent.CTRL_MASK));
    mi.addActionListener(this);
    menu.add(mi);
    
    mb = new JMenuBar();
    mb.add(menu);
    
    setJMenuBar(mb);
    

Tento poněkud delší úsek inicializátoru se zabývá přípravou nabídkové lišty. Vytvoříme nabídku "Soubor" (žádné jiné nebudou), přidáme do ní potřebné položky pro různé operace, a na závěr vytvoříme samotnou lištu a vložíme do ní menu. Všimněte si několika věcí. Nabídka se bude otvírat zvolenou klávesou (samozřejmě v kombinaci s Alt), každá položka nabídky má svoji klávesu (která se použije, je-li nabídka otevřená) a také klávesovou zkratku použitelnou kdykoli. Optimální je, pokud si klávesa položky a aplikační klávesová zkratka odpovídají (pokud to lze), ale často je to docela problém (máme zažité zkratky, např. Ctrl-S pro uložení, a to se slovem "Uložit" moc dohromady nejde). Mohli bychom ještě např. nastavit bublinovou nápovědu položek (setTooltipText()) apod.

Opět malé zdržení - a to u třídy JMenuItem a jejích potomků. Tato třída reprezentuje položku v menu a je rozšířením třídy AbstractButton, podobně jako třeba JButton. Může mít pouhý text nebo i ikonu, ostatně jako každá implementace abstraktního tlačíka (AbstractButton). Potomkem třídy JMenuItem je i třída JMenu, což znamená, že pokud se do menu vloží jiné menu (namísto obyčejné položky), prostě se tím vytvoří další úroveň. Dále jsou tu také potomci JCheckBoxMenuItem a JRadioButtonMenuItem, představující zaškrtávátko, resp. přepínač v menu. Nejsme tedy omezeni na obyčejné položky, ale lze pracovat i s tímto. Občas je v menu potřeba oddělovač - můžeme buď vložit instanci třídy JSeparator nebo zavolat addSeparator(), obě cesty jsou rovnocenné.

Zvolené řešení reakce na výběr položek menu je jen jedno z mnoha. Kromě rozlišení příkazu (action command) můžeme operace rozlišovat také podle zdroje události. Jinou možností je vytvořit ke každé položce anonymní třídu (implementující ActionListener) a odtud pak volat metody operací. Každé řešení má své pro i proti, u jednoduchých GUI na zvoleném postupu ale víceméně nezáleží.

    setTitle("Editor");
    setDefaultCloseOperation(DISPOSE_ON_CLOSE);
    setLocation(100, 100);
    setSize(400, 300);
}

Tyto příkazy by měl již každý znát. Nastavují titulek, výchozí zavírací operaci, polohu a velikost okna. Tím je inicializace dokončena.

public void actionPerformed(ActionEvent e) {
    String s = e.getActionCommand();
    if (s.equals("new"))
        clear();
    else if (s.equals("open"))
        load();
    else if (s.equals("save"))
        save();
    else if (s.equals("quit"))
        dispose();
}

Obsluha událostí od položek menu. Je to snad zřejmé na první pohled, porovnává se řetězec příkazu s definovanými hodnotami. Pro delší seznam by to bylo operačně náročné (a bylo by lepší použít jiný způsob rozlišení), zde nám to ale problémy nedělá.

public void clear() {
    ta.setText("");
}

Vytvoření "nového souboru". Spočívá prostě v tom, že se textová oblast vyprázdní.

public void load() {
    JFileChooser fc = new JFileChooser();
    fc.setDialogType(JFileChooser.OPEN_DIALOG);
    if (fc.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
        File f = fc.getSelectedFile();
        ta.setText("");
        try {
            BufferedReader br = new BufferedReader(new FileReader(f));
            StringBuilder sb = new StringBuilder();
            String s = "";
            boolean fl = false;
            while ((s = br.readLine()) != null) {
                if (!fl)
                    sb.append(sep);
                else
                    fl = true;
                
                sb.append(s);
            }
            br.close();
            ta.setText(sb.toString());
            ta.setCaretPosition(0);
        } catch (IOException e) {
            JOptionPane.showMessageDialog(this, "Soubor nelze otevřít.",
                "Chyba", JOptionPane.ERROR_MESSAGE);                
        }
    }
}

Načtení textu ze souboru. Swing má svoji implementaci dialogu pro práci se soubory. Dialog toho umí mnohem víc, než se zde používá (např. filtraci souborů, vícečetné výběry), ale nám stačí základní operace. Pokud výběr souboru proběhl správně (nedošlo k žádné chybě a uživatel potvrdil výběr), načteme soubor. Možností je opět více. Zvolil jsem čtení pomocí textového bufferovaného streamu po řádcích. Protože metoda readLine() konce řádků odřezává, opět je přidáme - vedlejším efektem (někdy vítaným, někdy ne) bude, že se všechny konce řádků změní tak, že to odpovídá platformě. K sestavení textu se použije třída StringBuilder. Po vložení řetězce do textové oblasti se přesune kurzor na začátek (jinak by zůstal na konci).

Nyní nastal čas upozornit na velice zajímavou a důležitou třídu - JOptionPane. Ta slouží pro práci s jednoduchými dialogy. Kromě toho, že s ní lze pracovat obvyklým způsobem a vytvářet si dialogy podle potřeby, má také řadu statických metod pro zobrazování primitivních informativních a potvrzovacích dialogů. To je užitečné právě v takových případech, jako je tento - k oznamování chyb, informování o ukončení časově náročných operací, dotazům typu ano/ne(/zrušit), vložení jediné hodnoty atd. Doporučuji vydatně používat.

public void save() {
    JFileChooser fc = new JFileChooser();
    fc.setDialogType(JFileChooser.SAVE_DIALOG);
    if (fc.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
        File f = fc.getSelectedFile();
        try {
            BufferedWriter bw = new BufferedWriter(new FileWriter(f));
            bw.write(ta.getText());
            bw.close();
        } catch (IOException e) {
            JOptionPane.showMessageDialog(this, "Soubor nelze uložit.",
                "Chyba", JOptionPane.ERROR_MESSAGE);                
        }
    }
}

K tomu snad není potřeba nic dodat. Od metody k načtení dat se liší prakticky pouze tím, že se celý textový obsah uloží zavoláním jediné metody. Nyní už chybí pouze hlavní metoda pro spuštění programu:

public static void main(String[] args) {
    SwingUtilities.invokeLater(new Runnable() {
        public void run() {
            Editor m = new Editor();
            m.setVisible(true);
       }
   });
}

Importy jsem jako obvykle vynechal. Po kompilaci a spuštění by se mělo objevit okno, které bude mít nahoře nabídkovou lištu. Ta bude fungovat obvyklým způsobem, pro ovládání půjde používat i klávesové zkratky.

Tvoříme s pomocníkem

Toto byla ukázka kompletně ruční tvorby aplikace. Hlavně u větších programů s GUI je mnohdy efektivnější aspoň částečně využít grafické prostředí, ve kterém si GUI vytvoříme "klikacím způsobem". V následujících odstavcích budu hovořit o tvorbě téže aplikace v IDE NetBeans verze 5, jiná prostředí se používají podobně. Popis nebude ve stylu, aby to podle toho udělala cvičená opice - spíše vyzdvihnu důležité body.

Předpokládám již založený projekt a v něm nějaký balík. Začneme vytvořením potomka JFrame - např. z kontextové nabídky balíku: New -> JFrame Form... IDE se automaticky přepne do vizuálního návrháře GUI a můžeme začít "klikat". Na plochu rámu vložíme komponentu JScrollPane a do ní pak JTextFrame. Pro každý objekt nastavíme potřebné vlastnosti na kartě vlastností (Properties).

Pak se přepneme do zobrazení zdrojového kódu (Source). Vygenerovaný kód je barevně označen, je sbalený a nelze do něj přímo zasahovat. Do souboru můžeme nyní vložit metody clear(), load() a save(). Není to ovšem úplně nutné, hned uvedu proč. Ve vizuálním editoru pak přidáme nabídkovou lištu, nabídku a její položky (stejné jako ručně psaného editoru). Klávesy položek a klávesové zkratky se dají nastavit přímo na kartě vlastností. Následující obrázky ukazují výslednou logickou strukturu GUI a panel s kartou vlastností:

Logická struktura GUI Karta vlastností pro položku menu

Zbývá už jen nastavit spouštění jednotlivých operací. Nejjednodušší je pro každou položku přes kontextovou nabídku zvolit Events -> Action -> actionPerformed. Tím se do kódu vygeneruje kostra metody, kam se zapíše reakce na danou událost (může to být přímo i výkonný kód, proto se můžeme obejít bez samostatných metod pro jednotlivé operace). Kdo si rozbalí generovaný blok kódu, zjistí, jak je to řešeno. IDE pro každý zdroj událostí vytvoří anonymní třídu, v níž implementuje příslušné rozhraní. Odtud pak volá metodu v naší hlavní třídě. Je to sice jednoduché a elegantní, ale u větších GUI budeme mít úplnou záplavu anonymních tříd, což není úplně nejvhodnější řešení. Pak je lepší postupovat jinak (např. si obsluhu událostí napsat ručně), ale zde se s tím plně spokojíme.

Po dopsání implementace vygenerovaných metod je aplikace hotova (metoda main() se generuje automaticky). Bylo-li vše provedeno správně, měla by se navenek chovat úplně stejně, jako ta ručně napsaná. Jak je vidět, psaní kódu (souvisejícího přímo s GUI) lze omezit na minimum a současně se k fungující aplikaci dopracujeme velice rychle. Na druhou stranu, výhodou je možnost obě cesty kombinovat a využívat je podle potřeby.

Seznamy, tabulky a stromy

Dosud jsme při tvorbě GUI využívali pouze jednoduché grafické komponenty. Mnoho programů ale vyžaduje používání různých seznamů (ať už obyčejných či rozbalovacích), stromů a tabulek. Zde se opět pořádně projeví výhody frameworku Swing, protože nám poskytuje velmi příjemné mechanismy pro práci s těmito grafickými komponentami a s daty, nad nimiž pracují. Příští díl seriálu bude kompletně věnován této oblasti, protože ta si takovou pozornost jednoznačně zaslouží.

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