6. Animacja, dźwięk i drukowanie w Javie


Ten wykład jest kontynuacją poprzedniego, dotyczącego grafiki. Prezentuje różne sposoby programowania animacji i pokazuje jak należy posługiwać się klipami dźwiękowym. Poznamy również metody drukowania bezpośrednio z API Javy.

1. Animacja

Animacja polega na cyklicznym wyświetlaniu różnych obrazów z częstością od kilku do kilkudziesięciu na sekundę. Kino używa 24 klatek na sekundę a telewizja - 25 (z przeplotem - za każdym razem rysowany jest co drugi wiersz obrazu). W praktyce, do zwykłych animacji wystarczy około 10 kadrów na sekundę, natomiast przy dynamicznych, szybko zmieniających się obrazach potrzeba więcej do zapewnienia wrażenia ciągłości.

Wykreślać można:

Ostatnie dwa sposoby są szczególnie użyteczne w apletach. Niezależne ściągnięcie każdego z plików graficznych potrzebnych do animacji wymaga nawiązania osobnego połączenia z serwerem dla każdego z nich, co wydłuża czas oczekiwania. Dlatego pakuje się je do jednego pliku graficznego, lub do archiwum jar.

Własne rysunki wykreśla się na kontekście graficznym komponentu - przeważnie klasy JPanel lub Canvas. Obrazki (ikony) można wyświetlać na etykietach, przyciskach, jak również bezpośrednio odmalowywać je na wykreślaczu.

Przygotowanie własnego rysunku może odbywać się: Bardziej skomplikowane rysunki można przygotowywać w buforze, który następnie wykreśla się jak zwykły obraz. Samo wykreślanie może być:
Można wyróżnić dwie techniki cyklicznego wykreślania. Pierwsza bazuje na metodzie sleep() z klasy Thread w celu ustalenia interwału pomiędzy wykreślaniem kolejnych klatek. Druga wykorzystuje zegary - obiekty cyklicznie wykonujące jakieś operacje.

1.1. Animowanie rysunków

Najprostszy schemat wygląda tak: wątek przygotowuje obraz, wywołuje metodę repaint() na rzecz odpowiedniego komponentu, po czym zasypia na czas od kilku do kilkudziesięciu milisekund. Po przebudzeniu przygotowuje kolejną klatkę.

Metoda repaint() powoduje wstawienie do kolejki zdarzeń żądania natychmiastowego odświeżenia komponentu lub jego części. Oznacza to, że zostanie wywołana metoda paint() (w AWT) lub paintComponent() (w Swingu) tak szybko, jak to będzie możliwe. Wielokrotne, następujące po sobie żądania odświeżenia mogą zostać zastąpione jednym.

Są trzy dodatkowe warianty metody repaint()
  1. repaint(long millis) - powoduje odświeżenie po upływie millis milisekund
  2. repaint(int x, int y, int w, int h) - odświeża tylko prostokąt zaczepiony w [x,y] o szerokości w i wysokości w,h
  3. repaint(long millis, int x, int y, int w, int h) - kombinacja powyższych

1.1.1. Najprostszy przykład

Program wykreśla okrąg odbijający się od krawędzi pulpitu. Kolejne jego współrzędne wyliczane są w pętli animacyjnej realizowanej w wątku głównym.

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

class BasicAnim extends JPanel
{
        // srednica
    int dim = 50;
        // polozenie
    int x = 75, y = 75;
        // kierunek ruchu
    int dx = 3, dy = 5;
        // opoznienie odswiezania
    int delay = 40;
    
    
    public void paintComponent(Graphics g){
        super.paintComponent(g);
        Graphics2D graph = (Graphics2D)g;
                    
        graph.setColor(Color.cyan);
        graph.drawOval(x, y, dim, dim);
    }

    
    public void startAnim(){
    
        while(true){    
                // odbicie
            if(x + dim > getWidth() || x < 0)
                dx = -dx;
            if(y + dim > getHeight() || y < 0)
                dy = -dy;
                // przesuniecie
            x += dx;
            y += dy;

            repaint(); 

            try{
                Thread.sleep(delay);
            }
            catch(InterruptedException e){
            }
        }
    }
    
    public Dimension getPreferredSize(){
        return new Dimension(200, 200);
    }
    
    public static void main(String[] args){

        JFrame frame = new JFrame("BasicAnim");
        BasicAnim anim  = new BasicAnim();
        frame.getContentPane().add(anim);
        frame.setLocation(300, 300);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.pack();
        frame.show();
            // rozpoczecie animacji
        anim.startAnim();
    }

}

Metoda startAnim() może zostać wywołana dopiero po zrealizowaniu głównego okna, ponieważ zawiera odwołania do jego rozmiarów.

1.2. Animowanie obrazów

Animowanie obrazów zawartych w zewnętrznych plikach graficznych może być wykonane na kilka sposobów.

1.2.1. Animowany GIF

Następny program wyświetla animowany plik GIF jako ikonę na etykiecie, oraz jako obraz w panelu klasy AnimPane dziedziczącej z JPanel. Panel jest obserwatorem ładowania - implementuje interfejs ImageObserver pośrednio poprzez klasę Component. Zatem nie trzeba dostarczać własnej implementacji metody imageUpdate() tego interfejsu, ponieważ znajduje się ona w odziedziczonej klasie Component. Metoda ta po wykreśleniu każdej ramki animacji generuje żądanie wykreślenia następnej.

images/AnimPane.png
import java.awt.*;
import java.awt.image.*;
import javax.swing.*;

class AnimPane extends JPanel
{
    static String animFName = "images/t2krun.gif";
    Image img = Toolkit.getDefaultToolkit().getImage(animFName);
    int off = 0;          // położenie obrazka w panelu

    public void paintComponent(Graphics g) {
        setForeground(Color.white);
        g.fillRect(0, 0, getWidth(), getHeight());
        off = (off + 1)%(580);
                // this implementuje ImageObserver 
                // i odpowiada za wykreślanie kolejnych ramek animacji
        g.drawImage(img, off, 0, this);
    }

    public static void main(String[] args){
        JFrame frame = new JFrame("AnimPane");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        Container cp = frame.getContentPane();
        cp.setLayout(new FlowLayout());
        Icon icon = new ImageIcon(animFName);
        JLabel animLab = new JLabel(icon);
        AnimPane anImage = new AnimPane();
        anImage.setPreferredSize(new Dimension(640, 41));
        cp.add(animLab);
        cp.add(anImage);
        frame.pack();
        frame.show();
    }
}

Aby mieć pełną kontrolę nad animacją należy zaimplementować metodę imageUpdate() interfejsu ImageObserver (dołączając ewentualnie frazę implements ImageObserver w nagłówku klasy implementującej). Oczywiście w tej sytuacji, jako argument typu ImageObserver dla metody drawImage() podajemy obiekt z naszą implementacją.

W jej ciele sprawdzamy stan wykreślania testując maskę infoflags. Jeśli bit ImageObserver.FRAMEBITS jest zapalony to znaczy, że wszystkie ramki animacji zostały wykreślone i aby rozpocząć kolejny cykl należy wywołać repaint() na rzecz komponentu, na którym odbywa się wykreślanie.

