Java (29) - správci rozložení

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×

Úvod do automatické správy rozložení

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í.

Jak to funguje

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í.

Existující správci rozložení

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.

FlowLayout (plovoucí rozložení)

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.

GridLayout (rozložení do mřížky)

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.

GridBagLayout (rozložení do zobrazovacích oblastí)

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.

BoxLayout ("krabicové" rozložení)

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.

BorderLayout (rozložení do pěti oblastí)

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).

Použití správce

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.

Tvorba vlastního správce

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()).

Fokus a věci související

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í.

Základní práce s fokusem

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).

Události generované změnou fokusu

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ů.

Pořadí předávání fokusu

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.

Dočasný fokus

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á).

Změna vzhledu

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ář.

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