5. Grafika dwuwymiarowa


Wykład jest poświęcony grafice dwuwymiarowej w Javie. Zobaczymy w jaki sposób można wykreślać własne obrazy oraz jak korzystać z gotowych, umieszczonych w plikach graficznych.

1. Wstęp

1.1. Kontekst graficzny

Do wykreślania obiektów graficznych w Javie służy wykreślacz - kontekst graficzny - będący obiektem (abstrakcyjnej) klasy java.awt.Graphics. Klasa ta udostępnia metody pozwalające na rysowanie figur geometrycznych jak również zewnętrznych obrazów.

Wykreślacz można pobrać metodą getGraphics() z:

Po użyciu wykreślacza, jeśli nie będzie on więcej potrzebny, należy mu wydać polecenie dispose(), które zniszczy zasoby przez niego wykorzystywane.

Nie wolno wywoływać na rzecz komponentu metody getGraphics(), ani żadnej innej z nim związanej (getWidth(), getHeight()) dopóki komponent, którego dotyczy wywołanie nie będzie w pełni zrealizowany (pack()). Aby to zapewnić stosuje się mechanizmy synchronizujące wątki, a także metodę SwingUtilities.invokeLater().

Zamiast rysować na wykreślaczu pobranym od komponentu, wygodniej jest umieścić kod rysujący w metodzie wykreślającej: paintComponent(Graphics g) w Swingu lub paint(Graphics g) dla AWT. Dzięki temu odświeżanie rysunku będzie następować automatycznie, ponieważ obie metody są wywoływane na zasadzie callback-u w wątku zdarzeniowym, gdy:

Nie należy umieszczać w tych metodach kodu wykonującego złożone operacje, ponieważ może to powodować opóźnienia w kolejce zdarzeniowej. Tym bardziej nie wolno tam używać metody Thread.sleep(). Należy również unikać bloków i metod synchronizowanych.

Aby użyć metody wykreślającej komponent do wykreślania własnej grafiki należy stworzyć podklasę klasy komponentu, w której się ją przedefiniuje. Pierwszą instrukcją w metodzie wykreślającej musi być wywołanie jej wersji z klasy bazowej: super.paintComponent(g) w Swingu lub super.paint(g) dla AWT.

Jako argument, metody te otrzymują odniesienie do kopii kontekstu graficznego komponentu, która zostanie zniszczona po wyjściu z metody odrysowującej, nawet jeśli istnieją jakieś odniesienia do niej stworzone w bloku paint() lub paintComponent().

Mimo, iż można rysować na dowolnym komponencie, to jednak do przedstawiania własnej grafiki służą głównie klasy Canvas dla AWT i JPanel w przypadku Swinga.

1.2. Położenia i rozmiary

Układ współrzędnych kontekstu graficznego jest zaczepiony w jego lewym-górnym rogu, tzn. jego współrzędne wynoszą [0,0] i rosną w prawo (pierwsza współrzędna) i w dół (druga). Współrzędne są umiejscowione pomiędzy pikselami urządzenia wyjściowego. Wykreślenie punktu o danych współrzędnych wyświetla najbliższy piksel leżący poniżej i po prawej stronie w stosunku do tego punktu.

Trzeba pamiętać o tym wykreślając kontury prostokątów (innych figur również): jeśli ma on szerokość w i wysokość h, to wypełniając go podaje się właśnie te wielkości, podczas gdy rysując jego krawędź należy przyjąć rozmiary o jeden piksel mniejsze: w-1, h-1.

1.2.1. Klasa Point

Położenia obiektów graficznych są przeważnie reprezentowane przy pomocy obiektu klasy java.awt.Point. Obiekt tej klasy przechowuje dwie liczby typu int, będące jego publicznymi składowymi (a więc można na nich bezpośrednio operować):

Konstruktor Point(int x, int y) inicjuje współrzędne podanymi wartościami, natomiast Point() nadaje współrzędnym domyślne wartości 0. Metody tej klasy umożliwiają dokonywanie prostych operacji na punktach:

1.2.2. Klasa Dimension

Do reprezentowania rozmiarów komponentów (i nie tylko) służy klasa java.awt.Dimension. Obiekt tej klasy zawiera publiczne składowe:

Konstruktor Dimension(int width, int height) inicjuje wymiary podanymi wartościami, natomiast Dimension() nadaje im domyślne wartości 0.


2. Wykreślanie

2.1. Obcięcia

Bardziej złożone kształty obcinania można uzyskać metodą setClip(Shape clip), przekazując jako argument odniesienie do obiektu klasy implementującj interfejs java.awt.Shape. Nie wszystkie tego typu kształty są honorowane.

Wykreślanie można ograniczyć do zadanego obszaru. Punkty leżące poza tym obszarem nie będą wykreślane, nawet jeśli rysowany obiekt z niego wystaje. Najprostszym sposobem zdefiniowania obszaru obcianania jest metoda setClip(int x, int y, int w, int h). Ogranicza ona wykreślanie do prostokąta zaczepionego w [x,y] i wymiarach [w,h].

Domyślny obszar wykreślania można przywrócić poprzez
setClip(0, 0, w-1, h-1) - gdzie w jest szerokością a h wysokością pierwotnego obszaru (komponentu). Innym sposobem jest setClip(null).

2.2. Kolory

Kolory są obiektami klasy java.awt.Color. Każdy kolor komponuje się z trzech składowych: czerwonej, zielonej i niebieskiej. Oprócz tego kolory mogą być mniej lub bardziej przezroczyste. Poziom nasycenia koloru daną składową określa liczba typu int z przedziału 0-255 lub typu float z przedziału 0.0-1.0 - zależnie od użytego konstruktora. Stopień przezroczystości reguluje składowa alpha przyjmująca wartości z powyższych przedziałów. Kolor całkowicie przezroczysty odpowiada wartości alpha równej 0 (lub 0.0), natomiast nieprzezroczysty odpowiada liczbie 255 (lub 1.0).

W klasie Color są zdefiniowane statyczne składowe określające 13 najczęściej używanych kolorów: black, blue, cyan, darkGray, gray, green, lightGray, magenta, orange, pink, red, white, yellow, oraz ich odpowiedniki pisane dużymi literami.

2.2.1. Konstruktory

2.2.2. Wybrane metody

2.2.3. Przykład użycia klasy Color

images/Colors.png

Poniższy program demonstruje stosowanie klasy Color, w szczególności przezroczystość kolorów. Cztery etykiety nakładają się na siebie w ten sposób, że prawa-dolna jest na spodzie, a lewa-górna na wierzchu. Skrajne etykiety mają kolory tła, które są nieprzezroczystymi wersjami kolorów tła etykiet środkowych. Kolory napisów na etykietach wewnętrznych są jaśniejszymi, a ich tła - ciemniejszymi wersjami kolorów dużych etykiet. Napis określa wartość czynnika alpha.

import java.awt.*;
import javax.swing.*;
import java.util.*;

class Colors extends JFrame {
    