Oto przykładowa implementacja obserwatora ładowania:
       // metoda z interfejsu ImageObserver
       // odpowiedzialna za odświeżenie obrazka
    public boolean  imageUpdate(Image img, int infoflags,
                                int x, int y,
                                int width, int height){
           // po wykreśleniu wszystkich ramek zaczynamy od początku
        if (isShowing() && (infoflags & FRAMEBITS) != 0)
            repaint();

            // true jeśli okno jest widoczne
        return isShowing();
    }

1.3. Zegary

Zamiast używać metody sleep() do sterowania czasem wykreślania kolejnych kadrów animacji lepiej jest zastosować zegar javax.swing.Timer. Obiekt tej klasy w ustalonych odstępach czasu generuje zdarzenie ActionEvent i propaguje je do zarejestrowanych słuchaczy. Słuchacz w metodzie obsługi zdarzenia może wykreślić kolejną klatkę animacji. Ten sposób ustalania rytmu wykreślania jest preferowany - w stosunku do usypiania - ze względu na kilka zalet:

Zegar tworzy się podając mu jako argument interwał czasowy oraz słuchacza zdarzeń. Uruchamia się metodą start(), a zatrzymuje metodą stop(). Większą liczbę słuchaczy można dodać metodą addActionListener().

1.3.1. Animowany rysunek

Program wykreśla złożony kształt, poruszający się wewnątrz panelu i odbijający się od jego krawędzi. Obiekt jest przygotowany w konstruktorze, a w metodzie obsługi zdarzenia generowanego przez zegar wyliczane jest tylko jego nowe położenie. Odświeżanie jest realizowane w wątku zdarzeniowym na skutek wywołania repaint(int, int, int, int), które powoduje odświeżenie tylko podanego prostokąta, a nie całego pulpitu. Ograniczenie odświeżania do zmodyfikowanej części komponentu jest bardziej efektywne i pozwala uniknąć czasem występującego migotania spowodowanego wykreślaniem całego komponentu. images/ShapeAnim.png Trzeba jednak wyliczyć jaki obszar uległ zmianie i wymaga odświeżenia, co jest wykonywane w metodzie actionPerformed(). Metoda getBounds() zwraca najmniejszy prostokąt zawierający animowany obiekt typu Shape. Potrzebny jest on do ustalenia obszaru odświeżania, który jest sumą prostokąta ograniczającego pierwotne położenie obiektu i po jego przesunięciu. Sumę wylicza metoda createUnion() z klasy Rectangle. Metoda startAnim() rozpoczyna animację od przesunięcia obiektu na środek panelu i uruchomienia zegara.

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

class ShapeAnim extends JPanel implements ActionListener
{
    Timer timer;
    int dx = 3, dy = 5;
    GeneralPath shape;
    
    ShapeAnim(int delay, int s)
    {
            // przygotowanie rysunku
        shape = new GeneralPath(GeneralPath.WIND_EVEN_ODD);
        shape.append(new Ellipse2D.Float(s, s, s, 2*s), false);
        shape.append(new Ellipse2D.Float(-2*s, s, s, 2*s), false);
        shape.moveTo(0, 0);
        shape.lineTo(s, 0);
        shape.quadTo(5*s, 0, 5*s, 5*s);
        shape.curveTo(s, 2*s, 3*s, 4*s, 0, 5*s);
        shape.curveTo(-3*s, 4*s, -s, 2*s, -5*s, 5*s);
        shape.quadTo(-5*s, 0, -s, 0);
        shape.closePath();
        
        timer = new Timer(delay, this);
    }
    
    public void startAnim(){
            // przesuniecie na srodek
        shape.transform(AffineTransform.getTranslateInstance(75, 75));
        timer.start();
    }
    
    public void actionPerformed(ActionEvent e){
            // poprzednie ograniczenie 
        Rectangle bound = shape.getBounds();
        if(bound.x + bound.width > getWidth() || bound.x < 0)
            dx = -dx;
        if(bound.y + bound.height > getHeight() || bound.y < 0)
            dy = -dy;
            // przesuniecie
        shape.transform(AffineTransform.getTranslateInstance(dx, dy));
            // nowe ograniczenie dodane do starego
        Rectangle cover = (Rectangle)shape.getBounds().createUnion(bound);
        repaint(cover.x, cover.y, cover.width, cover.height);
    }
    
    public void paintComponent(Graphics g){
        super.paintComponent(g);
        Graphics2D graph = (Graphics2D)g;
        graph.setRenderingHint(RenderingHints.KEY_ANTIALIASING, 
                               RenderingHints.VALUE_ANTIALIAS_ON);
        graph.setColor(Color.white);
        graph.fillRect(0, 0, getWidth(), getHeight());
        graph.setColor(Color.blue);
        graph.fill(shape);
    }

    public Dimension getPreferredSize(){
        return new Dimension(200, 200);  
    }
    
    public static void main(String[] args){
        
        JFrame frame = new JFrame("ShapeAnim");
        ShapeAnim anim  = new ShapeAnim(30, 8);
        frame.getContentPane().add(anim);
        frame.setLocation(300, 300);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.pack();
        frame.show();
        
        anim.startAnim();
    }
}

1.4. Buforowanie

W przypadku przygotowywania złożonych, wieloelementowych obrazów do animacji konieczne jest skorzystanie z bufora. Wykreślanie bezpośrednio na kontekście graficznym komponentu może powodować migotanie, może też wprowadzać opóźnienia do wątku zdarzeniowego. Przygotowanie poszczególnych składników w buforze, a następnie przeniesienie ich wszystkich razem na komponent jako pojedynczy obraz rozwiązuje te problemy.

Buforem jest obraz - obiekt typu Image - uzyskany metodą createImage(int w, int h) (z klasy Component), w jest jego szerokością a h wysokością. Można też tworzyć bufory klasy BufferedImage bezpośrednio konstruktorem. Aby móc wykreślać w buforze, należy pobrać od niego wykreślacz metodą getGraphics(). Po wykreśleniu w nim obrazka trzeba go przenieść na kontekst graficzny komponentu metodą drawImage(), podając jako argument typu Image bufor.

1.4.1. Animowanie wielu obrazków

images/AnimImage.png

Poniższy programik animuje kilka obrazów pobranych z osobnych plików graficznych. Obrazki są transparentne (nie mają tła), więc przy każdym wykreśleniu musi nastąpić odświeżenie całego komponentu. Powoduje to wystąpienie migotania i dlatego całość - wraz z dodanym tłem - jest wstępnie przygotowana w buforze, a dopiero potem wyświetlona w komponencie (w metodzie actionPerformed() !). Dzięki temu migotanie nie występuje.


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

class AnimImage extends JPanel implements ActionListener
{
    int iw, ih;
    Timer timer;
    int frame = 0;
    Image offScr;
    Image[] images;
    Dimension prefSize;
    Graphics graph, offScrBuf;
            
    AnimImage(Image[] imgSet){
        images = imgSet;
        iw = images[0].getWidth(null);
        ih = images[0].getHeight(null);
        prefSize = new Dimension(iw, ih);
        timer = new Timer(100, this);
    }
    
