Rozdział 12. Multimedia: grafika, dźwięk i wideo

Klasa Graphics oraz metoda paint() klasy Component nie uległy niemal żadnym zmianom od samego początku istnienia języka Java. Aktualnie zapewniają one proste, lecz jednocześnie całkiem funkcjonalne możliwości graficzne. Pierwszy graficzny interfejs programistyczny został udostępniony w JDK 1.1, jednak bardzo szybko zastąpiono go rozwiązaniami wprowadzonymi w Java 1.2, a następnie w Java 1.4. Na szczęście interfejsy programistyczne służące do rysowania bazują na wykorzystaniu obiektów Graphics, dzięki czemu kod realizujący operacje graficzne nie musiał się zmieniać — konieczne było jedynie zmodyfikowanie pewnych szczegółów związanych z pobieraniem odpowiedniego rodzaju obiektów Graphics. Pakiet 2D, obsługujący grafikę dwuwymiarową, także bazuje na wykorzystaniu obiektów Graphics — klasa Graphisc2D jest bowiem klasą potomną klasy Graphics.

Aby spojrzeć na grafikę dwuwymiarową z odpowiedniej perspektywy, należy pomyśleć, jak ogromnym krokiem naprzód, jeśli chodzi o możliwości publikowania w świecie komputerów osobistych, było wprowadzenie języka PostScript. PostScript jest językiem skryptowym, lecz jednocześnie mechanizmem znaczącym. Java jest już rozbudowanym i kompletnym językiem programowania, dlatego też tworząc interfejs programistyczny służący do generowania grafiki dwuwymiarowej, należało opracować wyłącznie mechanizm znaczący. Problem ten został rozwiązany w bardzo dobry sposób dzięki wykorzystaniu kilku pomysłów wzorowanych na rozwiązaniach stosowanych w języku PostScript, co z kolei było możliwe dzięki uczestnictwu firmy Adobe we wczesnych etapach projektowania 2D API.

Także od samego początku istnienia Javy była w niej dostępna klasa AudioClip. W Javie 2 została ona szybko rozbudowana o możliwości obsługi dodatkowych formatów (w tym także MIDI) oraz wykorzystania jej z poziomu aplikacji. Jednocześnie Java Media Framework (standardowe rozszerzenie javax.media) oraz nowszy szkielet JavaFX pozwalają na odtwarzanie dźwięku, obrazu, a być może także i innych mediów zapewniających znacznie większą kontrolę nad sposobem prezentacji. W dalszej części rozdziału podałem stosowne przykłady.

Jednak w pierwszej kolejności przyjrzymy się klasie Graphics.

Klasa Graphics dysponuje obszerną grupą metod realizujących podstawowe operacje graficzne. Pozwalają one na rysowanie zarówno konturów, jak i wypełnionych prostokątów, łuków, elips oraz wielokątów, przy czym kontury i wypełnione kształty są rysowane przez dwie różne grupy metod. Nie trzeba korzystać jednocześnie z obu, chyba że krawędzie oraz wnętrze figury mają mieć różne kolory. Metoda drawString() oraz pozostałe metody z nią związane pozwalają na wyświetlanie tekstów (patrz „12.3. Wyświetlanie tekstu”). Dostępna jest także metoda drawLine() rysująca odcinki proste, metody setColor(), getColor(), setFont(), getFont() oraz wiele, wiele innych. W rzeczywistości klasa Graphics definiuje zbyt wiele metod, abym w niniejszej książce był w stanie przedstawić je wszystkie; informacje na ich temat można znaleźć w dokumentacji klasy java.awt.Graphics.

W przeszłości podstawowym błędem popełnianym przez osoby rozpoczynające naukę i wykorzystywanie języka Java było wywoływanie metody getGraphics() oraz metod rysujących obiektu Graphics bezpośrednio w programie głównym bądź w konstruktorze obiektu klasy rozszerzającej klasę Component. Na szczęście obecnie dostępnych jest już bardzo wiele książek, które informują, że poprawnym sposobem rozwiązania tego problemu jest umieszczenie kodu rysującego wewnątrz metody paint() komponentu. Dlaczego? Otóż wynika to z faktu, że nie można niczego narysować w oknie, zanim okno to nie zostanie utworzone i (w większości systemów wykorzystujących okna) skojarzone z ekranem, a te czynności zabierają znacznie więcej czasu, niż mają program główny oraz konstruktor. Kod rysujący musi zatem cierpliwie poczekać aż do chwili, gdy system poinformuje wirtualną maszynę Javy, że nadszedł już czas, by narysować i wyświetlić zawartość okna.

Gdzie zatem należy umieszczać kod rysujący? Otóż jest to jedna z sytuacji, w których należy wykorzystać różne rozwiązania w zależności od tego, czy korzystamy z biblioteki AWT, czy też Swing. AWT, prosty system do obsługi aplikacji z graficznym interfejsem użytkownika, wykorzystuje w tym celu metodę paint(). Metoda ta jest dostępna także w pakiecie Swing, jednak w tym przypadku ze względu na obsługę krawędzi oraz inne zagadnienia zalecane jest przesłanianie metody paintComponent(). Obie te metody dysponują jednym argumentem wywołania — obiektem klasy Graphics. Tworzona przez nas metoda paintComponent() powinna się zaczynać od wywołania super.paintComponent() i przekazania w jej wywołaniu tego samego argumentu — w ten sposób zapewniamy, że wszystkie komponenty zostaną wyświetlone w odpowiedniej kolejności, zaczynając od tych na samym „spodzie”, a kończąc na tych widocznych na samej „górze”. Z kolei metoda paint() nie powinna wywoływać tej samej metody w klasie bazowej. Niektóre przykłady zamieszczone w tym rozdziale wykorzystują metodę paint(), a inne metodę paintComponent(); te drugie są zazwyczaj klasami potomnymi klasy JPanel. Takie rozwiązanie zapewnia lepszą integrację z pakietem Swing i pozwala na umieszczenie naszego komponentu jako głównego komponentu ramki (JFrame) przy użyciu metody setContentPane(), dzięki czemu eliminowana jest dodatkowa warstwa pojemników. (Informacje na temat klasy JFrame i paneli można znaleźć w „14.1. Wyświetlanie komponentów graficznego interfejsu użytkownika”).

CompRunner to niewielki program, który pobiera nazwę klasy podaną w wierszu wywołania, następnie tworzy obiekt tej klasy (patrz „23.4. Dynamiczne ładowanie i instalowanie klas”), po czym umieszcza go w komponencie JFrame. Dodatkowo program zapewnia, że wyświetlone okno będzie odpowiednio duże. Wiele zagadnień związanych z graficznym interfejsem użytkownika, a nie z tworzeniem grafiki, zostało omówionych w Rozdział 14.