    int xy = 5;
    Random rand = new Random();
    Container cp = getContentPane();
    
    Colors()
    {
        super("Colors");
        setSize(220, 240);
        setLocation(200, 200);
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        cp.setLayout(null);
        
        Color rIntClr = new Color(rand.nextInt(256),
                                  rand.nextInt(256),  
                                  rand.nextInt(256),  
                                  rand.nextInt(256)  
                                  );
        Color rFltClr = new Color(rand.nextFloat(),
                                  rand.nextFloat(),
                                  rand.nextFloat(),
                                  rand.nextFloat()
                                  );
        int ri = rIntClr.getRed();
        int gi = rIntClr.getGreen();
        int bi = rIntClr.getBlue();
                                  
        int rf = rFltClr.getRed();
        int gf = rFltClr.getGreen();
        int bf = rFltClr.getBlue();
        
        Color opI  = new Color(ri, gi, bi);
        Color opF  = new Color(rf, gf, bf);
        
        addLab(opF);
        addLab(rIntClr);
        addLab(rFltClr);
        addLab(opI);
        
        setVisible(true);
    }    
    
    void addLab(Color c)
    {
        JLabel inner = new JLabel("a=" + c.getAlpha());
        inner.setOpaque(true);
        inner.setBackground(c.darker());
        inner.setForeground(c.brighter());
        inner.setBorder(BorderFactory.createEtchedBorder());
        inner.setHorizontalAlignment(JLabel.CENTER);
        inner.setBounds(10, 50, 60, 20);
        
        JLabel label = new JLabel();
        label.setOpaque(true);
        label.setBackground(c);
        label.setBounds(xy, xy, 80, 80);
        label.setBorder(BorderFactory.createEtchedBorder());
        label.setLayout(null);
        label.add(inner);
        cp.add(label);
        xy += 40;
    }
    
    public static void main(String[] args){
        new Colors();
    }
}

2.3. Fonty

Czcionki są obiektami klasy java.awt.Font. Tworzy się je konstruktorem

Font(String name, int style, int size), przy czym:
  1. name jest
    • nazwą logiczną czcionki: Dialog, DialogInput, Monospaced, Serif, SansSerif, Symbol.
    • lub nazwą (kroju) czcionki, która identyfikuje ją w macierzystym systemie operacyjnym - np.: Arial Bold, Courier Bold Italic. Ta metoda jest preferowana, ponieważ odnosi się do konkretnych fontów zainstalowanych w systemie. Może jednak w związku z tym powodować problemy z przenośnością.
  2. style jest stałą (z klasy Font) określającą styl: PLAIN, BOLD, ITALIC, bądź sumą bitową dwu ostatnich.
  3. size jest rozmiarem czcionki w punktach (1 pt = 1/72 cala).

Czcionki można modyfikować przeciążonymi metodami deriveFont() klasy Font, które na podstawie tej czcionki tworzą nową, o atrybutach określonych przez parametry konkretnej wersji metody. Np. poniższa metoda tworzy wersję wytłuszczoną czcionki font:

Font bold = font.deriveFont(Font.BOLD) 

Aby umiejscowić napis w kontekście graficznym, potrzebna jest znajomość jego rozmiarów. Klasa FontMetrics zawiera informacje o sposobie wykreślania konkretnych fontów. Odniesienie do obiektu tej klasy zwraca metoda getFontMetrics() z klasy Graphics.

Linią bazową tekstu jest linia, na której jest on oparty. Jest to linia przechodząca bezpośrednio pod znakiem '_'. Wszystkie rozmiary i położenia podaje się względem tej linii (jej lewego początku).

Przy ustalaniu położenia tekstu mogą być potrzebne następujące metody:

2.4. Obiekty graficzne

Klasa Graphics dostarcza metody umożliwiające wykreślanie prostych obiektów graficznych. Bardziej złożone obiekty komponuje się przy pomocy tych elementarnych metod. Obiekty są wykreślane aktualnie obowiązującym kolorem, który można zmienić metodą setColor(Color).

2.4.1. Linie

Metody wykreślające odcinki lub obszary przez nie ograniczone (aktualnie obowiązującym kolorem):

2.4.2. Prostokąty

Metody służące do wykreślania i wypełniania kolorem obszarów prostokątnych:

2.4.3. Elipsy

2.4.4. Łuki

Metoda

drawArc(int x, int y, int w, int h, int startAngle, int arcAngle) 
rysuje eliptyczny łuk wpisany w prostokąt zaczepiony w punkcie [x,y] i o bokach [w, h]. Parametr startAngle określa położenie kątowe początku łuku a arcAngle określa jego rozpiętość w stopniach. Kąty są interpretowane w ten sposób, że 0o odpowiada godzinie 3 na zegarku i rośnie w kierunku przeciwnym do ruchu wskazówek. Wartości ujemne odpowiadają godzinom następującym po 3. Zatem startAngle == 0 oznacza, że łuk będzie zaczynał się na godz. 3, a startAngle == 90 - na godz. 12. Podobnie arcAngle == 360 oznacza, że łuk będzie elipsą wpisaną w ten prostokąt, a arcAngle == -180 tworzy jej dolną połowę.

Metoda fillArc(int x, int y, int w, int h, int startAngle, int arcAngle) wypełnia łuk kolorem.

2.4.5. Napisy

Oprócz koloru kontekst graficzny przechowuje również obowiązujący krój pisma. Można go zmienić metodą setFont(Font f), pobrać metodą getFont(). Wykreślając napis podaje się jako współrzędne położenie jego linii bazowej.

2.4.6. Przykład z obiektami geometrycznymi

images/JHelloWorld.png

Następny program ilustruje niektóre metody wykreślające klasy Graphics. Tworzymy własny panel dziedzicząc klasę JPanel. Wykreślanie odbywa się w metodzie paintComponent(), która jest wywoływana na zasadzie callback-u zawsze, gdy komponent wymaga odrysowania. Przedefiniowana metoda getPreferredSize() jest potrzebna do ustalenia rozmiarów naszego komponentu i wywoływana przez zarządcę rozkładu. Należy pamiętać o wywołaniu super.paintComponent(g) w pierwszej instrukcji metody odrysowującej.

import java.awt.*;
import javax.swing.*;

class JHelloWorld extends JPanel {

    public void paintComponent(Graphics g){
        
	super.paintComponent(g);
        
        g.setColor(Color.blue);
        g.fillRect(0, 0, 200, 200);
        
        g.setColor(Color.yellow);
        g.fillOval(50, 50, 100, 100);
        
        g.setColor(Color.red);
        g.drawArc(75, 100, 50, 33, -20, -140);

        g.setColor(Color.black);
        g.fillOval(76, 83, 8, 10);
        g.fillOval(116, 83, 8, 10);        
        
        g.setColor(Color.orange);
        g.fillPolygon(new int[]{100, 95, 105}, new int[]{95, 115, 115}, 3);
        
        g.setColor(Color.green);
        g.setFont(new Font("Serif", Font.BOLD, 30));
        g.drawString("Hello World", 3, 199);
    }

