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.
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.
paint()
lub
paintComponent()
, ale tylko w najprostszych przypadkach
repaint()
w celu odświeżenia komponentu
sleep()
z klasy Thread
w
celu ustalenia interwału pomiędzy wykreślaniem kolejnych klatek.
Druga wykorzystuje zegary - obiekty cyklicznie wykonujące jakieś operacje.
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ę.
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.
repaint()
repaint(long millis)
- powoduje odświeżenie po upływie millis
milisekund
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
repaint(long millis, int x, int y, int w, int h)
- kombinacja powyższych
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.
Animowanie obrazów zawartych w zewnętrznych plikach graficznych może być wykonane na kilka sposobów.
drawImage()
.
drawImage()
. W drugim przypadku koniecznie trzeba przekazać jej
jako argument typu ImageObserver
odniesienie do obiektu, który
implementuje ten interfejs i będzie generował żądania wykreślenia kolejnych ramek.
Zwykle będzie to komponent, którego dotyczy wykreślanie
(ponieważ klasa Component
implementuje ten interfejs).
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.
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ą.
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.
// 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(); }
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()
.
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.
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(); } }
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.
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.
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.
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); } }
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.
Java Sound Engine zawarta w Java 2 SDK umożliwia odgrywanie klipów muzycznych w następujących formatach:
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.
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.
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.
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.
Aby drukować tą techniką należy zatroszczyć się o dwie rzeczy:
Printable
w celu dostarczenia obiektu wykreślającego
wszystkie strony w jednej metodzie. Ma zastosowanie w najprostszych przypadkach.
Pageable
dla dostarczenia wielu metod wykreślających
dla różnych typów stron w dokumencie. Nadaje się do przygotowania dokumentów
posiadających różnorodną strukturę. Umożliwia określenie liczby stron w dokumencie.
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ć:
PrinterJob.getPrinterJob()
. Zwraca ona
zadanie drukowania typu PrinterJob
. Na rzecz tego obiektu będą wywoływane
kolejne metody.
defaultPage()
lub interaktywnie dialogiem wywoływanym metodą pageDialog()
.
setPrintable(Printable)
lub setPageable(Pageable)
. Jako argument przekazuje się obiekt klasy
implementującej jeden z tych interfejsów, zawierający metodę drukującą.
setCopies(int)
.
printDialog()
.
Jego zawartość zależy od systemu operacyjnego i drukarki.
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.
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()
.
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
).
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
.
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.
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.
Paper
lub PageFormat
:
double getImageableHeight(); double getImageableWidth(); double getImageableX(); double getImageableY();Dwie ostatnie zwracają położenie lewego-górnego rogu obszaru drukowania.
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); } }
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:
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.
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
.
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ść.
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.
Format dokumentu przesyłanego do drukarki definiuje klasa
javax.print.DocFlavor
. Składa się on z dwóch części:
java.io.InputStream
dla danych czytanych ze strumienia.
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]); } }
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.
Copies
przekazując konstruktorowi liczbę kopii.
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
:
PrintRequestAttribute
- dotyczące całego zadania drukowaniaDocAttribute
- definiujące właściwości dokumentuPrintJobAttribute
- określające sposób wykonania zadaniaPrintServiceAttribute
- do raportowania statusu drukowaniaSupportedValuesAttribute
- pozwalają określać dozwolone wartości atrybutówAttributeSet
. Główną klasą
implementującą jest HashAttributeSet
, jednak aplikacje korzystają
raczej z jej podklas przeznaczonych do grupowania określonych kategorii atrybutów:
HashPrintRequestAttributeSet
HashPrintServiceAttributeSet
HashPrintJobAttributeSet
HashDocAttributeSet
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
):
OrientationRequested
- pozwala określić orientację wydruku przy pomocy
statycznych stałych tej klasy: LANDSCAPE
, PORTRAIT
itp.
Copies
- do określania liczby kopii wydruku, którą podaje się konstruktorowi.
Destination
- do przekierowania wydruku do pliku, którego URL
przekazuje się konstruktorowi.
Media
do określenia rodzaju papieru i jego źródła. Ponieważ jest to klasa
abstrakcyjna, do bezpośredniego użytku są przeznaczone jej podklasy MediaName
,
MediaSizeName
i inne.
Sides
dla drukarek potrafiących drukować dwustronniePrintRequestAttributeSet 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"));
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.
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.
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.
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:
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/.
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).
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.
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/.