Testowana klasa musi być klasą pochodną klasy Component, w przeciwnym przypadku na ekranie zostanie wyświetlony komunikat o błędzie. Rozwiązanie to jest bardzo przydatne przy uruchamianiu niewielkich komponentów i zarówno w tym, jak i w kolejnym rozdziale przedstawię wiele przykładów jego użycia. Sposób korzystania z tego programu jest wyjątkowo prosty. Na przykład aby stworzyć obiekt klasy DrawStringDemo2 przedstawiony w „12.3. Wyświetlanie tekstu”, wystarczy wydać następujące polecenie:

java gui.CompRunner graphics.DrawStringDemo2

Wygląd tego komponentu został przedstawiony z lewej strony na Rysunek 12-1. Ciekawym doświadczeniem może być użycie tego programu w celu wyświetlenia obiektu jednej z predefiniowanych klas Javy. Wywołanie bezargumentowego konstruktora klasy JTree (elementu interfejsu graficznego Javy umożliwiającego prezentację informacji w formie listy hierarchicznej, przedstawionego w „17.8. Program MailClient”) powoduje utworzenie komponentu JTree zawierającego przykładowe dane (został on zaprezentowany z prawej strony na Rysunek 12-1).

Ponieważ program ten ma niewiele wspólnego z zagadnieniami omawianymi w tym rozdziale, nie przedstawię tu jego kodu źródłowego, można go jednak znaleźć w przykładach dołączonych do niniejszej książki.

Program DrawStringDemo2, przedstawiony na Przykład 12-1, określa szerokość i wysokość łańcucha znaków (patrz Rysunek 12-2, prezentujący niektóre atrybuty tekstu). Następnie program odejmuje wielkość tekstu od wielkości komponentu, a uzyskane wyniki dzieli przez dwa, co powoduje wyśrodkowanie tekstu w obszarze komponentu.

Przedstawione operacje są na tyle częste, że można by oczekiwać, iż Java będzie je udostępniać. I faktycznie, Java daje takie możliwości. Przedstawiony komponent w większości rozwiązań umożliwiających tworzenie graficznych interfejsów użytkownika nosi nazwę etykiety. Jak się przekonamy w Rozdział 14., Java udostępnia komponent Label (etykieta), który pozwala na wyśrodkowanie tekstu (jak również wyrównanie go do lewej bądź prawej strony komponentu) oraz wybór czcionki i koloru wyświetlonego tekstu. Dostępny jest także komponent JLabel, w którym oprócz tekstu lub zamiast niego można wyświetlić również ikonę.

Przedstawiony na Przykład 12-2 program DropShadow robi dokładnie to, co opisałem w powyższym rozwiązaniu. W przykładzie wykorzystywany jest także obiekt Font, który umożliwia nam uzyskanie kontroli nad wyglądem czcionki używanej przy wyświetlaniu tekstu.

Standardowa biblioteka AWT wykorzystuje bardzo prosty model rysowania. Sądzę, że to właśnie z tego powodu metodę, którą należy tworzyć, nazwano paint(). Wróćmy na chwilę do czasu, kiedy stosowano jeszcze zwyczajny papier. Co się dzieje, jeśli rysujemy coś na papierze, a następnie musimy narysować na tym coś innego w innym kolorze? Jeśli Czytelnik pamięta, co to jest papier, to z całą pewnością wie, że drugi kolor przesłoni pierwszy. Cóż, AWT działa w podobny sposób. (Nie mówimy tu o farbach wodnych, które można ze sobą łączyć). Możliwości rysowania w Javie przypominają raczej farby olejne. Dzięki temu, że AWT pozostawia w niezmienionym stanie wszystkie bity (piksele lub elementy rysunku), które nie zostały zmodyfikowane podczas rysowania, oraz ze względu na to, że metody takie jak drawString() mają ściśle zdefiniowany cel działania, można bardzo łatwo tworzyć efekt cienia oraz inne ciekawe efekty graficzne.

Warto pamiętać, aby w pierwszej kolejności rysować elementy, które mają być „pod spodem”, a dopiero potem te, które mają być „na wierzchu” rysunku. Aby sprawdzić, dlaczego należy postępować właśnie w taki sposób, można zamienić kolejność wywołań dwóch metod drawString() w ostatnim przykładzie.

I jeszcze jedno ostrzeżenie: nie należy mieszać rysowania z wykorzystaniem komponentów GUI (graficznego interfejsu użytkownika; patrz Rozdział 14.). Załóżmy na przykład, że zdefiniowaliśmy metodę paint() apletu lub innego komponentu, a następnie dodaliśmy do niego przycisk (posługując się w tym celu metodą add()). W niektórych implementacjach języka Java rozwiązanie takie będzie działać, jednak w pozostałych widoczny będzie bądź tylko przycisk, bądź też to, co zostało narysowane w metodzie paint(). Takie rozwiązanie nie jest przenośne i nie należy go stosować. Aby poradzić sobie z tym problemem, można wykorzystać kilka różnych komponentów. Można także użyć metod getContentPane() oraz getGlassPane() klasy JFrame, opisanych szczegółowo w Rozdział 8. książki Java Swing.

Inny sposób tworzenia cienia został przedstawiony w „12.6. Wyświetlanie tekstu przy użyciu biblioteki grafiki dwuwymiarowej”.

Możliwości tworzenia grafiki dwuwymiarowej wprowadzone w Javie 2 same w sobie mogłyby stanowić temat osobnej książki. I tak zresztą jest — książka Jonathana Knudsena zatytułowana Java 2D Graphics (wydana przez wydawnictwo O’Reilly) prezentuje wszystkie możliwe aspekty tego kompletnego pakietu graficznego. W niniejszej książce przedstawię tylko jeden przykład wykorzystania tego pakietu, pokazujący sposób wyświetlania tekstu na tle o podanej teksturze.

Graphics2D jest bezpośrednią klasą potomną oryginalnej klasy Graphics. Tak naprawdę w Javie 2 do metody paint() zawsze jest przekazywany obiekt tej właśnie klasy. A zatem aby uzyskać obiekt tej klasy, wystarczy na początku metody paint() wykonać odpowiednie rzutowanie typów:

public void paint(Graphics g) {
    Graphics2D g2 = (Graphics2D) g;

Teraz można korzystać z dowolnych metod klas Graphics2D oraz Graphics, wywołując je przy użyciu odwołania do obiektu g2. Jedną z dodatkowych metod klasy Graphics2D jest setPaint(). Można jej używać zamiast metody setColor(), aby określić kolor, jakim chcemy rysować. Niemniej jednak do metody setPaint() można przekazywać także dane kilku innych typów; w naszym przypadku przekażemy do niej obiekt TexturedPaint, który określa wzór. Nasz wzór jest jedynie zbiorem ukośnych linii, jednak równie dobrze można wykorzystać dowolny wzór, a nawet mapę bitową zapisaną w pliku (patrz „12.8. Wyświetlanie obrazu”). Wygląd tekstu wyświetlanego przez program został przedstawiony na Rysunek 12-4 (w kolorze prezentuje się on jeszcze lepiej), natomiast sam kod programu — na Przykład 12-3.

Przykład 12-3. /graphics/TexturedText.java

public class TexturedText extends JComponent {

    private static final long serialVersionUID = 8898234939386827451L;
    /** Obraz stanowiący teksturę. */
    protected BufferedImage bim;
    /** Tekstura używana przy rysowaniu. */
    TexturePaint tp;
    /** Wyświetlany łańcuch znaków. */
    String mesg = "Lampasy";
    /** Czcionka. */
    Font myFont = new Font("Lucida Regular", Font.BOLD, 72);

    /** Metoda "programu głównego" - tworzymy i wyświetlamy okno programu. */
    public static void main(String[] av) {
        // Tworzymy obiekt TexturedText i wyświetlamy go.
        final Frame f = new Frame("TexturedText");
        TexturedText comp = new TexturedText();
        f.add(comp);
        f.addWindowListener(new WindowAdapter() {
            public void windowClosing(WindowEvent e) {
                f.setVisible(false);
                f.dispose();
                System.exit(0);
            }
        });
        f.pack();
        f.setLocation(200, 200);
        f.setVisible(true);
    }

    protected static Color[] colors = {
        Color.red, Color.blue, Color.yellow,
    };

    /** Tworzymy obiekt. */
    public TexturedText() {
        super();
        setBackground(Color.white);
        int width = 8, height = 8;
        bim = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g2 = bim.createGraphics();
        for (int i=0; i<width; i++) {
            g2.setPaint(colors[(i/2)%colors.length]);
            g2.drawLine(0, i, i, 0);
            g2.drawLine(width-i, height, width, height-i);
        }
        Rectangle r = new Rectangle(0, 0, bim.getWidth(), bim.getHeight());
        tp = new TexturePaint(bim, r);
    }

    @Override
    public void paintComponent(Graphics g) {
        Graphics2D g2 = (Graphics2D)g;
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
            RenderingHints.VALUE_ANTIALIAS_ON);
        g2.setPaint(tp);
        g2.setFont(myFont);
        g2.drawString(mesg, 20, 100);
    }

    @Override
    public Dimension getMinimumSize() {
         return new Dimension(250, 100);
    }

    @Override
    public Dimension getPreferredSize() {
         return new Dimension(320, 150);
    }
}

Java udostępnia statyczną metodę Font.createFont(), pozwalającą na tworzenie „prywatnych” czcionek, które można stosować w aplikacji bez konieczności instalowania ich z wykorzystaniem systemowego mechanizmu do obsługi czcionek. W ten sposób użytkownicy mogą korzystać z aplikacji oraz jej niestandardowych czcionek, mimo że nie posiadają przywilejów administracyjnych ani dostępu do konta „root”, w systemach, w których instalowanie czcionek zazwyczaj tego wymaga.

Metoda createFont() wymaga przekazania dwóch argumentów. Pierwszym z nich jest wartość typu int, która musi być statycznym polem Font.TRUETYPE_FONT, natomiast drugim musi być strumień InputStream (patrz „10.17. Odczytywanie i zapisywanie danych binarnych”), otworzony do odczytu z pliku binarnego. Jak można się domyślić na podstawie wymogu, by pierwszym argumentem była wartość Font.TRUETYPE_FONT, obecnie metoda ta obsługuje wyłącznie czcionki typu TrueType. Dokumentacja klasy Font, mająca niezmiennie optymistyczny wydźwięk, stwierdza, że w przyszłości pole to może pozwalać na obsługę innych formatów czcionek, choć nie ma takiej gwarancji. Zważywszy na dostępność darmowych mechanizmów prezentujących czcionki PostScript, takich jak te stosowane w XFree86 — implementacji X Window System — można sądzić, że w przyszłości powinna się pojawić obsługa czcionek PostScript. Przykład 12-4 przedstawia kod niewielkiej, niezależnej aplikacji, która wyświetla okno przedstawione na Rysunek 12-5.

Przykład 12-4. Stosowanie czcionek aplikacji (/graphics/TTFontDemo.java)

public class TTFontDemo extends JLabel {

    private static final long serialVersionUID = -2774152065764538894L;

    /** Konstruktor klasy TTFontDemo -- tworzy nową czcionkę
     * wczytaną z pliku TTF.
     */
    public TTFontDemo(String fontFileName, String text)
    throws IOException, FontFormatException {
        super(text, JLabel.CENTER);

        setBackground(Color.white);

        // W pierwszej kolejności sprawdzamy, czy można wczytać
        // plik czcionki.
        InputStream is = this.getClass().getResourceAsStream(fontFileName);
        if (is == null) {
            throw new IOException("Nie można otworzyć pliku " + fontFileName);
        }

        // Metoda createFont() tworzy czcionkę o wielkości 1 punktu,
        // odczytanie tekstu tej wielkości byłoby dosyć trudne :-)
        Font ttfBase = Font.createFont(Font.TRUETYPE_FONT, is);
        // Zatem skalujemy czcionkę do wielkości 24 punktów.
        Font ttfReal = ttfBase.deriveFont(Font.PLAIN, 24);

        setFont(ttfReal);
    }

    /** Metoda main() aplikacji TTFontDemo. */
    public static void main(String[] args) throws Exception {

        String DEFAULT_MESSAGE =
            "O szyby deszcz dzwoni, deszcz dzwoni jesienny.";
        // Plik jest wyczytywany jako zasób, dlatego nie musimy
        // przed nim umieszczać ścieżki "graphics/".
        String DEFAULT_FONTFILE = "Kellyag_.ttf";
        String message = args.length == 1 ? args[0] : DEFAULT_MESSAGE;
        JFrame f = new JFrame("Przykład stosowania czcionek aplikacji");

        TTFontDemo ttfd = new TTFontDemo(DEFAULT_FONTFILE, message);
        f.getContentPane().add(ttfd);

        f.setBounds(100, 100, 700, 250);
        f.setVisible(true);
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }
}

Ten sposób stosowania czcionek ma pewne ograniczenia. Przede wszystkim — zgodnie z tym, o czym wspominałem w komentarzu — można go stosować wyłącznie w klasach dziedziczących po klasie JComponent biblioteki Swing; nie jest to natomiast możliwe w przypadku korzystania z biblioteki AWT i jej klasy Component (patrz Rozdział 14.).

Co więcej, ze względu na zabezpieczenia metody tej nie można by w prosty sposób wykorzystać w apletach (gdyż konieczne byłoby utworzenie lokalnej kopii pliku czcionki). Można jej natomiast używać w aplikacjach uruchamianych przy użyciu technologii Java Web Start (patrz „21.11. Java Web Start”).

Obraz można pobrać przy użyciu metody, która nosi oczywistą nazwę — getImage(). Jeśli tworzony kod ma działać w aplecie, to można się posłużyć metodą getImage() klasy Applet, jeśli jednak chcemy z niego korzystać także w aplikacjach, to konieczne będzie wykorzystanie analogicznej metody udostępnianej przez AWT. Metoda ta wymaga podania nazwy pliku lub adresu URL. Oczywiście użycie nazwy pliku w przypadku apletów spowoduje zgłoszenie wyjątku sygnalizującego naruszenie zasad bezpieczeństwa, chyba że użytkownik zainstaluje odpowiedni plik polityki zabezpieczeń. Program GetImage pokazuje, jak wyświetlić zarówno obraz pobierany z pliku, jak i obraz o podanym adresie URL:

/graphics/GetImage.java

public class GetImage extends JApplet {

    private static final long serialVersionUID = 4288395022095915666L;
    private Image image;

    public void init() {
        loadImage();
    }

    public void loadImage() {

        // Wersja przenośna: getClass().getResource() działa zarówno
        // w apletach, jak i aplikacjach, w JDK 1.1 lub 1.3; zwraca URL
        // reprezentujący nazwę pliku.
        URL url = getClass().getResource("Duke.gif");
        image = getToolkit().getImage(url);
        // Lub po prostu:
        // image = getToolkit().getImage(getClass().getResource("Duke.gif"));
    }

    @Override
    public void paint(Graphics g) {
        g.drawImage(image, 20, 20, this);
    }

    public static void main(String[] args) {
        JFrame f = new JFrame("GetImage");
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        GetImage myApplet = new GetImage();
        f.setContentPane(myApplet);
        myApplet.init();
        f.setSize(100, 100);
        f.setVisible(true);
        myApplet.start();
    }
}

Czasami może się zdarzyć, że w jednym panelu będziemy chcieli wyświetlić ten sam obraz więcej niż jeden raz. Przykład 12-5 przedstawia program, który tworzy tło panelu poprzez wielokrotne wyświetlenie w nim tego samego obrazu. Aby rozwiązanie takie było możliwe, określamy wymiary obrazu przy użyciu metod getWidth() oraz getHeight(), a także wymiary samego panelu za pomocą metody getSize(). Jak zwykle w metodzie paint() nie określamy stałych wymiarów okna, gdyż użytkownik może zmienić jego wielkość, posługując się myszą.

W metodzie paint() musimy sprawdzić nie tylko to, czy obraz jest równy null, lecz także czy jego wysokość i szerokość nie są ujemne — jak widać, w tym przypadku jesteśmy bardziej ostrożni niż w poprzednim, nieco niefrasobliwym przykładzie. Obraz może mieć wartość null wyłącznie w sytuacji, jeśli coś poszło w jego konstruktorze nie tak, jak należy, jednak może mieć ujemny rozmiar. Jak to możliwe? Otóż w niektórych mitach związanych ze stworzeniem mówi się, że przed początkiem dziejów czas biegł w przeciwnym kierunku; dlatego zanim tworzenie obrazu zostanie całkowicie zakończone, jego wymiary będą przeciwne (czyli obraz będzie miał wysokość i szerokość równą -1). Okazuje się bowiem, że metoda getImage() tak naprawdę nie pobiera obrazu. Owszem, tworzy obiekt Image, lecz niekoniecznie pobiera wszystkie bity obrazu — metoda ta uruchamia wątek działający w tle, który niezależnie pobiera zawartość, a działanie samej metody się kończy. Rozwiązanie to powstało w czasach, gdy internet był wolniejszy i pobranie obrazu w całości zabierało znacznie więcej czasu. W rzeczywistości istnieją pewne formaty plików graficznych (być może jakieś rodzaje plików TIFF), w których rozmiar pliku można określić dopiero po pobraniu całości tego pliku. A zatem w momencie zakończenia realizacji metody getImage() obiekt Image jest utworzony, lecz wielkość samego obrazu wynosi -1, -1. Jednak w tym momencie istnieją już dwa działające wątki (patrz Rozdział 22.), a zatem może zajść jedna z dwóch sytuacji. Po pierwsze, wątek odczytujący obraz może pobrać wystarczająco dużo danych, aby móc określić jego wymiary, zanim będziemy ich potrzebować, lub po drugie, będziemy potrzebować tych wymiarów, zanim będzie je można określić. Ten dziwnie wyglądający fragment kodu umieszczony w metodzie paint() jest zabezpieczeniem przed tą sytuacją.

Jednak co zrobić, jeśli naprawdę będziemy potrzebowali wymiarów obrazu, na przykład aby odpowiednio rozmieścić większy panel? Po pobieżnym zapoznaniu się z dokumentacją klasy Image można by sądzić, że rozwiązaniem jest użycie metody prepareImage(), która gwarantuje, że obraz został w całości pobrany. Niestety, w przypadku gdy pobierany plik nie istnieje, użycie tej metody może spowodować powstanie pętli nieskończonej, gdyż jej wywołanie nigdy nie zwróci wartości true! Aby mieć całkowitą pewność, że obraz został pobrany, należy stworzyć obraz MediaTracker. Poniżej przedstawiłem przykład jego wykorzystania.

/**
 * Ten FRAGMENT KODU pokazuje sposób wykorzystania obiektu
 * klasy MediaTracker w celu zapewnienia, że obraz (Image)
 * został poprawnie i w całości pobrany, oraz określenia
 * wysokości i szerokości tego obrazu. Obiekt MediaTracker
 * może śledzić dowolną ilość obrazów, "0" jest liczbą
 * używaną, gdy chcemy śledzić konkretny obraz.
 */