    public Dimension getPreferredSize(){
        return new Dimension(200, 200);
    }
    
    public static void main(String[] args){
        
        JFrame frame = new JFrame("JHelloWorld");
        JPanel world = new JHelloWorld();
        frame.getContentPane().add(world);
        frame.setLocation(200, 200);
        frame.pack();
        frame.show();
    }

}

2.5. Tło

Kolorem tła jest jest kolor tła ciężkiego komponentu, na którym odbywa się wykreślanie. Zatem może on być zależny od systemu. Po wywołaniu metody clearRect(), która czyści prostokątny obszar ukaże się właśnie ten kolor jako tło - co ilustruje następny przykład. Aby mieć pewność co do koloru tła, należy wypełnić żądanym kolorem prostokątny obszar pokrywający cały pulpit, na którym odbywa się rysowanie. W praktyce oznacza to wywołanie metod setColor() oraz fillRect() na rzecz odniesienia do obiektu klasy Graphics pozyskanego od danego komponentu.

2.5.1. Przykład kolorów tła różnych obszarów

images/BackShow.png

Kolorem tła głównego okna JFrame jest green (widoczny poprzez wywołanie metody clearRect()). Jego contentPane ma kolor tła cyan (marginesy). Własny panel wstawiony do contentPane ma kolor tła blue. Umieszczona w nim etykieta ma ramkę, której kolory (są dwa) powstały na podstawie koloru tła tej etykiety. Jest on zdeterminowany przez domyślny L&F i ma wrtość javax.swing.plaf.ColorUIResource[r=204,g=204,b=204].


import java.awt.*;
import javax.swing.*;


class BackShow extends JFrame {

    BackShow()
    {
        super("BackShow");
        setLocation(200, 200);
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        
        Container cp = getContentPane();
        cp.setLayout(new FlowLayout());
        cp.add(new BackPane());

        cp.setBackground(Color.cyan);
        setBackground(Color.green);
        
        pack();
        show();
    }
    
    public static void main(String[] args)
    {
        new BackShow();
    }
}

class BackPane extends JPanel 
{
    BackPane()
    {
        setBackground(Color.blue);
        setLayout(null);
        
        JLabel lab = new JLabel("JLabel");
        lab.setBorder(BorderFactory.createEtchedBorder());
        lab.setBounds(90, 90, 100, 100);
        add(lab);
        
    }

    public void paintComponent(Graphics g)
    {
        super.paintComponent(g);
        g.setColor(Color.magenta);
        g.fillRect(50, 50, 100, 100);
        g.clearRect(10, 10, 100, 100);
        
    }
    
    public Dimension getPreferredSize()
    {
        return new Dimension(200, 200);
    }
}

2.6. Tryb XOR

Rysowanie może odbywać się w dwóch trybach przełączanych następującymi metodami:

  1. setPaintMode() - przywraca normalny sposób kolorowania (tryb domyślny)
  2. setXORMode(Color xorClr) - po wywołaniu tej metody kolory wykreślanych obiektów będą się zmieniać w następujący sposób:
    • nadrysowanie koloru bieżącego nim samym pokaże kolor xorClr
    • rysowanie kolorem xorClr nie wprowadza żadnych zmian.
    • rysowanie na kolorze xorClr daje normalny efekt.
    • rysowanie na innych kolorach daje nieprzewidywalny skutek, ale dwukrotne wykreślanie tym samym kolorem przywraca kolor pierwotny.

2.6.1. Demonstracja trybu XOR

images/PaintModes.png

Następny programik ilustruje tryb XOR. Pierwsze dwa kwadraty (licząc od lewego-górnego rogu) są wykreślane w trybie XOR z kolorem tła (niebieski). Pozostałe w trybie XOR z kolorem zielonym. Ponadto pierwsze cztery są rysowane kolorem białym, piąty zielonym, a pozostałe dwa niebieskim (!). Każdy z siedmiu kwadratów ma ramkę w kolorze tła, dla lepszej widoczności.


import java.awt.*;

class PaintModes extends Canvas
{
    int xy;
    int wh = 50;
    Color bgc = Color.blue;
    Color fgc = Color.white;
    Color xor = Color.green;
    
    PaintModes()
    {
        setBackground(bgc);
        setForeground(fgc);
    }
    
    public void paint(Graphics g)
    {
        super.paint(g);
        xy = 0;
                
        g.setXORMode(bgc);
        fRect(g, fgc);
        fRect(g, fgc);
        g.setXORMode(xor);
        fRect(g, fgc);
        fRect(g, fgc);
        fRect(g, xor);
        fRect(g, bgc);
        fRect(g, bgc);
    }

    
    void fRect(Graphics graph, Color c)
    {
        graph.setColor(bgc);
        graph.drawRect(xy, xy, wh, wh);
        graph.setColor(c);
        graph.fillRect(xy, xy, wh, wh);
        xy += (wh/2);
    }
    
    public Dimension getPreferredSize()
    {
        return new Dimension(200, 200);
    }
    
    public static void main(String[] args)
    {
        
        Frame  frame = new Frame("PaintModes");
        Canvas modes = new PaintModes();
        frame.add(modes);
        frame.setLocation(300, 300);
        frame.pack();
        frame.setVisible(true);
    }
}

3. Grafika 2D

Klasa java.awt.Graphics2D - będąc podklasą klasy Graphics - dodaje do niej nowe możliwości. Aby się nimi posługiwać należy rzutować odniesienie do wykreślacza typu Graphics, na Graphics2D. Jest to zawsze wykonalne, ponieważ klasa wykreślacza jest bezpośrednią podklasą klasy Graphics2D. Jest to podstawowa klasa do wykreślania 2-wymiarowych obiektów.

Klasa Graphics2D posiada następujące atrybuty, które determinują wynik wykreślania:

Ponadto możliwe są następujące operacje wykreślające:

Wszystkie te operacje wykonywane są przy użyciu aktualnego trybu nakładania kolorów i są ograniczone do obowiązującego obszaru obcinania (domyślnie cały wykreślacz), a ich położenia są poddawane obowiązującemu przekształceniu (domyślnie identyczność).

3.1. Nakładanie kolorów

images/PaintModes2D.png

Klasa Graphics dysponowała dwoma trybami nakładania kolorów: normalny i XOR. W klasie Graphics2D metoda setComposite(Composite comp) pozwala na sterowanie nakładaniem się kolorów w znacznie szerszym zakresie. Interfejs Composite określa sposób w jaki będzie nałożony aktualny kolor na już wykreślone piksele. Klasa AlphaComposite implementuje ten interfejs, dostarczając 12 stałych określających sposoby przenikania się kolorów.

Metodą getComposite() można pobrać aktualny sposób komponowania kolorów, co może być potrzebne do przywrócenia poprzedniego trybu.

