Java (25) - základní grafické třídy

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 42717×

Bez čeho se nelze obejít

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

Měříme

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.

Události

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.

"Bedny nástrojů"

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.

Grafický kontext

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.

Grafické komponenty

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

Kreslení a tisk

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.

Rozměry

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.

Události

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.

Časovač

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.

Z čeho stavět?

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

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