Java (30) - Look and Feel

Ne vždy se tvůrci nebo uživateli grafické aplikace v Javě zamlouvá výchozí vzhled GUI. Není ovšem problém vybrat si vzhled jiný, nebo si dokonce vytvořit svůj vlastní.

13.12.2006 10:00 | Lukáš Jelínek | přečteno 24311×

Princip ovládání vzhledu

Původní grafické rozhraní javovských aplikací, tedy AWT, využívalo k vykreslování nativní grafické objekty. Možnosti měnit jejich vzhled byly tedy jednak dost omezené, ale hlavně silně platformově závislé. S grafickým frameworkem Swing nastala podstatná změna. Vzhled komponent GUI je zcela samostatný, není svázán s jednotlivými objekty a není determinován platformou, na které aplikace běží (až na malé výjimky, které později zmíním).

Lze si to představit tak, že instance grafického objektu obsahuje všechny potřebné parametry k vykreslení dané komponenty, ale místo aby sama kreslila, deleguje tuto činnost na objekt nějakého grafického motivu. A tento "delegát" použije zase své parametry pro kreslení a složením dohromady vznikne výsledný vzhled.

Mohu uvést příklad. Máme instanci třídy JButton, tedy tlačítko. Tento objekt obsahuje údaje o tom, jak je objekt velký, kde je umístěn a zda je tlačítko stisknuté či nikoli (a také zda je/není viditelné, aktivní, má fokus apod.). Neobsahuje už ale žádné informace, jakou barvu má mít pozadí, jakou text, jaké písmo (font, řez, velikost) použít, jak nápis umístit, jak indikovat deaktivaci nebo fokus atd. Přesněji řečeno tam takové informace uloženy jsou, ale hlavně z důvodů zpětné kompatibility. Toto vše je totiž záležitost objektu konkrétního grafického motivu, tedy "look&feel". Každý vzhled může vnitřně podporovat i různá témata, jak si za chvíli ukážeme. Celý systém se nazývá Pluggable Look And Feel (PLAF).

Výchozí vzhled, změna vzhledu uživatelem

Pokud neurčíme jinak, bude spuštěná swingovská aplikace používat výchozí vzhled, tedy ten, který určili vývojáři Swingu. Většinou to bude vzhled označený jako Metal, který má v Javě 5.0 o něco lepším základní téma Ocean, kdežto to původní se nyní jmenuje Steel. Tento výchozí vzhled je k dispozici všude, kde jsou třídy frameworku Swing, na všech platformách.

Kromě základního vzhledového motivu existují ještě další. Nabídka se liší podle platforem. Na GNU/Linuxu je k dispozici vzhled GTK a CDE/Motif, na Windows pak vzhled Windows a podobně. Není problém přiinstalovat si další a používat je pro všechny nebo jen pro některé aplikace.

Výchozí vzhled může uživatel změnit dvěma způsoby (nepočítáme-li možnost, že vývojář programu poskytne možnost volby v přímo rámci své aplikace, jako to umožňuje třeba jEdit). První možností je určení v příkazové řádce, druhou pak nastavení v preferencích. Uvažujme např. následující příkazy:

java Editor
java -Dswing.metalTheme=steel Editor
java -Dswing.defaultlaf=com.sun.java.swing.plaf.gtk.GTKLookAndFeel Editor
java -Dswing.defaultlaf=com.sun.java.swing.plaf.motif.MotifLookAndFeel Editor

První příkaz spustí program Editor (z 26. dílu seriálu) s výchozím vzhledem. Příkaz na druhém řádku spouští program s tématem Steel vzhledu Metal (zde se samozřejmě předpokládá, že se opravdu použije Metal, že uživatel nenastavil v preferencích něco jiného). Třetí příkaz nastaví vzhled GTK a čtvrtý CDE/Motif. Je to jednoduché a snadno použitelné. Kdo by si chtěl změnit vzhled natrvalo, může to provést tak, že to souboru swing.properties (v adresáři $JDKHOME/lib/swing.properties) vloží něco podobného:

swing.defaultlaf=com.sun.java.swing.plaf.gtk.GTKLookAndFeel