Obiektów klasy AlphaComposite nie tworzy się konstruktorem (bo go nie ma), lecz używa się statycznej metody getInstance(), która zwraca obiekt tej klasy. Przekazuje się jej jako argumenty: alpha - współczynnik przenikania i (opcjonalnie) rule - stałą określającą regułę, np.: AlphaComposite.SRC_OVER. Można również użyć statycznych składowych tej klasy przechowujących odniesienia do gotowych obiektów reprezentujących nieprzezroczyste wersje (alpha==1.0) każdego trybu np.: AlphaComposite.SrcOver.

3.1.1. Demonstracja nakładania kolorów

Na obrazku widzimy efekty jakie dają wszystkie 12 reguł nakładania się kolorów dla stałej przenikania 1.0 i dla stałej przenikania 0.4. Napisy pod obrazkami są takie jak nazwy stałych z klasy AlphaComposite (wyjąwszy wartość przenikania). Obrazek jest wynikiem działania poniższego programu.

Poszczególne jego fragmenty są tworzone najpierw w buforze, a następnie wykreślane w kontekście graficznym komponentu po to, by uniknąć niepożądanego wpływu koloru tła. Dysponujemy dzięki temu czystą kartką - wykreślaczem bufora.

import java.awt.*;
import javax.swing.*;
import java.awt.image.*;
import java.awt.geom.*;

class PaintModes2D extends JPanel {

    int rule;
    float alpha;
    String rName;
    
    PaintModes2D(String desc, int acr, float al){
        setBorder(BorderFactory.createLineBorder(Color.cyan));
        setBackground(Color.white);
        rName = desc + "  " + al;
        rule = acr;
        alpha = al;
    }
    
    public void paintComponent(Graphics g){
        super.paintComponent(g);    
        Graphics2D g2d = (Graphics2D)g;    
        
        int w = getWidth();
        int h = getHeight();

        BufferedImage buffImg = 
            new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
        Graphics2D gbi = buffImg.createGraphics();
        
        gbi.setColor(Color.red);
        gbi.setFont(new Font("Sans", Font.BOLD, 10));
        gbi.drawString(rName, (w-6*rName.length())/2, getHeight()-5);

        gbi.setColor(Color.blue);
        gbi.fillRect(8, 8, 2*w/3, h/2);

        gbi.setComposite(AlphaComposite.getInstance(rule, alpha));
        gbi.setColor(Color.green);
        gbi.fillOval(w/4, h/4, 2*w/3, h/2);

        g2d.drawImage(buffImg, null, 0, 0);
    }

    public Dimension getPreferredSize(){
        return new Dimension(100, 80);
    }

    static void addMode(Container cp, String title, int mode){
        cp.add(new PaintModes2D(title, mode, 1f));
        cp.add(new PaintModes2D(title, mode, 0.4f));
    }

    static void addModes(Container cp){
        addMode(cp, "CLEAR",    AlphaComposite.CLEAR);
        addMode(cp, "XOR",      AlphaComposite.XOR);
        addMode(cp, "SRC",      AlphaComposite.SRC);
        addMode(cp, "SRC_IN",   AlphaComposite.SRC_IN);
        addMode(cp, "SRC_OUT",  AlphaComposite.SRC_OUT);
        addMode(cp, "SRC_OVER", AlphaComposite.SRC_OVER);
        addMode(cp, "SRC_ATOP", AlphaComposite.SRC_ATOP);
        addMode(cp, "DST",      AlphaComposite.DST);
        addMode(cp, "DST_IN",   AlphaComposite.DST_IN);
        addMode(cp, "DST_OUT",  AlphaComposite.DST_OUT);
        addMode(cp, "DST_OVER", AlphaComposite.DST_OVER);
        addMode(cp, "DST_ATOP", AlphaComposite.DST_ATOP);
    }

    public static void main(String[] args){

        JFrame frame = new JFrame("PaintModes2D");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setLocation(200, 200);
        Container cp = frame.getContentPane();
        cp.setLayout(new GridLayout(12, 2));
        
        addModes(cp);
        
        frame.pack();
        frame.show();
    }
}

Metoda addMode() dodaje pojedynczy podobrazek dla danej reguły nakładania kolorów i dwóch stałych przenikania: 1 i 0.4. Jest ona wywoływana w metodzie addModes() dla wszystkich stałych z klasy AlphaComposite. Metoda paintComponent() jest odpowiedzialna za wykreślanie poszczególnych komórek. Wywołuje metodę setComposite() na rzecz wykreślacza (klasy Graphics2D), która ustala nowy tryb nakładania.

3.2. Kształty i obiekty graficzne

Kształty są obiektami klas implementujących interfejs java.awt.Shape. Można je obrysowywać metodą draw(Shape s) lub wypełniać metodą fill(Shape s) - obie z klasy Graphics2D.

Pakiet java.awt.geom dostarcza kilku predefiniowanych kształtów:

Większość klas tego pakietu, reprezentujących obiekty graficzne, ma trzy warianty:

  1. XXX2D - klasa abstrakcyjna, określająca właściwości figury, zawierająca poniższe dwie statyczne klasy wewnętrzne:
  2. XXX2D.Float - współrzędne punktów są w pojedynczej precyzji (typu float)
  3. XXX2D.Double - współrzędne są liczbami podwójnej precyzji (typ double)

Klasa java.awt.Rectangle, będąca podklasą Rectangle2D, reprezentuje prostokąt o współrzędnych całkowitoliczbowych (int).

3.2.1. Kontury

Klasa java.awt.geom.GeneralPath jest kształtem złożonym z kilku krzywych (stopni 1, 2, lub 3). Taką ścieżkę tworzy się konstruktorem GeneralPath(), a następnie buduje - dodając do niej kolejne krzywe, punkty lub inne kontury - następującymi metodami:

Wnętrze konturu ograniczonego przez zestaw krzywych jest określone na jeden z dwóch sposobów (określonych w konstruktorze, lub metodą setWindingRule()): Od tego jak określimy wnętrze złożonego obszaru zależy, gdzie będą pojawiać się dziury po jego wypełnieniu pędzlem. Domyślnie przyjmowany jest sposób WIND_NON_ZERO.

3.2.2. Obszary

Klasa Area definiuje obszary i operacje algebraiczne, jakie można na nich wykonywać: dodawanie, odejmowanie i przecinanie obszarów. Obiekt tej klasy tworzy się konstruktorem bezargumentowym Area() lub podając początkowy kształt: Area(Shape s), którego wnętrze będzie tworzyć ten obszar.

Bardziej złożone obszary uzyskuje się wykonując następujące operacje:

images/Shapes.png

W kolejnym programie metoda octopus() pokazuje jak tworzyć złożone kształty. Jej pierwszy i drugi argument określa położenie figury, a trzeci wielkość. Warto zauważyć, że elipsy dodawane jako oczy, leżą we wnętrzu tworzonych obszarów - ale można to zmienić podając do konstruktora GeneralPath argument GeneralPath.WIND_EVEN_ODD. Zastosowanie antyaliasingu do wykreślania zielonych kształtów wygładza ich kontury.