Image im;
int imWidth, imHeight;
public void setImage(Image i) {
    im = i;
    MediaTracker mt = new MediaTracker(this);
    // Wykorzystując "this", zakładamy, że bieżąca klasa
    // jest klasą potomną klasy Component.
    mt.addImage(im, 0);
    try {
        mt.waitForID(0);
    } catch (InterruptedException e) {
        throw new IllegalArgumentException(
            "Zgłoszono przerwanie InterruptedException podczas pobierania
            obrazu!");
    }

    if (mt.isErrorID(0)) {
        throw new IllegalArgumentException("Nie można pobrać obrazu");
    }

    imWidth = im.getWidth(this);
    imHeight = im.getHeight(this);
}

W każdej chwili można pobrać aktualny status obiektu MediaTracker. Służy do tego metoda status(int ID, boolean load) zwracająca liczbę całkowitą utworzoną poprzez bitowe połączenie (przy użyciu operatora or) wartości przedstawionych w Tabela 12-1. Jeśli flaga logiczna load przyjmie wartość true, to obiekt rozpocznie pobieranie wszystkich obrazów, które jeszcze nie zostały pobrane ani aktualnie nie są pobierane. Inna metoda — statusAll() — zwraca wartość stanowiącą połączenie wszystkich flag określających stany wszystkich aktualnie pobieranych obrazów.

Przedstawiony wcześniej fragment kodu można uprościć poprzez wykorzystanie klasy ImageIcon z pakietu Swing, która udostępnia omawiane możliwości funkcjonalne. Klasa ImageIcon dysponuje konstruktorami o kilku różnych postaciach — jedna z nich umożliwia przekazanie nazwy pliku. Klasa ta wewnętrznie stosuje obiekt MediaTracker; jego status można pobrać za pomocą metody getImageLoadStatus() klasy ImageIcon. Metoda ta zwraca takie same wartości co metody statusAll() oraz statusID() klasy ImageIcon.

Pakiet imageio udostępnia klasę java.awt.image.BufferedImage, która reprezentuje obraz przechowywany w pamięci. Dziedziczy ona po klasie java.awt.Image, pozwala więc na stosowanie standardowych metod graficznych, z metodą getGraphics() na czele. Statyczne metody read() oraz write() klasy ImageIO pozwalają odpowiednio na odczytanie pliku i zapisanie go w obiekcie BufferedImage oraz na zapisanie takiego obiektu w pliku.

Program przedstawiony w tej recepturze korzysta z tych metod, by stworzyć początkowe wersje obrazków przedstawiających umieszczone w kółkach cyfry 6, 7 i 8, używane w tej książce do oznaczania, która wersja języka Java jest konieczna do skompilowania kodu prezentowanego w recepturze. Program korzysta z grafiki przedstawiającej filiżankę z kawą, narysowanej przez artystę o pseudonimie TikiGiki i opublikowanej na witrynie http://OpenClipArt.org na zasadach licencji Creative Commons.

Metoda getStringBound() klasy Font przypomina nieco analogiczną metodę klasy FontMetrics zastosowaną w „12.4. Wyświetlanie wyśrodkowanego tekstu w komponencie” — podobnie jak ona mierzy faktyczną szerokość wyświetlanego tekstu. Ta metoda oraz proste działania arytmetyczne pozwalają nam wyświetlić dużą, czarną cyfrę (zapisaną w poniższym przykładzie w zmiennej v) na samym środku obrazka, nieco powyżej filiżanki. Zmodyfikowane obrazki są zapisywane na dysku. Obrazki te wstawiałem następnie do tekstu książki, używając standardowych mechanizmów obsługi obrazów dostępnych w bibliotece AsciiDoc. Przykład 12-6 przedstawia kompletny kod programu służącego do generowania grupy czterech obrazków.

Kod programu musi się kończyć wywołaniem System.exit(0), gdyż kod graficzny uruchamia dodatkowy wątek działający w tle.

Poradnik dostępny na stronie http://docs.oracle.com/javase/tutorial/2d/images/index.html znacznie dokładniej opisuje możliwości i sposób korzystania z biblioteki ImageIO.

Może się zdawać, że odtwarzanie dźwięków nie pasuje do tego całego zamieszania związanego z grafiką, przedstawionego we wcześniejszej części rozdziału. Niemniej jednak stanowi pewien wzorzec. Otóż wcześniej zajmowaliśmy się prostszymi formami graficznymi, natomiast teraz przejdziemy do zagadnień związanych z bardziej dynamicznymi multimediami. Pliki dźwiękowe są reprezentowane przez użycie obiektów AudioClip, które pozwalają także na odtwarzanie zawartości tych plików.

/graphics/AudioPlay.java

public class AudioPlay {

    static String defSounds[] = {
        "/audio/test.wav",
        "/music/midi/Beet5th.mid",
    };

    public static void main(String[] av) {
        if (av.length == 0)
            main(defSounds);
        else for (String a : av) {
            System.out.println("Odtwarzam " + a);
            try {
                URL snd = AudioPlay.class.getResource(a);
                if (snd == null) {
                    System.err.println("Nie można pobrać zasobu: "  + a);
                    continue;
                }
                AudioInputStream audioInputStream =
                    AudioSystem.getAudioInputStream(snd);
                final Clip clip = AudioSystem.getClip();
                clip.open(audioInputStream);
                clip.start();
            } catch (Exception e) {
                System.err.println(e);
            }
         }
    }
}

Jeśli na komputerze jest zainstalowany pakiet JavaFX (patrz ramka „Ewolucja klienckich aplikacji pisanych w Javie: aplety, wojny przeglądarek, Swing oraz JavaFX” w Dodatek B), to plik dźwiękowy będzie także można odtworzyć przy użyciu JavaFX API:

/javafx/AudioPlay.java

/** Proste odtwarzanie pliku dźwiękowego przy użyciu JavaFX API. */
public class AudioPlay {
    public static void main(String[] args) {
        String clipName = "demo.mp3";
        Media clip = new Media(clipName);
        MediaPlayer mediaPlayer = new MediaPlayer(clip);
        mediaPlayer.play();
    }
}

Program JMFPlayer przedstawiony w „12.11. Prezentacja ruchomego obrazu” nie tylko odtwarza pliki dźwiękowe, lecz także udostępnia panel pozwalający na regulację głośności.

Java udostępnia kilka bibliotek do pracy z multimediami. W pierwszej kolejności przyjrzymy się Java Media Framework (JMF).

Biblioteka Java Media Framework została udostępniona już wiele lat temu przez firmę Sun, lecz przez dosyć długi czas nie była właściwie rozwijana. Jest ona rozszerzeniem, zatem trzeba ją osobno pobrać. W przykładach dołączonych do książki w katalogu głównym dostępny jest plik pom.xml, który dodaje odpowiednią zależność pozwalającą pobrać tę bibliotekę przy użyciu programu Maven. Program przedstawiony na Przykład 12-7 wyświetla film lub inny plik multimedialny, którego nazwa została podana w wierszu wywołania programu. JMF jest bardzo elastycznym narzędziem — jeśli wskazany plik multimedialny nie będzie zawierać filmu, lecz dźwięk, to poniższy program wyświetli panel umożliwiający regulację głośności. Rysunek 12-6 przedstawia wygląd programu JMFPlayer w przypadku odtwarzania pliku dźwiękowego oraz klipu wideo.

Przykład 12-7. JMFPlayer.java

public class JMFPlayer extends JPanel implements ControllerListener {

    /** Obiekt odtwarzacza */
    Player thePlayer = null;
    /** Nadrzędna ramka, w której działa program. */
    JFrame parentFrame = null;
    /** Panel zawartości */
    Container cp;
    /** Komponent wizualny (jeśli jakiś jest) */
    Component visualComponent = null;
    /** Domyślny komponent kontrolny (jeśli jest) */
    Component controlComponent = null;
    /** Nazwa używanego pliku multimedialnego. */
    String mediaName;
    /** Adres URL używanego pliku dźwiękowego. */
    URL theURL;

    /** Tworzymy obiekt odtwarzacza oraz graficzny interfejs użytkownika. */
    public JMFPlayer(JFrame pf, String media) {
        parentFrame = pf;
        mediaName = media;
        // cp = getContentPane();
        cp = this;
        cp.setLayout(new BorderLayout());
        try {
            theURL = new URL(getClass().getResource("."), mediaName);
            thePlayer = Manager.createPlayer(theURL);
            thePlayer.addControllerListener(this);
        } catch (MalformedURLException e) {
            System.err.println("JMF - błąd tworzenia adresu URL: " + e);
        } catch (Exception e) {
            System.err.println("Błąd tworzenia odtwarzacza JMF: " + e);
            return;
        }
        System.out.println("URL pliku = " + theURL);

        // Uruchamiamy odtwarzacz: spowoduje to przekazanie informacji
        // do naszego obiektu ControllerListener.
        thePlayer.start();        // Rozpoczynamy odtwarzanie
    }

    /** Metoda wywoływana w celu zatrzymania odtwarzania, na przykład
     * w wyniku kliknięcia przycisku Stop lub wybrania odpowiedniej
     * opcji z menu programu
     */
    public void stop() {
        if (thePlayer == null)
            return;
        thePlayer.stop();          // Zatrzymujemy odtwarzanie!
        thePlayer.deallocate();    // Zwalniamy zasoby systemowe
    }

    /** Metoda wywoływana, gdy chcemy zamknąć program (na przykład
     * po kliknięciu przycisku Wyjście)
     */
    public void destroy() {
        if (thePlayer == null)
            return;
        thePlayer.close();
    }

    /** Metoda wywoływana przez JMF, gdy odtwarzacz chce nas o czymś
     * poinformować
     */
    public synchronized void controllerUpdate(ControllerEvent event) {
        // System.out.println("controllerUpdate(" + event + ")");
        if (event instanceof RealizeCompleteEvent) {
            if ((visualComponent = thePlayer.getVisualComponent()) != null)
                    cp.add(BorderLayout.CENTER, visualComponent);
            if ((controlComponent =
                thePlayer.getControlPanelComponent()) != null)
                    cp.add(BorderLayout.SOUTH, controlComponent);
            // Zmieniamy wielkość głównego okna aplikacji
            if (parentFrame != null) {
                parentFrame.pack();
                parentFrame.setTitle(mediaName);
            }
        }
    }

    public static void main(String[] argv) {
        JFrame f = new JFrame("Odtwarzacz JMF - demonstracja");
        Container frameCP = f.getContentPane();
        final String musicURL = argv.length == 0 ?
                "file:/home/ian/Music/Classical/Rachmaninoff Prelude C_ min.mp3" :
                argv[0];
        JMFPlayer p = new JMFPlayer(f, musicURL);
        frameCP.add(BorderLayout.CENTER, p);
        f.setSize(200, 200);
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.setVisible(true);
    }
}

Java Media Framework daje znacznie większe możliwości, niż pokazuje przedstawiony przykład. Niemniej możliwość wyświetlania filmów zapisanych w formacie QuickTime lub MPEG przy użyciu zaledwie kilku wierszy kodu jest jedną z największych zalet JMF. Plik multimedialny pobieramy z pliku o podanym adresie URL, a następnie tworzymy obiekt Player przeznaczony do zarządzania tym plikiem. Jeśli dany odtwarzacz może mieć panel sterujący, to jest on tworzony i dodawany u dołu okna programu. W skład panelu sterującego mogą wchodzić: elementy umożliwiające kontrolę głośności, przyciski przewijania do przodu i do tyłu, suwak umożliwiający określenie miejsca odtwarzania i tak dalej. Jednak w ogóle nie musimy się tym przejmować — tworząc odtwarzacz dla pliku multimedialnego konkretnego typu, otrzymujemy komponent zawierający wszystkie odpowiednie elementy sterujące. Jeśli dany odtwarzacz reprezentuje medium dysponujące komponentem wizualnym (takim jak film bądź mapa bitowa), umieszczamy ten komponent w centrum okna programu.

Oczywiście możliwości Java Media Framework są znacznie większe. Na przykład istnieje możliwość zsynchronizowania odtwarzania pliku dźwiękowego oraz klipu wideo bądź uzależnienia ich odtwarzania od innych zdarzeń zewnętrznych.

Często stosowane jest także rozwiązanie polegające na wykorzystaniu „otwartego” programu VLC Media Player i podłączeniu go do obiektu Canvas w celu wyświetlania obrazu wideo. Oczywiście, oprócz samej biblioteki vlcj konieczne jest także samodzielne pobranie i zainstalowanie programu VLC. Czynności te wykraczają poza zakres tematyczny tej książki, a ich realizacja jest w całości uzależniona od używanego systemu operacyjnego.

Prezentowany tu program wymaga JNA — nieobsługiwanego już pakietu com.sun. Jest on używany do poinformowania VLCJ, gdzie został zainstalowany odtwarzacz VLC — to jedna z pierwszych czynności wykonywanych w konstruktorze klasy MyVideoCanvas, przedstawionej na Przykład 12-8. Kolejnymi czynnościami są: utworzenie obiektu EmbeddedMediaPlayerComponent, wywołanie jego metody getMediaPlayer() i w końcu wywołanie metod prepareMedia(), parseMedia() i play() na rzecz obiektu odtwarzacza. Wszystkie te czynności zostały przedstawione w kontekście kompletnego przykładu zaprezentowanego na Przykład 12-8. Adres URL pliku jest podawany w wierszu wywołania programu i może wskazywać plik lokalny, zdalny strumień wideo bądź też dowolne inne źródło, które jest w stanie obsługiwać odtwarzacz VLC.

Interfejs programistyczny do obsługi drukowania — Java Print Service API — pozwala na komunikację z usługami drukowania systemu operacyjnego. Poniżej opisałem proces korzystania z tego API:

Wysyłane dane zostaną podzielone na strony przez usługę drukowania, a zatem mogą mieć postać dowolnego strumienia, który może zostać wydrukowany. W przedstawionym przykładzie drukowany jest niewielki plik tekstowy, a program potrafi zadbać o to, by został on prawidłowo wyświetlony w kolejce wydruków i wydrukowany.

Kod źródłowy tego programu przedstawia Przykład 12-10.

Przykład 12-10. /printjdk14printservice/PrintServiceDemo.java

/**
 * Program przedstawia najnowsze wcielenie mechanizmów drukowania
 * PrintService z wykorzystaniem graficznego interfejsu użytkownika;
 * interfejs ten składa się co prawda tylko z jednego przycisku -
 * Drukuj - a nazwa drukowanego pliku została podana na stałe,
 * jednak ma to być przecież jak najprostszy przykład...
 */
public class PrintServiceDemo extends JFrame {

    private static final long serialVersionUID = 923572304627926023L;

    private static final String INPUT_FILE_NAME = "/demo.txt";

    /** Program główny: utworzenie obiektu i wyświetlenie okna.
     * @throws IOException */
    public static void main(String[] av) throws Exception {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                try {
                    new PrintServiceDemo(
                               "Przykład drukowania").setVisible(true);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }

    /** Konstruktor tworzący graficzny interfejs użytkownika
     * składający się z jednego przycisku: Drukuj. */
    PrintServiceDemo(String title) {
        super(title);
        System.out.println("PrintServiceDemo.PrintServiceDemo()");
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setLayout(new FlowLayout());
        JButton b;
        add(b = new JButton("Drukuj"));
        b.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                System.out.println(
                    "PrintServiceDemo.PrintServiceDemo...actionPerformed()");
                try {
                    print(INPUT_FILE_NAME);
                } catch (Exception e1) {
                    JOptionPane.showMessageDialog(
                        PrintServiceDemo.this, "Błąd: " + e1, "Błąd",
                        JOptionPane.ERROR_MESSAGE);
                    e1.printStackTrace();
                }
            }
        });
        pack();
        UtilGUI.center(this);
    }

    /** Drukujemy plik o podanej nazwie.
     * @throws IOException
     * @throws PrintException
     */
    public void print(String fileName) throws IOException, PrintException {
        System.out.println("PrintServiceDemo.print(): " +
            "Drukowanie pliku: " + fileName);
        DocFlavor flavor = DocFlavor.INPUT_STREAM.TEXT_PLAIN_UTF_8;
        PrintRequestAttributeSet aset = new HashPrintRequestAttributeSet();
        //aset.add(MediaSize.NA.LETTER);
        aset.add(MediaSizeName.NA_LETTER);
        //aset.add(new JobName(INPUT_FILE_NAME, null));
        PrintService[] pservices =
            PrintServiceLookup.lookupPrintServices(flavor, aset);
        int i;
        switch(pservices.length) {
        case 0:
            System.err.println(0);
            JOptionPane.showMessageDialog(PrintServiceDemo.this,
                "Błąd: brak usługi drukowania", "Błąd",
                JOptionPane.ERROR_MESSAGE);
            return;
        case 1:
            i = 0;    // Dostępna jest tylko jedna drukarka, używamy jej.
            break;
        default:
            i = JOptionPane.showOptionDialog(this,
                "Wybierz drukarkę", "Wybór",
                JOptionPane.OK_OPTION, JOptionPane.QUESTION_MESSAGE,
                null, pservices, pservices[0]);
            break;
        }
        DocPrintJob pj = pservices[i].createPrintJob();
        InputStream is = getClass().getResourceAsStream(INPUT_FILE_NAME);
        if (is == null) {
            throw new NullPointerException(
                         "Pusty strumień wejściowy: nie znaleziono pliku");
        }
        Doc doc = new SimpleDoc(is, flavor, null);

        pj.print(doc, aset);
    }
}