    public void actionPerformed(ActionEvent ae){
        offScrBuf.setColor(Color.white);
        offScrBuf.fillRect(0, 0, iw, ih);
        offScrBuf.drawImage(images[frame], 0, 0, null);
        graph.drawImage(offScr, 0, 0, null);              
        frame = (frame+1)%images.length;
    }
    
    void startAnimation(){
        offScr = createImage(iw, ih);
        offScrBuf = offScr.getGraphics();
        graph = getGraphics();
        timer.start();
    }
    
    void stopAnimation(){
        timer.stop();
    }
    
    public Dimension getPreferredSize(){
        return prefSize;
    }

    public static void main(String[] args){
        Image[] images = new Image[10];
        for(int i = 0; i < images.length; i++){
            ImageIcon icon = new ImageIcon("images/Duke0" + i + ".gif");
            images[i] = icon.getImage();
        }
        final AnimImage aimage = new AnimImage(images);
        final JToggleButton runBut = new JToggleButton("START");
        runBut.addActionListener(new ActionListener(){
            public void actionPerformed(ActionEvent ae){
                if(runBut.isSelected()){
                    runBut.setText("STOP ");
                    aimage.startAnimation();
                }
                else {
                    runBut.setText("START");
                    aimage.stopAnimation();
                }
            }
        });
        JFrame frame = new JFrame("AnimImage");
        Container cp = frame.getContentPane();
        cp.setLayout(new GridLayout(1, 2));
        cp.add(aimage);
        cp.add(runBut);
        frame.setLocation(100, 300);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.pack();
        frame.show();
    }
}

Obrazy są umieszczone w plikach o nazwach Duke00.gif do Duke09.gif. Dwustanowy przycisk JToggleButton służy do rozpoczynania i kończenia animacji. Kończenie polega na zatrzymaniu zegara poprzez wywołanie stopAnimation(). Rozpoczynając animację metodą startAnimation() trzeba utworzyć bufor, pobrać wykreślacze, a następnie uruchomić zegar.

1.5. Animacje wielowątkowe

Przygotowywanie, jak również wykreślanie wielu obiektów może odbywać się w niezależnych wątkach. Wykreślanie powinno w takich sytuacjach odbywać się w buforze, a przenoszeniem jego zawartości na wykreślacz ekranowy powinien zajmować się osobny wątek, ale może to być wątek główny. W animacjach wielowątkowych należy używać zegara do ustalenia rytmu wykreślania kolejnych kadrów z powodów opisanych wcześniej.

Bufor jest wspólnym zasobem wielu wątków, ale nie wymaga on synchronizacji. Jest tak dlatego, że dostęp do niego odbywa się z metody obsługi zdarzenia, która wywoływana jest w wątku zdarzeniowym sekwencyjnie z poszczególnych wątków.

1.5.1. Praktyczne podsumowanie różnych technik

images/MultiThreadedTimerAnim.png

Program wykreśla kwadraty, które się przemieszczają, odbijają od krawędzi pulpitu, obracają i zmieniają rozmiar w trakcie animacji. Z każdym z nich jest związany osobny wątek oparty na obiekcie klasy Sprite, który w metodzie run() przygotowuje kolejne klatki. Wątki wykreślają swoje kwadraty w ogólnie dostępnym buforze image, a raczej na jego wykreślaczu buffer, w metodzie obsługi zdarzenia ActionEvent rozsyłanego do nich przez zegar timer. Główny panel klasy MultiThreadedTimerAnim również jest słuchaczem tych zdarzeń. Jego rola polega na przenoszeniu zawartości bufora na wykreślacz pulpitu device i czyszczeniu bufora.

import java.util.Random;
import java.awt.*;
import java.awt.geom.*;
import java.awt.image.*;
import java.awt.event.*;
import javax.swing.*;

class MultiThreadedTimerAnim 
    extends JPanel 
    implements ActionListener
{
    public static void main(String[] args){
        JFrame frame = new JFrame("MultiThreadedTimerAnim");
        final MultiThreadedTimerAnim anim  = new MultiThreadedTimerAnim();
        frame.getContentPane().add(anim);
        frame.setLocation(100, 100);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.pack();
        frame.show();
        
        int delay;  // odswiezanie
        int count;  // liczba obiektow
        try {        
        delay = Integer.parseInt(args[0]);
        count = Integer.parseInt(args[1]);
        }
        catch(Exception e){
            delay = 70;
            count = 30;
        }
        anim.startAnimation(delay, count);
    }

    public Dimension getPreferredSize(){
        return new Dimension(300, 300);
    }
    
        // bufor
    Image image;
        // wykreslacz ekranowy
    Graphics2D device;
        // wykreslacz bufora
    Graphics2D buffer;
    
        // przygotowanie wykreslaczy
        // uruchomienie watkow animacyjnych i zegara
    void startAnimation(int delay, int count){
        int width = getWidth();
        int height = getHeight();
        
        image = createImage(width, height);
        buffer = (Graphics2D)image.getGraphics();
        buffer.setRenderingHint(RenderingHints.KEY_ANTIALIASING, 
                                RenderingHints.VALUE_ANTIALIAS_ON);
        device = (Graphics2D)getGraphics();
        device.setRenderingHint(RenderingHints.KEY_ANTIALIASING, 
                                RenderingHints.VALUE_ANTIALIAS_ON);
        Timer timer = new Timer(delay, this);
            
        for(int i = 0; i < count; i++){
            Sprite spr = new Sprite(buffer, delay, width, height);
            timer.addActionListener(spr);
            new Thread(spr).start();
        }
        timer.start();
    }
    
        // przeniesienie bufora na ekran i wyczyszczenie go
    public void actionPerformed(ActionEvent evt){
        device.drawImage(image, 0, 0, null);
        buffer.clearRect(0, 0, getWidth(), getHeight());
    }
}

    // animowane obiekty
    // moga byc dowolnymi obiektami typu Shape
    // ale tu sa kwadratami
class Sprite implements Runnable, ActionListener {
        
        // wspolny bufor
    private Graphics2D buffer;
        // rozmiary pulpitu
    private int width, height;
    private int delay;    

    private Color clr;
        // do transformacji
    private Area area;
        // do wykreslania
    private Shape shape;
        // przeksztalcenie obiektu
    private AffineTransform aft;
        
        // przesuniecie
    private int dx, dy;
        // rozciaganie
    private double sf;
        // kat obrotu
    private double an;
        // ziarno dla generatora liczb losowych
    static private int seed = 0;

    public Sprite(Graphics2D buf, int del, int w, int h){
        delay = del;
        buffer = buf;
        width = w;
        height = h;
        
        Random rand = new Random(seed++);
        dx = 1+rand.nextInt(5);
        dy = 1+rand.nextInt(5);
        sf = 1+0.05*rand.nextDouble();
        an = 0.1*rand.nextDouble();
        
        clr = new Color(rand.nextInt(255), 
                        rand.nextInt(255), 
                        rand.nextInt(255), 
                        rand.nextInt(255));        
        shape = new Rectangle2D.Float(0, 0, 10, 10);
        aft = new AffineTransform();                                  
        area = new Area(shape);
    }