import java.awt.*;
import javax.swing.*;
import java.awt.geom.*;

class Shapes extends JPanel
{
    
    public void paintComponent(Graphics g){
        
        super.paintComponent(g);
        Graphics2D g2d = (Graphics2D)g;
                
        Shape ob = octopus(60, 100, 11);
        g2d.setColor(Color.blue);
        g2d.fill(ob);
        g2d.setColor(Color.yellow);
        g2d.draw(ob);
        
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, 
                             RenderingHints.VALUE_ANTIALIAS_ON);
        
        Area af = new Area(octopus(130, 130, 11));
        af.subtract(new Area(new Ellipse2D.Float(135, 145, 22, 11)));
        af.subtract(new Area(new Ellipse2D.Float(102, 145, 22, 11)));
        g2d.setColor(Color.green.darker().darker());
        g2d.fill(af);

        Shape so = octopus(110, 20, 14);            
        Area ao = new Area(so);
        ao.add(new Area(octopus(80, 70, 5)));
        ao.add(new Area(octopus(140, 70, 5)));
        g2d.draw(so);        
        g2d.setColor(new Color(180, 220, 60, 150));
        g2d.fill(ao);
        g2d.setColor(Color.black);
        g2d.draw(ao);        
    }


    Shape octopus(int x, int y, int s){
        GeneralPath gp = new GeneralPath();
        
        gp.append(new Ellipse2D.Float(x+s, y+s, s, 2*s), false);
        gp.append(new Ellipse2D.Float(x-2*s, y+s, s, 2*s), false);
        
        gp.moveTo(x, y);
        gp.lineTo(x+s, y);
        gp.quadTo(x+5*s, y, x+5*s, y+5*s);
        gp.curveTo(x+s, y+2*s, x+3*s, y+4*s, x, y+5*s);
        gp.curveTo(x-3*s, y+4*s, x-s, y+2*s, x-5*s, y+5*s);
        gp.quadTo(x-5*s, y, x-s, y);
        gp.closePath();
        return gp;
    }
        
    public Dimension getPreferredSize(){
        return new Dimension(200, 200);
    }
    
    public static void main(String[] args){
        
        JFrame frame = new JFrame("Shapes");
        JPanel shape = new Shapes();
        frame.getContentPane().add(shape);
        frame.setLocation(300, 300);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.pack();
        frame.show();
    }
}

3.2.3. Teksty

Do graficznej reprezenatacji napisów służy klasa java.awt.font.TextLayout. Obiekt tej klasy jest odpowiedzialny za zwymiarowanie napisu w aktualnym kontekście. Podstawowy konstruktor

TextLayout(String s, Font f, FontRenderContext frc)
pobiera jako trzeci argument frc odniesienie do obiektu obrazowania czcionki. Można go uzyskać metodą z klasy Graphics2D o nazwie getFontRenderContext().

Wykreślanie napisu przy pomocy klasy TextLayout może być wykonane na dwa sposoby:

W obu przypadkach może być potrzebna metoda Rectangle2D getBounds() - zwracająca prostokąt, w którym mieści się napis.

Współrzędna pionowa jego położenia (lewego-górnego rogu) będzie ujemna, ponieważ jest ona liczona względem lewego początku linii bazowej tekstu. Współrzędna pozioma może być ujemna, jeśli pismo jest pochyłe.

images/Text2D.png

Przykładowy program wyświetla dwoma sposobami napisy, używając klasy AlphaComposite do ustalenia sposobu nakładania kolorów. Wykreślanie w buforze jest potrzebne dla uniknięcia niepożądanej interakcji z innymi, już wykreślonymi, kolorami kontekstu graficznego. Użyta metoda translate(int x, int y) z klasy Graphics przemieszcza współrzędne wykreślacza tak, że jego początek ([0,0]) będzie w miejscu określonym przez argumenty [x,y]. Należy zwrócić uwagę na sposób pozycjonowania tekstu - różny w obu przypadkach.

import java.awt.*;
import javax.swing.*;
import java.awt.font.*;
import java.awt.geom.*;
import java.awt.image.*;

class Text2D extends JPanel
{
    public void paintComponent(Graphics g){
        
        super.paintComponent(g);
        Graphics2D g2d = (Graphics2D)g;
        int h = getHeight();
        int w = getWidth();    
        FontRenderContext frc = g2d.getFontRenderContext();  
            // napisy wykreslamy w buforze
        BufferedImage bimg = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
        Graphics2D gbi = bimg.createGraphics();
  
            // wykreslenie napisu "2D" 
        Font fb = new Font("Lucida Bright", Font.BOLD, getHeight());
        TextLayout tlb = new TextLayout("2D", fb, frc);
            // dla ustalenia polozenia napisu
        Rectangle2D rb = tlb.getBounds();
        gbi.setColor(Color.cyan);
        tlb.draw(gbi, 
                (float)(w-rb.getWidth()-rb.getX())/2, 
                h/2+(float)rb.getHeight()/2);
                        
            // wykreslenie napisu "Java" 
        Font ff = new Font("Serif", 
                            Font.BOLD|Font.ITALIC, 
                            (int)(0.6*h));
        TextLayout tlf = new TextLayout("Java", ff, frc);                                             
            // dla ustalenia polozenia napisu
        Rectangle2D rf = tlf.getBounds();
        Shape st = tlf.getOutline(null);
        int yPos = (int)((h-rf.getHeight())/2);
        int xPos = (int)((w-rf.getWidth())/2);
            // przemieszczenie układu współrzędnych
        gbi.translate(xPos-rf.getX(), yPos-rf.getY());
            // ustalenie trybu nakladania kolorow w buforze
        gbi.setComposite(AlphaComposite.SrcOut);
        gbi.setColor(Color.blue);
        gbi.fill(st);
        gbi.setComposite(AlphaComposite.Src);
        gbi.setColor(Color.yellow);
        gbi.draw(st);
        
        g2d.setColor(new Color(217, 127, 255));
        g2d.fillRect(0, 0, w, h);
        g2d.drawImage(bimg, null, 0, 0);
        
    }
    
    public Dimension getPreferredSize(){
        return new Dimension(300, 200);
    }
    
    public static void main(String[] args){
        
        JFrame frame = new JFrame("Text2D");
        JPanel textd = new Text2D();
        frame.getContentPane().add(textd);
        frame.setLocation(300, 300);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.pack();
        frame.show();
    }
}

3.2.4. Zawieranie

Interfejs Shape zawiera następujące metody (typu boolean) umożliwiające rozstrzyganie wzajemnego położenia danego obszaru względem innych:
Sprawdzanie zawierania się punktów można również wykonywać w stosunku do tekstów. Klasa TextLayout udostępnia w tym celu kilka metod, zwracających obiekt klasy TextHitInfo. Do pobrania prostokąta ograniczającego dany obiekt służy metoda getBounds(). Jest ona zdefiniowana w klasie TextLayout, jak również w interfejsie Shape.