Poniżej przedstawiłem wyniki sprawdzenia kolejki wydruku przy użyciu polecenia lpstat -t:

MacKenzie$ lpstat -t
scheduler is running
system default destination: HP_Deskjet_F4200_series
device for HP_Deskjet_F4200_series: usb://HP/Deskjet%20F4200%20series
HP_Deskjet_F4200_series accepting requests since Wed Jan 1 15:04:16 2014
printer HP_Deskjet_F4200_series ... enabled since Wed Jan 1 15:04:16 2014
HP_Deskjet_F4200_series-28 ian
1024  Wed Jan 1 15:03:22 2014
MacKenzie$

Nie ma także żadnego wymogu, który nakazywałby aplikacji posiadanie graficznego interfejsu użytkownika (i to niezależnie od tego, czy miałby on być stworzony przy użyciu biblioteki Swing, czy AWT). Przykład 12-11 przedstawia aplikację uruchamianą z poziomu wiersza poleceń, która konwertuje zwyczajny tekst na format PostScript, nie korzystając przy tym z żadnego graficznego interfejsu użytkownika.

Przykład 12-11. /printjdk14printservice/PrintPostScript.java, program drukujący z poziomu wiersza poleceń

/** Program demonstrujący odnajdywanie usługi PrintService
 * i drukowanie pliku przy jej użyciu. */