Bohužel to nejde nastavit pro uživatele, jen pro celý systém. Snad se to v pozdějších verzích zlepší.

Změna vzhledu v programu

Někdy je vhodné měnit vzhled GUI uvnitř běžícího programu - ať už chceme poskytnout uživateli možnost snadného nastavení nebo mu prostě vnutit nějaký konkrétní vzhled. O správu vzhledů se ve Swingu stará statická třída UIManager. Zabývá se jak nastavováním vzhledu, tak použitím výchozích hodnot, získaných podle pravidel popsaných v přechozích odstavcích. Kromě toho má i různé další metody. Na tuto třídu se nyní podíváme.

Základem je dvojice metod setLookAndFeel(), které volí aktuální vzhled aplikace. Jedna přijímá přímo instanci třídy LookAndFeel (ještě o ní bude řeč), druhá pak název třídy. Již v tuto chvíli můžeme snadno změnit vzhled - stačí znát metody getSystemLookAndFeelClassName() a getCrossPlatformLookAndFeelClassName(). První vrací název třídy se systémovým vzhledem (tedy např. na GNU/Linuxu to může být GTK), druhá pak název třídy platformově přenositelného vzhledu (typicky Metal). Jak to celé vypadá, ukazuje následující příklad:

try {
  UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException ex) {
  // reakce na výjimku
} catch (IllegalAccessException ex) {
  // reakce na výjimku
} catch (InstantiationException ex) {
  // reakce na výjimku
} catch (UnsupportedLookAndFeelException ex) {
  // reakce na výjimku
}

Všimněte si, že je potřeba pochytat řadu výjimek (lze to samozřejmě udělat souhrnně, v příkladu jsou rozepsány jen kvůli názornosti). To vyplývá z toho, že se třída získává pomocí metod reflexe (vzpomeňte na Class.forName()). Implementace totiž počítá s tím, že daná třída může být známa až v okamžiku, kdy ji někdo hodlá použít.

Dále jsou tu metody addAuxiliaryLookAndFeel() a removeAuxiliaryLookAndFeel(). Ty si necháme na později. Co však zmíním už nyní, je řada různých metod pro zjišťování vlastností podle klíčů. Sahají od primitivních hodnot (getInt(), getBoolean()) až ke složitým (getFont(), getBorder()) a jsou poskytují jednoduchý přístup k hodnotám aktuálního vzhledu. Při zjišťování hodnoty má největší prioritu uživatelské nastavení (metoda put()), následuje výchozí hodnota aktuálního vzhledu, a poslední je systémová hodnota.

Ještě bych připomněl dvě metody, a sice addPropertyChangeListener() a removePropertyChangeListener(). Přidávají a odstraňují odběratele událostí - na změny vlastností lze tak snadno reagovat, jsou zasílány v podobě událostí PropertyChangeEvent.

Jak to všechno funguje

Objekty grafických komponent, jak známo, kreslí v metodě paint(). Ta ve výchozí implementaci v zásadě volá metodu paintComponent() pro vykreslení vlastní komponenty a pak paintChildren() pro potomky (komponenty vložené uvnitř). Je to sice ve skutečnosti trochu složitější, ale to teď není důležité. Nyní nás zajímá kreslení komponenty jako takové, u potomků to totiž bude stejné. A důležité právě je, že nyní dochází k oné delegaci kreslení.

Každá komponenta má totiž svého delegáta (potomka třídy ComponentUI, reálně třeba MetalButtonUI), který se stará o vlastní kreslení. Tomuto delegátovi se zavolá metoda update() a předá se mu grafický kontext - ale pozor, ne ten původní, nýbrž kopie. To proto, aby nějaká manipulace s kontextem neměla vliv na pozdější kreslení. Metoda update() ve výchozí implementaci vymaže pozadí (u neprůhledných komponent) a zavolá paint(), samozřejmě stále v rámci delegáta. V metodě paint() a odtud volaných metodách se pak již přímo kreslí. Je to velice jednoduché.

