Java (28) - renderery a editory

Při tvorbě GUI si většinou vystačíme s tím, co nám poskytují existující grafické komponenty. Někdy je ale lepší chování komponent nějak změnit - a jejich modulární stavba to velice dobře umožňuje. Pojďme se podívat na to, jak upravit způsob vykreslování a editace dat.

2.10.2006 06:00 | Lukáš Jelínek | přečteno 25010×

Vykreslovače čili renderery

Složitější grafické komponty ve Swingu jsou hierarchicky složeny z komponent menších, jednodušších. Například minule popisované tabulky mají vnitřní strukturu poměrně komplikovanou, skládají se z řady různých grafických objektů. Patří mezi ně i objekty, které slouží k vykreslování dat obsažených v tabulce (resp. jiné komponentě, pokud bychom to brali obecně). Říká se jim renderery a používají se proto, že mít samostatnou komponentu pro každou hodnotu by bylo pomalé a paměťově náročné. A protože mají renderery jednoduché a snadno implementovatelné rozhraní, není vůbec problém nahrazovat je jinými implementacemi.

Na začátku myšlenky vytvoření vlastního rendereru je nějaký požadavek, který nelze stávajícími prostředky realizovat. Můžeme například potřebovat, aby se záporná čísla v tabulce (třeba jen v některých buňkách) zobrazovala červeně. Lze se vydat dvěma cestami - buď si třídu napsat kompletně celou, nebo využít nějakou hotovou implementaci (nejčastěji tu výchozí) a jen změnit potřebné věci. První cesta se používá pro změny zásadního charakteru, často bude vhodnější ta druhá.

Podívejme se nyní, jak by vypadal onen zmíněný případ, že bychom chtěli zobrazovat záporná čísla červeně. Upravená implementace rendereru bude vycházet z té výchozí (DefaultTableCellRenderer) a bude fungovat pro všechny číselné datové typy.

class MyRenderer extends DefaultTableCellRenderer {
  public MyRenderer() {
    super();
    setHorizontalAlignment(RIGHT);
  }
  
  public Component getTableCellRendererComponent(JTable table, Object value,
      boolean isSelected, boolean hasFocus, int row, int column) {
    Component c = super.getTableCellRendererComponent(table, value, isSelected,
        hasFocus, row, column);
    c.setForeground(null);
    if (Number.class.isAssignableFrom(value.getClass())) {
      if (((Number) value).doubleValue() < 0.0) {
        c.setForeground(Color.RED);
      }
    }
    return c;
  }
}

Nejprve je nutné si říci, jak se vlastně renderer zvenku používá. Z tabulky se zavolá metoda getTableCellRendererComponent(), ve které renderer vrátí grafickou komponentu (odvozenou od java.awt.Component) určenou pro vykreslení dané buňky. Třída DefaultTableCellRenderer je potomkem JLabel, je tedy schopna zobrazovat hodnoty a vrátí proto referenci přímo na sebe.

Při implementaci začneme konstruktorem. Ten zavolá konstruktor předka a pak nastaví zarovnávání textu (vpravo). To je důležité, jinak by se zarovnávalo výchozím způsobem, tedy nalevo. Všimněte si, že pokud tabulce nenastavíte žádný renderer, budou se číselné hodnoty zarovávat automaticky vpravo. JTable totiž nepoužívá přímo třídu DefaultTableCellRenderer, nýbrž její speciální verze pro jednotlivé datové typy (třeba pro třídy Float a Double bere v úvahu ještě nastavený formát desetinných čísel). Tady se ale o nastavení musíme postarat sami.

Nyní už se pustíme do hlavní metody - getTableCellRendererComponent(). Stačí zjistit, že je hodnota ze třídy Number (resp. nějakého potomka, což jsou všechny číselné třídy) a provést test na zápornost hodnoty. Lze to provést různě, zde v příkladu se testuje kompatibilita hodnoty s třídou Number metodou isAssignableFrom(), ale šlo by to udělat i prostým přetypováním a odchycením výjimky ClassCastException, protože výskyt nesprávného typu může být pouze výsledkem nesprávného použití. Následně se pak provede porovnání pro zjištění zápornosti.

Velice důležité je správné naložení s nastavením barvy. Pokud bychom totiž pouze nastavili barvu popředí (červenou) v těch případech, kdy to potřebujeme, dočkali bychom se nepříjemného efektu. Červená barva by se použila i u dalších buněk (pokud by nebyly vybrány). Proto je potřeba barvu resetovat - můžeme klidně použít null, protože pak se automaticky použije výchozí barva tabulky (což je vhodnější přístup než hádat nebo nějak komplikovaně zjišťovat, jaká barva se má ve výchozím případě použít).

Tak to by byla implementace, a teď to ještě musíme použít v tabulce. Třeba takto:

MyRenderer mr = new MyRenderer();
        
JTable table = new JTable();
table.setDefaultRenderer(Number.class, mr);

Vytvoří se instance rendereru a ta se přiřadí tabulce k použití pro hodnoty třídy Number (všechna čísla). Při kreslení se totiž tabulka dotáže (metodou getColumnClass() modelu) na třídu sloupce a podle toho zvolí renderer. Svůj renderer může mít ovšem také objekt třídy TableColumn - toto nastavení má přednost před nastavením v tabulce.

