Klasa Graphics
oraz metoda paint()
klasy Component
nie uległy niemal żadnym zmianom od samego początku istnienia języka Java. Aktualnie zapewniają one proste, lecz jednocześnie całkiem funkcjonalne możliwości graficzne. Pierwszy graficzny interfejs programistyczny został udostępniony w JDK 1.1, jednak bardzo szybko zastąpiono go rozwiązaniami wprowadzonymi w Java 1.2, a następnie w Java 1.4. Na szczęście interfejsy programistyczne służące do rysowania bazują na wykorzystaniu obiektów Graphics
, dzięki czemu kod realizujący operacje graficzne nie musiał się zmieniać — konieczne było jedynie zmodyfikowanie pewnych szczegółów związanych z pobieraniem odpowiedniego rodzaju obiektów Graphics
. Pakiet 2D, obsługujący grafikę dwuwymiarową, także bazuje na wykorzystaniu obiektów Graphics
— klasa Graphisc2D
jest bowiem klasą potomną klasy Graphics
.
Aby spojrzeć na grafikę dwuwymiarową z odpowiedniej perspektywy, należy pomyśleć, jak ogromnym krokiem naprzód, jeśli chodzi o możliwości publikowania w świecie komputerów osobistych, było wprowadzenie języka PostScript. PostScript jest językiem skryptowym, lecz jednocześnie mechanizmem znaczącym. Java jest już rozbudowanym i kompletnym językiem programowania, dlatego też tworząc interfejs programistyczny służący do generowania grafiki dwuwymiarowej, należało opracować wyłącznie mechanizm znaczący. Problem ten został rozwiązany w bardzo dobry sposób dzięki wykorzystaniu kilku pomysłów wzorowanych na rozwiązaniach stosowanych w języku PostScript, co z kolei było możliwe dzięki uczestnictwu firmy Adobe we wczesnych etapach projektowania 2D API.
Także od samego początku istnienia Javy była w niej dostępna klasa AudioClip
. W Javie 2 została ona szybko rozbudowana o możliwości obsługi dodatkowych formatów (w tym także MIDI) oraz wykorzystania jej z poziomu aplikacji. Jednocześnie Java Media Framework (standardowe rozszerzenie javax.media
) oraz nowszy szkielet JavaFX pozwalają na odtwarzanie dźwięku, obrazu, a być może także i innych mediów zapewniających znacznie większą kontrolę nad sposobem prezentacji. W dalszej części rozdziału podałem stosowne przykłady.
Jednak w pierwszej kolejności przyjrzymy się klasie Graphics
.
Należy napisać klasę dziedziczącą po klasie Component
, a następnie w jej metodzie paint()
trzeba użyć metod przekazanego obiektu klasy Graphics
.
/graphics/PaintDemo.java
public class PaintDemo extends Component { private static final long serialVersionUID = -5595189404659801913L; int rectX = 20, rectY = 30; int rectWidth = 50, rectHeight = 50; /** * Klasy potomne klasy Component mogą przesłaniać metodę * paint(), jednak klasy potomne klasy JComponent * powinny raczej używać metody paintComponent(), gdyż w ten * sposób mogą uniknąć niepotrzebnego rysowania krawędzi itd. */ @Override public void paint(Graphics g) { g.setColor(Color.red); g.fillRect(rectX, rectY, rectWidth, rectHeight); } @Override public Dimension getPreferredSize() { return new Dimension(100, 100); } }
Klasa Graphics
dysponuje obszerną grupą metod realizujących podstawowe operacje graficzne. Pozwalają one na rysowanie zarówno konturów, jak i wypełnionych prostokątów, łuków, elips oraz wielokątów, przy czym kontury i wypełnione kształty są rysowane przez dwie różne grupy metod. Nie trzeba korzystać jednocześnie z obu, chyba że krawędzie oraz wnętrze figury mają mieć różne kolory. Metoda drawString()
oraz pozostałe metody z nią związane pozwalają na wyświetlanie tekstów (patrz „12.3. Wyświetlanie tekstu”). Dostępna jest także metoda drawLine()
rysująca odcinki proste, metody setColor()
, getColor()
, setFont()
, getFont()
oraz wiele, wiele innych. W rzeczywistości klasa Graphics
definiuje zbyt wiele metod, abym w niniejszej książce był w stanie przedstawić je wszystkie; informacje na ich temat można znaleźć w dokumentacji klasy java.awt.Graphics
.
W przeszłości podstawowym błędem popełnianym przez osoby rozpoczynające naukę i wykorzystywanie języka Java było wywoływanie metody getGraphics()
oraz metod rysujących obiektu Graphics
bezpośrednio w programie głównym bądź w konstruktorze obiektu klasy rozszerzającej klasę Component
. Na szczęście obecnie dostępnych jest już bardzo wiele książek, które informują, że poprawnym sposobem rozwiązania tego problemu jest umieszczenie kodu rysującego wewnątrz metody paint()
komponentu. Dlaczego? Otóż wynika to z faktu, że nie można niczego narysować w oknie, zanim okno to nie zostanie utworzone i (w większości systemów wykorzystujących okna) skojarzone z ekranem, a te czynności zabierają znacznie więcej czasu, niż mają program główny oraz konstruktor. Kod rysujący musi zatem cierpliwie poczekać aż do chwili, gdy system poinformuje wirtualną maszynę Javy, że nadszedł już czas, by narysować i wyświetlić zawartość okna.
Gdzie zatem należy umieszczać kod rysujący? Otóż jest to jedna z sytuacji, w których należy wykorzystać różne rozwiązania w zależności od tego, czy korzystamy z biblioteki AWT, czy też Swing. AWT, prosty system do obsługi aplikacji z graficznym interfejsem użytkownika, wykorzystuje w tym celu metodę paint()
. Metoda ta jest dostępna także w pakiecie Swing, jednak w tym przypadku ze względu na obsługę krawędzi oraz inne zagadnienia zalecane jest przesłanianie metody paintComponent()
. Obie te metody dysponują jednym argumentem wywołania — obiektem klasy Graphics
. Tworzona przez nas metoda paintComponent()
powinna się zaczynać od wywołania super.paintComponent()
i przekazania w jej wywołaniu tego samego argumentu — w ten sposób zapewniamy, że wszystkie komponenty zostaną wyświetlone w odpowiedniej kolejności, zaczynając od tych na samym „spodzie”, a kończąc na tych widocznych na samej „górze”. Z kolei metoda paint()
nie powinna wywoływać tej samej metody w klasie bazowej. Niektóre przykłady zamieszczone w tym rozdziale wykorzystują metodę paint()
, a inne metodę paintComponent()
; te drugie są zazwyczaj klasami potomnymi klasy JPanel
. Takie rozwiązanie zapewnia lepszą integrację z pakietem Swing i pozwala na umieszczenie naszego komponentu jako głównego komponentu ramki (JFrame
) przy użyciu metody setContentPane()
, dzięki czemu eliminowana jest dodatkowa warstwa pojemników. (Informacje na temat klasy JFrame
i paneli można znaleźć w „14.1. Wyświetlanie komponentów graficznego interfejsu użytkownika”).
Chcielibyśmy uniknąć konieczności pisania niewielkich programów głównych z ramką za każdym razem, gdy tworzymy klasę potomną klasy Component
.
Można się posłużyć napisaną przeze mnie klasą CompRunner
, której metoda main()
tworzy ramkę i instaluje w niej tworzony przez nas komponent.
CompRunner
to niewielki program, który pobiera nazwę klasy podaną w wierszu wywołania, następnie tworzy obiekt tej klasy (patrz „23.4. Dynamiczne ładowanie i instalowanie klas”), po czym umieszcza go w komponencie JFrame
. Dodatkowo program zapewnia, że wyświetlone okno będzie odpowiednio duże. Wiele zagadnień związanych z graficznym interfejsem użytkownika, a nie z tworzeniem grafiki, zostało omówionych w Rozdział 14.
Testowana klasa musi być klasą pochodną klasy Component
, w przeciwnym przypadku na ekranie zostanie wyświetlony komunikat o błędzie. Rozwiązanie to jest bardzo przydatne przy uruchamianiu niewielkich komponentów i zarówno w tym, jak i w kolejnym rozdziale przedstawię wiele przykładów jego użycia. Sposób korzystania z tego programu jest wyjątkowo prosty. Na przykład aby stworzyć obiekt klasy DrawStringDemo2
przedstawiony w „12.3. Wyświetlanie tekstu”, wystarczy wydać następujące polecenie:
java gui.CompRunner graphics.DrawStringDemo2
Wygląd tego komponentu został przedstawiony z lewej strony na Rysunek 12-1. Ciekawym doświadczeniem może być użycie tego programu w celu wyświetlenia obiektu jednej z predefiniowanych klas Javy. Wywołanie bezargumentowego konstruktora klasy JTree
(elementu interfejsu graficznego Javy umożliwiającego prezentację informacji w formie listy hierarchicznej, przedstawionego w „17.8. Program MailClient”) powoduje utworzenie komponentu JTree
zawierającego przykładowe dane (został on zaprezentowany z prawej strony na Rysunek 12-1).
Rysunek 12-1. Program CompRunner przedstawiający komponenty DrawStringDemo2 (z lewej) oraz javax.swing.JTree (z prawej)
Ponieważ program ten ma niewiele wspólnego z zagadnieniami omawianymi w tym rozdziale, nie przedstawię tu jego kodu źródłowego, można go jednak znaleźć w przykładach dołączonych do niniejszej książki.
Wystarczy wywołać metodę drawString()
klasy Graphics
:
public class DrawStringDemo extends JComponent { private static final long serialVersionUID = -7199469682507443122L; int textX = 10, textY = 20; @Override public void paintComponent(Graphics g) { g.drawString("Witaj, Javo!", textX, textY); } public Dimension getPreferredSize() { return new Dimension(100, 100); } }
Należy określić wysokość i szerokość łańcucha znaków przy zastosowaniu wybranej czcionki i odjąć je od wysokości i szerokości komponentu. Uzyskane wyniki należy następnie podzielić przez dwa, a rezultat potraktować jako współrzędne punktu, w którym powinien zostać wyświetlony tekst.
Program DrawStringDemo2
, przedstawiony na Przykład 12-1, określa szerokość i wysokość łańcucha znaków (patrz Rysunek 12-2, prezentujący niektóre atrybuty tekstu). Następnie program odejmuje wielkość tekstu od wielkości komponentu, a uzyskane wyniki dzieli przez dwa, co powoduje wyśrodkowanie tekstu w obszarze komponentu.
Przykład 12-1. /graphics/DrawStringDemo2.java
public class DrawStringDemo2 extends JComponent { private static final long serialVersionUID = -6593901790809089107L; //- String message = "Witaj, Javo!"; /** Metoda paint() jest wywoływana (przez AWT), gdy nadszedł * czas, by wyświetlić tekst. */ @Override public void paintComponent(Graphics g) { // Pobieramy bieżącą czcionkę oraz jej metryki (FontMetrics). FontMetrics fm = getFontMetrics(getFont()); // Dysponując metrykami czcionki, określamy szerokość łańcucha // znaków. Odejmujemy ją od szerokości komponentu, dzielimy // wynik przez 2 i w ten sposób wyznaczamy współrzędne punktu, // w którym należy wyświetlić tekst. int textX = (getSize().width - fm.stringWidth(message))/2; if (textX<0) // Jeśli łańcuch jest zbyt duży, zaczynamy textX = 0; // w punkcie 0. // To samo co wyżej, lecz dla wysokości łańcucha znaków. int textY = (getSize().height - fm.getAscent())/2 - fm.getDescent(); if (textY < 0) textY = getSize().height - fm.getDescent() - 1; // A teraz wyświetlamy tekst w wyznaczonym punkcie. g.drawString(message, textX, textY); } //- public Dimension getPreferredSize() { return new Dimension(100, 100); } public static void main(final String[] args) { final JFrame jf = new JFrame(); jf.add(new DrawStringDemo2()); jf.setBounds(100, 100, 100, 100); jf.setVisible(true); } }
Przedstawione operacje są na tyle częste, że można by oczekiwać, iż Java będzie je udostępniać. I faktycznie, Java daje takie możliwości. Przedstawiony komponent w większości rozwiązań umożliwiających tworzenie graficznych interfejsów użytkownika nosi nazwę etykiety. Jak się przekonamy w Rozdział 14., Java udostępnia komponent Label
(etykieta), który pozwala na wyśrodkowanie tekstu (jak również wyrównanie go do lewej bądź prawej strony komponentu) oraz wybór czcionki i koloru wyświetlonego tekstu. Dostępny jest także komponent JLabel
, w którym oprócz tekstu lub zamiast niego można wyświetlić również ikonę.
Chcemy wyświetlić tekst lub inny kształt oraz jego cień, jak pokazałem na Rysunek 12-3.
Należy dwukrotnie wyświetlić zawartość komponentu, za pierwszym razem w ciemnym kolorze (jako cień), a za drugim — w wybranym kolorze. Zawartość komponentu wyświetlana za drugim razem musi być nieco przesunięta.
Przedstawiony na Przykład 12-2 program DropShadow
robi dokładnie to, co opisałem w powyższym rozwiązaniu. W przykładzie wykorzystywany jest także obiekt Font
, który umożliwia nam uzyskanie kontroli nad wyglądem czcionki używanej przy wyświetlaniu tekstu.
Przykład 12-2. /graphics/DropShadow.java
public class DropShadow extends JComponent { /** Wyświetlany tekst. */ protected String theLabel; /** Nazwa czcionki. */ protected String fontName; /** Obiekt czcionki. */ protected Font theFont; /** Wielkość czcionki. */ protected int fontSize = 18; /** Przesunięcie cienia. */ protected int theOffset = 3; /** * Przygotowanie graficznego interfejsu użytkownika, * ograniczamy się do wszechobecnego wyjątku IllegalArgumentException. */ public DropShadow() { this("DropShadow"); } public DropShadow(String theLabel) { this.theLabel = theLabel == null ? "DropShadow" : theLabel; // A teraz zajmiemy się czcionką. fontName = "Sans"; fontSize = 24; if (fontName != null || fontSize != 0) { theFont = new Font(fontName, Font.BOLD + Font.ITALIC, fontSize); System.out.println("Nazwa czcionki " + fontName + ", czcionka " + theFont); } setBackground(Color.green); } /** Metoda paint() generująca efekt cienia. */ public void paint(Graphics g) { g.setFont(theFont); g.setColor(Color.black); g.drawString(theLabel, theOffset+30, theOffset+50); g.setColor(Color.white); g.drawString(theLabel, 30, 50); } }
Standardowa biblioteka AWT wykorzystuje bardzo prosty model rysowania. Sądzę, że to właśnie z tego powodu metodę, którą należy tworzyć, nazwano paint()
. Wróćmy na chwilę do czasu, kiedy stosowano jeszcze zwyczajny papier. Co się dzieje, jeśli rysujemy coś na papierze, a następnie musimy narysować na tym coś innego w innym kolorze? Jeśli Czytelnik pamięta, co to jest papier, to z całą pewnością wie, że drugi kolor przesłoni pierwszy. Cóż, AWT działa w podobny sposób. (Nie mówimy tu o farbach wodnych, które można ze sobą łączyć). Możliwości rysowania w Javie przypominają raczej farby olejne. Dzięki temu, że AWT pozostawia w niezmienionym stanie wszystkie bity (piksele lub elementy rysunku), które nie zostały zmodyfikowane podczas rysowania, oraz ze względu na to, że metody takie jak drawString()
mają ściśle zdefiniowany cel działania, można bardzo łatwo tworzyć efekt cienia oraz inne ciekawe efekty graficzne.
Warto pamiętać, aby w pierwszej kolejności rysować elementy, które mają być „pod spodem”, a dopiero potem te, które mają być „na wierzchu” rysunku. Aby sprawdzić, dlaczego należy postępować właśnie w taki sposób, można zamienić kolejność wywołań dwóch metod drawString()
w ostatnim przykładzie.
I jeszcze jedno ostrzeżenie: nie należy mieszać rysowania z wykorzystaniem komponentów GUI (graficznego interfejsu użytkownika; patrz Rozdział 14.). Załóżmy na przykład, że zdefiniowaliśmy metodę paint()
apletu lub innego komponentu, a następnie dodaliśmy do niego przycisk (posługując się w tym celu metodą add()
). W niektórych implementacjach języka Java rozwiązanie takie będzie działać, jednak w pozostałych widoczny będzie bądź tylko przycisk, bądź też to, co zostało narysowane w metodzie paint()
. Takie rozwiązanie nie jest przenośne i nie należy go stosować. Aby poradzić sobie z tym problemem, można wykorzystać kilka różnych komponentów. Można także użyć metod getContentPane()
oraz getGlassPane()
klasy JFrame
, opisanych szczegółowo w Rozdział 8. książki Java Swing.
Inny sposób tworzenia cienia został przedstawiony w „12.6. Wyświetlanie tekstu przy użyciu biblioteki grafiki dwuwymiarowej”.
Możliwości tworzenia grafiki dwuwymiarowej wprowadzone w Javie 2 same w sobie mogłyby stanowić temat osobnej książki. I tak zresztą jest — książka Jonathana Knudsena zatytułowana Java 2D Graphics (wydana przez wydawnictwo O’Reilly) prezentuje wszystkie możliwe aspekty tego kompletnego pakietu graficznego. W niniejszej książce przedstawię tylko jeden przykład wykorzystania tego pakietu, pokazujący sposób wyświetlania tekstu na tle o podanej teksturze.
Graphics2D
jest bezpośrednią klasą potomną oryginalnej klasy Graphics
. Tak naprawdę w Javie 2 do metody paint()
zawsze jest przekazywany obiekt tej właśnie klasy. A zatem aby uzyskać obiekt tej klasy, wystarczy na początku metody paint()
wykonać odpowiednie rzutowanie typów:
public void paint(Graphics g) { Graphics2D g2 = (Graphics2D) g;
Teraz można korzystać z dowolnych metod klas Graphics2D
oraz Graphics
, wywołując je przy użyciu odwołania do obiektu g2
. Jedną z dodatkowych metod klasy Graphics2D
jest setPaint()
. Można jej używać zamiast metody setColor()
, aby określić kolor, jakim chcemy rysować. Niemniej jednak do metody setPaint()
można przekazywać także dane kilku innych typów; w naszym przypadku przekażemy do niej obiekt TexturedPaint
, który określa wzór. Nasz wzór jest jedynie zbiorem ukośnych linii, jednak równie dobrze można wykorzystać dowolny wzór, a nawet mapę bitową zapisaną w pliku (patrz „12.8. Wyświetlanie obrazu”). Wygląd tekstu wyświetlanego przez program został przedstawiony na Rysunek 12-4 (w kolorze prezentuje się on jeszcze lepiej), natomiast sam kod programu — na Przykład 12-3.
Przykład 12-3. /graphics/TexturedText.java
public class TexturedText extends JComponent { private static final long serialVersionUID = 8898234939386827451L; /** Obraz stanowiący teksturę. */ protected BufferedImage bim; /** Tekstura używana przy rysowaniu. */ TexturePaint tp; /** Wyświetlany łańcuch znaków. */ String mesg = "Lampasy"; /** Czcionka. */ Font myFont = new Font("Lucida Regular", Font.BOLD, 72); /** Metoda "programu głównego" - tworzymy i wyświetlamy okno programu. */ public static void main(String[] av) { // Tworzymy obiekt TexturedText i wyświetlamy go. final Frame f = new Frame("TexturedText"); TexturedText comp = new TexturedText(); f.add(comp); f.addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent e) { f.setVisible(false); f.dispose(); System.exit(0); } }); f.pack(); f.setLocation(200, 200); f.setVisible(true); } protected static Color[] colors = { Color.red, Color.blue, Color.yellow, }; /** Tworzymy obiekt. */ public TexturedText() { super(); setBackground(Color.white); int width = 8, height = 8; bim = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); Graphics2D g2 = bim.createGraphics(); for (int i=0; i<width; i++) { g2.setPaint(colors[(i/2)%colors.length]); g2.drawLine(0, i, i, 0); g2.drawLine(width-i, height, width, height-i); } Rectangle r = new Rectangle(0, 0, bim.getWidth(), bim.getHeight()); tp = new TexturePaint(bim, r); } @Override public void paintComponent(Graphics g) { Graphics2D g2 = (Graphics2D)g; g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2.setPaint(tp); g2.setFont(myFont); g2.drawString(mesg, 20, 100); } @Override public Dimension getMinimumSize() { return new Dimension(250, 100); } @Override public Dimension getPreferredSize() { return new Dimension(320, 150); } }
Nie opisywałem, w jaki sposób można skalować, obracać oraz w inny sposób modyfikować obrazy przy wykorzystaniu klasy AffineTransform
, gdyż zagadnienia te wykraczają poza zakres niniejszej pozycji. Informacje na ten temat można znaleźć w książce Java 2D Graphics, o której wspominałem we wcześniejszej części rozdziału.
Chcemy udostępnić czcionkę wraz z aplikacją, jednak nie chcemy zmuszać użytkowników do instalowania jej jako „czcionki systemowej”.
Należy użyć metody Font.createFont(...)
, a następnie przeskalować utworzoną czcionkę przy użyciu metody deriveFont(int nPoints)
.
Java udostępnia statyczną metodę Font.createFont()
, pozwalającą na tworzenie „prywatnych” czcionek, które można stosować w aplikacji bez konieczności instalowania ich z wykorzystaniem systemowego mechanizmu do obsługi czcionek. W ten sposób użytkownicy mogą korzystać z aplikacji oraz jej niestandardowych czcionek, mimo że nie posiadają przywilejów administracyjnych ani dostępu do konta „root”, w systemach, w których instalowanie czcionek zazwyczaj tego wymaga.
Metoda createFont()
wymaga przekazania dwóch argumentów. Pierwszym z nich jest wartość typu int
, która musi być statycznym polem Font.TRUETYPE_FONT
, natomiast drugim musi być strumień InputStream
(patrz „10.17. Odczytywanie i zapisywanie danych binarnych”), otworzony do odczytu z pliku binarnego. Jak można się domyślić na podstawie wymogu, by pierwszym argumentem była wartość Font.TRUETYPE_FONT
, obecnie metoda ta obsługuje wyłącznie czcionki typu TrueType
. Dokumentacja klasy Font
, mająca niezmiennie optymistyczny wydźwięk, stwierdza, że w przyszłości pole to może pozwalać na obsługę innych formatów czcionek, choć nie ma takiej gwarancji. Zważywszy na dostępność darmowych mechanizmów prezentujących czcionki PostScript, takich jak te stosowane w XFree86 — implementacji X Window System — można sądzić, że w przyszłości powinna się pojawić obsługa czcionek PostScript. Przykład 12-4 przedstawia kod niewielkiej, niezależnej aplikacji, która wyświetla okno przedstawione na Rysunek 12-5.
Przykład 12-4. Stosowanie czcionek aplikacji (/graphics/TTFontDemo.java)
public class TTFontDemo extends JLabel { private static final long serialVersionUID = -2774152065764538894L; /** Konstruktor klasy TTFontDemo -- tworzy nową czcionkę * wczytaną z pliku TTF. */ public TTFontDemo(String fontFileName, String text) throws IOException, FontFormatException { super(text, JLabel.CENTER); setBackground(Color.white); // W pierwszej kolejności sprawdzamy, czy można wczytać // plik czcionki. InputStream is = this.getClass().getResourceAsStream(fontFileName); if (is == null) { throw new IOException("Nie można otworzyć pliku " + fontFileName); } // Metoda createFont() tworzy czcionkę o wielkości 1 punktu, // odczytanie tekstu tej wielkości byłoby dosyć trudne :-) Font ttfBase = Font.createFont(Font.TRUETYPE_FONT, is); // Zatem skalujemy czcionkę do wielkości 24 punktów. Font ttfReal = ttfBase.deriveFont(Font.PLAIN, 24); setFont(ttfReal); } /** Metoda main() aplikacji TTFontDemo. */ public static void main(String[] args) throws Exception { String DEFAULT_MESSAGE = "O szyby deszcz dzwoni, deszcz dzwoni jesienny."; // Plik jest wyczytywany jako zasób, dlatego nie musimy // przed nim umieszczać ścieżki "graphics/". String DEFAULT_FONTFILE = "Kellyag_.ttf"; String message = args.length == 1 ? args[0] : DEFAULT_MESSAGE; JFrame f = new JFrame("Przykład stosowania czcionek aplikacji"); TTFontDemo ttfd = new TTFontDemo(DEFAULT_FONTFILE, message); f.getContentPane().add(ttfd); f.setBounds(100, 100, 700, 250); f.setVisible(true); f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); } }
Ten sposób stosowania czcionek ma pewne ograniczenia. Przede wszystkim — zgodnie z tym, o czym wspominałem w komentarzu — można go stosować wyłącznie w klasach dziedziczących po klasie JComponent
biblioteki Swing; nie jest to natomiast możliwe w przypadku korzystania z biblioteki AWT i jej klasy Component
(patrz Rozdział 14.).
Co więcej, ze względu na zabezpieczenia metody tej nie można by w prosty sposób wykorzystać w apletach (gdyż konieczne byłoby utworzenie lokalnej kopii pliku czcionki). Można jej natomiast używać w aplikacjach uruchamianych przy użyciu technologii Java Web Start (patrz „21.11. Java Web Start”).
W metodzie paint()
należy wywołać metodę drawImage()
klasy Graphics
. Obiekty obrazów reprezentują mapy bitowe. Obrazy można pobierać z plików przy użyciu metody getImage()
, można je także tworzyć za pomocą metody createImage()
. Jednak obrazów nie można tworzyć samodzielnie — klasa Image
jest bowiem klasą abstrakcyjną. Gdy już dysponujemy obiektem Image
, wyświetlenie obrazu jest wyjątkowo proste:
/graphics/DrawImageDemo.java
public void paint(Graphics g) { g.drawImage(0, 0, myImage, this); }
Obraz można pobrać przy użyciu metody, która nosi oczywistą nazwę — getImage()
. Jeśli tworzony kod ma działać w aplecie, to można się posłużyć metodą getImage()
klasy Applet
, jeśli jednak chcemy z niego korzystać także w aplikacjach, to konieczne będzie wykorzystanie analogicznej metody udostępnianej przez AWT. Metoda ta wymaga podania nazwy pliku lub adresu URL. Oczywiście użycie nazwy pliku w przypadku apletów spowoduje zgłoszenie wyjątku sygnalizującego naruszenie zasad bezpieczeństwa, chyba że użytkownik zainstaluje odpowiedni plik polityki zabezpieczeń. Program GetImage
pokazuje, jak wyświetlić zarówno obraz pobierany z pliku, jak i obraz o podanym adresie URL:
public class GetImage extends JApplet { private static final long serialVersionUID = 4288395022095915666L; private Image image; public void init() { loadImage(); } public void loadImage() { // Wersja przenośna: getClass().getResource() działa zarówno // w apletach, jak i aplikacjach, w JDK 1.1 lub 1.3; zwraca URL // reprezentujący nazwę pliku. URL url = getClass().getResource("Duke.gif"); image = getToolkit().getImage(url); // Lub po prostu: // image = getToolkit().getImage(getClass().getResource("Duke.gif")); } @Override public void paint(Graphics g) { g.drawImage(image, 20, 20, this); } public static void main(String[] args) { JFrame f = new JFrame("GetImage"); f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); GetImage myApplet = new GetImage(); f.setContentPane(myApplet); myApplet.init(); f.setSize(100, 100); f.setVisible(true); myApplet.start(); } }
Czasami może się zdarzyć, że w jednym panelu będziemy chcieli wyświetlić ten sam obraz więcej niż jeden raz. Przykład 12-5 przedstawia program, który tworzy tło panelu poprzez wielokrotne wyświetlenie w nim tego samego obrazu. Aby rozwiązanie takie było możliwe, określamy wymiary obrazu przy użyciu metod getWidth()
oraz getHeight()
, a także wymiary samego panelu za pomocą metody getSize()
. Jak zwykle w metodzie paint()
nie określamy stałych wymiarów okna, gdyż użytkownik może zmienić jego wielkość, posługując się myszą.
Przykład 12-5. /graphics/TiledImageComponent.java
public class TiledImageComponent extends JComponent { private static final long serialVersionUID = -8771306833824134974L; protected TextField nameTF, passTF, domainTF; protected Image im; public static final String DEFAULT_IMAGE_NAME = "graphics/background.gif"; /** Rozmieszczamy elementy w oknie. */ public TiledImageComponent() { setLayout(new FlowLayout()); add(new Label("Nazwa:", Label.CENTER)); add(nameTF=new TextField(10)); add(new Label("Hasło:", Label.CENTER)); add(passTF=new TextField(10)); passTF.setEchoChar('*'); add(new Label("Domena:", Label.CENTER)); add(domainTF=new TextField(10)); im = getToolkit().getImage(DEFAULT_IMAGE_NAME); } /** paint() - wielokrotnie wyświetlamy obraz tła. */ @Override public void paintComponent(Graphics g) { if (im == null) return; int iw = im.getWidth(this), ih=im.getHeight(this); if (iw < 0 || ih < 0) // Obraz nie jest gotowy. return; // Kończymy, aby spróbować później. int w = getSize().width, h = getSize().height; for (int i = 0; i<=w; i+=iw) { for (int j = 0; j<=h; j+=ih) { Debug.println("draw", "drawImage(im,"+i+","+j+")"); g.drawImage(im, i, j, this); } } } public static void main(String[] av) { JFrame f = new JFrame("Program TiledImageComponent"); f.getContentPane().add(new TiledImageComponent()); f.setSize(200, 200); f.setVisible(true); f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); } }
W metodzie paint()
musimy sprawdzić nie tylko to, czy obraz jest równy null
, lecz także czy jego wysokość i szerokość nie są ujemne — jak widać, w tym przypadku jesteśmy bardziej ostrożni niż w poprzednim, nieco niefrasobliwym przykładzie. Obraz może mieć wartość null
wyłącznie w sytuacji, jeśli coś poszło w jego konstruktorze nie tak, jak należy, jednak może mieć ujemny rozmiar. Jak to możliwe? Otóż w niektórych mitach związanych ze stworzeniem mówi się, że przed początkiem dziejów czas biegł w przeciwnym kierunku; dlatego zanim tworzenie obrazu zostanie całkowicie zakończone, jego wymiary będą przeciwne (czyli obraz będzie miał wysokość i szerokość równą -1
). Okazuje się bowiem, że metoda getImage()
tak naprawdę nie pobiera obrazu. Owszem, tworzy obiekt Image
, lecz niekoniecznie pobiera wszystkie bity obrazu — metoda ta uruchamia wątek działający w tle, który niezależnie pobiera zawartość, a działanie samej metody się kończy. Rozwiązanie to powstało w czasach, gdy internet był wolniejszy i pobranie obrazu w całości zabierało znacznie więcej czasu. W rzeczywistości istnieją pewne formaty plików graficznych (być może jakieś rodzaje plików TIFF), w których rozmiar pliku można określić dopiero po pobraniu całości tego pliku. A zatem w momencie zakończenia realizacji metody getImage()
obiekt Image
jest utworzony, lecz wielkość samego obrazu wynosi -1
, -1
. Jednak w tym momencie istnieją już dwa działające wątki (patrz Rozdział 22.), a zatem może zajść jedna z dwóch sytuacji. Po pierwsze, wątek odczytujący obraz może pobrać wystarczająco dużo danych, aby móc określić jego wymiary, zanim będziemy ich potrzebować, lub po drugie, będziemy potrzebować tych wymiarów, zanim będzie je można określić. Ten dziwnie wyglądający fragment kodu umieszczony w metodzie paint()
jest zabezpieczeniem przed tą sytuacją.
Jednak co zrobić, jeśli naprawdę będziemy potrzebowali wymiarów obrazu, na przykład aby odpowiednio rozmieścić większy panel? Po pobieżnym zapoznaniu się z dokumentacją klasy Image
można by sądzić, że rozwiązaniem jest użycie metody prepareImage()
, która gwarantuje, że obraz został w całości pobrany. Niestety, w przypadku gdy pobierany plik nie istnieje, użycie tej metody może spowodować powstanie pętli nieskończonej, gdyż jej wywołanie nigdy nie zwróci wartości true
! Aby mieć całkowitą pewność, że obraz został pobrany, należy stworzyć obraz MediaTracker
. Poniżej przedstawiłem przykład jego wykorzystania.
/** * Ten FRAGMENT KODU pokazuje sposób wykorzystania obiektu * klasy MediaTracker w celu zapewnienia, że obraz (Image) * został poprawnie i w całości pobrany, oraz określenia * wysokości i szerokości tego obrazu. Obiekt MediaTracker * może śledzić dowolną ilość obrazów, "0" jest liczbą * używaną, gdy chcemy śledzić konkretny obraz. */ Image im; int imWidth, imHeight; public void setImage(Image i) { im = i; MediaTracker mt = new MediaTracker(this); // Wykorzystując "this", zakładamy, że bieżąca klasa // jest klasą potomną klasy Component. mt.addImage(im, 0); try { mt.waitForID(0); } catch (InterruptedException e) { throw new IllegalArgumentException( "Zgłoszono przerwanie InterruptedException podczas pobierania obrazu!"); } if (mt.isErrorID(0)) { throw new IllegalArgumentException("Nie można pobrać obrazu"); } imWidth = im.getWidth(this); imHeight = im.getHeight(this); }
W każdej chwili można pobrać aktualny status obiektu MediaTracker
. Służy do tego metoda status(int ID, boolean load)
zwracająca liczbę całkowitą utworzoną poprzez bitowe połączenie (przy użyciu operatora or
) wartości przedstawionych w Tabela 12-1. Jeśli flaga logiczna load
przyjmie wartość true
, to obiekt rozpocznie pobieranie wszystkich obrazów, które jeszcze nie zostały pobrane ani aktualnie nie są pobierane. Inna metoda — statusAll()
— zwraca wartość stanowiącą połączenie wszystkich flag określających stany wszystkich aktualnie pobieranych obrazów.
Tabela 12-1. Wartości statusu obiektów MediaTracker
Flaga | Znaczenie |
---|---|
| Przerwano proces pobierania przynajmniej jednego obrazu. |
| Pobieranie wszystkich obrazów zostało poprawnie zakończone. |
| Podczas pobierania przynajmniej jednego obrazu wystąpiły błędy. |
| Proces pobierania obrazów trwa. |
Przedstawiony wcześniej fragment kodu można uprościć poprzez wykorzystanie klasy ImageIcon
z pakietu Swing, która udostępnia omawiane możliwości funkcjonalne. Klasa ImageIcon
dysponuje konstruktorami o kilku różnych postaciach — jedna z nich umożliwia przekazanie nazwy pliku. Klasa ta wewnętrznie stosuje obiekt MediaTracker
; jego status można pobrać za pomocą metody getImageLoadStatus()
klasy ImageIcon
. Metoda ta zwraca takie same wartości co metody statusAll()
oraz statusID()
klasy ImageIcon
.
Należy użyć statycznej klasy ImageIO
należącej do pakietu javax.imageio
, która udostępnia metody read()
oraz write()
.
Pakiet imageio
udostępnia klasę java.awt.image.BufferedImage
, która reprezentuje obraz przechowywany w pamięci. Dziedziczy ona po klasie java.awt.Image
, pozwala więc na stosowanie standardowych metod graficznych, z metodą getGraphics()
na czele. Statyczne metody read()
oraz write()
klasy ImageIO
pozwalają odpowiednio na odczytanie pliku i zapisanie go w obiekcie BufferedImage
oraz na zapisanie takiego obiektu w pliku.
Program przedstawiony w tej recepturze korzysta z tych metod, by stworzyć początkowe wersje obrazków przedstawiających umieszczone w kółkach cyfry 6, 7 i 8, używane w tej książce do oznaczania, która wersja języka Java jest konieczna do skompilowania kodu prezentowanego w recepturze. Program korzysta z grafiki przedstawiającej filiżankę z kawą, narysowanej przez artystę o pseudonimie TikiGiki i opublikowanej na witrynie http://OpenClipArt.org na zasadach licencji Creative Commons.
Metoda getStringBound()
klasy Font
przypomina nieco analogiczną metodę klasy FontMetrics
zastosowaną w „12.4. Wyświetlanie wyśrodkowanego tekstu w komponencie” — podobnie jak ona mierzy faktyczną szerokość wyświetlanego tekstu. Ta metoda oraz proste działania arytmetyczne pozwalają nam wyświetlić dużą, czarną cyfrę (zapisaną w poniższym przykładzie w zmiennej v
) na samym środku obrazka, nieco powyżej filiżanki. Zmodyfikowane obrazki są zapisywane na dysku. Obrazki te wstawiałem następnie do tekstu książki, używając standardowych mechanizmów obsługi obrazów dostępnych w bibliotece AsciiDoc. Przykład 12-6 przedstawia kompletny kod programu służącego do generowania grupy czterech obrazków.
Przykład 12-6. /graphics/ReadWriteImage.java
public class ReadWriteImage { final static int VERSIONS[] = { 5,6,7,8}; final static String DIRECTORY = "images" + File.separatorChar; public static void main(String[] args) throws Exception { String dir = DIRECTORY; for (int v : VERSIONS) { BufferedImage image = ImageIO.read(new File(dir + "coffeecup.png")); Graphics2D g = image.createGraphics(); Font f = new Font("Serif", Font.BOLD, 160); g.setFont(f); g.setColor(Color.black); String bigNumberLabel = Integer.toString(v); Rectangle2D lineMetrics = f.getStringBounds(bigNumberLabel, g.getFontRenderContext() ); int x = (int) ((image.getWidth() - lineMetrics.getWidth() ) / 2); x -= 10; // przesunięcie int y = (int) ((image.getHeight() + lineMetrics.getHeight()) / 2); g.drawString(bigNumberLabel, x, y); ImageIO.write(image, "png", new File(String.format("%sjava%d.png",dir, v))); } System.exit(0); // Kod graficzny uruchamia dodatkowy wątek... } }
Kod programu musi się kończyć wywołaniem System.exit(0)
, gdyż kod graficzny uruchamia dodatkowy wątek działający w tle.
Poradnik dostępny na stronie http://docs.oracle.com/javase/tutorial/2d/images/index.html znacznie dokładniej opisuje możliwości i sposób korzystania z biblioteki ImageIO
.
Chcemy dysponować szybkim i prostym sposobem na „zrobienie hałasu”, czyli odtworzenie istniejącego pliku dźwiękowego.
Może się zdawać, że odtwarzanie dźwięków nie pasuje do tego całego zamieszania związanego z grafiką, przedstawionego we wcześniejszej części rozdziału. Niemniej jednak stanowi pewien wzorzec. Otóż wcześniej zajmowaliśmy się prostszymi formami graficznymi, natomiast teraz przejdziemy do zagadnień związanych z bardziej dynamicznymi multimediami. Pliki dźwiękowe są reprezentowane przez użycie obiektów AudioClip
, które pozwalają także na odtwarzanie zawartości tych plików.
public class AudioPlay { static String defSounds[] = { "/audio/test.wav", "/music/midi/Beet5th.mid", }; public static void main(String[] av) { if (av.length == 0) main(defSounds); else for (String a : av) { System.out.println("Odtwarzam " + a); try { URL snd = AudioPlay.class.getResource(a); if (snd == null) { System.err.println("Nie można pobrać zasobu: " + a); continue; } AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(snd); final Clip clip = AudioSystem.getClip(); clip.open(audioInputStream); clip.start(); } catch (Exception e) { System.err.println(e); } } } }
Jeśli na komputerze jest zainstalowany pakiet JavaFX (patrz ramka „Ewolucja klienckich aplikacji pisanych w Javie: aplety, wojny przeglądarek, Swing oraz JavaFX” w Dodatek B), to plik dźwiękowy będzie także można odtworzyć przy użyciu JavaFX API:
/javafx/AudioPlay.java
/** Proste odtwarzanie pliku dźwiękowego przy użyciu JavaFX API. */
public class AudioPlay {
public static void main(String[] args) {
String clipName = "demo.mp3";
Media clip = new Media(clipName);
MediaPlayer mediaPlayer = new MediaPlayer(clip);
mediaPlayer.play();
}
}
Program JMFPlayer
przedstawiony w „12.11. Prezentacja ruchomego obrazu” nie tylko odtwarza pliki dźwiękowe, lecz także udostępnia panel pozwalający na regulację głośności.
Należy wykorzystać możliwości standardowego rozszerzenia o nazwie Java Media Framework bądź innych narzędzi, takich jak VLCJ lub JavaFX.
Java udostępnia kilka bibliotek do pracy z multimediami. W pierwszej kolejności przyjrzymy się Java Media Framework (JMF).
Biblioteka Java Media Framework została udostępniona już wiele lat temu przez firmę Sun, lecz przez dosyć długi czas nie była właściwie rozwijana. Jest ona rozszerzeniem, zatem trzeba ją osobno pobrać. W przykładach dołączonych do książki w katalogu głównym dostępny jest plik pom.xml, który dodaje odpowiednią zależność pozwalającą pobrać tę bibliotekę przy użyciu programu Maven. Program przedstawiony na Przykład 12-7 wyświetla film lub inny plik multimedialny, którego nazwa została podana w wierszu wywołania programu. JMF jest bardzo elastycznym narzędziem — jeśli wskazany plik multimedialny nie będzie zawierać filmu, lecz dźwięk, to poniższy program wyświetli panel umożliwiający regulację głośności. Rysunek 12-6 przedstawia wygląd programu JMFPlayer w przypadku odtwarzania pliku dźwiękowego oraz klipu wideo.
Rysunek 12-6. Program JMFPlayer podczas działania: odtwarzany plik dźwiękowy (z lewej) oraz klip wideo (z prawej)
Przykład 12-7. JMFPlayer.java
public class JMFPlayer extends JPanel implements ControllerListener { /** Obiekt odtwarzacza */ Player thePlayer = null; /** Nadrzędna ramka, w której działa program. */ JFrame parentFrame = null; /** Panel zawartości */ Container cp; /** Komponent wizualny (jeśli jakiś jest) */ Component visualComponent = null; /** Domyślny komponent kontrolny (jeśli jest) */ Component controlComponent = null; /** Nazwa używanego pliku multimedialnego. */ String mediaName; /** Adres URL używanego pliku dźwiękowego. */ URL theURL; /** Tworzymy obiekt odtwarzacza oraz graficzny interfejs użytkownika. */ public JMFPlayer(JFrame pf, String media) { parentFrame = pf; mediaName = media; // cp = getContentPane(); cp = this; cp.setLayout(new BorderLayout()); try { theURL = new URL(getClass().getResource("."), mediaName); thePlayer = Manager.createPlayer(theURL); thePlayer.addControllerListener(this); } catch (MalformedURLException e) { System.err.println("JMF - błąd tworzenia adresu URL: " + e); } catch (Exception e) { System.err.println("Błąd tworzenia odtwarzacza JMF: " + e); return; } System.out.println("URL pliku = " + theURL); // Uruchamiamy odtwarzacz: spowoduje to przekazanie informacji // do naszego obiektu ControllerListener. thePlayer.start(); // Rozpoczynamy odtwarzanie } /** Metoda wywoływana w celu zatrzymania odtwarzania, na przykład * w wyniku kliknięcia przycisku Stop lub wybrania odpowiedniej * opcji z menu programu */ public void stop() { if (thePlayer == null) return; thePlayer.stop(); // Zatrzymujemy odtwarzanie! thePlayer.deallocate(); // Zwalniamy zasoby systemowe } /** Metoda wywoływana, gdy chcemy zamknąć program (na przykład * po kliknięciu przycisku Wyjście) */ public void destroy() { if (thePlayer == null) return; thePlayer.close(); } /** Metoda wywoływana przez JMF, gdy odtwarzacz chce nas o czymś * poinformować */ public synchronized void controllerUpdate(ControllerEvent event) { // System.out.println("controllerUpdate(" + event + ")"); if (event instanceof RealizeCompleteEvent) { if ((visualComponent = thePlayer.getVisualComponent()) != null) cp.add(BorderLayout.CENTER, visualComponent); if ((controlComponent = thePlayer.getControlPanelComponent()) != null) cp.add(BorderLayout.SOUTH, controlComponent); // Zmieniamy wielkość głównego okna aplikacji if (parentFrame != null) { parentFrame.pack(); parentFrame.setTitle(mediaName); } } } public static void main(String[] argv) { JFrame f = new JFrame("Odtwarzacz JMF - demonstracja"); Container frameCP = f.getContentPane(); final String musicURL = argv.length == 0 ? "file:/home/ian/Music/Classical/Rachmaninoff Prelude C_ min.mp3" : argv[0]; JMFPlayer p = new JMFPlayer(f, musicURL); frameCP.add(BorderLayout.CENTER, p); f.setSize(200, 200); f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); f.setVisible(true); } }
Java Media Framework daje znacznie większe możliwości, niż pokazuje przedstawiony przykład. Niemniej możliwość wyświetlania filmów zapisanych w formacie QuickTime lub MPEG przy użyciu zaledwie kilku wierszy kodu jest jedną z największych zalet JMF. Plik multimedialny pobieramy z pliku o podanym adresie URL, a następnie tworzymy obiekt Player
przeznaczony do zarządzania tym plikiem. Jeśli dany odtwarzacz może mieć panel sterujący, to jest on tworzony i dodawany u dołu okna programu. W skład panelu sterującego mogą wchodzić: elementy umożliwiające kontrolę głośności, przyciski przewijania do przodu i do tyłu, suwak umożliwiający określenie miejsca odtwarzania i tak dalej. Jednak w ogóle nie musimy się tym przejmować — tworząc odtwarzacz dla pliku multimedialnego konkretnego typu, otrzymujemy komponent zawierający wszystkie odpowiednie elementy sterujące. Jeśli dany odtwarzacz reprezentuje medium dysponujące komponentem wizualnym (takim jak film bądź mapa bitowa), umieszczamy ten komponent w centrum okna programu.
Oczywiście możliwości Java Media Framework są znacznie większe. Na przykład istnieje możliwość zsynchronizowania odtwarzania pliku dźwiękowego oraz klipu wideo bądź uzależnienia ich odtwarzania od innych zdarzeń zewnętrznych.
Często stosowane jest także rozwiązanie polegające na wykorzystaniu „otwartego” programu VLC Media Player i podłączeniu go do obiektu Canvas
w celu wyświetlania obrazu wideo. Oczywiście, oprócz samej biblioteki vlcj konieczne jest także samodzielne pobranie i zainstalowanie programu VLC. Czynności te wykraczają poza zakres tematyczny tej książki, a ich realizacja jest w całości uzależniona od używanego systemu operacyjnego.
Prezentowany tu program wymaga JNA — nieobsługiwanego już pakietu com.sun
. Jest on używany do poinformowania VLCJ, gdzie został zainstalowany odtwarzacz VLC — to jedna z pierwszych czynności wykonywanych w konstruktorze klasy MyVideoCanvas
, przedstawionej na Przykład 12-8. Kolejnymi czynnościami są: utworzenie obiektu EmbeddedMediaPlayerComponent
, wywołanie jego metody getMediaPlayer()
i w końcu wywołanie metod prepareMedia()
, parseMedia()
i play()
na rzecz obiektu odtwarzacza. Wszystkie te czynności zostały przedstawione w kontekście kompletnego przykładu zaprezentowanego na Przykład 12-8. Adres URL pliku jest podawany w wierszu wywołania programu i może wskazywać plik lokalny, zdalny strumień wideo bądź też dowolne inne źródło, które jest w stanie obsługiwać odtwarzacz VLC.
Przykład 12-8. /graphics/VlcjVideo.java
public class VlcjVideo extends JFrame { private static final long serialVersionUID = 1L; public static void main(String[] args) { new VlcjVideo(args[0]); } public VlcjVideo(String url) { setTitle("VLCJ - odtwarzacz wideo"); setDefaultCloseOperation(EXIT_ON_CLOSE); setSize(800, 600); JPanel player = new MyVideoPanel(); add(player, BorderLayout.CENTER); pack(); setVisible(true); ((MyVideoPanel)player).play(url); } class MyVideoPanel extends JPanel { private static final long serialVersionUID = 1L; private File vlcWhere = new File("/usr/local/lib"); private EmbeddedMediaPlayer player; public MyVideoPanel() { NativeLibrary.addSearchPath("libvlc", vlcWhere.getAbsolutePath()); EmbeddedMediaPlayerComponent videoCanvas = new EmbeddedMediaPlayerComponent(); setLayout(new BorderLayout()); add(videoCanvas, BorderLayout.CENTER); player = videoCanvas.getMediaPlayer(); } public void play(String media) { player.prepareMedia(media); player.parseMedia(); player.play(); } } }
Biblioteka JavaFX została zintegrowana z większością wersji Java 7 JDK i miejmy nadzieję, że podobnie będzie z wersjami Java 8. Oczywiście można ją także pobrać niezależnie z witryny firmy Oracle. Przykład 12-9 przedstawia kod prostego odtwarzacza multimedialnego korzystającego z tej biblioteki. Program ten tworzy klip (obiekt Media
), używając w tym celu testowego filmu wideo z witryny MediaCollege.com, następnie tworzy odtwarzacz i wywołuje jego metodę play()
; oprócz tego program musi także przygotować okno do oglądania klipu.
Przykład 12-9. /graphics/JfxVideo.java
public class JfxVideo extends Application { public static void main(String[] args) { launch(args); } @Override public void start(Stage primaryStage) throws Exception { primaryStage.setTitle("JavaFX - odtwarzanie wideo"); final List<String> args = getParameters().getRaw(); String url = args.size() > 0 ? args.get(args.size() - 1) : "http://www.mediacollege.com/" + "video-gallery/testclips/20051210-w50s.flv"; Media media = new Media(url); MediaPlayer player = new MediaPlayer(media); player.play(); MediaView view = new MediaView(player); Group root = new Group(); root.getChildren().add(view); Scene scene = SceneBuilder.create(). width(360).height(288). root(root). fill(Color.WHITE). build(); primaryStage.setScene(scene); primaryStage.show(); } }
Chcemy sporządzić wydruk lub stworzyć strumień wyjściowy, którego zawartość będzie przygotowana pod kątem wydruku.
Interfejs programistyczny do obsługi drukowania — Java Print Service API — pozwala na komunikację z usługami drukowania systemu operacyjnego. Poniżej opisałem proces korzystania z tego API:
Pobranie obiektu DocFlavor
dostosowanego do rodzaju danych, jakie należy wydrukować.
Utworzenie obiektu AttributeSet
i podanie atrybutów, takich jak wielkość papieru.
Odszukanie obiektu PrintService
, który będzie w stanie wykonać zadanie, wykorzystując w tym celu dwa utworzone wcześniej obiekty.
Utworzenie obiektu PrintJob
przy użyciu obiektu PrintService
.
Wywołanie metody print()
obiektu PrintJob
.
Wysyłane dane zostaną podzielone na strony przez usługę drukowania, a zatem mogą mieć postać dowolnego strumienia, który może zostać wydrukowany. W przedstawionym przykładzie drukowany jest niewielki plik tekstowy, a program potrafi zadbać o to, by został on prawidłowo wyświetlony w kolejce wydruków i wydrukowany.
Kod źródłowy tego programu przedstawia Przykład 12-10.
Przykład 12-10. /printjdk14printservice/PrintServiceDemo.java
/** * Program przedstawia najnowsze wcielenie mechanizmów drukowania * PrintService z wykorzystaniem graficznego interfejsu użytkownika; * interfejs ten składa się co prawda tylko z jednego przycisku - * Drukuj - a nazwa drukowanego pliku została podana na stałe, * jednak ma to być przecież jak najprostszy przykład... */ public class PrintServiceDemo extends JFrame { private static final long serialVersionUID = 923572304627926023L; private static final String INPUT_FILE_NAME = "/demo.txt"; /** Program główny: utworzenie obiektu i wyświetlenie okna. * @throws IOException */ public static void main(String[] av) throws Exception { SwingUtilities.invokeLater(new Runnable() { public void run() { try { new PrintServiceDemo( "Przykład drukowania").setVisible(true); } catch (Exception e) { e.printStackTrace(); } } }); } /** Konstruktor tworzący graficzny interfejs użytkownika * składający się z jednego przycisku: Drukuj. */ PrintServiceDemo(String title) { super(title); System.out.println("PrintServiceDemo.PrintServiceDemo()"); setDefaultCloseOperation(EXIT_ON_CLOSE); setLayout(new FlowLayout()); JButton b; add(b = new JButton("Drukuj")); b.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { System.out.println( "PrintServiceDemo.PrintServiceDemo...actionPerformed()"); try { print(INPUT_FILE_NAME); } catch (Exception e1) { JOptionPane.showMessageDialog( PrintServiceDemo.this, "Błąd: " + e1, "Błąd", JOptionPane.ERROR_MESSAGE); e1.printStackTrace(); } } }); pack(); UtilGUI.center(this); } /** Drukujemy plik o podanej nazwie. * @throws IOException * @throws PrintException */ public void print(String fileName) throws IOException, PrintException { System.out.println("PrintServiceDemo.print(): " + "Drukowanie pliku: " + fileName); DocFlavor flavor = DocFlavor.INPUT_STREAM.TEXT_PLAIN_UTF_8; PrintRequestAttributeSet aset = new HashPrintRequestAttributeSet(); //aset.add(MediaSize.NA.LETTER); aset.add(MediaSizeName.NA_LETTER); //aset.add(new JobName(INPUT_FILE_NAME, null)); PrintService[] pservices = PrintServiceLookup.lookupPrintServices(flavor, aset); int i; switch(pservices.length) { case 0: System.err.println(0); JOptionPane.showMessageDialog(PrintServiceDemo.this, "Błąd: brak usługi drukowania", "Błąd", JOptionPane.ERROR_MESSAGE); return; case 1: i = 0; // Dostępna jest tylko jedna drukarka, używamy jej. break; default: i = JOptionPane.showOptionDialog(this, "Wybierz drukarkę", "Wybór", JOptionPane.OK_OPTION, JOptionPane.QUESTION_MESSAGE, null, pservices, pservices[0]); break; } DocPrintJob pj = pservices[i].createPrintJob(); InputStream is = getClass().getResourceAsStream(INPUT_FILE_NAME); if (is == null) { throw new NullPointerException( "Pusty strumień wejściowy: nie znaleziono pliku"); } Doc doc = new SimpleDoc(is, flavor, null); pj.print(doc, aset); } }
Poniżej przedstawiłem wyniki sprawdzenia kolejki wydruku przy użyciu polecenia lpstat -t
:
MacKenzie$ lpstat -t scheduler is running system default destination: HP_Deskjet_F4200_series device for HP_Deskjet_F4200_series: usb://HP/Deskjet%20F4200%20series HP_Deskjet_F4200_series accepting requests since Wed Jan 1 15:04:16 2014 printer HP_Deskjet_F4200_series ... enabled since Wed Jan 1 15:04:16 2014 HP_Deskjet_F4200_series-28 ian 1024 Wed Jan 1 15:03:22 2014 MacKenzie$
Nie ma także żadnego wymogu, który nakazywałby aplikacji posiadanie graficznego interfejsu użytkownika (i to niezależnie od tego, czy miałby on być stworzony przy użyciu biblioteki Swing, czy AWT). Przykład 12-11 przedstawia aplikację uruchamianą z poziomu wiersza poleceń, która konwertuje zwyczajny tekst na format PostScript, nie korzystając przy tym z żadnego graficznego interfejsu użytkownika.
Przykład 12-11. /printjdk14printservice/PrintPostScript.java, program drukujący z poziomu wiersza poleceń
/** Program demonstrujący odnajdywanie usługi PrintService * i drukowanie pliku przy jej użyciu. */ public class PrintPostScript { private static final String INPUT_FILE_NAME = "/demo.txt"; public static void main(String[] args) throws IOException, PrintException { new PrintPostScript().print(); } public void print() throws IOException, PrintException { DocFlavor inputFlavor = DocFlavor.INPUT_STREAM.TEXT_PLAIN_UTF_8; // Odnajdujemy obiekt StreamPrintServiceFactory, który będzie // w stanie skonwertować właściwe dane wejściowe na // odpowiedni format wyjściowy. StreamPrintServiceFactory[] psfactories = StreamPrintServiceFactory.lookupStreamPrintServiceFactories( inputFlavor, DocFlavor.BYTE_ARRAY.POSTSCRIPT.getMimeType()); if (psfactories.length == 0) { System.err.println("Nie udało się znaleźć obiektu " + "StreamPrintFactory do wykonania tego zadania!"); } StreamPrintService printService = psfactories[0].getPrintService(new FileOutputStream("demo.ps")); PrintRequestAttributeSet attrs = new HashPrintRequestAttributeSet(); attrs.add(OrientationRequested.LANDSCAPE); attrs.add(MediaSizeName.NA_LETTER); attrs.add(new Copies(1)); attrs.add(new JobName(INPUT_FILE_NAME, null)); InputStream is = getClass().getResourceAsStream(INPUT_FILE_NAME); if (is == null) { throw new NullPointerException( "Pusty strumień wejściowy: nie znaleziono pliku"); } Doc doc = new SimpleDoc(is, inputFlavor, null); DocPrintJob printJob = printService.createPrintJob(); printJob.print(doc, attrs); } }
W „8.12. Program Plotter” przedstawiłem grupę klas Plotter
. Klasa PlotterAWT
zaprezentowana na Przykład 12-12 rozszerza ich możliwości, pozwalając na stworzenie usługi „podglądu rysunku” — zanim rysunek zostanie sporządzony przez (prawdopodobnie wolny w działaniu) ploter, zostanie on wyświetlony w oknie aplikacji graficznej z wykorzystaniem podstawowych metod rysujących klasy Graphics
.
Przykład 12-12. /plotter/PlotterAWT.java
public class PlotterAWT extends Plotter { private JFrame f; private PCanvas p; private Graphics g; private Font font; private FontMetrics fontMetrics; PlotterAWT() { f = new JFrame("Ploter"); Container cp = f.getContentPane(); p = new PCanvas(MAXX, MAXY); cp.add(p, BorderLayout.CENTER); f.pack(); f.setVisible(true); f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); g = p.getOsGraphics(); } public void drawBox(int w, int h) { g.drawRect(curx, cury, w, h); p.repaint(); } public void rmoveTo(int incrx, int incry){ moveTo(curx += incrx, cury += incry); } public void moveTo(int absx, int absy){ if (!penIsUp) g.drawLine(curx, cury, absx, absy); curx = absx; cury = absy; } public void setdir(float deg){} void penUp(){ penIsUp = true; } void penDown(){ penIsUp = false; } void penColor(int c){ switch(c) { case 0: g.setColor(Color.white); break; case 1: g.setColor(Color.black); break; case 2: g.setColor(Color.red); break; case 3: g.setColor(Color.green); break; case 4: g.setColor(Color.blue); break; default: g.setColor(new Color(c)); break; } } void setFont(String fName, int fSize) { font = new Font(fName, Font.BOLD, fSize); fontMetrics = p.getFontMetrics(font); } void drawString(String s) { g.drawString(s, curx, cury); curx += fontMetrics.stringWidth(s); } /** Klasa wewnętrzna zawierająca "pozaekranowy" obiekt * Image, w którym rysujemy. Metoda paint() tego komponentu * kopiuje rysunek z obrazu na ekran. Dzięki temu nie trzeba * sporządzać listy elementów rysunku. */ class PCanvas extends Canvas { private static final long serialVersionUID = 6827371843858633606L; Image offScreenImage; int width; int height; Graphics pg; PCanvas(int w, int h) { width = w; height = h; setBackground(Color.white); setForeground(Color.red); } public Graphics getOsGraphics() { return pg; } /** Ta metoda jest wywoływana przez AWT po utworzeniu obiektu * odpowiednika rodzimego okna, lecz przed pierwszym wywołaniem * metody paint(), zatem doskonale nadaje się do tworzenia * obrazów itp. */ public void addNotify() { super.addNotify(); offScreenImage = createImage(width, height); // assert (offScreenImage != null); pg = offScreenImage.getGraphics(); } public void paint(Graphics pg) { pg.drawImage(offScreenImage, 0, 0, null); } public Dimension getPreferredSize() { return new Dimension(width, height); } } }
Grapher
to prosty program, który odczytuje tabelę punktów i wyświetla je. Dane wejściowe zapisywane są w jednym lub wielu wierszach, przy czym każdy z nich zawiera współrzędne X i Y opisujące położenie jednego punktu. Wyniki wyświetlane są w oknie, lecz można je także wydrukować. Wyniki wykonania programu operującego na poniższych danych testowych zostały przedstawione na Rysunek 12-7. Pierwsza kolumna danych określa wartość współrzędnej X, a druga — współrzędnej Y. Program skaluje dane tak, aby pasowały one do wymiarów okna.
1.5 5 1.7 6 1.8 8 2.2 7
Kod źródłowy tego programu został przedstawiony na Przykład 12-13.
Przykład 12-13. graphics/Grapher.java
public class Grapher extends JPanel { private static final long serialVersionUID = -1813143391310613248L; /** Mnożnik zakresu umożliwiający zostawienie miejsca na obramowanie */ public final static double BORDERFACTOR = 1.1f; /** Lista punktów (obiektów Apoint). */ protected List<Point2D> data; /** Minimalna i maksymalna wartość współrzędnej X */ protected double minx = Integer.MAX_VALUE, maxx = Integer.MIN_VALUE; /** Minimalna i maksymalna wartość współrzędnej Y */ protected double miny = Integer.MAX_VALUE, maxy = Integer.MIN_VALUE; /** Zakres wartości X i Y */ protected double xrange, yrange; public Grapher() { data = new ArrayList<Point2D>(); figure(); } /** Przygotowanie listy danych na podstawie listy łańcuchów znaków, * przy czym współrzędna X jest inkrementowana automatycznie, * a wartości współrzędnych Y punktów są wczytywane z przekazanych * łańcuchów znaków. */ public void setListDataFromYStrings(List<String> newData) { data.clear(); for (int i=0; i < newData.size(); i++) { Point2D p = new Point2D.Double(); p.setLocation(i, java.lang.Double.parseDouble(newData.get(i))); data.add(p); } figure(); } /** Ustawiamy używaną listę danych na listę przekazaną w wywołaniu, * na przykład zwracaną przez wywołanie GraphReader.read(). */ public void setListData(List<Point2D> newData) { data = newData; figure(); } /** Metoda oblicza nowe dane po zmianie listy. */ private void figure() { // Odnajdujemy minimum i maksimum. for (int i=0 ; i < data.size(); i++) { Point2D d = (Point2D)data.get(i); if (d.getX() < minx) minx = d.getX(); if (d.getX() > maxx) maxx = d.getX(); if (d.getY() < miny) miny = d.getY(); if (d.getY() > maxy) maxy = d.getY(); } // Obliczamy zakresy. xrange = (maxx - minx) * BORDERFACTOR; yrange = (maxy - miny) * BORDERFACTOR; Debug.println("Zakres", "minx,x,r = " + minx +' '+ maxx +' '+ xrange); Debug.println("Zakres", "miny,y,r = " + miny +' '+ maxy +' '+ yrange); } /** Metoda wywoływana, gdy należy wyświetlić zawartość okna. * Oblicza zakres X i Y, po czym odpowiednio skaluje współrzędne. */ @Override public void paintComponent(Graphics g) { super.paintComponent(g); Dimension s = getSize(); if (data.size() < 2) { g.drawString("Niewystarczające dane: " + data.size(), 10, 40); return; } // Obliczamy współczynniki skali double xfact = s.width / xrange; double yfact = s.height / yrange; // Skalujemy i wyświetlamy dane for (int i=0 ; i < data.size(); i++) { Point2D d = (Point2D)data.get(i); double x = (d.getX() - minx) * xfact; double y = (d.getY() - miny) * yfact; Debug.println("Punkt", "Dane " + i + " " + d + "; " + "x = " + x + "; y = " + y); // Rysujemy wycentrowany prostokąt o boku 5 pikseli — // stąd od współrzędnych X i Y wierzchołka prostokąta // odejmowana jest liczba 2. W AWT współrzędne Y są // liczone od dołu ku górze, a zatem musimy odjąć Y. g.drawRect(((int)x)-2, s.height-2-(int)y, 5, 5); } } @Override public Dimension getPreferredSize() { return new Dimension(150, 150); } public static void main(String[] args) throws IOException { final JFrame f = new JFrame("Grapher"); f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); Grapher grapher = new Grapher(); f.setContentPane(grapher); f.setLocation(100, 100); f.pack(); List<Point2D> data = null; if (args.length == 0) data = GraphReader.read("Grapher.txt"); else { String fileName = args[0]; if ("-".equals(fileName)) { data = GraphReader.read(new InputStreamReader(System.in), "System.in"); } else { data = GraphReader.read(fileName); } } grapher.setListData(data); f.setVisible(true); } }
W programie Grapher najbardziej skomplikowanym zadaniem jest określenie zakresów współrzędnych oraz ich skalowania. Oczywiście program można by rozbudować, dodając do niego bardziej wymyślne możliwości, takie jak rysowanie wykresów słupkowych i tak dalej.