public class PrintPostScript {

    private static final String INPUT_FILE_NAME = "/demo.txt";

    public static void main(String[] args) throws IOException, PrintException {
        new PrintPostScript().print();
    }

    public void print() throws IOException, PrintException {

        DocFlavor inputFlavor = DocFlavor.INPUT_STREAM.TEXT_PLAIN_UTF_8;

        // Odnajdujemy obiekt StreamPrintServiceFactory, który będzie
        // w stanie skonwertować właściwe dane wejściowe na
        // odpowiedni format wyjściowy.
        StreamPrintServiceFactory[] psfactories =
            StreamPrintServiceFactory.lookupStreamPrintServiceFactories(
                inputFlavor, DocFlavor.BYTE_ARRAY.POSTSCRIPT.getMimeType());
        if (psfactories.length == 0) {
            System.err.println("Nie udało się znaleźć obiektu " +
                  "StreamPrintFactory do wykonania tego zadania!");
        }
        StreamPrintService printService =
            psfactories[0].getPrintService(new FileOutputStream("demo.ps"));
        PrintRequestAttributeSet attrs = new HashPrintRequestAttributeSet();
        attrs.add(OrientationRequested.LANDSCAPE);
        attrs.add(MediaSizeName.NA_LETTER);
        attrs.add(new Copies(1));
        attrs.add(new JobName(INPUT_FILE_NAME, null));

        InputStream is = getClass().getResourceAsStream(INPUT_FILE_NAME);
        if (is == null) {
            throw new NullPointerException(
                "Pusty strumień wejściowy: nie znaleziono pliku");
        }
        Doc doc = new SimpleDoc(is, inputFlavor, null);

        DocPrintJob printJob = printService.createPrintJob();
        printJob.print(doc, attrs);
    }
}

W „8.12. Program Plotter” przedstawiłem grupę klas Plotter. Klasa PlotterAWT zaprezentowana na Przykład 12-12 rozszerza ich możliwości, pozwalając na stworzenie usługi „podglądu rysunku” — zanim rysunek zostanie sporządzony przez (prawdopodobnie wolny w działaniu) ploter, zostanie on wyświetlony w oknie aplikacji graficznej z wykorzystaniem podstawowych metod rysujących klasy Graphics.

Przykład 12-12. /plotter/PlotterAWT.java

public class PlotterAWT extends Plotter {

    private JFrame f;
    private PCanvas p;
    private Graphics g;
    private Font font;
    private FontMetrics fontMetrics;

    PlotterAWT() {
        f = new JFrame("Ploter");
        Container cp = f.getContentPane();
        p = new PCanvas(MAXX, MAXY);
        cp.add(p, BorderLayout.CENTER);
        f.pack();
        f.setVisible(true);
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        g = p.getOsGraphics();
    }

    public void drawBox(int w, int h) {
        g.drawRect(curx, cury, w, h);
        p.repaint();
    }

    public void rmoveTo(int incrx, int incry){
        moveTo(curx += incrx, cury += incry);
    }

    public void moveTo(int absx, int absy){
        if (!penIsUp)
            g.drawLine(curx, cury, absx, absy);
        curx = absx;
        cury = absy;
    }

    public void setdir(float deg){}
    void penUp(){ penIsUp = true; }
    void penDown(){ penIsUp = false; }
    void penColor(int c){
        switch(c) {
        case 0: g.setColor(Color.white); break;
        case 1: g.setColor(Color.black); break;
        case 2: g.setColor(Color.red); break;
        case 3: g.setColor(Color.green); break;
        case 4: g.setColor(Color.blue); break;
        default: g.setColor(new Color(c)); break;
        }
    }
    void setFont(String fName, int fSize) {
        font = new Font(fName, Font.BOLD, fSize);
        fontMetrics = p.getFontMetrics(font);
    }
    void drawString(String s) {
        g.drawString(s, curx, cury);
        curx += fontMetrics.stringWidth(s);
    }

