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 56078×
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.
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:
rootPane
) - instance třídy JRootPane
, obsahuje další
plochy, žádný zvláštní význam nemá.glassPane
) - překrývá shora celou oblast okna,
umožňuje sledovat pohyb kurzoru myši přes okno. Může být tvořena prakticky
libovolnou grafickou komponentou, lze do ní tedy i kreslit. Za normálních
okolností je skrytá.layeredPane
) - reprezentována instancí JLayeredPane
.
Má důležitý význam, a to hloubkové (souřadnice z
) uspořádání komponent
v různých situacích. Týká se to např. plovoucích panelů, vyskakovacích oken
nebo přetahování komponent.contentPane
) - sem se vkládají běžné komponenty GUI.
S touto plochou jsme se již setkali v příkladu na jednoduchý program s GUI.
Tato plocha je vložena do vrstvené plochy a opět ji může tvořit jakákoli komponenta.
menuBar
) - tvořena instancí JMenuBar
. Používá se jen
v případě potřeby. Je součástí vrstvené plochy.
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č.
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.
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
.
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
.
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
.
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ě.
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.
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í:
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.
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ží.