    public void run(){
            // przesuniecie na srodek
        aft.translate(100, 100);
        area.transform(aft);
        shape = area;
        
        while(true){
            // przygotowanie nastepnego kadru
            shape = nextFrame();
            try{
                Thread.sleep(delay);
            }
            catch(InterruptedException e){
            }
        }
    }

    protected Shape nextFrame(){
            // zapamietanie na zmiennej tymczasowej
            // aby nie przeszkadzalo w wykreslaniu
        area = new Area(area);
        aft = new AffineTransform();                                  
        Rectangle bounds = area.getBounds();
        int cx = bounds.x + bounds.width/2;
        int cy = bounds.y + bounds.height/2;
            // odbicie
        if(cx < 0 || cx > width)
            dx = -dx;
        if(cy < 0 || cy > height)
            dy = -dy;
            // zwiekszenie lub zmniejszenie
        if(bounds.height > height/3 || bounds.height < 10)
            sf = 1/sf;
            // konstrukcja przeksztalcenia
        aft.translate(cx, cy);
        aft.scale(sf, sf);
        aft.rotate(an);
        aft.translate(-cx, -cy);
        aft.translate(dx, dy);
            // przeksztalcenie obiektu
        area.transform(aft);
        return area;
    }
    
    public void actionPerformed(ActionEvent evt){
            // wypelnienie obiektu
        buffer.setColor(clr);
        buffer.fill(shape);
            // wykreslenie ramki
        buffer.setColor(clr.darker());
        buffer.draw(shape);
    }
}    
Obiekt klasy Sprite może animować dowolny kształt (ustala się to w konstruktorze), jednak obracanie innych kształtów niż prostokąty jest bardzo czasochłonne i spowalnia animację.

Metoda nextFrame() generuje następne położenie animowanego obiektu, który jest potem wykreślany w buforze w metodzie actionPerformed(). Przekształcany jest nowy obiekt area, ponieważ stary (na zmiennej shape) może być w tym czasie wykreślony.


2. Dźwięk

Java Sound Engine zawarta w Java 2 SDK umożliwia odgrywanie klipów muzycznych w następujących formatach:

Java posiada wsparcie dla plików kompresowanych metodą liniową PCM. Pliki kompresowane przy użyciu ADPCM nie będą odtwarzane.
Dane mogą być 8 lub 16 bitowe przy praktycznie dowolnej częstotliwości próbkowania.

2.1. Odtwarzanie klipów w apletach

Przed odtworzeniem klipu trzeba go najpierw załadować. Służy do tego metoda klasy java.applet.Applet

AudioClip getAudioClip(URL url);
Jednak nie powoduje ona fizycznego załadowania pliku audio. Niezależnie od tego, czy podany plik audio istnieje, następuje natychmiastowy powrót z tej metody. Faktyczne załadowanie pliku zostanie wykonane dopiero przy pierwszym odtworzeniu. Uzyskawszy odniesienie do obiektu typu AudioClip możemy na nim wykonywać następujące operacje:
void play();
void stop();
void loop();
Pierwsza rozpoczyna odtwarzanie, druga kończy. Trzecia metoda odtwarza klip cyklicznie. Klip można również odtworzyć metodą klasy Applet
void play(URL url);
Zastosowanie tej metody może spowodować wstrzymanie działanie apletu na czas ładowania klipu.

2.2. Odtwarzanie klipów w aplikacjach

Aplikacje mogą używać następującej statycznej metody klasy Applet do stworzenia klipu:

AudioClip newAudioClip(URL url);
Po uzyskaniu odniesienia do klipu można go odtwarzać metodami klasy AudioClip. Ponieważ jest to metoda statyczna, nie wymaga istnienia obiektu apletu.

2.3. Java Sound API

Oprócz powyższych, skromnych możliwości posługiwania się klipami muzycznymi, Java 2 SDK dostarcza bardzo rozbudowanego i zaawansowanego interfejsu programistycznego przeznaczonego do pracy z dźwiękiem. Klasy pakietów javax.sound.sampled oraz javax.sound.midi pozwalają na nagrywanie, przetwarzanie, miksowanie i odtwarzanie danych audio i midi na profesjonalnym poziomie. Złożoność Java Sound API nie pozwala nawet na pobieżne przedstawienie tego tematu w przeglądowym wykładzie. Zainteresowanych odsyłamy do bogatej dokumentacji zawierającej przewodnik programisty.


3. Drukowanie

Poznamy dwa sposoby drukowania w Javie. Pierwszy, historycznie starszy, nosi nazwę Java 2D Printing API i jest reprezentowany przez klasy pakietu java.awt.print. Drukowanie tą metodą polega na dostarczeniu kodu wykreślającego drukowany obiekt na wykreślaczu typu Graphics przekazanym do metody print() interfejsu Printable. Jest ona wywoływana przez system na zasadzie wywołań zwrotnych (callback), podobnie jak metody wykreślające komponenty AWT (paint()) czy Swing (paintComponent()). Ten sposób nadaje się przede wszystkim do drukowania własnej grafiki. Druga technika, o nazwie Java Print Service, oparta jest na klasach i interfejsach pakietu javax.print i jego podpakietów. Jest dość złożona i posiada duże możliwości. Zobaczymy jak ją wykorzystać do drukowania dokumentów znanych typów, jak gif, jpeg, postscript czy plików tekstowych.

3.1. Java 2D Printing API

Aby drukować tą techniką należy zatroszczyć się o dwie rzeczy:

  1. Przygotowanie zadania drukowania
  2. Przygotowanie kodu wykreślającego
Kod wykreślający można przygotować na dwa sposoby:

3.1.1. Przygotowanie zadania drukowania

images/page.png
dialog konfiguracji strony

Przed rozpoczęciem procesu drukowania należy określić kilka parametrów, takich jak: rodzaj papieru, drukarka, której będzie dotyczyć drukowanie, liczba kopii itp. Służą do tego dialogi, które wymagają interakcji użytkownika. Parametry te można określić również programistycznie wywołując odpowiednie metody. Oczywiście najważniejsze jest utworzenie zadania drukowania - obiektu klasy PrinterJob (a właściwie pewnej jej podklasy, bo jest ona abstrakcyjna). Oto czynności, które należy wykonać:

  1. Wywołanie statycznej metody PrinterJob.getPrinterJob(). Zwraca ona zadanie drukowania typu PrinterJob. Na rzecz tego obiektu będą wywoływane kolejne metody.
  2. Opcjonalne określenie formatu strony metodą defaultPage() lub interaktywnie dialogiem wywoływanym metodą pageDialog().
  3. Wskazanie kodu drukującego metodą setPrintable(Printable) lub setPageable(Pageable). Jako argument przekazuje się obiekt klasy implementującej jeden z tych interfejsów, zawierający metodę drukującą.
  4. Opcjonalne ustalenie liczby kopii metodą setCopies(int).
  5. Opcjonalne wyświetlenie dialogu zatwierdzającego metodą printDialog(). Jego zawartość zależy od systemu operacyjnego i drukarki.
  6. Zainicjowanie drukowania metodą print() (z klasy PrinterJob). Jeśli czas drukowania może być długi, należy wywołać ją w osobnym wątku, aby nie blokowała programu. Wyjście z tej metody zwykle następuje przed zakończeniem drukowania.

images/print.png
dialog zatwierdzania wydruku

