Rozložení komponent GUI lze definovat napevno. Má-li ale aplikační okno nebo jeho část proměnnou velikost, je pevně definované GUI na obtíž. A v tu chvíli sáhneme po správcích rozložení - layout managerech. Umožňují nám rozložení komponent automaticky přizpůsobovat aktuální situaci.
9.11.2006 06:00 | Lukáš Jelínek | přečteno 23852×
Grafické uživatelské rozhraní aplikace můžeme navrhnout jednoduše tak, že si na milimetrovém papíře nakreslíme rozložení komponent a pak tento návrh přeneseme do kódu. Je to jednoduché, spolehlivé, ale má to jednu zásadní nevýhodu. Pokud může uživatel nějak měnit velikost "kontejneru" (okna, nějakého seskupovacího prvku apod.), nepůsobí pevně definované rozložení zrovna nejlíp.
Pevné rozložení se hodí především pro jednoduché panely a dialogy, kde uživatel nemá možnost do GUI nějak zasahovat. Ovšem i tam si lze (s ohledem na použitý vzhled) pomoci automatickou správou. Hlavní doménou této správy jsou ovšem okna s proměnnou velikostí.
Je to jednoduché. Máme specializovaný objekt, který spravuje svůj GUI kontejner a podle jeho velikosti mění polohu a velikost komponent uvnitř. Může (ale také nemusí) reflektovat též změny vlastností těchto komponent provedené uživatelem.
Základem je rozhraní java.awt.LayoutManager
. To obsahuje několik metod, z nichž
nejvýznamnější je metoda layoutContainer()
, která rozmístí komponenty podle
pravidel daného správce. Jak to udělá? To už záleží právě na konkrétní
implementaci. Základem je samozřejmě volání metod setSize()
a setLocation()
(příp. setBounds()
) jednotlivým komponentám, ovšem podstatně se mohou lišit parametry, které se
těmto metodám předávají.
Podívejme se nejdřív na několik implementací, které se nacházejí ve standardních
javovských balících (java.awt
a javax.swing
). Právě na nich je vidět, jak
odlišně mohou takoví správci fungovat.
Tento správce uspořádává komponenty podobně, jako kdyby šlo o znaky v textu. Tedy začne vlevo (příp. vpravo) nahoře, pokračuje v řádku dál, a když už není místo, přejde o řádek níž. Způsob vodorovného zarovnání, stejně tak jako mezery mezi komponentami, lze nastavit.
Zde se komponenty umísťují na virtuální mřížku. Plocha kontejneru se rozdělí na obdélníky stejné velikosti a v každém bude jedna komponenta. Lze nastavit počet řádků a sloupců, přičemž počet řádků (je-li nastaven jako nenulový) má přednost.
Poněkud složitější layout manager. Opět se vytvoří virtuální mřížka, ovšem komponenta může zabírat i více buněk než jen jedinou (je to podobné jako "slučování buněk" v tabulkovém procesoru). Na chování správce lze aplikovat řadu různých pravidel a získat tak velmi příjemně se chovající GUI.
Jednoduchý správce, umísťující komponenty do řádku nebo sloupce (podle
nastavení). Souvisí s ním také třída Box
, což je speciální kontejner s tímto
layout managerem, vhodný pro zjednodušení práce.
Rozdělí kontejner na pět oblastí - centrální oblast a čtyři okrajové. Hodí se zejména pro hlavní aplikační okna. Lze si představit např. vývojové prostředí, kde je v centrální části editor kódu, nahoře zobrazení zásobníku, vlevo strom projektu, vpravo detaily aktuální třídy a dole textová konzole.
Kromě správců obsažených ve standardních balících jsou často k dispozici ještě další - například v projektu swing-layout (některé třídy budou součástí balíků Javy 6).
Samotné použití je velmi snadné. Jen se zavolá metoda setLayout()
a předá se
jí příslušný správce. Pokud se místo reference na správce použije null
, nebude
se rozložení spravovat automaticky a je plně v rukou programátora.
Podívejme se na následující program. Vytvoří 6 tlačítek, 6 různých správců rozložení (resp. 3, každý ve 2 verzích; ostatní správci se pro tuto ukázku nehodí), a to vše propojí tak, že stiskem tlačítka se nastaví příslušný layout manager.
public class Test extends JFrame implements ActionListener { private JButton ba [] = { new JButton("FlowLayout 1"), new JButton("FlowLayout 2"), new JButton("GridLayout 1"), new JButton("GridLayout 2"), new JButton("BoxLayout 1"), new JButton("BoxLayout 2") }; private LayoutManager lma [] = { new FlowLayout(), new FlowLayout(FlowLayout.LEFT, 20, 20), new GridLayout(), new GridLayout(0, 2), new BoxLayout(getContentPane(), BoxLayout.X_AXIS), new BoxLayout(getContentPane(), BoxLayout.Y_AXIS) }; private HashMap<JButton, LayoutManager> map = new HashMap<JButton, LayoutManager>(); public Test() { super(); init(); } public void init() { setSize(400, 300); setDefaultCloseOperation(DISPOSE_ON_CLOSE); Container c = getContentPane(); for (int i=0; i<ba.length; i++) { ba[i].addActionListener(this); c.add(ba[i]); map.put(ba[i], lma[i]); } c.setLayout(lma[0]); } public void actionPerformed(ActionEvent e) { JButton but = (JButton) e.getSource(); getContentPane().setLayout(map.get(but)); getContentPane().validate(); } public static void main(String[] args) { SwingUtilities.invokeLater(new Runnable() { public void run() { Test t = new Test(); t.setVisible(true); } }); } }
Změny lze pozorovat v reálném čase. Všimněte si, že se po nastavení nového správce
musí zavolat metoda validate()
. Pokud bychom to neudělali, změna by se
projevila až při změně velikosti kontejneru.
I když si v drtivé většině případů vystačíme s těmi správci, které máme již k dispozici, někdy nastane důvod k implementaci vlastního. Není to nic složitého, jen se vyplatí při tom trochu přemýšlet.
Nejprve obecně. Každá komponenta má 4 rozměrové parametry: aktuální, minimální,
maximální a preferovanou velikost. Aktuální již známe, pracuje se s ní pomocí
metod setSize()
a getSize()
. Zde ovšem můžeme využít i ty zbývající - nezmenšovat
pod minimální velikost, nezvětšovat nad maximální, a přednostně nastavovat
podle té preferované. Není to ovšem nezbytné, každý nechť uváží, co se v daném
případě hodí.
To však není všechno. Rozhraní LayoutManager
má mj. také metody
minimumLayoutSize()
a preferredLayoutSize()
, kterými se dává najevo, jaká je
minimální a preferovaná velikost celého kontejneru. Tyto parametry se pak
využívají o úroveň výše, kde je může opět zpracovat nějaký správce rozložení,
nebo třeba metoda pack()
.
Pusťme se tedy do tvorby. Bude to správce, který bude uspořádávat komponenty
vodorovně (podobně jako BoxLayout
) bez mezer, s tím, že žádná komponenta nebude
zmenšena pod minimální velikost a jinak budou mít všechny komponenty stejný rozměr.
class MyLayout implements LayoutManager { public void addLayoutComponent(String name, Component comp) {} public Dimension preferredLayoutSize(Container parent) { Dimension d = new Dimension(0, 0); int cc = parent.getComponentCount(); for (int i=0; i<cc; i++) { Dimension cd = parent.getComponent(i).getPreferredSize(); if (cd.width > 0) d.width += cd.width; if (cd.height > d.height) d.height = cd.height; } Insets in = parent.getInsets(); d.width += in.left + in.right; d.height += in.top + in.bottom; return d; } public Dimension minimumLayoutSize(Container parent) { Dimension d = new Dimension(0, 0); int cc = parent.getComponentCount(); for (int i=0; i<cc; i++) { Dimension cd = parent.getComponent(i).getMinimumSize(); if (cd.width > 0) d.width += cd.width; if (cd.height > 0) d.height = cd.height; } Insets in = parent.getInsets(); d.width += in.left + in.right; d.height += in.top + in.bottom; return d; } public void layoutContainer(Container parent) { Dimension csize = parent.getSize(); Insets in = parent.getInsets(); csize.width -= in.left + in.right; csize.height -= in.top + in.bottom; Component ca[] = parent.getComponents(); if (ca.length == 0) return; Dimension da[] = new Dimension[ca.length]; // zmenseni na minimalni velikost int wtot = 0; for (int i=0; i<ca.length; i++) { Dimension d = ca[i].getMinimumSize(); d.height = d.height > csize.height ? d.height : csize.height; da[i] = new Dimension(d); wtot += d.width; } // dopocet zbytku, pokud je potreba if (wtot < csize.width) { int diff = (csize.width - wtot) / ca.length; for (int i=0; i<ca.length; i++) { da[i].width += diff; } } // umisteni komponent wtot = in.left; for (int i=0; i<ca.length; i++) { ca[i].setBounds(wtot, in.top, da[i].width, da[i].height); wtot += da[i].width; } } public void removeLayoutComponent(Component comp) {} }
Všimněte si několika věcí. Nejprve toho, že jsou dvě metody ponechány prázdné. Je to proto, že zde nemají význam, ačkoliv u některých správců význam mít mohou (abych byl přesný, je to ještě trochu jinak, a brzy se o tom zmíním). Za druhé je tu ten "problém" (který se objevuje i u některých standardních managerů), že v určitých případech bude za poslední komponentou malé volné místo. Je to proto, že při dělení nějaké pixely zbývají a nejsou pak nikam přiděleny. Pokud by to někomu vadilo, může o ně například rozšířit poslední komponentu nebo je nějak rozdistribuovat mezi více komponent.
Dále bych chtěl upozornit, že se nesmí zapomenout na případné vnitřní
okraje (insets) a odečíst je tedy od celkového prostoru v kontejneru.
Kromě metod uvedených v příkladu se doporučuje implementovat také metodu
toString()
.
Jak jsem se zmínil, dvě prázdné metody někdy mají svůj význam. Jenže většinou
se používá spíše metoda novější, nacházející se v rozhraní LayoutManager2
.
Podívejte se např. na třídu GridBagLayout
. Je tam použita (novější) metoda
addLayoutComponent()
a samozřejmě také removeLayoutComponent()
. Narozdíl od
jednodušších layout managerů zde totiž potřebujeme určit, jak se s komponentou
naloží - a to lze právě tak, že se s ní pracuje pomocí těchto metod (kromě
toho je tam také metoda setConstraints()
).
Slíbil jsem přiblížit práci s fokusem ("zaměřením" komponenty), a už je to tady. Fokus je velice důležitá věc pro ovládání programů klávesnicí. Proto považuji za důležité ukázat některé věci, které se v tomto ohledu velice hodí.
Začneme tím nejjednodušším - vyžádáním fokusu. Potřebujeme-li zajistit, aby
ho v nějakém okamžiku získalo např. textové pole, máme k dispozici dvě metody.
První je requestFocus()
, která získá fokus odkudkoliv, tedy i v případě, že je
příslušné okno neaktivní. Metodu se ale příliš nedoporučuje používat, protože
jednak se může na různých platformách chovat jinak, ale hlavně je velice
netaktní k uživateli urvat si drze fokus, jako to dělají některé nechvalně
proslulé dialogy.
Vhodnější je metoda requestFocusInWindow()
, která umožňuje získání fokusu
v rámci okna (okno musí být již aktivní). Chová se na všech platformách stejně
a navíc vrací výsledek pokusu. Vrátí-li false
, získání fokusu selhalo, kdežto
hodnota true značí s vysokou pravděpodobností získání fokusu (přestože není
úplně zaručeno, že se to skutečně povedlo).
Dále nás zajímají ještě metody transferFocus()
(předává fokus na další komponentu
v pořadí), isFocusOwner()
(zjišťuje, zda komponenta vlastní fokus),
isFocusable()
(zjišťuje možnost získání fokusu) a setFocusable()
(nastavuje
možnost získat fokus).
Není skoro co vysvětlovat. Při získání a ztrátě fokusu komponenta generuje
události FocusEvent
, které se posílají všem zaregistrovaným odběratelům
implementujícím rozhraní FocusListener
. To má dvě metody, focusGained()
a
focusLost()
. Místo implementace rozhraní lze též použít třídu FocusAdapter
.
Metodou getOppositeComponent()
lze z instance události získat referenci na
komponentu, od které byl získán nebo které byl předán fokus.
Zvláštní případ se týká fokusu okna (JFrame
, JDialog
). Zde nelze
použít výše uvedené rozhraní, musí se použít rozhraní WindowFocusListener
s metodami windowGainedFocus()
a windowLostFocus()
. Zpracovává se událost typu
WindowEvent
. Není zde samostatný adapter, používá se třída WindowAdapter
,
o které byla řeč již dříve a která implementuje ještě řadu dalších handlerů.
Pro určení pořadí je k dispozici silný aparát. Nejprve si ale musíme říct něco
o stromové hierarchii, která zde platí. Každý fokusový cyklus (tedy "okruh"
v němž se fokus předává stále dokola) má svůj kořen. Kořen se chová tak, že
běžným způsobem (např. tabulátorem na klávesnici nebo metodou transferFocus()
)
nelze fokus přenést výše v hierarchii. Za normálních okolností je kořenem vždy
okno (rám, dialog), lze si ovšem kořeny vytvářet (metodou setFocusCycleRoot()
)
podle potřeby, např. v rámci skupiny textových polí.
Samotné pořadí určuje politika předávání fokusu, vyjádřená implementací
abstraktní třídy FocusTraversalPolicy
. Zde se definuje, která komponenta
má v dané situaci získat fokus. Tuto politiku si můžeme definovat dle libosti,
existují ale samozřejmě již předem připravené implementace. Je to například
SortingFocusTraversalPolicy
, kde se pro vyhodnocení použije řazení pomocí
komparátoru (Comparator
).
Potomkem této třídy je LayoutFocusTraversalPolicy
, umožňující "přirozený
průchod" komponentami. Fokus se předává nejprve vodorovně (v řádku) a pak
svisle (po řádcích dolů), jako při čtení/psaní textu. Toto je výchozí politika
a používá se ve všech swingovských GUI, pokud explicitně nepoužijeme něco
jiného.
Politiku předávání nastavíme metodou setFocusTraversalPolicy()
, ovšem bude
aktivní pouze v případě, že jednak je tento kontejner kořenem (viz výše),
současně je také poskytovatelem politiky (nastavuje se to metodou
setFocusTraversalPolicyProvider()
). Není-li poskytovatelem, využije se
politika nejblíže vyššího předka, který je kořenem.
V některých případech je získání/ztráta fokusu jen dočasnou záležitostí.
Například při rozbalení menu nebo přetahování komponenty může dojít k dočasnému
předání fokusu - je to ale platformově závislá věc. Zda se jedná o dočasnou
změnu, lze z události zjistit pomocí isTemporary()
, ale není to příliš
spolehlivé (např. dočasná ztráta fokusu se může tvářit jako trvalá).
I příště se zaměříme na vzhled GUI, ovšem z trochu jiné stránky. Bude řeč o tzv. Look&Feel, tedy jinak řečeno "tématech" vzhledu aplikací. Programy tak mohou snadno získat zcela jinou tvář.