Zcela logicky nyní vyvstává otázka, odkud se berou ony objekty delegátů, které se starají o kreslení jednotlivých komponent. Poskytuje je UIManager, a to prostřednictvím metody getUI(). Pro danou komponentu vrátí delegáta, který se komponentě přiřadí na metodou setUI(). Obvykle je to tak, že přiřazení zajistí metoda updateUI(), která je určena k nastavení základního vzhledu komponenty - při inicializaci komponenty (proběhne automaticky) nebo po změně vzhledu (musí se zavolat explicitně).

Ještě by se někdo mohl ptát, kde delegáty vezme UIManager. On samotný je nemá, za to má ale mechanismus, jak je získávat. Toto činí třída UIDefaults. Ta si podle potřeby načítá třídy delegátů (druh potřebné třídy se dozví od komponenty pomocí metody getUIClassId()), vytváří jejich instance a poskytuje je třídě UIManager.

Multiplexovaný vzhled

Za normálních okolností má každá grafická komponenta právě jednoho delegáta. Někdy je ale potřeba, aby jich bylo víc - například kromě klasického zobrazení též zvukový výstup. K tomu slouží Multiplexing Look & Feel, zastupovaný třídou MultiLookAndFeel a souvisejícími delegáty.

Vnitřní fungování je velice jednoduché. Každý delegát tohoto vzhledu obsahuje kolekci jiných delegátů a jejich metody (např. update()) volá jednu po druhé. Vždy je určen výchozí vzhled, který je první v pořadí a je důležitý například pro dotazování na velikost komponenty.

Nyní se vrátíme k odloženým metodám addAuxiliaryLookAndFeel() a removeAuxiliaryLookAndFeel(). Právě ty totiž slouží přidávání a odebírání vzhledů, které používá multiplexovaný vzhled. Lze tak ale učinit i uživatelsky, a to přidáním takovéhoto řádku do souboru swing.properties:

swing.auxiliarylaf=com.sun.java.swing.plaf.motif.MotifLookAndFeel,
    mypackage.MySpecialLookAndFeel
    

Aktualizace GUI po změně vzhledu

Změníme-li za běhu programu vzhled GUI, změna se neprojeví ihned. Projeví se pouze u nově vytvářených komponent, ne však u těch již existujících. Aby se komponenty překreslily s novým vzhledem, musí se každé z nich zavolat již zmíněná metoda updateUI().

Naštěstí se to nemusí provádět ručně pro každou komponentu zvlášť, protože máme statickou metodu SwingUtilities.updateComponentTreeUI(). Té se předá reference na komponentu od které se GUI aktualizuje. Když tedy předáme nejvyšší komponentu v hierarchii, aktualizují se i všechny pod ní.

Vytvoření vlastního vzhledu

Kdo si chce vytvořit vlastní vzhled, má v zásadě dvě možnosti - buď implementovat celý balík tříd, obsahující popis vzhledu a všechny potřebné delegáty, anebo se vydat cestou mnohem méně pracnou - použít vzhled Synth. Synth je plně implementovaný vzhled obsažený v Javě od verze 5.0, umožňující kromě jiného snadno uživatelsky definovat, jak bude GUI vypadat.

Vzhled Synth lze používat buď na základě uživatelských souborů s popisem vzhledu nebo se vzhledovými objekty definovanými v programu. První možnost je důležitější, nicméně si připomeneme i tu druhou.

Definice vzhledu pomocí XML souboru

Než se pustíme do vytváření souboru s popisem vzhledu, je nezbytné sáhnout do programu a připravit ho pro takovýto způsob práce. Můžeme si napsat třeba takovouto metodu:

public static void prepareGui() {
  try {
    SynthLookAndFeel slf = new SynthLookAndFeel();
    InputStream is = App.class.getResourceAsStream("skin.xml");
    if (is == null)
      throw new IOException("Nelze otevřít XML soubor.");
    try {
      slf.load(is, App.class);
      UIManager.setLookAndFeel(slf);
    } finally {
      is.close();   // musí se zavřít vždycky
    }
  } catch (Exception e) {
    String msg = "Nelze použít Synth:"
        + System.getProperty("line.separator") + e.getMessage();
    JOptionPane.showMessageDialog(null, msg, "Chyba",
        JOptionPane.ERROR_MESSAGE);
  }
}