W klasie Graphics2d jest metoda hit(Rectangle r, Shape s, boolean stroke), która sprawdza czy prostokąt r przecina kontur s. Jeśli stroke ma wartość false, to jest brane pod uwagę wnętrze kształtu s, w przeciwnym wypadku sprawdzane jest przecięcie jego zarysu (wykonanego przy pomocy obowiązującego pióra) z prostokoątem r.

3.3. Rysowanie linii

W przeciwieństwie do klasy Graphics, w Graphics2D można wykreślać linie o dowolnej grubości. Przez środek pogrubionej linii przechodzi linia środkowa, której współrzędne podaje się przy wykreślaniu. Ponadto, oprócz linii prostych i łamanych można wykreślać krzywe określonych typów.

3.3.1. Pióra

Pióra są obiektami klas implementujących interfejs Stroke. Klasa BasicStroke implementuje ten interfejs i dostarcza dodatkowe metody określające grubość, kształty zagięcia, zakończenia lub przerywanie linii.

Konstruktor BasicStroke() konstruuje pióro domyślne, a BasicStroke(float width) - pióro o podanej szerokości. Pozostałe konstruktory zostaną opisane dalej. Pióro domyślne wykreśla ciągły ślad o szerokości jednego piksela. Metoda setStroke(Stroke s) (w klasie Graphics2D) ustala nowy kształt pióra, który jest zazwyczaj obiektem klasy BasicStroke, natomiast metoda Stroke getStroke() dostarcza aktualne pióro.

3.3.2. Zakończenia linii

Linie mogą mieć trzy rodzaje zakończeń określanych stałymi klasy BasicStroke:

3.3.3. Połączenia linii

Połączenie dwóch linii może być zakończone na trzy sposoby:

Konstruktor BasicStroke(float width, int cap, int join) pozwala ustalić sposób zakończenia i łączenia linii inny niż domyślne. Jeśli linie są nachylone do siebie pod małym kątem a łączenie jest zaostrzone - to może okazać się zbyt długie. Można je ograniczyć dodatkowym argumentem konstruktora, który określi maksymalną długość zaostrzenia:
BasicStroke(float width, int cap, int join, float miterLimit). Jeśli łączenie przekracza miterLimit, to zostaje zastąpione połączeniem spłaszczonym.

3.3.4. Linie przerywane

Wykreślana linia nie musi być ciągła - może być przerywana. Parametry określające sposób przerywania i moment jego rozpoczęcia podaje się w tablicy jako argument konstruktora:

BasicStroke(float w, int cap, int join, float mLim, float[] dash, float phase).
Parametr float[] dash jest tablicą zawierającą długości kresek i odstępów. Przerywanie linii nastąpi od miejsca określonego przez parametr phase (jesli phase == 0, to od razu).

3.3.5. Przykład rysowania linii

images/LinesDemo.png

Program będzie rysował linie różnymi piórami, o różnych zakończeniach i sposobach łączenia. Każda z nich jest rysowana dwoma piórami: cienkie pokazuje linię środkową grubego.Należy zwrócić uwagę na podobieństwa i różnice pomiędzy zakończeniem ściętym (kolor magenta) i kwadratowym (kolor zielony). Linia przerywana jest wykreślana od lewego-górnego końca z przesunięciem 10 pikseli i ma zaostrzone łączenie.

import java.awt.*;
import javax.swing.*;

class LinesDemo extends JPanel
{
    
    public void paintComponent(Graphics g){
        super.paintComponent(g);
        Graphics2D g2d = (Graphics2D)g;
        
        drawZ(g2d, Color.green, 50, 50, 15, 
              new BasicStroke(20f,
                              BasicStroke.CAP_SQUARE,
                              BasicStroke.JOIN_MITER,
                              10
                              ));
        
        drawZ(g2d, Color.blue, 90, 100, 40, 
              new BasicStroke(5f,
                              BasicStroke.CAP_ROUND,
                              BasicStroke.JOIN_MITER,
                              5f,
                              new float[]{10f, 15f},
                              10f
                              ));
        
        drawZ(g2d, Color.cyan, 60, 25, 105, 
              new BasicStroke(30f,
                              BasicStroke.CAP_ROUND,
                              BasicStroke.JOIN_ROUND
                              ));
                      
        drawZ(g2d, Color.magenta, 30, 140, 150, 
              new BasicStroke(10f,
                              BasicStroke.CAP_BUTT,
                              BasicStroke.JOIN_BEVEL
                              ));
    }
    
        // rysuje duza litere 'Z'
    void drawZ(Graphics2D g2d, Color c, 
               int st, int sx, int sy, 
               BasicStroke stroke){
        int[] xv = new int[]{sx, sx+st, sx, sx+st};
        int[] yv = new int[]{sy, sy, sy+st, sy+st};
        g2d.setColor(c);
        g2d.setStroke(stroke);  
        g2d.drawPolyline(xv, yv, 4);
        g2d.setColor(c.darker());
        g2d.setStroke(new BasicStroke());  
        g2d.drawPolyline(xv, yv, 4);
    }
  
    public Dimension getPreferredSize(){
        return new Dimension(200, 200);
    }
    
    public static void main(String[] args){
        JFrame frame = new JFrame("LinesDemo");
        JPanel lines = new LinesDemo();
        frame.getContentPane().add(lines);
        frame.setLocation(200, 200);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.pack();
        frame.show();
    }
}

3.4. Wypełnianie obszarów

Do malowania i wypełniania służą pędzle - obiekty klas implementujących interfejs Paint. Trzy takie klasy dostarczone przez Java API znajdują się w pakiecie java.awt. Odpowiadają one trzem sposobom wypełniania obszarów:

Do ustalania pędzla służy polecenie setPaint(Paint p), a jego aktualną wartość można pobrać metodą: getPaint().

3.4.1. Wypełnianie gradientem

Malowanie gradientem polega na podaniu dwóch punktów P1,P2 i ich kolorów C1,C2. Kolory punktów leżących na odcinku [P1,P2] będą zmieniać się od C1 do C2. Punkty prostej wyznaczonej przez ten odcinek mają kolory:

Pozostałe punkty mają taki kolor, jak ich rzut prostopadły na prostą [P1,P2].

W klasie GradientPaint są cztery konstruktory:

GradientPaint(float x1, float y1, Color c1, float x2, float y2, Color c2)
GradientPaint(float x1, float y1, Color c1, float x2, float y2, Color c2, boolean ac)
GradientPaint(Point p1, Color c1, Point p2, Color c2)
GradientPaint(Point p1, Color c1, Point p2, Color c2, boolean ac)

Pierwszy i trzeci konstruują domyślny gradient acykliczny, drugi i czwarty pozwalają ustalić acykliczność.

3.4.2. Wypełnianie teksturą

Malowanie teksturą polega na powielaniu obrazu graficznego we wszystkich kierunkach. Konstruktor pędzla teksturowego