Drukowanie będzie wykonywane przez system poprzez wywoływanie kodu drukującego określonego w p.2.

Dialog konfiguracji strony wywoływany metodą pageDialog(PageFormat pf) pobiera jako argument pf obiekt reprezentujący wartości domyślne (można go uzyskać metodą defaultPage()). Następnie go klonuje (wykonuje kopię) i zwraca ze zmienionymi przez użytkownika parametrami. Można go potem przekazać metodzie setPrintable(Printable, PageFormat) uwzględniającej niestandardowe parametry strony. Jeśli użytkownik wybrał przycisk CANCEL, zostanie zwrócony argument pf.

Dialog zatwierdzający drukowanie wywołuje się metodą printDialog(). Jeśli użytkownik wybrał opcję OK, metoda zwraca true, w przeciwnym wypadku false. Dialog ten zawiera w sobie poprzedni jako zakładkę.

Anulowanie drukowania można wykonać metodą cancel().


3.1.2. Przygotowanie wykreślacza strony

Interfejs Printable definiuje wykreślacza strony. Deklaruje on metodę

int print(Graphics graphics, PageFormat pageFormat, int pageIndex) 
          throws PrinterException;
System drukujący będzie ją wywoływał w celu wydrukowania kolejnych stron. Wszystkie strony będą miały ten sam format papieru pageFormat (w przeciwieństwie do wykreślania poprzez interfejs Pageable ).

Liczba wywołań tej metody dla dla danej strony nie jest określona - może być większa od 1 !.
W związku z tym należy tak zaprojektować jej kod, by mógł wykreślić każdą stronę dowolną liczbę razy.

Pierwsza strona ma indeks 0. Metoda print() powinna zwrócić wartość Printable.NO_SUCH_PAGE, aby poinformować system, że wydrukowano wszystkie strony lub przekazany indeks jest niewłaściwy. Jeśli drukowanie strony przebiegło pomyślnie należy zwrócić Printable.PAGE_EXISTS.

Metoda print() będzie wywołana dla wszystkich indeksów stron począwszy od 0. Jeśli użytkownik w dialogu konfiguracyjnym zażąda wydrukowania stron 2 i 3, to metoda drukująca będzie wywołana dla indeksów 0, 1 i 2.

Wykreślanie odbywa się na wykreślaczu typu Graphics, który można bezpiecznie rzutować na Graphics2D, aby wykorzystać jego większe możliwości. Współrzędne na tym wykreślaczu liczone są w punktach drukarskich (1 pt = 1/72 cala). Klasa tego wykreślacza implementuje również interfejs PrinterGraphics, dostarczający metodę getPrinterJob(). Pozwala ona odwoływać się do zadania drukowania z wnętrza metody drukującej print().

Klasa PageFormat opisuje orientację i rozmiar papieru. Do określenia orientacji służą stałe PORTRAIT i LANDSCAPE oraz metody getOrientation() i setOrientation(int). Rodzaj papieru można poznać metodą

Paper getPaper();
Zwraca ona obiekt klasy Paper, której metodami można poznać wymiary papieru.

Rzeczywisty rozmiar obszaru drukowania może być mniejszy niż wymiary wykreślacza i zależy od modelu drukarki. Nie wszystkie drukarki potrafią drukować na całej powierzchni papieru.

Do określnia rzeczywistych rozmiarów papieru służą metody klas Paper lub PageFormat:
double getImageableHeight();
double getImageableWidth();
double getImageableX();
double getImageableY();
Dwie ostatnie zwracają położenie lewego-górnego rogu obszaru drukowania.

3.1.3. Przykład: drukowanie komponentu

images/logo.png
drukowanie pulpitu

Wykorzystując podobieństwo w mechaniźmie drukowania i wykreślania komponentów można łatwo wydrukować ich zawartość. Jest to szczególnie użyteczne w sytuacji, gdy na komponencie wykreślamy własne rysunki, np. wykresy przedstawiające jakieś dane. Poniższy program wyświetla okno z logo uczelni PJWSTK i podpisem. Kliknięcie w pulpit powoduje wydrukowanie zawartości, jednak logo i podpis znajdą się na różnych stronach. Dodatkowo wokół rysunków pojawi się ramka, co pozwoli zorientować się gdzie kończy się obszar drukowania użytej drukarki.

Aby móc drukować musimy oczywiście zaimportować pakiet java.awt.print. W metodzie main() budujemy GUI. Metoda obsługująca kliknięcia myszką wywołuje metodę doPrint(). Jej zadaniem jest utworzenie zadania drukowania. Po ustaleniu wykreślacza strony (którym jest obiekt klasy SimplePrint, bowiem implementuje ona interfejs Printable), wywoływany jest dialog zatwierdzający drukowanie. Jeśli metoda printDialog() zwróci true - rozpocznie się drukowanie. Wywołanie metody print() musi być umieszczone w bloku przechwytywania wyjątków, bo może ona zgłosić PrinterException.

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

public class SimplePrint 
    extends JPanel 
    implements Printable {

    public static void main(String[] args) {     
	JFrame frame = new JFrame("SimplePrint");
	final SimplePrint simple = new SimplePrint();
	simple.setPreferredSize(new Dimension(300, 300));
	simple.addMouseListener(new MouseAdapter(){
		public void mouseClicked(MouseEvent me){
		    simple.doPrint();
		}
	    });
	frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
	frame.getContentPane().add(simple);
	frame.setLocation(300, 300);
	frame.pack();
	frame.show();
    }

    public void doPrint(){
	PrinterJob pjob = PrinterJob.getPrinterJob();
	pjob.setPrintable(this);
	if (pjob.printDialog()){
	    try {
		pjob.print();
	    }
	    catch(Exception e){
	    }
	}
    }

    private final static BasicStroke stroke = new BasicStroke(4.0f);
    private final static Font font = new Font("Helvetica", Font.PLAIN, 72);
    private final static Paint paint = 
          new GradientPaint(150, 280, Color.red, 150, 255, Color.white, true);

    private void drawLogo(Graphics2D graph){
	graph.setPaint(Color.red);
	graph.setStroke(stroke);
        graph.fill(new Ellipse2D.Double(100, 100, 100, 100));
	graph.setPaint(Color.white);
	graph.fillRect(100, 100, 100, 50);
	graph.setPaint(Color.red);
        graph.draw(new Ellipse2D.Double(100, 100, 100, 100));
    }

    private void drawName(Graphics2D graph){
	graph.setFont(font);
	graph.setPaint(paint);
	graph.drawString("PJWSTK", 25, 280);
    }

Metoda drawLogo() wykreśla logo składające się z kółka, którego dolna połowa jest czerwona a górna biała. Metoda drawName() rysuje podpis. Obie będą wywoływane z metody paintComponent() wykreślającej komponent oraz z metody print() służącej do drukowania. Posługują się one wykreślaczem typu Graphics2D, przekazanym z metody wykreślającej komponent lub drukującej, po wykonaniu niezbędnego, ale możliwego rzutowania. Na początku metody print() musimy dokonać przesunięcia współrzędnych wykreślacza, ze względu na granice obszaru drukowania drukarki. Wykonuje to metoda translate(). Brak tego przesunięcia spowodowałby obcięcie rysunku na wydruku. Dalej rysujemy ramkę, która jest nieobecna w komponencie i pojawi się tylko na wydruku po to, by uwidocznić granice obszaru drukowania. W bloku switch w zależności od numeru strony przekazanego jako argument pi wykreślamy logo (na pierwszej stronie) lub napis (na drugiej). W pozostałych przypadkach zwracamy informację o błędzie (NO_SUCH_PAGE), co pozwoli również zakończyć drukowanie (po zwróceniu takiej wartości drukowanie kończy się)..


    public int print(Graphics graph, PageFormat pf, int pi) 
	throws PrinterException {
	Graphics2D g2d = (Graphics2D)graph;
	g2d.translate(pf.getImageableX(), pf.getImageableY());
	g2d.setPaint(Color.red);
	g2d.drawRect(0, 0, 299, 299);
	switch(pi){
	case 0:
	    drawLogo(g2d);
	    return Printable.PAGE_EXISTS;
	case 1:
	    drawName(g2d);
	    return Printable.PAGE_EXISTS;
	default:
            return Printable.NO_SUCH_PAGE;
	}
    }

    public void paintComponent(Graphics graph){
	super.paintComponent(graph);
	graph.setColor(Color.white);
	graph.fillRect(0, 0, 300, 300);
	drawLogo((Graphics2D)graph);
	drawName((Graphics2D)graph);
    }
}

