Při vývoji GUI pro javovské aplikace se vyplatí dobře znát některé základní třídy. I v případě, kdy za nás "černou práci" dělá grafický návrhář v IDE, pomůže znalost těchto tříd výrazně zefektivnit vývoj a snadno nalézat případné chyby.
8.6.2006 09:00 | Lukáš Jelínek | přečteno 43096×
Přehled důležitých tříd začnu tím úplně nejzákladnějším, co bude každého doprovázet po celou dobu vývoje GUI a grafiky obecně. Dobrá práce s těmito třídami je první nutnou (nikoli postačující) podmínkou k efektivnímu vývoji kvalitních aplikací.
Ať už pracujeme s jakýmkoli grafickým objektem, často potřebujeme vědět, jak je daný objekt velký a kde se nachází. Proto potřebujeme rozumný způsob, jak s těmito informacemi pracovat.
Začneme s rozměry. Máme abstraktní třídu Dimension2D
, která pracuje s rozměry
naprosto obecně, a proto používá datový typ double
. To sice budeme později
potřebovat (a ještě se k tomu vrátíme), ale při práci s GUI nám obvykle postačí
celočíselné hodnoty, protože pracujeme s celými pixely. A k tomu slouží třída
Dimension
, která je potomkem zmíněné abstraktní třídy.
Třída neobsahuje (po datové stránce) nic jiného, než právě tyto celočíselné
rozměry. Instance třídy vrací některé metody, nejčastěji využijeme getSize()
definovanou již v java.awt.Component
(a všech potomcích, kam patří i naprostá
většina tříd frameworku Swing). Podívejme se na příklad:
JLabel lab = new JLabel("text"); ... Dimension d = lab.getSize(); ... d = lab.getSize(d);
V příkladu se nejprve vytvoří instance objektu JLabel
(popisek), s ní se
provedou nějaké operace a pak se zjišťuje jeho velikost. Metoda vytvoří novou
instanci třídy Dimension
a tu vrátí. V dalším případě ovšem metodě předáme
již vytvořenou instanci - ušetříme tím zbytečné volání konstruktoru objektu
a tím jak alokaci další paměti, tak i čas procesoru. Při častém volání se
to může výrazně projevit, proto vždy, když to jde, opakovaně využíváme
vytvořený objekt.
Když už jsme u toho šetření, každý grafický objekt má také metody getWidth()
a getHeight()
- pokud potřebujeme pouze jeden rozměr, namísto getSize()
voláme raději tyto metody (vrací přímo hodnotu jako typ int
). Podobně při
nastavování rozměrů volíme přednostně setSize(int, int)
oproti
setSize(Dimension)
, pokud máme k dispozici jednotlivé hodnoty. Ještě připomenu,
že hodnoty rozměrů mohou být i záporné (nekontrolují se), ale chování objektů
se zápornými rozměry obvykle není definováno.
U pozice objektů je to podobné. Je tu abstraktní třída Point2D
,
která má kromě potomka Point
také vnořené implementace Point2D.Float
a
Point2D.Double
(opět ponechávám na později). Záporné hodnoty už zde ale
samozřejmě mají smysl a lze je normálně používat. Krátký příklad:
Dimension d = new Dimension(100, 20); JButton but1 = new JButton("button 1"); JButton but2 = new JButton("button 2"); ... but1.setSize(d); but2.setSize(d); ...
Příklad ukazuje použití třídy Dimension
pro nastavení určitých rozměrů
většímu počtu objektů. Mohli bychom samozřejmě pracovat i s jednotlivými
rozměry, ale v tomto případě by se většinou mnoho neušetřilo.
Třídy Dimension
a Point
najdeme v balíku java.awt
, zmíněné abstraktní třídy
pak v balíku java.awt.geom
.
Při obsluze událostí získáme v metodě vždy objekt představující příslušnou
událost. Ten nám poskytuje cenné informace o tom, co, kde a jak se stalo.
Všechny třídy pro události vycházejí z jedné společné, a to
java.util.EventObject
. Tato nejvyšší třída má jen jednu specifickou metodu
- getSource()
. Ta vrací objekt, kde se daná událost stala. Pokud např.
stiskneme tlačítko, vytvoří se událost, kde bude zdrojem toto tlačítko.
Většina grafických událostí je odvozena od třídy AWTEvent
, potomka výše uvedené třídy.
Zde nás zajímá především metoda setSource()
. Umožňuje
změnit zdroj události, což se hodí např. při generování událostí ve složených
grafických komponentách. Událost postupně "probublává" skrz jednotlivé objekty
až k tomu zastřešujícímu a cestou se její zdroj mění. Příjemci události se pak
jeví, jako kdyby událost vygenerovala tato komponenta, přestože to bylo úplně jinak.
Nyní se ještě zmíním o jedné specifické třídě, která se často používá.
Je to ActionEvent
(pro "akce", např. stisk tlačítka). Nejzajímavější
metodou je getActionCommand()
, umožňující rozlišovat události podle příkazu,
který je spjat s generujícím objektem. Můžeme tak rozlišovat události podle
příkazů, i když pocházejí z různých zdrojů nebo může tentýž zdroj (podle svého
stavu) generovat různé události. Viz příklad:
JButton but1 = new JButton("Konec"); but1.setActionCommand("quit"); JMenuItem item1 = new JMenuItem("Konec"); item1.setActionCommand("quit"); ... public void actionPerformed(ActionEvent e) { if (e.getActionCommand().equals("quit")) { ... } }
Uvedený příklad ukazuje, jak by to vypadalo, kdyby se stejná akce generovala tlačítkem a položkou v menu. Rozlišovalo by se jen a pouze podle řetězce příkazu.
Začneme třídou, která se objevila již v minulém dílu seriálu. Ze třídy
SwingUtilities
jsme použili metodu invokeLater()
, která zajistí vykonání kódu
ve "správném" vlákně. Jenže tato třída obsahuje spoustu dalších metod, které
výrazně usnadňují některé operace.
Jsou tu např. metody na přepočet souřadných systémů (convertPoint()
,
convertPointFromScreen()
, convertPointToScreen()
atd.), operace s obdélníkovými
oblastmi (calculateInnerArea()
, computeDifference()
, computeIntersection()
ad.),
výpočet rozměrů textu (computeStringWidth()
), zkoumání kompozitních grafických
komponent (getRoot()
, getDeepestComponentAt()
) a různé další. Jsou mezi nimi
i speciální operace (jako např. paintComponent()
), které při nesprávném
použití dokáží nadělat pěknou paseku. Před používáním metod třídy SwingUtilities
doporučuji důkladné přečtení dokumentace - ale rozhodně tyto metody nezavrhujte,
jsou velice užitečné.
Zajímavé a důležité je rozhraní SwingConstants
. Implementuje ho celá řada
swingovských tříd a obsahem tohoto rozhraní jsou konstanty pro určení polohy
a orientace komponent. Rozhraní mimochodem porušuje důležitou zásadu - do rozhraní by
neměla patřit žádná data (ani konstanty). Nicméně někdo to takto - zřejmě
z pohodlnosti - navrhl, proto jeho existenci berme jako anomálii, ze které
bychom si ale neměli brát příklad.
Pojem grafického kontextu úzce souvisí hlavně se samotným kreslením, ale i v GUI je velice důležitý - hlavně v případě, kdy tvoříme nějaké vlastní komponenty. Lze si ho představit jako abstraktní kreslítko, kdy sice nevíme, jak a kam se kreslí, ale zajímá nás, co se kreslí. Tedy například kružnice, obdélník, rastrový obrázek nebo text.
Obvykle se setkáme s třídou Graphics
(i když se většinou jedná o instanci
jejího potomka Graphics2D
), poskytující celou řadu metod pro kreslení.
Instance tříd grafického kontextu běžně nevytváříme (kromě dvou případů; buď si
od existujícího kontextu odvozujeme nový, s jiným počátkem souřadnic a jinou
ořezovou oblastí, anebo tehdy, potřebujeme-li kontext zkopírovat kvůli
ochraně před nežádoucími změnami), pouze je používáme ke kreslení.
Jak takové kreslení vypadá, ukazuje příklad. Opět se nejedná o nic složitého, použití metod je velice snadné:
Graphics g = ... // odněkud získáme grafický kontext g.setColor(new Color(100, 100, 100)); // tmavě šedá barva g.fillRect(0, 0, 200, 100); // šedý vyplněný obdélník g.setColor(Color.RED); // nastavíme červenou barvu g.drawOval(10, 10, 180, 80); // červená elipsa
Z příkladu jsou zřejmé i základy práce s barvami (zde je v jednom případě použita barevná konstanta, ve druhém vytvoření barvy ze složek RGB). Blíže se na barvy podíváme později.
Nyní se zaměříme na třídu javax.swing.JComponent
. Z ní jsou totiž odvozeny téměř všechny
třídy grafických komponent, které se v prostředí Swing používají. Všimněte si,
že třída JComponent
je odvozena od třídy java.awt.Container
, a proto dědí
její metody (a metody nadtříd), kterých je požehnaně. Některé z nich jsou už
zastaralé a u nových programů se nepoužívají - ale i tak jich zbývá dost.
Nejprve to nejjednodušší. S některými metodami jsme pracovali již minule.
Máme tu dvojice setVisible()/isVisible()
, setEnabled()/isEnabled()
a známé
metody na práci s pozicí a rozměry (viz výše). Uvedu velice krátký příklad, není to
žádná velká věda:
JComponent c = ... // odněkud získáme komponentu if (c.isVisible()) // pokud je komponenta viditelná, c.setEnabled(false); // deaktivujeme ji
Snad je vše dostatečně zřejmé. Jen připomenu, že neaktivní komponenta nepřijímá žádné uživatelské vstupy. Ještě by se asi slušelo zmínit, že až na výjimky (na které později zvlášť upozorním) jsou ve výchozím stavu všechny komponenty aktivní a viditelné (zobrazované).
Na kreslení se teď podíváme z té druhé strany - teď už nebudeme tedy přímo kreslit,
ale zajímá nás, jak to vypadá zvenku. Těžiště spočívá v metodě paint()
,
kterou třída JComponent
obsahuje. Tuto metodu GUI volá k překreslení komponenty.
Standardní implementace volá metody paintComponent()
, paintBorder()
a
paintChildren()
- nejprve se tedy překreslí komponenta samotná, pak okraje
a nakonec potomci (myšleno komponenty vložené uvnitř).
Pokud potřebujeme v komponentě kreslit něco vlastního, máme několik možností.
První je předefinovat metodu paint()
- v ní dostaneme k dispozici grafický
kontext a můžeme kreslit dle libosti. Tento postup asi použijeme nejčastěji.
Příklad ukazuje, jak to vypadá:
public void paint(Graphics g) { Dimension d = getSize(); g.setColor(Color.LIGHT_GRAY); g.fill3DRect(0, 0, d.width, d.height, true); g.setColor(Color.BLACK); String s = ... // odněkud získáme řetězec Rectangle2D r = g.getFontMetrics().getStringBounds(s, g); g.drawString(s, (int) ((d.width - r.getWidth()) / 2), (int) ((d.height - r.getHeight()) / 2)); }
V příkladu se nejprve nakreslí "vystouplý" obdélník. Pak se na něj umístí text obsažený v odněkud získaném řetězci. Text je vodorovně i svisle vycentrován (centruje se opsaný obdélník, proto může text v některých případech vypadat podivně) - uvedené řešení je jen jedno z možných, lze to provést i jinak. Uvedené řešení také nijak nepočítá s okraji.
Druhou možností je předefinovat paintComponent()
. Ve výchozí implementaci tato
metoda, pokud existuje definice vzhledu UI (budeme se tomu věnovat v některé
z dalších kapitol), volá metodu UI, na níž se kreslení deleguje. Pokud takovou
delegaci nebudeme používat nebo potřebujeme něco nakreslit nezávisle,
můžeme metodu předefinovat. Pozor však, abychom po skončení kreslení
neponechali grafický kontext modifikovaný - mělo by to dopady na kreslení
okrajů a případných potomků. Proto buď (po změnách) vrátíme kontext do
původního stavu, nebo na něj vůbec nesaháme a pracujeme s kopií (metoda create()
třídy Graphics
).
I když v naprosté většině případů nemáme žádný důvod explicitně vyžadovat
překreslení nějaké komponenty, výjimečně taková potřeba může nastat. K tomu
slouží dvě metody - repaint()
a revalidate()
. Liší se tím, že zatímco první
vyvolá pouze překreslení komponenty jako takové (v podstatě naplánuje zavolání
metody paint()
), druhá způsobí aktualizaci zobrazení od kořene stromu
komponent (nejbližší nadřazená komponenta, pro kterou zavolání metody
isValidateRoot()
vrací true
) a případné překreslení všech komponent, které to
potřebují. Obě metody lze volat z libovolného vlákna (omezení, o kterých jsem
se zmiňoval, zde tedy neplatí) a s nutností jejich použití se setkáme skutečně
jen výjimečně.
Ještě pár slov k tisku. Komponenty mají metody print()
, printAll()
,
printComponent()
, printBorder()
a printChildren()
. Výchozí implementace
funguje tak, že jednotlivé metody volají své paintXXX()
protějšky a tisková
podoba GUI je tedy shodná s tou, jakou vidíme na obrazovce. V případě potřeby
můžeme metody předefinovat. To se ale netýká metod print()
a printAll()
,
které by se měnit neměly - k jejich použití se dostaneme později, v souvislosti
s problematikou tisku.
Kromě aktuálních rozměrů komponenty, se kterými se pracuje pomocí setSize()
a dalších metod, tu máme ještě trojici dalších parametrů - minimální, maximální
a preferovanou velikost. Tyto hodnoty mají smysl při používání tzv. správců
rozložení (layout managers), které podle nich umísťují komponenty. K práci
s těmito rozměry slouží metody setMinimumSize()
, setMaximumSize()
a
setPreferredSize()
, resp. jejich varianty getXXX()
. Všechny pracují pouze
s objektem třídy Dimension
, používání primitivního typu int
není možné.
Až se zanedlouho dostaneme k layout managerům, uvedu příklad i pro tyto parametry. Zatím bych to ponechal pouze na této teoretické úrovni.
Když jsem popisoval mechanismus zpracování událostí, možná někoho napadlo,
jak se takové události generují. Nepočítám-li nízkoúrovňové události od okenního
systému, generují většinu událostí přímo grafické komponenty - a také
zajišťují distribuci přihlášeným odběratelům. Často poskytují přímo
mechanismus, jak lze událost vygenerovat prostým zavoláním metody s patřičnými
parametry. Tyto metody se snadno poznají podle toho, že začínají fireXXX()
.
Např. JComponent
disponuje metodami firePropertyChange()
a fireVetoableChange()
.
Podobně mohou jiné třídy poskytovat různé jiné metody tohoto typu. Metody se
volají většinou v potomcích třídy (výjimečně i z jiných tříd) k vygenerování
události. Např. zmíněná (přetížená) metoda firePropertyChange()
se zavolá
poté, co se v kódu změnila nějaká pojmenovaná vlastnost. Podobně jsou třeba
v tabulce metody pro generování událostí při změnách v buňkách, přidání nebo
odebrání řádků a podobně. Příklad opět odložím na později, až to bude mít
větší význam.
Poslední třídou, na kterou se dnes dostane, je třída pro časovač
- javax.swing.Timer
. Hodí se pro všechny časově řízené operace v rámci GUI.
Kromě tohoto časovače máme ještě druhý, java.util.Timer
. Ten však při
"alarmu" vytvoří nové vlákno, čímž se jeho použití v GUI stává komplikovanější.
Proto pro GUI použijeme vždy javax.swing.Timer
, který vše provádí
v event-dispatching threadu.
Časovač pracuje tak, že v okamžiku vypršení prodlevy (nastal čas něco provést)
přidá do fronty patřičnou událost (v aktuální implementaci stejným způsobem,
jako kdybychom zavolali invokeLater()
), která nakonec vyústí v instanci třídy
ActionEvent
. Na časovač lze "navěsit" libovolný počet odběratelů této události.
Jak se s časovačem pracuje, ukazuje příklad:
class MyLabel extends JLabel implements ActionListener { javax.swing.Timer t = null; public MyLabel() { super(); t = new javax.swing.Timer(10000, this); t.start(); } public void actionPerformed(ActionEvent e) { setText(""); } }
Jedná se o rozšíření třídy JLabel
o zvláštní vlastnost - každých
10 sekund se obsah vymaže. K implementaci není co dodat, snad je vše dostatečně
zřejmé. Jen bych doporučil u třídy Timer
uvádět úplnou specifikaci, aby
třeba někdy později nedošlo k případné kolizi se stejnojmennou třídou
z balíku java.util
.
Tak to by bylo z oblasti "základních stavebních kamenů" všechno. Příště nás čekají složitější třídy frameworku Swing. Budeme z nich stavět složitější celky, podíváme se taktéž na některá úskalí, která tato oblast skýtá. Je to problematika velice zajímavá, proto doufám, že nejsem sám, kdo se na to těší.