TexturePaint(BufferedImage img, Rectangle2D rec) 
pobiera jako argumenty powielany obraz (który powinien być możliwie mały) i punkt jego zaczepienia w kontekście graficznym. Obraz może być tworzony dynamicznie poprzez rysowanie na wykreślaczu pozyskanym od obiektu img, ale może też być ikoną wczytaną z pliku (a następnie odrysowaną na tym wykreślaczu). Argument rec określa obszar wewnątrz kontekstu graficznego (tego, którego dotyczy zmiana pędzla). Zostanie na nim odrysowany obraz dostarczony przez argument img, a następnie powielony na tym wykreślaczu we wszystkich kierunkach. Położenie prostokąta rec wpływa na przesunięcie tekstury w kontekście graficznym, natomiast jego rozmiary określają rozmiar wynikowy pojedynczego obrazu i w związku z tym umożliwiają - jeśli są różne od rozmiarów tego obrazu - jego skalowanie.

3.4.3. Przykład zastosowania pędzli

images/PaintShow.png

Poniższy programik ilustruje sposób użycia gradientów (cykliczny i acykliczny), oraz tekstur utworzonych z pliku, bądź dynamicznie, poprzez rysowanie na wykreślaczu.

import java.awt.*;
import javax.swing.*;
import java.awt.image.*;
import java.awt.geom.*;

class PaintShow extends JPanel
{
    ImageIcon imic;
    Image img;
    
    PaintShow(String icName){
        imic = new ImageIcon(icName);
        img = imic.getImage();
    }

    public void paintComponent(Graphics g){
        
        super.paintComponent(g);
        
        Graphics2D g2d = (Graphics2D)g;
        int w = getWidth();
        int h = getHeight();
        
        Paint paint;
        
        paint = new GradientPaint(w/7, h/7, Color.red, 
                                  w/3, h/3, Color.green);
        g2d.setPaint(paint);
        g2d.fillRect(0, 0, w/2, h/2);    
        
        paint = new GradientPaint(w*2/3, h*2/3, Color.yellow, 
                                  w*3/4, h*3/4, Color.blue, 
                                  true);
        g2d.setPaint(paint);
        g2d.fillRect(w/2, h/2, w/2, h/2);    
        
        BufferedImage bimg;
        Graphics imgr;
        Rectangle rect;
        
        int iw = 15;
        int ih = 15;                
        bimg = new BufferedImage(iw, ih, BufferedImage.TYPE_INT_RGB);
        imgr = bimg.createGraphics();
        imgr.setColor(Color.magenta);
        imgr.fillRect(0, 0, iw-1, ih-1);
        imgr.setColor(Color.cyan);
        imgr.fillOval(0, 0, iw-2, ih-2);
        rect = new Rectangle(iw/2, h/2+ih/2, iw, ih);
        paint = new TexturePaint(bimg, rect);
        g2d.setPaint(paint);
        g2d.fillRect(0, h/2, w/2, h/2);    
            
        iw = imic.getIconWidth();        
        ih = imic.getIconHeight();        
        bimg = (BufferedImage)this.createImage(iw, ih);
        imgr = bimg.getGraphics();
        imgr.drawImage(img, 0, 0, null);
        rect = new Rectangle(w/2, 0, iw/2, ih/2); // skalowanie
        paint = new TexturePaint(bimg, rect);
        g2d.setPaint(paint);
        g2d.fillRect(w/2, 0, w/2, h/2);    
    }

    public Dimension getPreferredSize(){
        return new Dimension(200, 200);
    }
    
    public static void main(String[] args){
        
        JFrame frame = new JFrame("PaintShow");
        JPanel paint = new PaintShow(args[0]);
        frame.getContentPane().add(paint);
        frame.setLocation(300, 300);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.pack();
        frame.show();
    }
}

Jako argument wywołania trzeba przekazać nazwę pliku z ikonką.

3.5. Przekształcenia afiniczne

Przypomnienie:
Przekształcenie afiniczne przeprowadza proste na proste i zachowuje ich równoległość. Każde takie przekształcenie jest złożeniem przekształceń elementarnych: przesunięcia, rozciągania, symetrii względem prostej (i ew. obrotu).

Każdy obiekt graficzny może zostać poddany afinicznemu przekształceniu, które jest obiektem klasy java.awt.geom.AffineTransform. Obiekty przekształcenia można konstruować podając jako argument konstruktora jego macierz. Jednak w większości przypadków wygodniej jest wykorzystać konstruktor bezparametrowy AffineTransform() i składać przekształcenie wywołując metody będące przekształceniami elementarnymi:

Innym sposobem jest wykorzystanie statycznych metod getXxxInstance(...), (gdzie Xxx jest nazwą jednego z powyższych przekształceń) które zwracają gotowy obiekt .

Każdy kontekst graficzny ma obowiązujące przekształcenie afiniczne, które jest aplikowane do jego układu współrzędnych przed wykreślaniem. Domyślnie jest to identyczność. Można je zmienić metodą setTransform(AffineTransform af) (ale potem koniecznie trzeba przywrócić oryginalne, uzyskane metodą getTransform()), albo składać z przekształceń elementarnych metodami rotate(), scale(), shear(), translate() - odpowiednikami metod z klasy AffineTransform. Drugi sposób jest preferowany, ponieważ podmiana obowiązującego przekształcenia może wprowadzić w błąd zarządców rozkładów, którzy z niego korzystają.

Napisy można poddawać przekształceniom bezpośrednio - nie korzystając z przekształcania kontekstu graficznego. Umożliwia to metoda Font deriveFont(AffineTransform af) z klasy Font, jak i wspomniana wcześniej metoda Shape getOutline(AffineTransform af) z klasy TextLayout. Pierwsza dostarcza przekształconą wersję czcionki, druga - napisu.

3.5.1. Przykład przekształceń afinicznych

images/AffineTransform.png

Kolejny obrazek wraz z programem ilustrują przekształcanie afiniczne kontekstu graficznego. Najpierw jest on przesuwany, aby punkt [0,0] znalazł się na środku.Następnie zostaje odbity względem osi poziomej (przy pomocy skalowania o współczynniku -1), aby współrzędne pionowe rosły w górę. Potem wykreślany jest układ współrzędnych, koło i kwadrat, które mają pokazywać jak działają kolejne przekształcenia.

import java.awt.*;
import javax.swing.*;
import java.awt.font.*;
import java.awt.geom.*;

class Affine extends JPanel
{
    public void paintComponent(Graphics g){
        
        super.paintComponent(g);
        Graphics2D g2d = (Graphics2D)g;
        
        g2d.translate(100, 100);
        g2d.scale(1, -1);
        drawAxes(g, new Color(10, 10, 200));
        
        g2d.shear(0.25, 0.25);
        drawAxes(g, new Color(10, 200, 10));                

        g2d.rotate(Math.PI/3);                      
        drawAxes(g, new Color(200, 10, 10));                
        
    }