3.1.4. Drukowanie dokumentów

Interfejs Printable definiował najprostsze dokumenty o jednolitej strukturze. Implementując interfejs Pageable możemy wydrukować dokumenty o znanej liczbie stron, różniące się między sobą formatem papieru. Obiekt klasy implementującej ten interfejs przygotowujemy do drukowania wywołując metodę setPageable(Pageable p) na rzecz obiektu klasy PrinterJob (zamiast setPrintable(Printable)).

Interfejs Pageable deklaruje trzy metody:

int getNumberOfPages();
PageFormat getPageFormat(int pageIndex);
Printable getPrintable(int pageIndex);
Implementując ten interfejs musimy dla każdej strony z osobna określić format papieru i wykreślacz strony. Trzeba również uwzględnić fakt, że:

System może drukować strony w dowolnej kolejności. Niektóre z nich mogą być pominięte.

Podobnie jak przy prostych wykreślaczach strony metoda print() obiektu zwracanego przez getPrintable() może być wywoływana wielokrotnie dla tej samej strony.

Zamiast bezpośrednio implementować interfejs Pageable można posłużyć się implementującą go klasą Book. Dostarcza ona metody

void append(Printable painter, PageFormat page);
Można nią budować dokument dodając kolejne strony.

3.1.5. Przykład drukowania z klasą Book

Wydrukujemy to samo, co w poprzednim przykładzie, korzystając z klasy Book. Tym razem możemy określić różne orientacje papieru, więc stronę tytułową wydrukujemy w pozycji landscape. Będzie ona zawierała logo, ale będzie ono na wydruku obrócone o 90 stopni w stosunku do poprzedniej wersji. Napis wydrukujemy na drugiej stronie w domyślnej orientacji portrait.

Program będzie składał się z trzech klas. Klasa BookPrint będzie zawierała wyłącznie metodę main() przygotowującą dokument i zadanie drukowania. Klasy BookCover i BookText implementują interfejs Printable i zawierają wyłącznie metody print(). Ich kod jest podobny do kodu z poprzedniego przykładu.

Przygotowanie dokumentu klasy Book jest proste. Po utworzeniu obiektu dodajemy do niego kolejne strony metodą append() określając przy tym format papieru. Po dołączeniu wszystkich elementów przygotowujemy dokument do druku wywołując metodę setPageable(book) na rzecz obiektu-zadania drukowania. Okładka będzie w formacie landscape, który przygotowujemy metodą setOrientation() z klasy PageFormat.

Wywołanie metody System.exit() na końcu jest konieczne, ponieważ posługujemy się okienkami dialogowymi. Jeśli jednak drukowanie trwa długo, to należy z tym poczekać na jego zakończenie.
import java.awt.*; 
import java.awt.print.*;
import java.awt.geom.*;

public class BookPrint {

    public static void main(String[] args) {     

	PrinterJob job = PrinterJob.getPrinterJob();     
	PageFormat pf = job.defaultPage();
	pf.setOrientation(PageFormat.LANDSCAPE);
	Book book = new Book();     
	book.append(new BookCover(), pf);     
	book.append(new BookText(), job.defaultPage());     
	job.setPageable(book);     
	if (job.printDialog()) {
	    try { 
		job.print(); 
	    }
	    catch (Exception e) {
	    }     
	}     
	System.exit(0);   
    }
}

class BookCover
    implements Printable {   

    private final static BasicStroke stroke = new BasicStroke(4.0f);

    public int print(Graphics graph, PageFormat pf, int pi)
	throws PrinterException {     
	Graphics2D g2d = (Graphics2D)graph;
	g2d.translate(pf.getImageableX() + pf.getImageableWidth()/2 - 150, 
		      pf.getImageableY() + pf.getImageableHeight()/2 -150);
	g2d.setPaint(Color.red);
	g2d.drawRect(0, 0, 299, 299);
	g2d.setStroke(stroke);
        g2d.fill(new Ellipse2D.Double(100, 100, 100, 100));
	g2d.setPaint(Color.white);
	g2d.fillRect(100, 100, 100, 50);
	g2d.setPaint(Color.red);
        g2d.draw(new Ellipse2D.Double(100, 100, 100, 100));
	return Printable.PAGE_EXISTS;
    }
}

class BookText
    implements Printable {   

    private final static Font font = new Font("Helvetica", Font.PLAIN, 72);
    private final static Paint paint = 
          new GradientPaint(150, 280, Color.red, 150, 255, Color.white, true);

    public int print(Graphics graph, PageFormat pf, int pi)
	throws PrinterException {     
	Graphics2D g2d = (Graphics2D)graph;
	g2d.translate(pf.getImageableX() + pf.getImageableWidth()/2 -150, 
		      pf.getImageableY() + pf.getImageableHeight()/2 -150);
	g2d.setPaint(Color.red);
	g2d.drawRect(0, 0, 299, 299);
	g2d.setFont(font);
	g2d.setPaint(paint);
	g2d.drawString("PJWSTK", 25, 280);
	return Printable.PAGE_EXISTS;
    }
}
Metodzie translate() przekazujemy takie argumenty, aby napis oraz logo znalazły się na środku kartki.

Tworzenie dla każdej strony osobnej implementacji interfejsu Pageable może byc uciążliwe. Metodą

void append(Printable painter, PageFormat page, int numPages);
można dodać za jednym zamachem numPages stron drukowanych przez jeden obiekt painter typu Printable. Jego metoda print() będzie wtedy odpowiedzialna za wydrukowanie pewnej liczby różnych stron. Jest to wygodne, gdy wiele kolejnych stron zawiera podobną treść.

3.2. Java Print Service (JPS)

Niestety, niewiele drukarek posiada wsparcie dla wystarczająco szerokiej gamy formatów, dlatego w pewnych przypadkach potrzebne są konwersje. Jeśli chcemy wydrukować dokument pdf na drukarce, która nie posiada wbudowanej możliwości jego drukowania, to należy dokument przekonwertować na jakiś znany drukarce format np. postscript.

