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 27568×
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).
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ší.
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
.
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
.
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
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í.
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.
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 UIManager
u. 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 .*
.
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.
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ů.