    void drawAxes(Graphics g, Color c){
        g.setColor(c);
        g.drawLine(-100, 0, 100, 0);
        g.drawLine(0, 100, 0, -100);
        g.setColor(c.brighter());
        g.drawRect(20, 20, 50, 50);
        g.setColor(c.darker());
        g.fillOval(-70, 20, 50, 50);
    }
    
    public Dimension getPreferredSize(){
        return new Dimension(200, 200);
    }
    
    public static void main(String[] args){
        
        JFrame frame = new JFrame("AffineTransform");
        JPanel affine = new Affine();
        frame.getContentPane().add(affine);
        frame.setLocation(300, 300);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.pack();
        frame.show();
    }

}

4 Obrazy

Oprócz rysowania własnych obrazków istnieje możliwość wykreślania obrazów zawartych w plikach graficznych typu GIF, JPEG lub PNG. Obrazy są obiektami klasy java.awt.Image, a właściwie jej podklas - ponieważ jest to klasa abstrakcyjna.

4.1. Pozyskiwanie obrazów

Odniesienie do obiektu reprezentującego obraz można uzyskać na kilka sposobów.
  1. Metodami klasy java.awt.Toolkit:

    • Image createImage(String fileName) - tworzy nowy obrazek z pliku fileName
    • Image createImage(URL url) - tworzy obrazek z pliku umiejscowionego w url
    • getImage(String fileName), getImage(URL url) - buforowane warianty powyższych metod. Aktualny przybornik (obiekt klasy Toolkit) przechowuje obrazki tworzone tymi metodami w pamięci podręcznej aby przyspieszyć kolejne wywołania tych metod z identycznym argumentami (fileName lub url).

    Uzyskanie w ten sposób odniesienia nie gwarantuje, że obrazek jest od razu w całości załadowany do pamięci operacyjnej i gotowy do wyświetlenia. Jeśli trzeba - ładowanie zostanie automatycznie dokończone w osobnym wątku (systemowym).

    Odniesienie do właściwego obiektu klasy Toolkit można uzyskać jej statyczną metodą: Toolkit.getDefaultToolkit(), albo metodą getToolkit() z klasy Component - a więc dostępną w każdym komponencie.

  2. Tworząc obiekt klasy javax.swing.ImageIcon konstruktorem:

    • ImageIcon(String fileName) - podając nazwę pliku lub,
    • ImageIcon(URL url) - podając jego lokalizację

    i wydając mu polecenie Image getImage(). Po utworzeniu obiektu ImageIcon obrazek będzie znajdował się w pamięci operacyjnej. Jeśli ładowanie obrazu trwa powoli, to wywołanie tego konstruktora może zuważalnie wstrzymać wykonanie wątku. W takich przypadkach lepiej użyć metod klasy Toolkit.

    Nawet jeśli obrazka nie uda się załadować, metoda getImage() zwróci wartość różną od null.

  3. W apletach używa się metody getImage(URL url) z klasy java.applet.Applet (nie działa ona do momentu uzyskania przez aplet pełnego kontekstu, zatem nie można jej wywoływać w konstruktorze apletu, ani też w inicjatorach jego składowych).

4.2. Wykreślanie obrazów

Wykreślenie obrazka również może być wykonane na kilka sposobów:

Wszystkie metody drawImage() wymagają podania jako jednego z argumentów odniesienia do obiektu typu ImageObserver, który będzie doglądał procesu ładowania obrazu. W praktyce podaje się this odnoszące się do komponentu, na którym odbywa się wykreślanie. Podobną rolę gra pierwszy argument w paintIcon().

4.3. Kontrola ładowania

Jeśli szybkość ładowania obrazka jest mała w stosunku do jego rozmiarów, to należy wykorzystać obserwator ładowania. Pozwoli on zaczekać (wstrzymać wykonanie wątku) do momentu, gdy obrazek będzie załadowany w odpowiedniej części lub w całości. Dodatkowo można uzyskiwać informacje o postępach ładowania.

4.3.1. Interfejs ImageObserver

Interfejs java.awt.image.ImageObserver zawiera metodę imageUpdate(), która jest wywoływana (poprzez wywołania zwrotne), gdy pojawi się jakaś nowa informacja dotycząca obrazka - np. załadowano kolejny fragment. Obiekty tego typu przekazuje się przeważnie jako argumenty metody drawImage() w celu asynchronicznego wykreślenia pozostałej części obrazka, niedostępnej w chwili jej wywołania. Klasa Component implementuje ten interfejs, więc każdy komponent jest obserwatorem ładowania. Zaimplementowana metoda imageUpdate() powoduje odświeżenie komponentu (repaint()), gdy tylko kolejna porcja obrazka zostanie dostarczona - skutkuje to dorysowaniem kolejnej jego części.

Metoda boolean imageUpdate(Image img, int flags, int x, int y, int w, int h) ma zwracać true, jeśli kolejne doładowania będą potrzebne i false w przeciwnym przypadku - gdy uzyskano potrzebną informację. Argument img jest odniesieniem do obserwowanego obrazka, a flags dostarcza informacji o postępie ładowania, które można odczytać używając stałych interfejsu ImageObserver. Interpretacja pozostałych argumentów zależy od aktualnego stanu określonego przez flags.

4.3.2. Klasa MediaTracker

Inny - prostszy - sposób oczekiwania na ładowanie udostępnia klasa java.awt.MediaTracker. Umożliwia ona jednoczesne śledzenie postępu ładowania wielu obrazków. Obiekt tej klasy tworzy się konstruktorem MediaTracker(Component c) - podając jako argument komponent, na którym zostaną wyświetlone obrazki. Po uzyskaniu obiektu dodaje się kolejne obrazki metodą addImage(Image img, int id) każdemu przypisując identyfikator id definiujący ich grupę. Metodą waitForAll() rozpoczyna się ładowanie wszystkich obrazków, a po ich załadowaniu następuje wyjście z metody. Ładowanie grupy (być może jednoelementowej) następuje po wywołaniu waitForId(int id). Można również sprawdzać stan ładowania metodami checkAll(), checkId(), statusAll(), statusId().

Klasa ImageIcon wykorzystuje obiekt klasy MediaTracker do śledzenia postępu ładowania obrazów - ikon będących obiektami tej klasy.

4.4. Buforowanie

W kilku przypadkach może być użyteczne umieszczenie obrazu w buforze - obiekcie klasy BufferedImage, która zresztą jest jedyną znaną podklasą abstrakcyjnej klasy Image:

Ostatnia technika zwana jest podwójnym buforowaniem i jest często stosowana w animacji. Swing używa jej domyślnie dla wszystkich komponentów. Jej zastosowanie zobaczymy w następnym wykładzie.

Dokumentacja i literatura

Java Tutorial
Rozdział poświęcony grafice dwuwymiarowej w podręczniku on-line.
Podręcznik programisty
Dostępny w dokumentacji Javy w katalogu docs/guide/2d/spec/index.html oraz w wersji on-line
Strona domowa projektu Java 2D