Nie będziemy tu opisywać złożonej architektury tego podsystemu w całości. Ograniczymy się jedynie do pokazania w jaki sposób drukować dokumenty niektórych znanych typów. Współczesne drukarki posiadają wbudowane możliwości drukowania dokumentów w pewnych formatach - np. postscript czy obrazy gif, jpg. Posługując się JPS łatwo wykorzystać te możliwości. W szczególności można drukować pliki tekstowe dużo prostszym sposobem niż korzystając z Java 2D Printing API.

3.2.1. Typy dokumentów

Format dokumentu przesyłanego do drukarki definiuje klasa javax.print.DocFlavor. Składa się on z dwóch części:

  1. Typ MIME określający sposób w jaki dane będą reprezentowane. Np. image/gif lub text/plain. Może zawierać dodatkowo informację o sposobie kodowania np. charset="iso-8859-2".
  2. Nazwa klasy reprezentującej dane. Określa w jaki sposób będą one dostarczone. Np. java.io.InputStream dla danych czytanych ze strumienia.

Klasa DocFlavor ma podklasy (które są jej klasami wewnętrznymi !) odpowiadające różnym możliwym reprezentacjom danych. Np. klasa DocFlavor.INPUT_STREAM reprezentuje dane przesyłane w strumieniu a DocFlavor.URL reprezentuje dane o określonym położeniu. Zawierają one statyczne składowe (będące obiektami tych właśnie klas) odpowiadające wielu typom MIME. Np. DocFlavor.INPUT_STREAM.GIF reprezentuje obrazy typu gif przekazywane do drukarki w strumieniu a DocFlavor.URL.POSTSCRIPT reprezentuje dokumenty postscript o ustalonym położeniu.

Poniższy program pokazuje zestaw typów dokumentów, jakie potrafi drukować domyślna drukarka w pewnym systemie współpracując z JPS.

import javax.print.*;

public class PrTest {

    public static void main(String[] args){

	PrintService service =
	    PrintServiceLookup.lookupDefaultPrintService();

	DocFlavor[] flavors = service.getSupportedDocFlavors();
	for (int i = 0; i < flavors.length; i++)
	    System.out.println(flavors[i]);
    }
}
Oczywiście zestaw akceptowanych formatów zależy od drukarki. Dokumenty w innych formatach wymagają dodatkowych filtrów konwertujących. JPS potrafi się nimi posługiwać, ale nie będziemy tutaj się w to zagłębiać.
Na początku pobieramy odniesienie do domyślnej usługi drukowania. Następnie zapytujemy o jej możliwości. Oto fragment wydruku, całość znajduje się tu.
application/postscript; class="java.io.InputStream"
image/gif; class="java.io.InputStream"
image/jpeg; class="java.io.InputStream"
image/png; class="java.io.InputStream"
text/plain; charset="iso-8859-2"; class="java.io.InputStream"
text/plain; charset="iso-8859-2"; class="java.net.URL"
text/plain; charset="us-ascii"; class="java.net.URL"

Przygotowując dokument do wydruku musimy określić jego typ, czyli reprezentujący go obiekt klasy DocFlavor a właściwie jej podklasy. W typowych przypadkach wystarcza posłużenie się gotowymi składowymi tych klas.

3.2.2. Atrybuty

Na przykład chcąc wydrukować dwie kopie dokumentu tworzymy obiekt klasy Copies przekazując konstruktorowi liczbę kopii.
W JPS do określania atrybutów drukowania służą klasy pakietów javax.print.attribute i javax.print.attribute.standard. Każdy atrybut jest reprezentowany przez klasę implementującą interfejs Attribute. Atrybuty są podzielone na kategorie o specyficznych przeznaczeniach, zdefiniowane przez podinterfejsy interfejsu Attribute: Atrybuty (właściwie obiekty przechowujące ich konkretne wartości) grupuje się w kolekcjach implementujących interfejs AttributeSet. Główną klasą implementującą jest HashAttributeSet, jednak aplikacje korzystają raczej z jej podklas przeznaczonych do grupowania określonych kategorii atrybutów: Do tych kolekcji atrybuty dodaje się metodą add(), a następnie przekazuje jako argument metodzie inicjującej drukowanie. Do najczęściej używanych atrybutów należą (są to klasy pakietu javax.print.attribute.standard implementujące interfejs PrintRequestAttribute): Oto przykład użycia atrybutów:
PrintRequestAttributeSet aset = new HashPrintRequestAttributeSet();
aset.add(new Copies(5)); 
aset.add(MediaSize.ISO_A4); 
aset.add(Sides.DUPLEX); 
aset.add(OrientationRequested.LANDSCAPE);
aset.add(new Destination("file:c:\out.prn"));

3.2.3. Przygotowanie dokumentu

Po zdefiniowaniu formatu dokumentu i ewentualnie jego atrybutów należy przygotować dokument. Jest to obiekt klasy implementującej interfejs javax.print.Doc, który definiuje pojedynczy dokument przekazywany do druku. Dla ułatwienia przygotowano klasę SimpleDoc, która go implementuje. Obiekt reprezentujący dokument tworzy się konstruktorem

SimpleDoc(Object printData, DocFlavor flavor, DocAttributeSet attributes);
Jeśli nie zdefiniowaliśmy atrybutów dokumentu, jako ostatni argument można podać null. Jako pierwszy argument printData podaje się zawartość dokumentu. Musi ona być zgodna z określoną w jego typie flavor. Jeśli typ reprezentuje dane oparte na strumieniu, to jako printData podaje się strumień związany np. z plikiem zawierającym dane.

3.2.4. Drukowanie

Kiedy dokument jest przygotowany, trzeba zlokalizować drukarkę (a raczej usługę drukowania) potrafiącą wydrukować ten dokument ze zdefiniowanymi wcześniej atrybutami drukowania (np. drukowanie dwustronne lub w kolorze jest obsługiwane tylko przez niektóre drukarki). Do zlokalizowania usługi drukowania służą statyczne metody klasy javax.print.PrintServiceLookup:

PrintService[] lookupPrintServices(DocFlavor flavor, AttributeSet attributes);
PrintService lookupDefaultPrintService();
Pierwsza poszukuje wśród dostępnych drukarek tej, która potrafi wykonać zadanie i zwraca je w tablicy. Jeśli nie znaleziono spełniającej wymagania, to tablica ma długość 0. Druga zwraca drukarkę domyślną.

Następnie należy utworzyć zadanie drukowania metodą createPrintJob() interfejsu PrintService i zainicjować je. Służy do tego metoda interfejsu DocPrintJob (tego typu jest utworzone zadanie drukowania):

void print(Doc doc, PrintRequestAttributeSet attributes) throws PrintException;
Jako argumenty przekazuje się dokument i atrybuty drukowania.

3.2.5. Praktyczny przykład

Poniższy program drukuje plik o nazwie podanej jako pierwszy argument zakładając format podany jako drugi argument. Może nim być cyfra od 0 do 4 i odpowiada ona dokumentowi typu tekstowego, postscriptowego lub obrazom png, gif lub jpeg.