    /** Klasa wewnętrzna zawierająca "pozaekranowy" obiekt
     * Image, w którym rysujemy. Metoda paint() tego komponentu
     * kopiuje rysunek z obrazu na ekran. Dzięki temu nie trzeba
     * sporządzać listy elementów rysunku.
     */
    class PCanvas extends Canvas {
        private static final long serialVersionUID = 6827371843858633606L;
        Image offScreenImage;
        int width;
        int height;
        Graphics pg;

        PCanvas(int w, int h) {
            width = w;
            height = h;
            setBackground(Color.white);
            setForeground(Color.red);
        }

        public Graphics getOsGraphics() {
            return pg;
        }

        /** Ta metoda jest wywoływana przez AWT po utworzeniu obiektu
         * odpowiednika rodzimego okna, lecz przed pierwszym wywołaniem
         * metody paint(), zatem doskonale nadaje się do tworzenia
         * obrazów itp.
         */
        public void addNotify() {
            super.addNotify();
            offScreenImage = createImage(width, height);
            // assert (offScreenImage != null);
            pg = offScreenImage.getGraphics();
        }

        public void paint(Graphics pg) {
            pg.drawImage(offScreenImage, 0, 0, null);
        }
        public Dimension getPreferredSize() {
            return new Dimension(width, height);
        }
    }
}

Grapher to prosty program, który odczytuje tabelę punktów i wyświetla je. Dane wejściowe zapisywane są w jednym lub wielu wierszach, przy czym każdy z nich zawiera współrzędne X i Y opisujące położenie jednego punktu. Wyniki wyświetlane są w oknie, lecz można je także wydrukować. Wyniki wykonania programu operującego na poniższych danych testowych zostały przedstawione na Rysunek 12-7. Pierwsza kolumna danych określa wartość współrzędnej X, a druga — współrzędnej Y. Program skaluje dane tak, aby pasowały one do wymiarów okna.

1.5 5
1.7 6
1.8 8
2.2 7

Kod źródłowy tego programu został przedstawiony na Przykład 12-13.

Przykład 12-13. graphics/Grapher.java

public class Grapher extends JPanel {

    private static final long serialVersionUID = -1813143391310613248L;

    /** Mnożnik zakresu umożliwiający zostawienie miejsca na obramowanie */
    public final static double BORDERFACTOR = 1.1f;

    /** Lista punktów (obiektów Apoint). */
    protected List<Point2D> data;

    /** Minimalna i maksymalna wartość współrzędnej X */
    protected double minx = Integer.MAX_VALUE, maxx = Integer.MIN_VALUE;
    /** Minimalna i maksymalna wartość współrzędnej Y */
    protected double miny = Integer.MAX_VALUE, maxy = Integer.MIN_VALUE;
    /** Zakres wartości X i Y */
    protected double xrange, yrange;

    public Grapher() {
        data = new ArrayList<Point2D>();
        figure();
    }

    /** Przygotowanie listy danych na podstawie listy łańcuchów znaków,
     * przy czym współrzędna X jest inkrementowana automatycznie,
     * a wartości współrzędnych Y punktów są wczytywane z przekazanych
     * łańcuchów znaków.
     */
    public void setListDataFromYStrings(List<String> newData) {
        data.clear();
        for (int i=0; i < newData.size(); i++) {
            Point2D p = new Point2D.Double();
            p.setLocation(i, java.lang.Double.parseDouble(newData.get(i)));
            data.add(p);
        }
        figure();
    }

    /** Ustawiamy używaną listę danych na listę przekazaną w wywołaniu,
     * na przykład zwracaną przez wywołanie GraphReader.read(). */
    public void setListData(List<Point2D> newData) {
        data = newData;
        figure();
    }

    /** Metoda oblicza nowe dane po zmianie listy. */
    private void figure() {
        // Odnajdujemy minimum i maksimum.
        for (int i=0 ; i < data.size(); i++) {
            Point2D d = (Point2D)data.get(i);
            if (d.getX() < minx) minx = d.getX();
            if (d.getX() > maxx) maxx = d.getX();
            if (d.getY() < miny) miny = d.getY();
            if (d.getY() > maxy) maxy = d.getY();
        }

        // Obliczamy zakresy.
        xrange = (maxx - minx) * BORDERFACTOR;
        yrange = (maxy - miny) * BORDERFACTOR;
        Debug.println("Zakres", "minx,x,r = " + minx +' '+ maxx +' '+ xrange);
        Debug.println("Zakres", "miny,y,r = " + miny +' '+ maxy +' '+ yrange);
    }

    /** Metoda wywoływana, gdy należy wyświetlić zawartość okna.
     * Oblicza zakres X i Y, po czym odpowiednio skaluje współrzędne.
     */
    @Override
    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        Dimension s = getSize();
        if (data.size() < 2) {
            g.drawString("Niewystarczające dane: " + data.size(), 10, 40);
            return;
        }

        // Obliczamy współczynniki skali
        double xfact =  s.width  / xrange;
        double yfact =  s.height / yrange;

        // Skalujemy i wyświetlamy dane
        for (int i=0 ; i < data.size(); i++) {
            Point2D d = (Point2D)data.get(i);
            double x = (d.getX() - minx) * xfact;
            double y = (d.getY() - miny) * yfact;
            Debug.println("Punkt", "Dane " + i + " " + d + "; " +
                "x = " + x + "; y = " + y);
            // Rysujemy wycentrowany prostokąt o boku 5 pikseli —
            // stąd od współrzędnych X i Y wierzchołka prostokąta
            // odejmowana jest liczba 2. W AWT współrzędne Y są
            // liczone od dołu ku górze, a zatem musimy odjąć Y.
            g.drawRect(((int)x)-2, s.height-2-(int)y, 5, 5);
        }
    }

    @Override
    public Dimension getPreferredSize() {
        return new Dimension(150, 150);
    }

    public static void main(String[] args) throws IOException {
        final JFrame f = new JFrame("Grapher");
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        Grapher grapher = new Grapher();
        f.setContentPane(grapher);
        f.setLocation(100, 100);
        f.pack();
        List<Point2D> data = null;
        if (args.length == 0)
            data = GraphReader.read("Grapher.txt");
        else {
            String fileName = args[0];
            if ("-".equals(fileName)) {
                data = GraphReader.read(new InputStreamReader(System.in),
                    "System.in");
            } else {
                data = GraphReader.read(fileName);
            }
        }
        grapher.setListData(data);
        f.setVisible(true);
    }
}

W programie Grapher najbardziej skomplikowanym zadaniem jest określenie zakresów współrzędnych oraz ich skalowania. Oczywiście program można by rozbudować, dodając do niego bardziej wymyślne możliwości, takie jak rysowanie wykresów słupkowych i tak dalej.