Změna barvy je jen jednou z mnoha věcí, které můžeme v rámci vlastní implementace rendereru provádět. Mezi další časté patří změna písma, zvláštní formátování, speciální chování pro některé třídy atd. Chcete-li s tím experimentovat, inspirujte se třeba v nějakém tabulkovém procesoru nebo jiném programu a zkuste stejnou věc realizovat zde. Není to složité. Zde je další příklad:

class LsRenderer extends DefaultTreeCellRenderer {
  public LsRenderer() {
      super();
      try {
          URL url = new URL("http://www.linuxsoft.cz/img/sipka1.png");
          ImageIcon ic = new ImageIcon(url);
          setLeafIcon(ic);
      } catch (MalformedURLException e) {
          JOptionPane.showMessageDialog(this, "Chyba v URL ikony: "
              + e.getLocalizedMessage(), "Chyba", JOptionPane.ERROR_MESSAGE);
      }
  }
}

Jistě každý poznal, že tento renderer načte ze serveru Linuxsoftu obrázek a použije ho jako ikonu - v tomto případě ikonu listu ve stromě. Vlastní renderery pro stromy se totiž vytvářejí v podstatě stejně jako u tabulek. Totéž platí i pro seznamy.

Editory

Kromě vlastního přístupu k vykreslování někdy potřebujeme něco podobného uplatnit i pro editaci. Ať už se to týká třeba validace hodnoty nebo způsobu samotné editace. Plané řeči nemají smysl, pojďme rovnou na příklad:

class MyEditor extends DefaultCellEditor {
  private double minVal = Double.NEGATIVE_INFINITY;
  private double maxVal = Double.POSITIVE_INFINITY;
  
  MyEditor() {
    super(new JTextField());
  }
  
  MyEditor(double min, double max) {
    this();
    minVal = min;
    maxVal = max;
  }
  
  public boolean stopCellEditing() {
    Object o = getCellEditorValue();
    
    try {
      double d = Double.parseDouble(o.toString());
      if (d >= minVal && d <= maxVal)
        return super.stopCellEditing();
    } catch (NumberFormatException e) {}
    
    JOptionPane.showMessageDialog(null, "Vložte prosím číslo v rozsahu "
        + minVal + " .. " + maxVal, "Chyba", JOptionPane.ERROR_MESSAGE);
    
    return false;
  }
}

Příklad ukazuje, jak by vypadal editor akceptující pouze reálná čísla, případně s omezením jejich rozsahu. Předefinováváme metodu stopCellEditing(), která se volá v okamžiku, kdy se ukončuje editace a připravuje se uložení hodnoty. Vrátí-li metoda true, je hodnota přijata, v případě false nikoliv a editace pokračuje.

V tomto případě se nejprve zkusí editovaný řetězec parsovat podle formátu typu double. Selže-li to (nejedná se o číslo), skončí to výjimkou NumberFormatException, kterou zde tiše ignorujeme (což se obecně nemá dělat, ale tady to nevadí). Pak se zkontroluje hodnota vůči platnému rozsahu, a pokud číslo vyhoví, vrátí se hodnota z metody stopCellEditing() předka. Nevyhoví-li hodnota při první nebo druhé kontrole, nejen že se vrátí false, ale navíc ještě vyskočí panel s upozorněním na chybu (všimněte si, že se ve volání používá null jako odkaz na rodiče dialogu; třída DefaultCellEditor totiž není potomkem třídy Component). Často je takový "výřečný" způsob signalizace chyby lepší než pouhé červené orámování, které používají standardní editory třídy JTable.

Použití editoru je velice podobné jako v případě rendereru. Prostě se nastaví tabulce (setDefaultEditor()) nebo sloupci (setCellEditor()). Obdobné to bude také u stromu a seznamů. Pozor ovšem na několik věcí, které jsou zde odlišné od rendererů. Jednak výchozí implementace vyžaduje předat editační komponentu (v příkladu to byla instance třídy JTextField), s čímž je třeba počítat a již při vytváření vědět, co bude potřeba.

Dále je potřeba si uvědomit, že abstraktní třída všech editorů je společná - AbstractCellEditor. Implementuje však jen obecné rozhraní CellEditor, kdežto specializovaná rozhraní TreeCellEditor a TableCellEditor nikoli. Výchozí implementace DefaultTreeCellEditor je potomkem AbstractCellEditor a implementuje obě rozhraní. A aby byl guláš dokonalý, je tu ještě třída DefaultTreeCellEditor, která mj. nevyžaduje poskytnutí editační komponenty.

Za esteticky vypadajícími aplikacemi...

...se vydáme v příštím dílu. Ten bude zaměřen na správce rozložení (layout managery), umožňující automatické řízení rozměrů a pozice komponent podle uživatelské činnosti. V rychlosti se také podíváme na problematiku "zaměření" (fokusu) komponent, protože se - možná trochu překvapivě - jedná o docela užitečné mechanismy.

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