Metoda vytvoří instanci vzhledu, otevře stream pro čtení z XML souboru se vzhledem (skinem), zpracuje soubor a nastaví nový vzhled v UIManageru. Pokud něco selže, zobrazí panel se zprávou o chybě a aplikace pracuje s původním vzhledem. Všimněte si několika věcí - jednak přístupu k prostředku (resource), a dále způsobu zacházení s výjimkami.

Načítání souboru jako prostředku se používá dost často (ještě se k tomu někdy později vrátíme), protože programátor nemusí řešit, jakým způsobem se aplikace spouští - zda jsou soubory jen tak na disku, načítají se ze sítě nebo jsou v JAR archivu. Co se týká zmíněných výjimek, tak pro výjimky vzniklé při parsování XML souboru je zvláštní blok try, a to proto, aby se v rámci bloku finally vždy hned uzavřel vstupní stream.

Nyní už můžeme přejít k vlastnímu XML souboru, zde tedy skin.xml. Může vypadat například takto (sice poněkud bláznivý styl, ale jeho fungování je dobře vidět):

<?xml version="1.0" encoding="iso-8859-1"?>

<synth>
  <style id="button">
    <opaque value="TRUE"/>
    <font name="Times New Roman" size="16"/>
    <state>
      <color value="green" type="FOREGROUND"/>
      <color value="#0000ff" type="BACKGROUND"/>
    </state>
    <state value="PRESSED">
      <color value="#ffff00" type="BACKGROUND"/>
    </state>    
  </style>
  <bind style="button" type="region" key="Button"/>
</synth>

Nebudu zde popisovat přesný formát, ten si každý může prostudovat. Řeknu tedy jen ve stručnosti, co výše uvedený text dělá. Bude se nastavovat styl tlačítek, proto si ho nazveme button (název není kritický). Dále řekneme, že se bude kreslit neprůhledně, a že se použije písmo Times New Roman o velikosti 16. Dále tu máme dvě stavová nastavení. První označuje všechny stavy (obecné nastavení), druhé pak konkrétní stav, tedy v tomto případě stisknuté tlačítko. Obecně nastavujeme zelené popředí a modré pozadí, pro stisknuté tlačítko pak bude pozadí žluté. Tím je styl vytvořen a je ho potřeba přiřadit klíči - v tomto případě Button (podle těchto klíčů se styly identifikují při nastavování komponentám). Pokud bychom chtěli styl přiřadit všem klíčům, stačí místo názvu použít .*.

Definice vzhledu v programu

I když bychom mohli i v programu vygenerovat XML data s popisem skinu, je to zbytečně složité a také výpočetně náročné. Místo toho lze použít jednodušší cestu, a to tvořicí třídu odvozenou od SynthStyleFactory. Tato třída bude generovat jednotlivé styly (instance SynthStyle) pro určité komponenty a jejich regiony (komponenta může být rozdělena do více různých regionů).

Třídy odvozené od SynthStyle se musí vytvořit ručně, žádná výchozí není k dispozici. Implementovat se musí přinejmenším metody getColorForState() a getFontForState() - stav komponenty se získá z kontextu (SynthContext) metodou getComponentState().

Obecně ale není moc dobrý nápad to řešit takto, prakticky vždy si vystačíme s XML souborem, případně doplněným pomocnými třídami - například potomky SynthPainter - v případech, kdy potřebujeme něco opravdu speciálního. K těmto třídám se vrátíme ještě někdy později, až se dostaneme k technologii JavaBeans.

Jak se tiskne?

V příští kapitole budu opět na základě čtenářských dotazů měnit plán. Původně jsem zamýšlel začít příště se základy technologie JavaBeans, ovšem nakonec bude všechno jinak. Ukazuje se, jak obrovský zájem je o problematiku tisku v Javě. A protože tisk hodně úzce souvisí s "běžnou" grafikou (používají se stejné mechanismy), je vhodná chvíle právě teď. Takže příští kapitola bude věnována právě oblasti tisku z javovských programů.

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