Najpierw przetwarzamy argumenty wywołania próbując otworzyć plik o podanej nazwie. Następnie tworzymy opis formatu jako obiekt klasy DocFlavor. W bloku switch zamieniamy kod podany jako argument wywołania programu na jeden z tych obiektów.

import java.io.*;
import javax.print.*;
import javax.print.attribute.*;
import javax.print.attribute.standard.*;
import javax.print.event.*;

public class DocPrint {

    public static void main(String[] args){

	FileInputStream docStream = null;  
	try { 
	    docStream = new FileInputStream(args[0]); 
	} catch (FileNotFoundException exc) { 
	    System.err.println("Nie znaleziono pliku");
	    return;
	} 
	int code = Integer.parseInt(args[1]);
	DocFlavor docFormat = null;
	switch(code){
	case 0:
	    docFormat = DocFlavor.INPUT_STREAM.TEXT_PLAIN_HOST;
	    break;
	case 1:
	    docFormat = DocFlavor.INPUT_STREAM.POSTSCRIPT;
	    break;
	case 2:
	    docFormat = DocFlavor.INPUT_STREAM.PNG;
	    break;
	case 3:
	    docFormat = DocFlavor.INPUT_STREAM.GIF;
	    break;
	case 4:
	    docFormat = DocFlavor.INPUT_STREAM.JPEG;
	    break;
	default:
	    System.err.println("Zły format dokumentu");
	    return;
	}
	Doc myDoc = new SimpleDoc(docStream, docFormat, null); 
	PrintRequestAttributeSet atrSet = new HashPrintRequestAttributeSet(); 
	atrSet.add(new Copies(1)); 
	atrSet.add(MediaSizeName.ISO_A4);

	PrintService[] services =
	    PrintServiceLookup.lookupPrintServices(docFormat, atrSet);

	if (services.length > 0) { 
	    DocPrintJob job = services[0].createPrintJob(); 
	    try { 
                job.print(myDoc, atrSet); 
	    } catch (PrintException pe) {
		System.err.println(pe);
	    } 
	} 
	else
	    System.out.println("Drukowanie niemożliwe");
    }
}
Po utworzeniu strumienia z danymi oraz opisu formatu możemy utworzyć sam dokument myDoc. Następnie definiujemy parametry drukowania takie jak rodzaj papieru czy liczba kopii. Przechowuje je obiekt typu PrintRequestAttributeSet, który później przekazuje się do polecenia drukowania. Dostępne drukarki dostajemy w tablicy obiektów typu PrintService. Na pierwszej z nich tworzymy zadanie drukowania metodą createPrintJob, któremu wydajemy polecenie drukowania dokumentu wraz z atrybutami.

JPS posiada również możliwości skierowania danych do strumienia zamiast drukarki. Pozwala to budować aplikacje służące do przeglądania plików w różnych formatach, jak również na dokonywanie konwersji.


4. Multimedia

Java Media API określa zestaw interfejsów programistycznych Javy przeznaczonych do programowania aplikacji multimedialnych. Zawiera on, oprócz klas dostępnych w standardowym zestawie J2SDK również zewnętrzne pakiety, które można pobrać z witryny java.sun.com/products/java-media/. Niektóre z nich są w fazie rozwoju i z tego powodu mogą być czasowo niedostępne. Większość z nich jest wspólnym dziełem wielu firm i instytucji. W skład Java Media API wchodzą następujące elementy:

  1. Java 2D API - częściowo omówione w poprzednim wykładzie
  2. Java 3D API - przeznaczone do tworzenia grafiki trójwymiarowej
  3. Java Advanced Imaging API - przeznaczone do przetwarzania obrazów
  4. Image I/O - zestaw narzędzi pozwalający na zapis i odczyt grafiki w różnych formatach
  5. Java Media Framework API - wspomagające przetwarzanie danych audio i wideo
  6. Java Sound API - wspomniane wcześniej, przy omawianiu dźwięku
  7. Java Speech API - przeznaczone do syntezy mowy

4.1. Java 3D

Java 3D API jest zewnętrznym zestawem pakietów służącym do pracy z grafiką trójwymiarową. Umożliwia tworzenie aplikacji interaktywnych oraz animowanych. Pozwala na operowanie oświetleniem i teksturami. Zawiera obszerny podręcznik programisty z licznymi przykładami. Pakiet można pobrać z witryny java.sun.com/products/java-media/3D/.

4.2. Java Advanced Imaging - Image I/O

Ten zestaw pakietów pozwala na wydajne, niezależne od platformy, zorientowane sieciowo przetwarzanie obrazów. Oprócz prostych operacji na obrazach takich jak operowanie kontrastem, wycinanie kształtów czy skalowanie (te możliwości są również zawarte w standardowym J2SDK) pozwala również na zaawansowane przekształcenia geometrii obrazu czy charakterystyki widma. Lista obsługiwanych formatów graficznych obejmuje BMP, JPEG, JPEG 2000, PNG, PNM, TIFF, WBMP i jest łatwo rozszerzalna o nowe dzięki mechanizmowi wtyczek (plug-in).

4.3. Java Media Framework

Java Media Framework jest dodatkowym zestawem pakietów przeznaczonym do tworzenia aplikacji multimedialnych. Umożliwia zaawansowane przetwarzanie dźwięku, grafiki dwu- i trójwymiarowej oraz wideo. Nie wchodzi w skład standardowego SDK Javy, ale można go pobrać z witryny java.sun.com/products/java-media/jmf/. Jest wspólnym dziełem kilku firm: Sun Microsystems, IBM, Silicon Graphics, Intel.

JMF obsługuje następujące protokoły:

Oprócz zestawu klas narzędziowych udostępniona jest przykładowa aplikacja JMStudio przeznaczona do odtwarzania, nagrywania i transmisji danych audio i wideo w różnych formatach.

Dostępne są cztery wersje implementacji JMF.

Trzy ostatnie implementacje korzystają z rodzimych bibliotek i sterowników dostępnych w systemach operacyjnych, dla których są przeznaczone.

4.4. Java Speech

Jest to zestaw klas umożliwiający komunikację głosową z aplikacjami multimedialnymi. Pozwala na syntezę mowy oraz jej rozpoznawanie. Wzorcowa implementacja firmy sun jest aktualnie niedostępna, jednak istnieją implementacje innych dostawców. Szczegóły na stronie java.sun.com/products/java-media/speech/.


Dokumentacja i literatura

Rozdział dotyczący klipów muzycznych w podręczniku Java Tutorial
Java Sound Programmer Guide
Dostępny w dokumentacji Javy w katalogu docs/guide/sound/programmer_guide/contents.html oraz w wersji on-line
The Java Sound Home Page
Rozdział dotyczący drukowania w podręczniku Java Tutorial
Rozdział dotyczący drukowania w przewodniku programisty grafiki dwuwymiarowej
Dostępny w dokumentacji Javy w katalogu docs/guide/2d/spec/j2d-print.fm1.html oraz w wersji on-line
Specyfikacja Java Print Service
Dostępna w dokumentacji Javy w katalogu docs/guide/jps/spec/JPSTOC.fm.html oraz w wersji on-line
Strona domowa projektu Java Media