Jan Bielecki Java 3 Programowanie współbieżne obiektowe i zdarzeniowe do Windows 95/98/NT Profesorowi Wojciechowi Cellaremu z wyrazami przyjaźni Spis treści Pierwsze kroki Projektowanie apletów Obsługiwanie zdarzeń Obsługiwanie myszki Obsługiwanie klawiatury Odtwarzanie pulpitu Dobieranie kolorów Wykreślanie napisów Wykreślanie obiektów Wykreślanie obrazów Odtwarzanie dźwięków Projektowanie oblicza Programowanie wątków Programowanie animacji Programowanie gier Grafika 2-wymiarowa Układ współrzędnych Definiowanie kształtu Wykreślanie linii Wypełnianie obszarów Przekształcanie obiektów Nakładanie kolorów Rozpoznawanie trafień Definiowanie obszarów Obcinanie wykreśleń Przekształcanie napisów Wykreślanie glifów Wykreślanie splajnów Dobieranie czcionek Lekkie komponenty Studium programowe Typy predefiniowane Deklaracje i instrukcje Wyrażenia i operatory Java po C++ Programowanie obiektowe Programowanie zdarzeniowe Projektowanie kolekcji Aplety i aplikacje Opisy apletów Klasy wewnętrzne Wykreślanie pulpitu Sytuacje wyjątkowe Obsługiwanie urządzeń Współrzędne pulpitu Wykreślanie figur Lokalizowanie zasobów Ładowanie obrazów Buforowanie wykreśleń Pakietowanie klas Udostępnianie zdarzeń Projektowanie zdarzeń Układanie komponentów Projektowanie oblicza Obsługiwanie okien Projektowanie menu Projektowanie dialogów Projektowanie przycisków Programowanie współbieżne Przetwarzanie plików Komponenty JavaBeans Programowanie wizualne Wykorzystywanie kostek Przechowywanie obiektów Czcionki i znaki narodowe Określanie daty i godziny Przezroczyste obrazy GIF Animowane obrazy GIF Rozbijanie plików GIF Przekształcanie obrazów Wyświetlanie komponentów Dekodowanie obrazów Używanie przyborników Wymiarowanie apletów Stosowanie modeli kolorów Interesujące przypadki Narzędzia pakietu JDK Studium programowe Dodatki A Priorytety operatorów B Definicje stałych C Klasa Debug D Symantec Visual Cafe E Borland JBuilder F Tek-Tools Kawa Od Autora Niepowstrzymany rozwój Javy powoduje, że wiele książek na jej temat było nieaktualnych już w chwili ich opublikowania. W minionym roku dezaktualizacja dosięgła także i moich tekstów: Java po C++ / Java od podstaw. Obroniła się tylko Java 2, ponieważ w całości została oparta na delegacyjnym modelu obsługiwania zdarzeń. Ale i w niej znajdują się programy, które na skutek wyeliminowania takich metod jak stop, suspend, resume oraz kilkunastu innych, należałoby obecnie nieco zmodyfikować. Niniejszą książkę napisałem w celu pokazania nowości, w tym nigdzie jeszcze nie opisanej grafiki 2D, dążąc do takiego wyłożenia współbieżności, aby mogła znaleźć zastosowanie w programowaniu wielowątkowych aplikacji animacyjnych. Pomogły mi w tym doświadczenia nabyte podczas nauczania Javy w Polsko-Japońskiej Wyższej Szkole Technik Komputerowych, w Instytucie Informatyki Politechniki Warszawskiej, w CITCOM oraz na szkoleniach organizowanych dla elit programistycznych wielkich firm. Jeśli podam, że w ubiegłym roku uczestniczyło w moich wykładach, projektach i laboratoriach ponad 400 informatyków, to zapewne lepiej niż słowa, ukaże to obecny stan zainteresowania Javą. Aby uczynić książkę łatwiejszą od poprzednich, wyposażyłem ją w rozdział Pierwsze kroki. Metodą praktycznych przykładów pokazałem, jak niemal bez wiedzy podstawowej, można szybko przystąpić do układania całkiem niebanalnych programów, wykorzystujących techniki programowania współbieżnego, obiektowego, zdarzeniowego, graficznego i animacyjnego. Nie mam wątpliwości, że każdy kto programował w języku obiektowym (np. w ANSI C++ albo w Delphi) oraz każdy kto poznał Javę w zakresie elementarnym, już po przeczytaniu kilku pierwszych rozdziałów nabierze umiejętności programowania profesjonalnego. Pozostałe rozdziały może już czytać w dowolnej kolejności, sięgając do nich po rozwiązania konkretnych problemów. Życząc Czytelnikom pożytecznej lektury, z przyjemnością informuję, że uwzględniając apele o udostępnienie programów źródłowych, zamieszczam w Internecie, na stronie www.ii.pw.edu.pl/~janb/java3/index.html wszystkie zawarte w książce programy oraz wymagane przez nie pliki dźwiękowe i graficzne. prof. Jan Bielecki Jan Bielecki Pierwsze kroki Programy dzielą się na aplikacje i aplety. Aplikacja jest programem wolnostojącym, a aplet jest programem wykonywanym pod nadzorem przeglądarki. W każdym z tych przypadków należy użyć Maszyny Wirtualnej, interpretującej B-kod programu powstałego po skompilowaniu programu źródłowego. Uwaga: Maszyna Wirtualna może być implementowana w sprzęcie albo może być emulowana za pomocą rodzimego programu platformy. Projektowanie apletów Aplet jest programem zapisanym za pomocą publicznej klasy apletowej (np. Master). Klasa ta dziedziczy pola i metody z klasy Applet oraz uzupełnia je swoimi. Jeśli w pewnym pliku występuje klasa publiczna Name, to nazwą pliku musi być Name.java. Stąd wynika, że w danym pliku może być zawarta co najwyżej jedna klasa publiczna. Jest nią zazwyczaj klasa apletowa. W ciele klasy występują odwołania do klas bibliotecznych. Nazwa klasy bibliotecznej (np. Applet) jest poprzedzona nazwą pakietu, do którego należy ta klasa (np. java.applet). Następujący program, pokazany na ekranie Pierwszy aplet, wykreśla koło o promieniu 30 pikseli, wpisane w domyślny prostokąt, którego lewy-górny wierzchołek znajduje się w punkcie (50, 50). Uwaga: Współrzędne punktów są liczone względem lewego-górnego narożnika apletu. Ekran Pierwszy aplet ### fapplet.gif public class Master extends java.applet.Applet { // klasa Master private int x = 50, // pola x, y, r, d y = 50, r = 30, d; public void init() // metoda init { d = 2 * r; } public void paint(java.awt.Graphics gDC) // metoda paint { // wybranie koloru gDC.setColor(java.awt.Color.red); // wykreślenie koła gDC.fillOval(x, y, d, d); // wybranie koloru gDC.setColor(java.awt.Color.black); // wykreślenie okręgu gDC.drawOval(x, y, d-1, d-1); } } Polecenia importu Nazwy klas występujących w programie można uprościć do identyfikatorów (np. Applet, Graphics albo Color). Aby to umożliwić, należy użyć poleceń importu. import java.applet.Applet; import java.awt.*; Dzięki poleceniu import java.applet.Applet; nazwę klasy java.applet.Applet można uprościć do Applet, a dzięki poleceniu import java.awt.*; odwołania do klas pakietu java.awt, których nazwy zaczynają się od java.awt można uprościć do identyfikatora kończącego taką nazwę (np. java.awt.Graphics można uprościć do Graphics). Wykorzystano to w następującym aplecie, uproszczonym dzięki użyciu poleceń importu. import java.applet.Applet; import java.awt.*; public class Master extends Applet { private int x = 50, y = 50, r = 30, d; public void init() { d = 2 * r; } public void paint(Graphics gDC) { gDC.setColor(Color.red); gDC.fillOval(x, y, d, d); gDC.setColor(Color.black); gDC.drawOval(x, y, d-1, d-1); } } Opisy apletów Pliki z rozszerzeniem .class, powstałe po po skompilowaniu klasy apletowej, są wykonywane przez Maszynę Wirtualną wbudowaną w przeglądarkę (np. Netscape 4.5) Aby Maszyna Wirtualna mogła wykonać B-kod zawarty w pliku *.class, należy przeglądarce dostarczyć opis apletu. Opis apletu jest umieszczany w pliku z rozszerzeniem .html (np. Index.html). W opisie podaje się m.in. nazwę i rozmiary apletu: jego szerokość (width) i wysokość (height), wyrażone w pikselach. Plik Index.html Po odnalezieniu B-kodu apletu przeglądarka tworzy obiekt apletowy, po czym na rzecz jego podobiektu klasy Applet, wywołuje metody init, start, paint, stop i destroy. Jeśli w klasie apletowej dostarczy się metodę o takiej samej sygnaturze, jaką ma metoda klasy Applet, to zostanie wywołana metoda klasy apletowej, a nie metoda klasy Applet. Ta ważna właściwość języka umożliwia przedefiniowywanie w klasie pochodnej metod jej klasy bazowej. Uwaga: Dwie metody mają taką samą sygnaturę, jeśli mają takie same nazwy, a ich nagłówki, pozbawione nazw parametrów i specyfikatorów (np. public, static, synchronized) są identyczne. Metoda start jest wywoływana przed każdym pojawieniem się apletu, a metoda stop przed każdym jego zniknięciem. Przed pierwszym pojawieniem się apletu jest wywoływana metoda init, a po ostatnim metoda destroy. Metoda paint jest wywoływana wówczas, gdy należy odtworzyć pulpit apletu oraz po każdym wywołaniu metody start. Tuż przed tym, zniszczony fragment pulpitu jest czyszczony kolorem tła apletu. Uwaga: Nawet gdy zniszczeniu ulega tylko fragment pulpitu, metodę paint należy zdefiniować w taki sposób, aby odtwarzała cały pulpit. Następujący program, pokazany na ekranie Maskotka Javy, zawiera metodę init, przedefiniowującą metodę init klasy Applet. A zatem właśnie ona zostanie wywołana przez przeglądarkę. W taki sam sposób będzie potraktowane wywołanie przedefiniowanej metody paint. Ekran Maskotka Javy ### mascot.gif Uwaga: Ponieważ nie przedefiniowano metod start, stop i destroy, więc przeglądarka będzie wywoływać metody klasy Applet. Ponieważ ciała tych metod są puste, więc ich wykonanie nie będzie miało żadnego skutku. =============================================== import java.applet.Applet; import java.awt.*; public class Master extends Applet { private Image img; public void init() { img = getImage(getDocumentBase(), "Duke.gif"); } public void paint(Graphics gDC) { gDC.drawImage(img, 50, 50, this); } } dla dociekliwych Klasy programu można umieszczać w pakietach. Definicję klasy pakietowej należy poprzedzić deklaracją pakietu, a we frazie code opisu apletu należy uwzględnić nazwę pakietu. Uwaga: Jeśli aplet nie jest wykonywany w środowisku uruchomieniowym, takim jak na przykład Kawa, to za pomocą parametru środowiska classpath albo parametru codebase, należy określić miejsce, skąd ma być ładowana jego klasa pakietowa. ===================================== package jbPackage; // pakiet jbPackage import java.applet.Applet; import java.awt.*; public class Master extends Applet { // klasa pakietowa public void paint(Graphics gDC) { // wykreślenie napisu gDC.rawString("Hello", 40, 40); } } Pulpit apletu Prostokątny obszar zdefiniowany w opisie apletu jest jego pulpitem. Lewy-górny narożnik pulpitu ma współrzędne (0,0). Współrzędne x pulpitu rosną w prawo, a współrzędne y rosną do dołu. Aktualne rozmiary pulpitu otrzymuje się za pomocą metody getSize. Dimension d = getSize(); int w = d.width, // szerokość h = d.height; // wysokość Operacje na pulpicie wykonuje się za pomocą wykreślacza, wydając mu takie polecenia jak setColor (wybierz kolor), fillOval (wypełnij owal), drawOval (wykreśl owal), itp. Wykreślacz jest udostępniany przez parametr metody paint i update albo może być utworzony za pomocą metody getGraphics. Każde jej wywołanie dostarcza odnośnik do odrębnego wykreślacza. Uwaga: Po zakończeniu korzystania z wykreślacza przydzielonego za pomocą metody getGraphics, należy go zwolnić za pomocą metody dispose. W przeciwnym razie, zwłaszcza gdy jest on przydzielany w pętli, może dojść do nadmiernej konsumpcji zasobów systemowych. Graphics gDC = getGraphics(); // przydzielenie // wybierz kolor gDC.setColor(Color.red); // wypełnij owal gDC.fillOval(x, y, d, d); // wybierz kolor gDC.setColor(Color.black); // wykreśl owal gDC.drawOval(x, y, d-1, d-1); gDC.dispose(); // zwolnienie Obsługiwanie zdarzeń Podczas wykonywania apletu mogą zachodzić zdarzenia. Obsługę zdarzenia deleguje się do obiektu nasłuchującego. Zrezygnowanie z delegowania powoduje, że zdarzenie jest ignorowane. Delegowanie obsługi Do delegowania obsługi zdarzeń służą metody addKindListener. Do obsługi przycisku służy metoda addActionListener, a do obsługi myszki i klawiatury metody addMouseListener, addMouseMotionListener i addKeyListener. Argumentem metody addKindListener jest odnośnik do obiektu klasy implementującej interfejs KindListener. W takiej klasie, zgodnie z tabelą Metody obsługi, należy dostarczyć komplet metod zdefiniowanych w interfejsie KindListener. Uwaga: Interfejsem jest klasa abstrakcyjna, która zawiera co najwyżej definicje stałych i deklaracje metod. Implementowanie interfejsu nie jest niczym innym jak wielo-dziedziczeniem takiej właśnie klasy. Jest to jedyna dozwolona forma wielodziedziczenia. Tabela Metody obsługi ### Delegacja: addActionListener Interfejs: ActionListener Metoda Parametr actionPerformed ActionEvent Delegacja: addMouseListener Interfejs: MouseListener Metoda Parametr mousePressed MouseEvent mouseReleased MouseEvent mouseClicked MouseEvent mouseEntered MouseEvent mouseExited MouseEvent Delegacja: addMouseMotionListener Interfejs: MouseMotionListener Metoda Parametr mouseMoved MouseEvent mouseDragged MouseEvent Delegacja: addKeyListener Interfejs: KeyListener Metoda Parametr keyPressed KeyEvent keyReleased KeyEvent keyTyped KeyEvent ### Uwaga: Zamiast implementować interfejs KindListener, można klasę obiektu nasłuchującego zdefiniować jako pochodną od pomocniczej klasy KindAdapter. W takiej klasie (nie ma jej dla interfejsu ActionListener) zdefiniowano wszystkie metody zadeklarowane w interfejsie KindListener. Następujący program, pokazany na ekranie Obszar apletu, w zależności od tego czy kursor znajduje się nad pulpitem apletu czy poza nim, wykreśla czerwony okrąg na żółtym tle albo żółty okrąg na czerwonym tle. Ekran Obszar apletu ### inside.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet { public void init() { // ustawienie koloru tła apletu setBackground(Color.white); // delegowanie obiektu nasłuchującego addMouseListener(new Watcher()); } class Watcher extends MouseAdapter { public void mouseEntered(MouseEvent evt) { setBackground(Color.yellow); } public void mouseExited(MouseEvent evt) { setBackground(Color.red); } } public void paint(Graphics gDC) { Color color = getBackground(); if(color.equals(Color.red)) gDC.setColor(Color.yellow); else gDC.setColor(Color.red); Dimension s = getSize(); int w = s.width, h = s.height; gDC.drawOval(0, 0, w-1, h-1); } } Obiekty zdarzeniowe Po zajściu zdarzenia jest tworzony obiekt zdarzeniowy. W obiekcie zdarzeniowym jest zarejestrowany opis zdarzenia. Elementy opisu zależą od rodzaju zdarzenia. Zdarzenie action Zdarzenie zachodzi m.in. po kliknięciu przycisku (Button) oraz po naciśnięciu klawisza Enter podczas wprowadzania tekstu do klatki (TextField). Informacji o zdarzeniu dostarczają metody klasy ActionEvent. Object getSource() Dostarcza odnośnik do obiektu, w którym zaszło zdarzenie. public void actionPerformed(ActionEvent evt) { Object obj = evt.getSource(); if(obj == startButton) // ... else if(obj == stopButton) // ... } String getActionCommand() Dostarcza napis na przycisku albo zawartość klatki. public void actionPerformed(ActionEvent evt) { String str = evt.getActionCommand(); // ... } Zdarzenie mouse, mouseMotion i key Zdarzenia powstają po wykonaniu operacji za pomocą myszki albo klawiatury. Informacji o zdarzeniu dostarczają metody klasy InputEvent oraz dodatkowo: dla zdarzeń związanych z myszką - metody klasy MouseEvent, a dla zdarzeń związanych z klawiaturą - metody klasy KeyEvent. long getWhen() Dostarcza czas (w ms) jaki dzieli chwilę zajścia zdarzenia i początek epoki (1 stycznia 1970); boolean isMetaDown() // prawy przycisk myszki boolean isCtrlDown() // klawisz Ctrl boolean isShiftDown() // klawisz Shift boolean isAltDown() // klawisz Alt Dostarcza informacji o naciśnięciu klawisza (lub ich kombinacji). public void mousePressed(MouseEvent evt) { if(evt.isMetaDown() && evt.isShiftDown()) // ... } Tylko zdarzenia mouse i mouseMotion Dodatkowych informacji dostarczają metody klasy MouseEvent. int getX() Dostarcza współrzędną x. int getY() Dostarcza współrzędną y. Point getPoint() Dostarcza punkt (x,y). int getClickCount() Dostarcza licznik wielo-kliknięcia (np. dla dwu-kliknięcia licznik 2). public void mouseReleased(MouseEvent evt) { if(evt.isCtrlDown() && evt.getClickCount() == 2) // ... } Tylko zdarzenie key Dodatkowych informacji dostarczają metody klasy KeyEvent. Uwaga: Zdarzenie key może być obsłużone tylko przez komponent (aplet, przycisk, klatkę), na który jest nastawiony celownik. Do nastawienia celownika na komponent, w szczególności na aplet, służy metoda requestFocus. int getKeyCode() Dostarcza wirtualny kod klawisza. W metodzie keyTyped dostarcza VK_UNDEFINED. char getKeyChar() Dostarcza znak Unikodu. Jeśli w Unikodzie nie ma takiego znaku, to dostarcza VK_UNDEFINED. Uwaga: Nazwy symboliczne klawiszy (np. VK_UNDEFINED, VK_SPACE) zdefiniowano w klasie KeyEvent. public void keyReleased(KeyEvent evt) { int keyCode = evt.getKeyCode(); if(keyCode == KeyEvent.VK_F1) // ... } Obsługiwanie myszki Następujący program, pokazany na ekranie Wykreślanie kół, ilustruje obsługę zdarzeń przez obiekty klasy wewnętrznej, zewnętrznej, anonimowej i apletowej. Ekran Wykreślanie kół ### triplet.gif Obsługa w klasie wewnętrznej =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet { private int r = 30, d = 2*r; private Graphics gDC; // odnośnik do wykreślacza public void init() { // utworzenie wykreślacza gDC = getGraphics(); // utworzenie obiektu nasłuchującego Watcher mouseWatcher = new Watcher(); // oddelegowanie obiektu nasłuchującego // do obsłużenia zdarzenia mouse addMouseListener(mouseWatcher); } // klasa obiektów nasłuchujących class Watcher extends MouseAdapter { // metoda do obsłużenia zdarzenia mouse public void mouseReleased(MouseEvent evt) { // pobranie współrzędnych kursora myszki int x = evt.getX(), y = evt.getY(); // wykreślenie koła i okręgu gDC.setColor(Color.red); gDC.fillOval(x-r, y-r, d, d); gDC.setColor(Color.black); gDC.drawOval(x-r, y-r, d-1, d-1); } } } Obsługa w klasie zewnętrznej =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet { private int r = 30; public void init() { // utworzenie obiektu nasłuchującego // dostarczenie mu wykreślacza i promienia Watcher mouseWatcher = new Watcher(getGraphics(), r); // oddelegowanie obiektu nasłuchującego // do obsłużenia zdarzenia mouse addMouseListener(mouseWatcher); } } class Watcher extends MouseAdapter { private Graphics gDC; private int r, d; public Watcher(Graphics gDC, int r) { this.gDC = gDC; this.r = r; d = 2 * r; } public void mouseReleased(MouseEvent evt) { int x = evt.getX(), y = evt.getY(); gDC.setColor(Color.red); gDC.fillOval(x-r, y-r, d, d); gDC.setColor(Color.black); gDC.drawOval(x-r, y-r, d-1, d-1); } } Obsługa w klasie anonimowej Klasą anonimową jest klasa zdefiniowana bez słowa kluczowego class i nazwy. Ciało klasy anonimowej może wystąpić tylko po fabrykatorze użytym do tworzenia obiektu new MouseAdapter() // fabrykator { // ... // ciało } Jeśli fabrykator zawiera nazwę klasy, to klasa anonimowa jest klasą pochodną klasy wymienionej w fabrykatorze. new MouseAdapter() { // ... public void mousePressed(MouseEvent evt) { // ... } public void mouseReleased(MouseEvent evt) { // ... } } Jeśli fabrykator zawiera nazwę interfejsu, to klasa anonimowa jest klasą pochodną klasy Object i implementuje ten interfejs. new ActionListener() { // ... public void actionPerformed(ActionEvent evt) { // ... } }; =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet { private int r = 30, d = 2*r; private Graphics gDC; public void init() { gDC = getGraphics(); addMouseListener( new MouseAdapter() { public void mouseReleased(MouseEvent evt) { int x = evt.getX(), y = evt.getY(); gDC.setColor(Color.red); gDC.fillOval(x-r, y-r, d, d); gDC.setColor(Color.black); gDC.drawOval(x-r, y-r, d-1, d-1); } } ); } } Obsługa w klasie apletowej Ponieważ nie istnieje wielodziedziczenie, więc klasa Master, która dziedziczy klasę Applet, nie może dodatkowo dziedziczyć klasy MouseAdapter. Dlatego, mimo iż używa tylko metody mouseReleased, musi dostarczyć także pozostałe metody implementowanego przez nią interfejsu MouseListener. =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet implements MouseListener { private int r = 30, d = 2*r; private Graphics gDC; public void init() { gDC = getGraphics(); addMouseListener(this); } public void mouseReleased(MouseEvent evt) { int x = evt.getX(), y = evt.getY(); gDC.setColor(Color.red); gDC.fillOval(x-r, y-r, d, d); gDC.setColor(Color.black); gDC.drawOval(x-r, y-r, d-1, d-1); } // wymagane, chociaż nie użyte public void mousePressed(MouseEvent evt) { } public void mouseClicked(MouseEvent evt) { } public void mouseEntered(MouseEvent evt) { } public void mouseExited(MouseEvent evt) { } } Obsługiwanie klawiatury Następujący program, pokazany na ekranie Wprowadzanie znaków, wykreśla znaki wprowadzone z klawiatury. Znaki są odbierane przez aplet. Jeśli wprowadzony znak nie należy do Unikodu, ale nie jest znakiem Shift, to rozlega się sygnał dźwiękowy. Uwaga: Wywołanie metody requestFocus nastawia celownik na aplet. Bez tego celownik jest ustawiony na okno przeglądarki i aplet nie reaguje na operacje klawiaturowe. Ekran Wprowadzanie znaków ### enterkey.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet { private Font font = new Font("Serif", Font.BOLD, 80); private char key = '!'; private String press = "..."; public void init() { addKeyListener( new KeyAdapter() { public void keyReleased(KeyEvent evt) { char keyChar = evt.getKeyChar(); int keyCode = evt.getKeyCode(); if(keyCode == KeyEvent.VK_SHIFT) return; if(keyChar == KeyEvent.CHAR_UNDEFINED) { // np. F1, Home, End, PgUp // ale nie: Tab, Enter, Del // bo należą do Unikodu Toolkit.getDefaultToolkit().beep(); key = '?'; } else { if(keyCode >= KeyEvent.VK_A && keyCode <= KeyEvent.VK_Z) { key = keyChar; } else key = '?'; } repaint(); } } ); } public void start() { // nastawienie celownika na aplet requestFocus(); } public void paint(Graphics gDC) { if(press.equals("...")) press = "Press a key!"; else gDC.setFont(font); gDC.drawString(press + key, 50, 100); press = ""; } } Odtwarzanie pulpitu Jednym z najważniejszych wymagań stawianych klasie apletowej jest odtwarzanie pulpitu apletu. Konieczność odtworzenia pulpitu zachodzi wówczas, gdy zasłonięty na chwilę pulpit zostanie odsłonięty. Bezpośrednio po tym, przeglądarka wyczyści pulpit kolorem tła apletu i na rzecz podobiektu klasy Applet wywoła metodę paint. Jeśli metoda paint jest napisana właściwie, to zmniejszenie okna programu do ikony (ikonizacja), a następnie przywrócenie okna (dezikonizacja), nie będzie miało wpływu na oblicze programu. Uwaga: Niejawne wywołanie metody paint ma miejsce także na skutek wywołania metody repaint. Po zakończeniu wykonywania funkcji, z której wywołano repaint jest wywoływana metoda update. Domyślna metoda update czyści pulpit apletu i wywołuje metodę paint. Jeśli dostarczy się własną metodę update, z której nie wywoła się metody domyślnej, to należy pamiętać o wyczyszczeniu pulpitu apletu. Następujący program, pokazany na ekranie Odtwarzanie pulpitu, ilustruje, jak wywołać domyślną metodę update. Jak się można przekonać, sygnał dźwiękowy rozlega się tylko po zwolnieniu klawisza klawiatury. Ekran Odtwarzanie pulpitu ### restore.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet { public void init() { addKeyListener( new KeyAdapter() { public void keyPressed(KeyEvent evt) { getGraphics(). drawString("KeyPressed", 20, 20); } public void keyReleased(KeyEvent evt) { repaint(); } } ); } public void start() { requestFocus(); } public void update(Graphics gDC) { Toolkit.getDefaultToolkit().beep(); super.update(gDC); // domyślna metoda update } public void paint(Graphics gDC) { gDC.drawString("Hello World", 100, 50); } } Prosta baza danych Następujący program umożliwia wykreślenie i odtworzenie nie więcej niż Limit kół o promieniu 30 pikseli. W celu umożliwienia odtwarzania pulpitu, zdefiniowano własną bazę danych, na którą składają się: licznik kół i tablice współrzędnych ich środków. =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet { final int Limit = 100; private int r = 30, d = 2*r; private Graphics gDC; // prosta baza danych private int[] xCen = new int [Limit], yCen = new int [Limit]; private int n = 0; // liczba kół public void init() { gDC = getGraphics(); addMouseListener( new MouseAdapter() { public void mouseReleased(MouseEvent evt) { if(n == Limit) { Toolkit.getDefaultToolkit().beep(); return; } int x = evt.getX(), y = evt.getY(); draw(gDC, x, y); xCen[n] = x; yCen[n++] = y; } } ); } private void draw(Graphics gDC, int x, int y) { gDC.setColor(Color.red); gDC.fillOval(x-r, y-r, d, d); gDC.setColor(Color.black); gDC.drawOval(x-r, y-r, d-1, d-1); } public void paint(Graphics gDC) { // odtworzenie pulpitu z bazy for(int i = 0; i < n ; i++) draw(gDC, xCen[i], yCen[i]); } } Ulepszona baza danych Przytoczone uprzednio rozwiązanie ogranicza liczbę wykreślanych kół. Można je ulepszyć, stosując dynamiczne zarządzanie pamięcią. Dzięki niemu liczba wykreślanych kół nie jest ograniczona. Uwaga: Po wypełnieniu tablicy o początkowej rezerwacji (np. 2), tworzy się tablicę o podwojonej pojemności (4, 8, 16, itd) i kopiuje zawartość starej do nowej. Usunięcie starej tablicy jest w ramach odzyskiwania nieużytków, automatycznie wykonywane przez System. =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet { private int r = 30, d = 2*r; private Graphics gDC; // baza danych private int s = 2, // rozmiar tablicy n = 0; // liczba kół private Point[] pCen = new Point [s]; public void init() { gDC = getGraphics(); addMouseListener( new MouseAdapter() { public void mouseReleased(MouseEvent evt) { // jeśli zabraknie miejsca if(n == s) { s *= 2; Point[] p2 = new Point [s]; // szybkie kopiowanie System.arraycopy(pCen, 0, p2, 0, n); pCen = p2; } int x = evt.getX(), y = evt.getY(); draw(gDC, pCen[n++] = new Point(x, y)); } } ); } private void draw(Graphics gDC, Point p) { int x = p.x, y = p.y; gDC.setColor(Color.red); gDC.fillOval(x-r, y-r, d, d); gDC.setColor(Color.black); gDC.drawOval(x-r, y-r, d-1, d-1); } public void paint(Graphics gDC) { for(int i = 0; i < n ; i++) draw(gDC, pCen[i]); } } Użycie klasy Vector Jeszcze lepszym rozwiązaniem jest użycie predefiniowanej klasy Vector, wchodzącej w skład pakietu java.util. =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; import java.util.Vector; public class Master extends Applet { private Graphics gDC; // baza danych private Vector dataBase = new Vector(); public void init() { gDC = getGraphics(); addMouseListener( new MouseAdapter() { public void mouseReleased(MouseEvent evt) { int x = evt.getX(), y = evt.getY(); Circle circle = new Circle(x, y); circle.draw(gDC); dataBase.addElement(circle); } } ); } public void paint(Graphics gDC) { int n = dataBase.size(); for(int i = 0; i < n ; i++) { Object object = dataBase.elementAt(i); Circle circle = (Circle)object; circle.draw(gDC); } } } class Circle { private int r = 30, d = 2*r, x, y; public Circle(int x, int y) { this.x = x; this.y = y; } public void draw(Graphics gDC) { gDC.setColor(Color.red); gDC.fillOval(x-r, y-r, d, d); gDC.setColor(Color.black); gDC.drawOval(x-r, y-r, d-1, d-1); } } Dobieranie kolorów Kolory są reprezentowane przez obiekty klasy Color. Dowolny 24-bitowy kolor opisują jego składowe: R (red), G (green), B (blue). Każda ze składowych jest liczbą z domkniętego przedziału 0..255. W szczególności obiekty new Color(255,0,0) oraz new Color(255<<16) reprezentują kolor czerwony, a obiekty new Color(255,255,0) oraz newColor((255<<16)+(255<<8)) reprezentują kolor żółty. Następujący program, pokazany na ekranie Kolorowe koła, wykreśla koła o kolorach przypadkowych, ale w metodzie paint odtwarza je w takich kolorach, w jakich je wykreślono. Ekran Kolorowe koła ### colorcir.gif Uwaga: Do generowania liczb pseudolosowych użyto obiektu klasy Random z pakietu java.util. Każde wywołanie na jego rzecz metody nextInt dostarcza kolejną, pseudolosową liczbę całkowitą. Na podstawie tej liczby jest tworzony przypadkowy kolor. =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; import java.util.*; public class Master extends Applet { private Vector dataBase = new Vector(); private Graphics gDC; private Random rand = new Random(); public void init() { gDC = getGraphics(); rand = new Random(); addMouseListener( new MouseAdapter() { public void mouseReleased(MouseEvent evt) { int x = evt.getX(), y = evt.getY(); Color c = new Color(rand.nextInt()); Circle circle = new Circle(x, y, c); circle.draw(gDC); dataBase.addElement(circle); } } ); } public void paint(Graphics gDC) { int n = dataBase.size(); for(int i = 0; i < n ; i++) { Object object = dataBase.elementAt(i); Circle circle = (Circle)object; circle.draw(gDC); } } } class Circle { private int r = 30, d = 2*r, x, y; private Color c; public Circle(int x, int y, Color c) { this.x = x; this.y = y; this.c = c; } public void draw(Graphics gDC) { gDC.setColor(c); gDC.fillOval(x-r, y-r, d, d); gDC.setColor(Color.black); gDC.drawOval(x-r, y-r, d-1, d-1); } } Wykreślanie napisów Polecenie wykreślenia napisu wydaje się wykreślaczowi. Służy do tego metoda drawString. Jej argumentami są: napis oraz współrzędne x i y lewego końca domyślnego odcinka, na którym napis spoczywa. void drawString(String str, int x, int y) Wykreślenie tekstu str spoczywającego na domyślnym, poziomym odcinku bazowym, którego lewy koniec ma współrzędne (x,y). Graphics gDC = getGraphics(); gDC.drawString("HelloWorld", 20, 40); Czcionka Wykreślenie napisu wybraną czcionką wymaga utworzenia obiektu klasy Font. Argumentami jej konstruktora są: krój (np. Serif, Sansserif, Monospaced), styl (np. Font.BOLD, Font.ITALIC) i rozmiar czcionki. Rozmiar wyraża się w punktach. 1 pt =  1/72 cala. Następujący program, pokazany na ekranie Wielkie litery, używa czcionki o kroju Serif, pochylonej i pogrubionej, o rozmiarze 80 pt. Odcinek bazowy napisu Isabel znajduje się w połowie wysokości pulpitu. Ekran Wielkie litery ### custfont.gif =============================================== import java.applet.Applet; import java.awt.*; public class Master extends Applet { private String theName = "Isabel"; private Font font; public void init() { // utworzenie czcionki int style = Font.BOLD | Font.ITALIC; font = new Font("Serif", style, 60); } public void paint(Graphics gDC) { // wyznaczenie wysokości Dimension d = getSize(); int h = d.height; // wstawienie czcionki gDC.setFont(font); // wstawienie koloru gDC.setColor(Color.red); // wykreślenie napisu gDC.drawString(theName, 10, h/2); } } Wykreślanie obiektów Polecenie wykreślenia obiektu wydaje się wykreślaczowi. Obszerny zestaw metod umożliwia wykreślanie figur i obiektów wypełnionych. void drawLine(xA, yA, xZ, yZ) Wykreślenie odcinka łączącego punkty o współrzędnych (xA, yA) i (xZ, yZ). void drawRect(int x, int y, int w, int h) Wykreślenie prostokąta o współrzędnych lewego-górnego wierzchołka (x,y) i rozmiarach w x h pikseli (por. uwaga). void drawOval(int x, int y, int w, int h) Wykreślenie owalu (okręgu albo elipsy) wpisanego w domyślny prostokąt, o współrzędnych lewego-górnego wierzchołka (x,y) i rozmiarach w x h pikseli (por. uwaga). void drawArc(int x, int y, int w, int h, int f, int t) Wykreślenie łuku wpisanego w domyślny prostokąt, o współrzędnych lewego-górnego wierzchołka (x,y) i rozmiarach w x h pikseli, od kąta początkowego f do kąta końcowego t. void drawPolygon(int x[], int y[], int n) Wykreślenie łamanej łączącej n punktów o współrzędnych (x[i], y[i]). void fillRect(int x, int y, int w, int h) Wykreślenie wypełnionego prostokąta (por. drawRect). void fillOval(int x, int y, int w, int h) Wykreślenie wypełnionego owalu (por. drawOval). void fillPolygon(int x[], int y[], int n) Wykreślenie wypełnionego wielokąta (por. drawPolygon). void clearRect(int x, int y, int w, int h) Wykreślenie wypełnionego prostokąta kolorem tła. Uwaga: Wykreślając prostokąt i owal (drawRect, drawOval) podaje się współrzędne lewego-górnego narożnika domyślnego prostokąta, w który je wpisano oraz rozmiary tego prostokąta zmniejszone o 1. Wykreślając wypełniony prostokąt i owal (fillRect, fillOval, clearRect) podaje się pełne rozmiary domyślnego prostokąta. Wykreślając odcinek podaje się współrzędne jego końców. Do wykreślenia punktu używa się metody drawLine. Następujący program, pokazany na ekranie Obiekty graficzne, wykreśla niebieski pierścień o środku w punkcie kliknięcia. Ekran Obiekty graficzne ### bluering.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet { private final int Thick = 5, r = 50; private Graphics gDC; private int x = -1, y; public void init() { gDC = getGraphics(); addMouseListener( new MouseAdapter() { public void mouseReleased(MouseEvent evt) { // wyczyszczenie poprzedniego gDC.clearRect(x-r, y-r, 2*r, 2*r); x = evt.getX(); y = evt.getY(); // wykreślenie następnego paint(gDC); } } ); } public void paint(Graphics gDC) { // jeśli jeszcze nie kliknięto if(x == -1) return; for(int i = 0; i < Thick ; i++) { if(i == 0 || i == Thick-1) gDC.setColor(Color.black); else gDC.setColor(Color.blue); int d = 2*r-1 - 2*i; gDC.drawOval(x-r+i, y-r+i, d, d); } } } Kolory wykreślacza Z każdym wykreślaczem jest związany kolor bieżący. Ustawienie i pobranie koloru bieżącego odbywa się za pomocą metod setColor i getColor. void setColor(Color color) Ustawia kolor bieżący na podany. Color getColor() Dostarcza kolor bieżący. Następujący program, pokazany na ekranie Lewy i prawy przycisk, posługuje się dwoma wykreślaczami. Jeden wykreśla okręgi w kolorze czerwonym, a drugi koła w kolorze zielonym. Ekran Lewy i prawy przycisk ### gdcpair.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet { final int r = 30; private Graphics lDC, rDC; public void init() { // utworzenie wykreślaczy lDC = getGraphics(); rDC = getGraphics(); // ustawienie kolorów lDC.setColor(Color.red); rDC.setColor(Color.green); addMouseListener( new MouseAdapter() { public void mouseReleased(MouseEvent evt) { int x = evt.getX(), y = evt.getY(); // wybranie wykreślacza boolean rightButton = evt.isMetaDown(); Graphics gDC = rightButton ? rDC : lDC; // wykreślenie w zamierzonym kolorze if(rightButton) gDC.fillOval(x-r, y-r, 2*r, 2*r); else gDC.drawOval(x-r, y-r, 2*r-1, 2*r-1); } } ); } } Kolor apletu Z każdym apletem jest związany kolor tła. Do zarządzania nim służą metody setBackground i getBackground. void setBackground(Color color) Ustawia kolor tła apletu. Color getBackground() Dostarcza kolor tła apletu. Następujący program, pokazany na ekranie Prawe-dwukliknięcie, wykreśla aplet o zielonym kolorze tła. p-dwu-kliknięcie pulpitu apletu zmienia kolor jego tła na czerwony. Ekran Prawe-dwukliknięcie ### shiftred.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet { public void init() { // początkowy kolor tła setBackground(Color.green); addMouseListener( new MouseAdapter() { public void mouseReleased(MouseEvent evt) { int count = evt.getClickCount(); boolean isMeta = evt.isMetaDown(); // zmiana koloru tła if(count == 2 && isMeta) setBackground(Color.red); } } ); } Tryb XOR Do takiego wykreślenia i wytarcia obiektu, po których nie następuje zmiana tła służy tryb XOR. void setXORMode(Color color) Dostosowuje wykreślacz do wykreślania w trybie XOR z podanym kolorem. Kolor wynikowy jest tworzony z podanego koloru, koloru tła i koloru bieżącego. Reprezentacje tych 3 kolorów, są sumowane pozycyjnie, modulo-2. void setPaintMode() Dostosowuje wykreślacz do wykreślania w trybie zwykłym. Następujący program, pokazany na ekranie Zachowywanie tła, wykreśla odcinek łączący punkt naciśnięcia i zwolnienia przycisku myszki. Podczas przeciągania kursora jest wykreślany czarny odcinek próbny. Pogrubiony odcinek ostateczny jest wykreślany w kolorze przypadkowym. Uwaga: Niektóre punkty odcinka próbnego nie są czarne, ponieważ wykreślanie w trybie XOR uwzględnia kolor tła. Ekran Zachowywanie tła ### xorline.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; import java.util.Random; public class Master extends Applet { private final int Thick = 6; private Graphics gDC; public void init() { gDC = getGraphics(); MouseWatcher watcher = new MouseWatcher(); addMouseListener(watcher); addMouseMotionListener(watcher); } class MouseWatcher extends MouseAdapter implements MouseMotionListener { private Random rand = new Random(); private int x0, y0, xOld, yOld; public void mousePressed(MouseEvent evt) { x0 = xOld = evt.getX(); y0 = yOld = evt.getY(); gDC.setColor(Color.black); gDC.setXORMode(Color.white); gDC.drawLine(x0, y0, xOld, yOld); } public void mouseDragged(MouseEvent evt) { gDC.drawLine(x0, y0, xOld, yOld); int x = evt.getX(), y = evt.getY(); gDC.drawLine(x0, y0, x, y); xOld = x; yOld = y; } public void mouseReleased(MouseEvent evt) { gDC.setPaintMode(); gDC.setColor(new Color(rand.nextInt())); drawThick(evt.getX(), evt.getY()); } public void mouseMoved(MouseEvent evt) { } public void drawThick(int x, int y) { int thick = Thick; if(Thick == 1) thick = 2; for(int i = -thick/2; i < thick/2; i++) { long sign =(x-x0) * (y-y0); if(sign > 0) gDC.drawLine(x0-i, y0+i, x-i, y+i); else if(sign < 0) gDC.drawLine(x0-i, y0-i, x-i, y-i); else if(x == x0) gDC.drawLine(x0-i, y0, x-i, y); else gDC.drawLine(x0, y0-i, x, y-i); } } } } Wykreślanie obrazów Obrazy są zazwyczaj przechowywane w formacie GIF i JPG a dźwięki w formacie AU i WAV. Jest wiele łatwo dostępnych programów, których można użyć do konwersji obrazów i dźwięków zapisanych w innych formatach. W celu załadowania obrazu należy użyć obiektów klas URL, Image i MediaTracker. Pierwszy należy zainicjować opisem miejsca skąd pochodzi obraz (getDocumentBase), w drugim należy utworzyć opis obrazu (getImage), a trzeciemu należy przekazać obraz (addImage). Następnie zaczekać na załadowanie obrazu (waitForID). Po wykonaniu tych czynności, można wykreślić obraz (drawImage). Następujący program, pokazany na ekranie Animowany obraz GIF, wykreśla obraz z pliku Spiral.gif. Plik znajduje się w tym samym katalogu, z którego pochodzi plik Index.html zawierający opis apletu. Ekran Animowany obraz GIF ### spiranim.gif Uwaga: W wywołaniu metody drawImage podaje się współrzędne punktu, w którym zostanie umieszczony lewy-górny narożnik obrazu. =============================================== import java.applet.Applet; import java.awt.*; import java.net.*; public class Master extends Applet { private Image img; public void init() { // miejsce skąd pochodzi obraz URL whereFrom = getDocumentBase(); // utworzenie opisu obrazu img = getImage(whereFrom, "Spiral.gif"); // utworzenie nadzorcy mediów MediaTracker tracker = new MediaTracker(this); // przekazanie nadzorcy opisu obrazu tracker.addImage(img, 0); // zaczekanie na obraz try { tracker.waitForID(0); } catch(InterruptedException e) { } } public void paint(Graphics gDC) { // wykreślenie obrazu gDC.drawImage(img, 0, 0, this); } } Odtwarzanie dźwięków W celu odtworzenia dźwięku należy użyć dwuparametrowej metody play. Jej pierwszym argumentem jest opis miejsca skąd pochodzi dźwięk (getDocumentBase), a drugim nazwa pliku dźwiękowego z rozszerzeniem .au albo .wav. Następujący program, pokazany na ekranie Generowanie dźwięku, w odpowiedzi na naciśnięcie klawisza klawiatury, odtwarza dźwięk zawarty w pliku Tada.wav. Uwaga: Plik dźwiękowy znajduje się w tym samym katalogu, z którego pochodzi plik z opisem apletu. Ekran Generowanie dźwięku ### gonggong.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; import java.net.*; public class Master extends Applet { private URL whereFrom; public void init() { whereFrom = getCodeBase(); addKeyListener( new KeyAdapter() { public void keyReleased(KeyEvent evt) { play(whereFrom, "Tada.wav"); } } ); // celownik na aplet requestFocus(); } public void paint(Graphics gDC) { gDC.drawString("Press any key", 20, 20); } } Projektowanie oblicza Najprostszym sposobem utworzenia oblicza graficznego apletu jest wywołanie metody setLayout z argumentem null i ręczne zaprojektowanie położenia i rozmiarów sterowników. Uwaga: Liczba predefiniowanych sterowników jest dość obszerna. Do najczęściej używanych należą przyciski (Button), klatki (TextField) i napisy (Label). Obsługiwanie przycisków Następujący program, pokazany na ekranie Aplet z przyciskiem, umieszcza na tle obrazu pobranego z pliku Asterix.gif, przycisk z napisem Play. Jego kliknięcie powoduje odtworzenie dźwięku zawartego w pliku Gong.au. Uwaga: Użyto metody newAudioClip, zdefiniowanej w pakiecie java.applet. Umożliwia ona pobieranie dźwięków nie tylko przez aplety, ale i przez aplikacje. W odróżnieniu od getAudioClip, jej wykonanie kończy się dopiero po pobraniu pliku dźwiękowego. Ekran Aplet z przyciskiem ### appbutt.gif =============================================== import java.applet.*; import java.awt.*; import java.awt.event.*; import java.net.*; public class Master extends Applet { private Image img; private AudioClip gong; private Button play; public void init() { // pobranie rozmiarów apletu Dimension s = getSize(); int w = s.width, h = s.height; // określenie sposobu rozmieszczania setLayout(null); // zdefiniowanie sterownika play = new Button("Play"); // określenie rozmiarów i położenia play.setSize(40, 40); play.setLocation((w-40)*3/4, (h-40)*3/4); // naniesienie sterownika na pulpit add(play); // obsłużenie kliknięcia play.addActionListener( new ActionListener() { public void actionPerformed(ActionEvent evt) { gong.play(); } } ); // określenie miejsca obrazu URL whereFrom = getDocumentBase(); // utworzenie opisu obrazu img = getImage(whereFrom, "Asterix.gif"); // utworzenie nadzorcy mediów MediaTracker tracker = new MediaTracker(this); // przekazanie opisu obrazu nadzorcy tracker.addImage(img, 0); // zaczekanie na obraz try { tracker.waitForID(0); } catch(InterruptedException e) { } // pobranie dźwięku URL url = null; try { url = new URL(whereFrom, "Gong.au"); } catch(MalformedURLException e) { } gong = newAudioClip(url); } public void paint(Graphics gDC) { // wykreślenie obrazu gDC.drawImage(img, 0, 0, this); } } Obsługiwanie klatek i etykiet Etykiety są wykorzystywane do umieszczania na pulpicie napisów, a klatki służą do wprowadzania danych. Następujący program, pokazany na ekranie Klatka i etykieta, wykreśla na pulpicie kwadrat liczby wprowadzonej do klatki. Wykreślenie odbywa się po naciśnięciu klawisza Enter. Ekran Klatka i etykieta ### editlab.gif Uwaga: Do nastawienia celownika na klatkę użyto funkcji requestFocus. Dzięki temu w klatce pojawia się pionowy znacznik wprowadzania, a znaki wprowadzone z klawiatury są odbierane przez klatkę. =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet { private TextField value; private Label label; public void init() { // pobranie rozmiarów apletu Dimension s = getSize(); int w = s.width, h = s.height; // określenie sposobu rozmieszczania setLayout(null); // zdefiniowanie etykiety label = new Label(""); label.setSize(100, 80); label.setLocation(10, 10); // zdefiniowanie klatki value = new TextField(); value.setSize(60, 20); value.setLocation((w-60)/2, h-20-5); // naniesienie etykiety i klatki add(label); add(value); // obsłużenie naciśnięcia klawisza Enter value.addActionListener( new ActionListener() { public void actionPerformed(ActionEvent evt) { String string = value.getText(); if(string.length() > 1 && string.charAt(0) == '+') string = string.substring(1); try { int val = Integer.parseInt(string); string = "" + val * val; } catch(NumberFormatException e) { string = "Not a number"; } label.setText(string); value.setText(""); } } ); } public void start() { // nastawienie celownika value.requestFocus(); } } Projektowanie kursorów Z każdym komponentem oblicza można związać odrębny kursor, zdefiniowany za pomocą przezroczystego obrazu GIF. Następujący program, pokazany na ekranie Własny kursor, ilustruje użycie kursora, zawartego w pliku Cursor.gif. Ekran Własny kursor ### mycursor.gif =============================================== import java.applet.Applet; import java.awt.*; import java.net.URL; public class Master extends Applet { private Image img; public void init() { // utworzenie przybornika Toolkit kit = Toolkit.getDefaultToolkit(); // pobranie obrazu kursora URL docBase = getDocumentBase(); img = getImage(docBase, "Cursor.gif"); // zdefiniowanie "gorącego punktu" kursora Point hotSpot = new Point(0, 0); // utworzenie kursora Cursor cursor = kit.createCustomCursor(img, hotSpot, "Cursor"); // związanie kursora z apletem setCursor(cursor); } } Jan Bielecki Programowanie wątków Wątkiem jest niezależny przepływ sterowania przez instrukcje programu. W komputerze z dostateczną liczbą procesorów, każdy wątek może być realizowany przez odrębny procesor. W pozostałych przypadkach, współbieżność wykonywania wątków musi być emulowana. Tworzenie wątków W celu utworzenia wątku, należy na rzecz obiektu wątku wywołać metodę start. Spowoduje podjęcie wykonywania metody run. Jej zakończenie spowoduje automatyczne zniszczenie wątku. Obiektem wątku jest obiekt klasy Thread. W fabrykatorze takiego obiektu należy wymienić obiekt klasy implementującej interfejs Runnable, a w niej zdefiniować metodę run. Przez instrukcje tej metody będzie przepływać sterowanie wątku utworzonego po wywołaniu metody start. W następujących 2 programach, pokazanych na ekranie Wyścigi wątków, nie można przewidzieć jaki napis pojawi się na pulpicie apletu. Uświadomienie sobie tej niejednoznaczności jest ważnym krokiem na drodze do programowania współbieżnego. Ekran Wyścigi wątków ### compete.gif Klasa Thread =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet { private String who = ""; public void init() { // utworzenie obiektu wątku Beeper beeper = new Beeper(); who = "from init"; } public void paint(Graphics gDC) { gDC.drawString(who, 50, 50); } class Beeper extends Thread { public Beeper() { // utworzenie wątku super.start(); } public void run() { who = "from run"; while(true) { // wygenerowanie dźwięku Toolkit.getDefaultToolkit().beep(); // uśpienie wątku na 1000 ms try { Thread.sleep(1000); } catch(InterruptedException e) { } } } } } Interfejs Runnable =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet implements Runnable { private String who = ""; public void init() { // utworzenie obiektu wątku Thread beeper = new Thread(this); // utworzenie wątku beeper.start(); who = "from init"; } public void run() { who = "from run"; while(true) { // wygenerowanie dźwieku Toolkit.getDefaultToolkit().beep(); // uśpienie wątku try { Thread.sleep(1000); } catch(InterruptedException e) { } } } public void paint(Graphics gDC) { gDC.drawString(who, 50, 50); } } Synchronizowanie wątków Wątki posługujące się wspólnym zasobem muszą być synchronizowane. Najprostszą formą synchronizacji jest zastosowanie sekcji krytycznej. Pełne synchronizowanie wątków realizuje się za pomocą metod synchronizujących. Uwaga: Wspólnym zasobem jest najczęściej zmienna, ale wspólnymi zasobami są również: pulpit, konsola i głośnik. A zatem synchronizacji muszą podlegać również wywołania takich metod jak drawString, System.out.print i play. Sekcje krytyczne Sekcją krytyczną jest sekwencja instrukcji wykonywanych w bloku instrukcji synchronized. W sekcji krytycznej związanej z ustalonym synchronizatorem, może w danej chwili znajdować się co najwyżej jeden wątek. Pozostałe wątki zostaną zatrzymane przed sekcją krytyczną, aż do chwili jej zwolnienia jej przez pierwszy wątek. W tym momencie, pozwolenie na wejście do sekcji krytycznej, otrzyma przypadkowy z zatrzymanych wątków. Należy zwrócić uwagę, że synchronizatorem nie jest wyrażenie występujące w nagłówku instrukcji synchronized, ale obiekt identyfikowany przez to wyrażenie (sic!). Object lock = // odnośnik do synchronizatora new Object(); // utworzenie synchronizatora // ... // miejsce zatrzymania synchronized(lock) { // użycie synchronizatora // ... } W szczególności, jeśli w programie występuje instrukcja synchronized(new Lock()) { // ... } to wyznacza ona sekcję krytyczną różną (sic!) od każdej innej sekcji. Następujący program, pokazany na ekranie Wspólny wykreślacz, ilustruje użycie sekcji krytycznej do wykluczenia jednoczesnego użycia tego samego wykreślacza przez dwa różne wątki. Uwaga: Brak sekcji krytycznej może spowodować, że pojawią się więcej niż 2 okręgi. Stanie się to wówczas, gdy dwa kolejne wykreślenia w trybie XOR odbędą się z innymi kolorami. Ekran Wspólny wykreślacz ### context.gif =============================================== import java.applet.Applet; import java.awt.*; public class Master extends Applet { protected final int r = 30; protected Graphics gDC; protected int w, h; protected Boolean monitor = new Boolean(false); public void init() { setBackground(Color.yellow); w = getSize().width; h = getSize().height; gDC = getGraphics(); new Runner(false).start(); new Runner(true).start(); } class Runner extends Thread { protected boolean up; protected int p = 0, d = 1, s, x, y; protected Color c; public Runner(boolean up) { this.up = up; s = up ? h-r : w-r; c = up ? Color.green : Color.yellow; gDC = getGraphics(); gDC.setXORMode(Color.white); } public void run() { while(true) { if(up) { x = w/2; y = p; } else { x = p; y = h/2; } draw(gDC); try { Thread.currentThread().sleep(10); } catch(InterruptedException e){ } draw(gDC); p += d; if(p < 0 || p > s-r) d = -d; } } public void draw(Graphics gDC) { synchronized(monitor) { gDC.setColor(c); gDC.fillOval(x, y, 2*r, 2*r); gDC.setColor(Color.black); gDC.drawOval(x, y, 2*r, 2*r); } } } } Metody synchronizujące Metodami synchronizującymi są wait, notify i notifyAll. Każda musi być wywołana z sekcji krytycznej i na rzecz tego samego synchronizatora, który został zidentyfikowany w jej nagłówku. Uwaga: Ponieważ wykonywanie metody wait może być przerwane, więc jej wywołanie musi być zawarte w bloku instrukcji try. synchronized(lock) { // ... lock.notify(); // try { // ... lock.wait(); // ... } catch(InterruptedException e) { // ... } // ... } Metoda wait Wykonanie metody wait powoduje zwolnienie sekcji krytycznej i wstrzymanie wykonywania wątku do chwili uwolnienia go na skutek wykonania metody notify albo notifyAll. Bezpośrednio po uwolnieniu, wątek wstrzymany staje się zatrzymany i będzie mógł kontynuować przepływ sterowania dopiero wówczas, gdy wątek uwalniający opuści sekcję krytyczną. Uwaga: Tabela Metoda wait wyjaśnia w sposób poglądowy, dlaczego wątek wstrzymany na synchronizatorze, staje się po uwolnieniu zatrzymany. Dzieje się tak, ponieważ w ramach wykonania metody uwalniającej jest niejawnie wykonywany nagłówek instrukcji synchronized identyfikujący ten sam synchronizator. Tabela Metoda wait ### Składnia wywołania Semantyka wywołania | synchronized(lock) { | synchronized(lock) { // ... | // ... lock.wait(); | lock.wait(); // ... | synchronized(lock) // niejawnie } | // ... | } | ### Metody notify i notifyAll Wykonanie na rzecz synchronizatora metody notify, uwalnia jeden, przypadkowo wybrany wątek, wstrzymany na tym synchronizatorze. Wykonanie na rzecz synchronizatora metody notifyAll, uwalnia wszystkie wątki, wstrzymane na tym synchronizatorze. Uwaga: Wywołanie metody notify albo notifyAll na rzecz synchronizatora, na którym nie wstrzymano ani jednego wątku, nie ma żadnego skutku (sic!). Następujący program, ilustrujący użycie metod wait i notify, cyklicznie generuje sygnały dźwiękowe. Uwaga: Może się zdarzyć, że jedno z wywołań funkcji notify nie będzie miało wpływu na drugi wątek. Ta mało prawdopodobna sytuacja nie czyni programu niepoprawnym. =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet implements Runnable { private Boolean lock = new Boolean(false); public void init() { new Thread(this).start(); new Thread() { public void run() { while(true) { synchronized(lock) { Toolkit.getDefaultToolkit().beep(); lock.notify(); try { lock.wait(); } catch(InterruptedException e) { } } } } }.start(); } public void run() { while(true) { synchronized(lock) { lock.notify(); try { lock.wait(); } catch(InterruptedException e) { } } } } } Występowanie impasu Impasem jest stan objawiający się trwałym brakiem postępu w wykonaniu programu. Jedną z przyczyn impasu jest źle zorganizowana synchronizacja wątków. Następujący program napisano w taki sposób, aby podczas jego wykonania doszło do impasu. Impas objawia się zaprzestaniem generowania dźwięku. Występuje to zazwyczaj po upływie dość znacznego czasu, ale zawsze przy następującym splocie wydarzeń. 1) Wątek bez beep zostaje wstrzymany na wait. 2) Wątek z beep wykonuje notify, ale nie dochodzi do wait. 3) Wątek bez beep wykonuje notify, ale nie wywołuje to żadnego skutku. 4) Wątek bez beep wykonuje wait. 5) Wątek z beep wykonuje wait. 6) Oba wątki są wstrzymane. Uwaga: Aby wystąpienie impasu uczynić bardziej prawdopodobnym, w programie umieszczono instrukcję „wytracania czasu”. Użyty w niej parametr 1000 okazał się wystarczający dla procesora Pentium II 350 MHz. =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet implements Runnable { private Boolean lock = new Boolean(false); public void init() { new Thread(this).start(); new Thread() { public void run() { while(true) { synchronized(lock) { Toolkit.getDefaultToolkit().beep(); lock.notify(); } // wytracanie czasu for(int i = 0; i < 1000 ; i++); synchronized(lock) { try { lock.wait(); } catch(InterruptedException e) { } } } } }.start(); } public void run() { while(true) { synchronized(lock) { lock.notify(); try { lock.wait(); } catch(InterruptedException e) { } } } } } Unikanie impasu Nie ma uniwersalnej metody unikania impasu. Jedną z najprostszych jest przydzielanie zasobów wątkom zawsze w tej samej kolejności. W celu uniknięcia sytuacji, kiedy wykonanie metody uwalniającej (notify i notifyAll) nie będzie miało żadnego skutku, zaleca się związanie z synchronizatorem zmiennej orzecznikowej (typu boolean) i posłużenie się następującym schematem komunikacji, który dotyczy tu wątków Producenta i Konsumenta, wymieniających dane poprzez bufor klasy Buffer. Deklaracje zmiennych Buffer buffer = new Buffer(); Object bufferLock = new Object(); boolean bufferIsFull = true; Wątek producenta while(true) { synchronized(bufferLock) { while(bufferIsFull) { try { // wstrzymanie bufferLock.wait(); } catch(InterruptedException e) { // ... } // ... } } // ... produkcja synchronized(bufferLock) { bufferIsFull = true; // uwolnienie bufferLock.notify(); } } Wątek konsumenta while(true) { synchronized(bufferLock) { while(!bufferIsFull) { try { // wstrzymanie bufferLock.wait(); } catch(InterruptedException e) { // ... } // ... } } // ... konsumpcja synchronized(bufferLock) { bufferIsFull = false; // uwolnienie bufferLock.notify(); } } Następujący program, pokazany na ekranie Kolorowe romby, ilustruje wymianę danych między parą wątków. Jeden wątek generuje romby, a drugi je wykreśla. Aby generowanie i wykreślanie odbywało się z pełną szybkością, zrezygnowano z użycia funkcji sleep. Ekran Kolorowe romby ### colordm.gif =============================================== import java.applet.Applet; import java.awt.*; import java.util.Random; public class Master extends Applet { private int d = 50; private Polygon poly = new Polygon(); private Boolean polyLock = new Boolean(false); private boolean polyReady = false, polyDrawn = true; private int w, h, x, y; private Random rand = new Random(); public void init() { w = getSize().width; h = getSize().height; if(w < d || h < d) d = 1; new Maker(); new Drawer(); } public Point getPoint(int w, int h) { return new Point( Math.abs(rand.nextInt()) % w, Math.abs(rand.nextInt()) % h ); } class Maker extends Thread { public Maker() { start(); } public void run() { while(true) { synchronized(polyLock) { while(!polyDrawn) { try { polyLock.wait(); } catch(InterruptedException e) { } } } // utworzenie rombu poly = new Polygon(); Point p = getPoint(w-d, h-d); int s = d/2; x = p.x + s; y = p.y + s; poly.addPoint(x-s, y); poly.addPoint(x, y-s); poly.addPoint(x+s, y); poly.addPoint(x, y+s); // polecenie wykreślenia rombu synchronized(polyLock) { polyReady = true; polyDrawn = false; polyLock.notify(); } } } } class Drawer extends Thread { private Graphics gDC; public Drawer() { gDC = getGraphics(); start(); } public void run() { while(true) { synchronized(polyLock) { while(!polyReady) { try { polyLock.wait(); } catch(InterruptedException e) { } } } // wykreślenie rombu gDC.setColor(new Color(rand.nextInt())); gDC.fillPolygon(poly); gDC.setColor(Color.black); gDC.drawPolygon(poly); // polecenie utworzenia rombu synchronized(polyLock) { polyDrawn = true; polyReady = false; polyLock.notify(); } } } } } Klasa monitorowa W celu zwiększenia czytelności programów można użyć wyspecjalizowanej klasy monitorowej. W przytoczonej tu implementacji wykorzystano spostrzeżenie, że z każdą komunikacją wątków poprzez sekcję krytyczną wiąże się wystąpienie pomocniczej zmiennej orzecznikowej. public class Monitor { /** Klasa monitorowa Copyright © Jan Bielecki 1999.01.23 */ private boolean monitorFlag; private Boolean monitor = new Boolean(false); public Monitor(boolean flag) { monitorFlag = flag; } public void jbWait() throws InterruptedException { synchronized(monitor) { while(!monitorFlag) monitor.wait(); monitorFlag = false; } } public void jbNotify() { synchronized(monitor) { monitorFlag = true; monitor.notify(); } } // wersja nieprzerywalna, zbędne try public void jbWait2() { synchronized(monitor) { while(!monitorFlag) { try { monitor.wait(); } catch(InterruptedException e) { } } monitorFlag = false; } } public void jbPause() { synchronized(monitor) { } } } Następujący program, generujący i wykreślający romby, posługuje się 2 zmiennymi monitorowymi. Ich użycie znacznie upraszcza synchronizację wątków. Gdyby zastąpiono je 1 zmienną, to zachowanie programu uległoby istotnej zmianie. =============================================== import java.applet.Applet; import java.awt.*; import java.util.Random; public class Master extends Applet { private int d = 50; private Polygon poly = new Polygon(); private Monitor maker = new Monitor(true), drawer = new Monitor(false); private int w, h, x, y; private Random rand = new Random(); public void init() { w = getSize().width; h = getSize().height; if(w < d || h < d) d = 1; new Maker(); new Drawer(); } public Point getPoint(int w, int h) { return new Point( Math.abs(rand.nextInt()) % w, Math.abs(rand.nextInt()) % h ); } class Maker extends Thread { public Maker() { start(); } public void run() { while(true) { // czekanie na wejście do monitora maker.jbWait2(); // utworzenie rombu poly = new Polygon(); Point p = getPoint(w-d, h-d); int s = d/2; x = p.x + s; y = p.y + s; poly.addPoint(x-s, y); poly.addPoint(x, y-s); poly.addPoint(x+s, y); poly.addPoint(x, y+s); // polecenie wykreślenia rombu drawer.jbNotify(); } } } class Drawer extends Thread { private Graphics gDC; public Drawer() { gDC = getGraphics(); start(); } public void run() { while(true) { // czekanie na wejście do monitora drawer.jbWait2(); // wykreślenie rombu gDC.setColor(new Color(rand.nextInt())); gDC.fillPolygon(poly); gDC.setColor(Color.black); gDC.drawPolygon(poly); // polecenie utworzenia rombu maker.jbNotify(); } } } } Zarządzanie wątkami Sposób zarządzania wątkami zależy od strategii Maszyny Wirtualnej. Więcej czasu procesora otrzymują wątki o wyższym priorytecie, ale nie wyklucza się możliwości przydzielenia procesora wątkowi, który nie ma najwyższego priorytetu, ani nie gwarantuje sprawiedliwego przydzielania procesora wątkom o równych priorytetach. Dlatego nadanie wątkom priorytetów powinno się odbyć dopiero po uruchomieniu programu. Uwaga: Pomocne w sprawiedliwym przydziale procesora są metody yield i setPriority. Pierwsza z nich jest niezbędna w systemach bez wywłaszczania, druga umożliwia utworzenie wątku dyspozytora, w celu powierzenia mu zarządzania pozostałymi wątkami. Metoda yield Wywołanie metod yield ma postać yield() Jej wykonanie powoduje dobrowolne ustąpienie procesora na rzecz innych wątków. Może to mieć znaczenie w nie-wywłaszczającym systemie z ograniczoną liczbą procesorów (np. Windows 3.11); Następujący program wyrównuje szanse wątków dzięki dobrowolnemu ustępowaniu procesora. Obserwację wykonań ułatwia ikonizacja i dezikonizacja okna przeglądarki. =============================================== import java.applet.Applet; import java.awt.*; import java.net.*; public class Master extends Applet { private final int Count = 7; private long count[] = new long [Count]; private Worker worker[] = new Worker [Count]; private boolean stopRun; public void start() { stopRun = false; System.out.println(); for(int i = 0; i < Count ; i++) worker[i] = new Worker(i); } public void stop() { stopRun = true; try { for(int i = 0; i < Count; i++) worker[i].join(); } catch(InterruptedException e) { } String counts = ""; for(int i = 0; i < Count ; i++) { long count = worker[i].getCount(); counts = counts + count + '\t'; } System.out.println(counts); } class Worker extends Thread { private int id; private long count = 0; private int who = 0; public Worker(int id) { this.id = id; super.start(); } public void run() { synchronized(Master.this) { } while(!stopRun) { count++; yield(); } } long getCount() { return count; } } } Metoda setPriority Wywołanie metod setPriority ma postać obj.setPriority(pr) w której obj jest odnośnikiem do obiektu wątku, a pr ma wartość z przedziału 1..10. Wykonanie metody setPriority powoduje nadanie wątkowi identyfikowanemu przez obj podanego priorytetu. Następujący program stosuje zasadę sprawiedliwego przydziału procesora. W tabeli Wyniki pracy pokazano typowe komunikaty. Uwaga: Instrukcja synchronizująca ma za zadanie uniemożliwienie podjęcia pracy dyspozytora przed utworzeniem wątków roboczych i tym samym wyrównanie ich szans na starcie. Tabela Wyniki pracy ### Thread 1 started Thread 2 started Thread 3 started 0 0 0 151906 1 1 151907 159609 3 151909 159611 159610 308243 159613 159611 308244 159615 309001 308246 323033 309003 ### =============================================== import java.applet.Applet; import java.awt.*; import java.net.*; public class Master extends Applet implements Runnable { private final int Count = 3; private long count[] = new long [Count]; private Worker worker[] = new Worker [Count]; private boolean stopRun; private Thread mainThread; private int who; public void start() { stopRun = false; synchronized(this) { mainThread = new Thread(this); mainThread.setPriority(Thread.MAX_PRIORITY); mainThread.start(); Thread thisThread = Thread.currentThread(); thisThread.setPriority(Thread.MIN_PRIORITY); System.out.println(); for(int i = 0; i < Count ; i++) { worker[i] = new Worker(i); System.out.println( "Thread " + (i+1) + " started" ); } } } public void stop() { stopRun = true; try { mainThread.join(); for(int i = 0; i < Count; i++) { worker[i].join(); System.out.println( "Thread " + (i+1) + " stopped" ); } } catch(InterruptedException e) { } } public void run() { synchronized(this) { } while(!stopRun) { worker[who].setPriority(Thread.MIN_PRIORITY); String counts = ""; long minCount = worker[0].getCount(); who = 0; for(int i = 0; i < Count ; i++) { long count = worker[i].getCount(); counts = counts + count + '\t'; if(count < minCount) { minCount = count; who = i; } } System.out.println(counts); worker[who].setPriority(Thread.NORM_PRIORITY); try { Thread.sleep(500); } catch(InterruptedException e) { } } } class Worker extends Thread { private int id; private long count = 0; private int who = 0; public Worker(int id) { this.id = id; super.start(); } public void run() { synchronized(Master.this) { } while(!stopRun) { count++; try { Thread.sleep(0); } catch(InterruptedException e) { } } } long getCount() { return count; } } } Grupy wątków W chwili utworzenia wątku można określić jego przynależność do grupy wątków. Istnienie grupy umożliwia wykonywanie operacji na więcej niż jednym wątku. Następujący program zawiera wiele istotnych elementów. W chwili każdego wywołania metody mouseMoved odgrywa z pliku dźwięk, a zwolnienie przycisku myszki sygnalizuje dźwiękiem generowanym przez metodę beep. Aby zrealizować to zadanie należało uwzględnić, że 1. Dostęp do wspólnych zasobów wątków, takich jak głośnik, monitor i konsola musi być synchronizowany; a więc nie wolno dopuścić do tego, aby różne wątki współbieżnie inicjowały odtwarzanie dźwięku, gdyż może to spowodować zawieszenie Systemu. 2. Ponieważ każde zdarzenie mouse jest obsługiwane przez ten sam wątek, więc nie wolno dopuścić do tego, aby wywołanie metody sleep odbyło się w wątku systemowym, gdyż opóźni to odbieranie zdarzeń. 3. Wygenerowanie dźwięku za pomocą metody beep jest możliwe tylko wówczas, gdy po wywołaniu metody play wywołano także metodę stop. Uwaga: Po zakończeniu każdego przeciągania jest podawana maksymalna liczba utworzonych w jego trakcie wątków. Liczba ta może być znaczna, ale w praktyce nie udawało się przekroczyć wartości 3. =============================================== import java.applet.*; import java.awt.*; import java.awt.event.*; import java.net.*; public class Master extends Applet { private String dragFile = "Chirp.au"; private AudioClip dragClip; private Toolkit kit; private ThreadGroup group; private Label counter; private int nowAlive, maxAlive; public void start() { counter.setText("Start dragging"); nowAlive = 0; group = new ThreadGroup(""); } public void stop() { while(group.activeCount() != 0) ; group.destroy(); } public void init() { kit = Toolkit.getDefaultToolkit(); URL docBase = getDocumentBase(); dragClip = getAudioClip(docBase, dragFile); add(counter = new Label("")); addMouseListener( new MouseAdapter() { public void mousePressed(MouseEvent evt) { nowAlive = maxAlive = 0; } public void mouseReleased(MouseEvent evt) { while(group.activeCount() != 0) ; counter.setText("" + maxAlive); dragClip.stop(); // ważne! kit.beep(); } } ); addMouseMotionListener( new MouseMotionAdapter() { private Object monitor = new Object(), alive = new Object(); public void mouseDragged(MouseEvent evt) { synchronized(monitor) { if(nowAlive > 0) return; } new Thread(group, new Runnable() { public void run() { synchronized(monitor) { setCounter(+1); if(nowAlive > maxAlive) maxAlive = nowAlive; dragClip.play(); } if(sleep(200)) return; synchronized(monitor) { setCounter(-1); } } } ).start(); } boolean sleep(int time) { try { Thread.sleep(time); } catch(InterruptedException e) { return true; } return false; } void setCounter(int val) { counter.setText("" + (nowAlive += val)); } } ); } } Niszczenie wątków Wątek jest niszczony w chwili zakończenia wykonywania jego metody run. Do zainicjowania tej operacji z innego wątku służy metoda interrupt, a do upewnienia się, że wątek został zniszczony, metoda join. Metoda interrupt Wywołanie metod interrupt ma postać obj.interrupt() w której obj jest odnośnikiem do obiektu wątku. Metoda ustawia w obiekcie wątku flagę interrupted. Jeśli metoda zostanie wywołana na rzecz obiektu wstrzymanego albo uśpionego wątku, to w najbliższej chwili gdy wątek wznowi przepływ sterowania (tj. z miejsca tuż po wait albo sleep) zostanie wysłany wyjątek klasy InterruptedException. Uwaga: Wywołanie metody interrupt na rzecz nieistniejącego wątku powoduje wysłanie wyjątku klasy AccessControlException. Metoda join Wywołanie metody join ma postać obj.join() w której obj jest odnośnikiem do obiektu wątku. Wykonanie metody powoduje zawieszenie wykonywania wątku do chwili, gdy zakończy się wykonywanie wątku opartego na obiekcie identyfikowanym przez obj. Następujący program tworzy wątek, który co 1 s wydaje sygnał dźwiękowy. Kliknięcie pulpitu apletu powoduje zniszczenie wątku, a więc zaprzestanie wydawania sygnałów dźwiękowych. =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet { private String text = "Press a a key"; private Beeper beeper; public void start() { requestFocus(); } public void init() { beeper = new Beeper(); addKeyListener( new KeyAdapter() { public void keyReleased(KeyEvent evt) { beeper.interrupt(); try { beeper.join(); } catch(InterruptedException e) { } text = "Beeper killed"; repaint(); } } ); } public void paint(Graphics gDC) { gDC.drawString(text, 20, 20); } class Beeper extends Thread { public Beeper() { super.start(); } public void run() { while(true) { // wydawanie dźwięków Toolkit.getDefaultToolkit().beep(); // spanie try { Thread.sleep(1000); } catch(InterruptedException e) { return; // po beeper.interrupt() } } } } } Metoda interrupted Wywołanie metody interrupted ma postać interrupted() Metoda dostarcza orzecznik o wartości „wątkowi ustawiono flagę interrupted”, a następnie zeruje tę flagę. Następujący program wykreśla kolorowe koła z obręczami wokół punktów, w których wywołano metodę mouseDragged. Między wykreśleniem koła i obręczy wątek wykreślający odpoczywa przez 200 ms. Nie wykreśla się kół zachodzących na siebie. Uwaga: Każde wykreślenie odbywa się w odrębnym wątku. Jeśli metoda mouseDragged zostanie wywołana w okresie kiedy wątek odpoczywa, to jest on natychmiast niszczony, a kolor koła zmienia się na biały. =============================================== import java.applet.*; import java.awt.*; import java.awt.event.*; import java.util.Random; public class Master extends Applet { private final int r = 10, s = 200; private Runner runner; private Graphics gDC; private boolean isSleeping; private Object sleepLock = new Object(); private int d = 2 * r, xOld = -r, yOld = -r; private Random rand = new Random(); public void init() { gDC = getGraphics(); addMouseMotionListener( new MouseMotionAdapter() { public void mouseDragged(MouseEvent evt) { int x = evt.getX(), y = evt.getY(); if((x-xOld)*(x-xOld) + (y-yOld)*(y-yOld) < d * d) return; xOld = x; yOld = y; stop(); runner = new Runner(evt); } } ); } public void stop() { synchronized(sleepLock) { if(isSleeping) runner.interrupt(); } if(runner != null) { try { runner.join(); } catch(InterruptedException e) { } } } class Runner extends Thread { private int x, y; public Runner(MouseEvent evt) { x = evt.getX(); y = evt.getY(); isSleeping = false; super.start(); } public void run() { Color color; do color = new Color(rand.nextInt()); while(color == Color.white); gDC.setColor(color); gDC.fillOval(x-r, y-r, d, d); synchronized(sleepLock) { try { if(interrupted()) throw new InterruptedException(); isSleeping = true; // porównaj z Thread.sleep(s); sleepLock.wait(s); } catch(InterruptedException e) { gDC.setColor(Color.white); gDC.fillOval(x-r, y-r, d, d); return; } finally { gDC.setColor(Color.black); gDC.drawOval(x-r, y-r, d-1, d-1); isSleeping = false; } } } } } Jan Bielecki Programowanie animacji Animacja polega na wykreślaniu obiektów w kolejnych fazach ruchu. Animowanie obiektu odbywa się w odrębnym wątku, wykonującym metodę run. Zalecanym sposobem utworzenia wątku jest zdefiniowanie klasy pochodnej od Thread i wywołanie w jej konstruktorze metody start. Animacja jednowątkowa Następujący program, pokazany na ekranie Ruchome koło, animuje koło o promieniu 30 pikseli, które ruchem wahadłowym przemieszcza się w poziomie, w połowie wysokości pulpitu. Zatrzymanie ruchu koła następuje po naciśnięciu spacji. Ekran Ruchome koło ### onemove.gif Uwaga: W programie użyto funkcji sleep. Jej wykonanie powoduje uśpienie wątku na podaną liczbę milisekund. Ponieważ w miejscu wykonania funkcji sleep może zajść zdarzenie klasy InterruptedException, więc wywołanie tej funkcji umieszczono w bloku instrukcji try. =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; import java.util.*; public class Master extends Applet { public void init() { new Circle(this); } } class Circle extends Thread { private Graphics gDC; private int w, h; private int r = 30, d = 2*r, x = r, y, dx = 1; private boolean enough = false; public Circle(Applet applet) { gDC = applet.getGraphics(); Dimension s = applet.getSize(); w = s.width; h = s.height; applet.requestFocus(); applet.addKeyListener( new KeyAdapter() { public void keyReleased(KeyEvent evt) { int key = evt.getKeyCode(); if(key == KeyEvent.VK_SPACE) enough = true; } } ); // utworzenie wątku start(); } public void run() { y = h / 2; while(!enough) { gDC.clearRect(x-r-dx, y-r, d+1, d+1); draw(gDC); try { Thread.sleep(5); } catch(InterruptedException e) { } x += dx; if(x < r || x > w-r) dx = -dx; } } public void draw(Graphics gDC) { gDC.setColor(Color.red); gDC.fillOval(x-r, y-r, d, d); gDC.setColor(Color.black); gDC.drawOval(x-r, y-r, d-1, d-1); } } Animacja buforowana Obserwując poprzedni program można zauważyć niemiłe dla oka migotanie ekranu. Wynika to stąd, że w krótkich odstępach czasu, na tym samym obszarze pulpitu, odbywa się wykreślanie i usuwanie kół. Efekt ten można wyeliminować, stosując wykreślanie do bufora w pamięci operacyjnej, a dopiero po skompletowaniu obrazu w buforze, skopiować bufor na ekran. =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; import java.util.*; public class Master extends Applet { private final int sleepTime = 5; public void init() { new Circle(this, sleepTime); } } class Circle extends Thread { private Applet applet; private int sleepTime; private Graphics gDC, mDC; private Image buffer; private int w, h; private int r = 30, d = 2*r, x = r, y, dx = 1; private boolean enough = false; public Circle(Applet applet, int sleepTime) { this.applet = applet; this.sleepTime = sleepTime; gDC = applet.getGraphics(); Dimension s = applet.getSize(); w = s.width; h = s.height; applet.requestFocus(); applet.addKeyListener( new KeyAdapter() { public void keyReleased(KeyEvent evt) { int key = evt.getKeyCode(); if(key == KeyEvent.VK_SPACE) enough = true; } } ); // utworzenie bufora o rozmiarach apletu buffer = applet.createImage(w, h); // utworzenie wykreślacza do bufora mDC = buffer.getGraphics(); // utworzenie wątku start(); } public void run() { y = h / 2; while(!enough) { // wykreślanie mDC.clearRect(0, 0, w, h); draw(mDC); gDC.drawImage(buffer, 0, 0, applet); // opóźnienie wykreślania try { Thread.sleep(sleepTime); } catch(InterruptedException e) { } x += dx; if(x < r || x > w-r) dx = -dx; } } public void draw(Graphics gDC) { gDC.setColor(Color.red); gDC.fillOval(x-r, y-r, d, d); gDC.setColor(Color.black); gDC.drawOval(x-r, y-r, d-1, d-1); } } Animacja wielowątkowa Następujący program, pokazany na ekranie Ruchome koła, ilustruje zasadę tworzenia wielu wątków realizujących przepływ sterowania przez instrukcje tej samej metody run. Ekran Ruchome koła ### manymove.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; import java.util.*; public class Master extends Applet { private final int Count = 5, circleSleep = 10, painterSleep = 10; private Circle[] circle = new Circle[Count]; private Random rand = new Random(); private boolean enough = false; private int w, h; public void init() { requestFocus(); addKeyListener( new KeyAdapter() { public void keyReleased(KeyEvent evt) { int key = evt.getKeyCode(); if(key == KeyEvent.VK_SPACE) enough = true; } } ); w = getSize().height; h = getSize().width; for(int i = 0; i < Count ; i++) circle[i] = new Circle((i+1) * h / (Count+1)); new Painter(); } class Painter extends Thread { public Painter() { start(); } public void run() { // przygotowanie bufora Image buffer = createImage(w, h); Graphics gDC = getGraphics(), mDC = buffer.getGraphics(); while(!enough) { // wykreślanie mDC.clearRect(0, 0, w, h); for(int i = 0; i < Count ; i++) circle[i].draw(mDC); gDC.drawImage(buffer, 0, 0, Master.this); // opóźnienie wykreślania try { Thread.sleep(painterSleep); } catch(InterruptedException e) { } } } } class Circle extends Thread { private int r = 30, d = 2*r, x = r, y = -1, dx = 1; private Color color = new Color(rand.nextInt()); private boolean enough = false; public Circle(int pos) { y = pos; start(); } public void run() { while(!enough) { // opóźnienie wykreślania try { Thread.sleep(circleSleep); } catch(InterruptedException e) { } // przemieszczenie synchronized(this) { x += dx; if(x < r || x > w-r) dx = -dx; } } } public void draw(Graphics gDC) { synchronized(this) { gDC.setColor(color); gDC.fillOval(x-r, y-r, d, d); gDC.setColor(Color.black); gDC.drawOval(x-r, y-r, d-1, d-1); } } } } Następujący program zmodyfikowaną wersją poprzedniego. Zastosowano w nim zliczanie wątków, w których zakończyło się wykonywanie konstruktora klasy Circle. Dopiero po upewnieniu się, że wszystkie wątki są gotowe, uwalnia się je ze stanu wstrzymania. Uwaga: Nazwa Master.class reprezentuje unikalny obiekt opisujący klasę Master. Obiekt ten jest używany jako synchronizator. =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; import java.util.*; public class Master extends Applet { private final int Count = 5, circleSleep = 6, painterSleep = 10; private Circle[] circle = new Circle[Count]; private Random rand = new Random(); private boolean enough = false; private int w, h; public static int tally = 0; public void init() { requestFocus(); addKeyListener( new KeyAdapter() { public void keyReleased(KeyEvent evt) { int key = evt.getKeyCode(); if(key == KeyEvent.VK_SPACE) enough = true; } } ); w = getSize().height; h = getSize().width; for(int i = 0; i < Count ; i++) circle[i] = new Circle((i+1) * h / (Count+1)); new Painter(); } class Painter extends Thread { public Painter() { // utworzenie wątku start(); } public void run() { // czekanie na gotowość wątków try { while(Master.tally < Count) Thread.sleep(10); } catch(InterruptedException e) { } // przygotowanie bufora Image buffer = createImage(w, h); Graphics gDC = getGraphics(), mDC = buffer.getGraphics(); // uwolnienie czekających wątków synchronized(Master.class) { Master.class.notifyAll(); } while(!enough) { // wykreślanie mDC.clearRect(0, 0, w, h); for(int i = 0; i < Count ; i++) circle[i].draw(mDC); gDC.drawImage(buffer, 0, 0, Master.this); // opóźnienie wykreślania try { Thread.sleep(painterSleep); } catch(InterruptedException e) { } } } } class Circle extends Thread { private int r = 30, d = 2*r, x = r, y, dx = 1; private Color color = new Color(rand.nextInt()); private boolean enough = false; public Circle(int pos) { y = pos; start(); } public void run() { // zliczanie gotowych wątków synchronized(Master.class) { try { Master.tally++; Master.class.wait(); } catch(InterruptedException e) { } } while(!enough) { // opóźnienie przemieszczania try { Thread.sleep(circleSleep); } catch(InterruptedException e) { } // przemieszczenie synchronized(this) { x += dx; if(x < r || x > w-r) dx = -dx; } } } public void draw(Graphics gDC) { synchronized(this) { gDC.setColor(color); gDC.fillOval(x-r, y-r, d, d); gDC.setColor(Color.black); gDC.drawOval(x-r, y-r, d-1, d-1); } } } } Animacja synchronizowana Eksperymenty z czasem uśpienia wykazują, że poprzedni program zachowuje się dziwnie przy czasach mniejszych niż np. 5 ms (w systemie Windows 95, na komputerze Pentium II 350 MHz 128 MB). Wynika to stąd, że przy krótkim czasie uśpienia, wątek może być potraktowany jako interakcyjny i uzyskać dodatkowy przydział procesora. W konsekwencji, koła będą się poruszać „jak oszalałe”. Efekt ten można wyeliminować, zwiększając czas między aktualizowaniami pulpitu. Wówczas jednak ruch kół okaże się skokowy. Dlatego najlepszym sposobem uniezależnienia się od specyfiki Systemu jest zrezygnowanie z uśpienia i zastąpienie go synchronizacją wątków. W następującym programie, wątek wykreślający jest powiadamiany o zmianie pozycji koła. Jeśli uzna, że od poprzedniego wykreślenia upłynęło więcej czasu niż wynosi opóźnienie wykreślania (parametr Delay), to wstrzymuje pozostałe wątki na wywołaniu metody wait i przystępuje do wykreślania. Po zakończeniu wykreślania następuje uwolnienie jednego (notify) albo wszystkich (notifyAll) wstrzymanych wątków. Uwaga: Niekiedy można zaobserwować gwałtowne przemieszczenie się koła. Zdarza się to wówczas, gdy metoda notify zostanie wywołana w chwili, gdy tylko jeden wątek jest wstrzymany, a zatem tylko on zostanie uwolniony. Ponadto musi to nastąpić kilka razy pod rząd, co jest niezwykle mało prawdopodobne. Aby efekt ten wyeliminować, należałoby uzależnić działanie programu nie tylko od opóźnienia wykreślania, ale również od przemieszczenia kół. =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; import java.util.*; public class Master extends Applet { private final int Count = 7, Delay = 100; private Painter painter; private boolean paintDone = true, circleMoved = false; private Circle[] circle = new Circle[Count]; private Random rand = new Random(); private boolean enough = false; private int w, h; private int tally = 0; public void init() { requestFocus(); addKeyListener( new KeyAdapter() { public void keyReleased(KeyEvent evt) { int key = evt.getKeyCode(); if(key == KeyEvent.VK_SPACE) enough = true; } } ); w = getSize().height; h = getSize().width; painter = new Painter(); synchronized(painter) { for(int i = 0; i < Count ; i++) circle[i] = new Circle((i+1) * h / (Count+1)); } } class Painter extends Thread { public Painter() { start(); } public void run() { // czekanie na gotowość wątków try { while(tally < Count) Thread.sleep(10); } catch(InterruptedException e) { } // przygotowanie bufora Image buffer = createImage(w, h); Graphics gDC = getGraphics(), mDC = buffer.getGraphics(); // uwolnienie czekających wątków synchronized(Master.class) { Master.class.notifyAll(); } long lastPaint = 0; while(!enough) { synchronized(painter) { while(!circleMoved) { try { painter.wait(); } catch(InterruptedException e) { } } } long time = System.currentTimeMillis(); if(time - lastPaint > Delay) { lastPaint = time; mDC.clearRect(0, 0, w, h); for(int i = 0; i < Count ; i++) circle[i].draw(mDC); gDC.drawImage(buffer, 0, 0, Master.this); } else { synchronized(painter) { paintDone = true; // por. painter.notify(); painter.notifyAll(); } } } } } class Circle extends Thread { private int r = 30, d = 2*r, x = r, y = -1, dx = 1; private Color color = new Color(rand.nextInt()); private boolean enough = false; public Circle(int pos) { y = pos; start(); } public void run() { // zliczanie gotowych wątków synchronized(Master.class) { try { tally++; Master.class.wait(); } catch(InterruptedException e) { } } while(!enough) { synchronized(painter) { circleMoved = true; paintDone = false; painter.notify(); while(!paintDone) { try { painter.wait(); } catch(InterruptedException e) { } } } synchronized(this) { x += dx; if(x < r || x > w-r) dx = -dx; } } } public void draw(Graphics gDC) { if(y == -1) Toolkit.getDefaultToolkit().beep(); synchronized(this) { gDC.setColor(color); gDC.fillOval(x-r, y-r, d, d); gDC.setColor(Color.black); gDC.drawOval(x-r, y-r, d-1, d-1); } } } } Jan Bielecki Programowanie gier Następujący program, pokazany na ekranie Statek i pocisk, implementuje współbieżną grę animacyjną, w której przemieszczający się statek należy trafić pociskiem, wystrzeliwanym po naciśnięciu klawisza spacji. Ekran Statek i pocisk ### shipshot.gif Zasady gry Po odpaleniu apletu nie dopuszcza się zmiany jego rozmiaru, ani położenia. W pobliżu lewego-górnego narożnika pulpitu wyświetla się licznik trafień. W pobliżu prawego-górnego narożnika pulpitu wyświetla się licznik sekund. Statek przemieszcza się od lewej-do-prawej, na wysokości ok. 1/5 od górnego obrzeża pulpitu. Każde odrębne przemieszczenie odbywa się z nieco inną szybkością i na nieco innej wysokości. Pocisk przemieszcza się w pionie, w odległości ok. 1/5 od prawego obrzeża pulpitu. Statek uznaje się za trafiony, gdy środek obrazu pocisku znajdzie się w odległości mniejszej niż 10 pikseli od środka obrazu statku. Pocisk znika po uderzeniu w statek albo gdy wpłynie pod górne obrzeże pulpitu. Punkt wysłania pocisku pokazuje pionowa kreseczka wykreślona przy dolnym obrzeżu pulpitu. Rozgrywka Rozgrywka zaczyna się natychmiast po odpaleniu apletu i trwa przez playTime sekund. Jeśli nie poda się parametru playTime albo gdy nie jest on liczbą większą 30 s, to rozgrywka trwa 30 s. Po upływie czasu rozgrywki, w miejscu licznika czasu, wyświetla się napis Done! Po zakończeniu rozgrywki, można rozpocząć następną, ale dopiero po kliknięciu w obrębie pulpitu. Naciśnięcie spacji gdy statek jest widoczny, wysyła pocisk. Trafienie pocisku w statek zwiększa licznik trafień. Naciśnięcie spacji, gdy statek jest niewidoczny, jest ignorowane. Naciśnięcie spacji, gdy jest widoczny pocisk, jest ignorowane. Naciśnięcie spacji po zakończeniu rozgrywki, ale przed rozpoczęciem następnej, jest ignorowane. Wykonanie ignorowanej akcji jest sygnalizowane. Wymagania projektowe Odmierzanie czasu odbywa się w odrębnym wątku. Ruchem statku steruje odrębny, nie kończący się, wątek. Ruchem pocisku steruje odrębny wątek, powoływany na czas wystrzału. Wybuchem steruje odrębny wątek powoływany na czas wybuchu. Statek nie pojawia się przed wyświetleniem wyrzutni i liczników. Nowy statek nie pojawia się, gdy jest jeszcze widoczny pocisk. Minimalne rozmiary apletu wynoszą 200 x 200 pikseli i nie są ustalone. Macierzyste okno apletu może być ikonizowane i dezikonizowane. Implementacja Obraz statku jest zawarty w pliku Ship.gif, a pocisku w pliku Shot.gif. Fazy wybuchu znajdują się w plikach FadeX.gif, dla X=0, 1, ... 6. Plikami dźwiękowymi są Explode.au i Ignore.au. Pliki znajdują się w katalogu określonym przez funkcję getDocumentBase. Obrazy statku i pocisku są przezroczystymi obrazami GIF. =============================================== import java.applet.*; import java.awt.*; import java.awt.event.*; import java.net.URL; import java.util.Random; public class Master extends Applet implements Runnable { // czas rozgrywki protected int playTime; // liczniki trafień i czasu protected Label counter, stopper; // obrazy statku i pocisku protected Image shipImg, shotImg; protected Thread timer, drawer, cruiser, shooter; protected Image img, fadeImg[] = new Image[7]; protected Graphics gDC, mDC; protected Master master; protected Toolkit kit = Toolkit.getDefaultToolkit(); protected int w, h, count, dx, dy, shipTop, shotLeft, shotTop, shotX, shotY, shipX, shipY, shotW, shotH, shipW, shipH, shipPos; protected AudioClip explode, ignore; protected boolean shipHit, shipDrawn, isFading, shotNotDone, gameDone, stopAll, gameNotReady, drawNotReady, tooSmall = false; protected Boolean gameReady = new Boolean(true), drawReady = new Boolean(true), shotDone = new Boolean(true); protected Random rand = new Random(); public void init() { master = this; try { String time = getParameter("playTime"); if((playTime = Integer.parseInt(time)) < 30) throw new NumberFormatException(); } catch(NumberFormatException e) { playTime = 30; } counter = new Label("", Label.CENTER); stopper = new Label("****"); setLayout(null); counter.setBounds( 0, 0, 40, 20); add(counter); Dimension d = getSize(); w = d.width; h = d.height; if(w < 300 || h < 200) { counter.setText("Small"); tooSmall = true; return; } stopper.setBounds(w-40, 0, 40, 20); add(stopper); shipTop = h / 5; shotTop = h; shotLeft = 4 * w / 5; MediaTracker tracker = new MediaTracker(this); URL docBase = getDocumentBase(); explode = getAudioClip(docBase, "Explode.au"); ignore = getAudioClip(docBase, "Ignore.au"); shipImg = getImage(docBase, "Ship.gif"); shotImg = getImage(docBase, "Shot.gif"); tracker.addImage(shipImg, 0); tracker.addImage(shotImg, 0); for(int i = 0; i < 7 ; i++) { fadeImg[i] = getImage(docBase, "Phade" + i + ".gif"); tracker.addImage(fadeImg[i], 0); } try { tracker.waitForID(0); } catch(InterruptedException e) { } shipW = shipImg.getWidth(this); shipH = shipImg.getHeight(this); shotW = shotImg.getWidth(this); shotH = shotImg.getHeight(this); addKeyListener( new KeyAdapter() { public void keyReleased(KeyEvent evt) { boolean space = evt.getKeyCode() == ' '; if(space && !gameDone && !shotNotDone && shipDrawn) shooter = new ShotThread(); else ignore.play(); } } ); gDC = getGraphics(); img = createImage(w, h); mDC = img.getGraphics(); addMouseListener( new MouseAdapter() { public void mouseReleased(MouseEvent evt) { if(gameDone) start(); } } ); } public void start() { if(tooSmall) return; // za mały pulpit stopAll = false; count = 0; counter.setText(" 0"); stopper.setText(" 0"); shotNotDone = gameDone = isFading = false; gameNotReady = drawNotReady = true; // włączenie zegara (timer = new Thread(this)).start(); // uruchomienie statku cruiser = new ShipThread(); // uruchomienie kreślarza drawer = new DrawThread(); shooter = new Thread(this); // nastawienie celownika requestFocus(); } public void paint(Graphics gDC) { // gra gotowa synchronized(gameReady) { gameNotReady = false; gameReady.notifyAll(); } } public void stop() { stopAll = true; try { drawer.interrupt(); cruiser.interrupt(); shooter.interrupt(); timer.interrupt(); drawer.join(); cruiser.join(); shooter.join(); timer.join(); } catch(NullPointerException e) { // za mały pulpit; bez pocisku } catch(InterruptedException e) { // nigdy } } // =============================================== Zegar public void run() { long startTime = System.currentTimeMillis(), gameTime; do { try { Thread.sleep(1000); } catch(InterruptedException e) { return; } long time = System.currentTimeMillis(); gameTime = time - startTime; stopper.setText("" + (500 + gameTime) / 1000); } while(!stopAll && gameTime < playTime * 1000); gameDone = true; stopper.setText("Done!"); } public synchronized boolean shipWasHit() { dx = (shipX + shipW/2) - (shotX + shotW/2); dy = (shipY + shipH/2) - (shotY + shotH/2); if(shotNotDone && (dx * dx + dy * dy) < 100) { if(!shipHit) { // statek trafiony isFading = true; shipPos = shipX; new FadeThread(); counter.setText(" " + ++count); } shipHit = true; } return shipHit; } // =============================================== Statek class ShipThread extends Thread { public ShipThread() { super.start(); } public void run() { synchronized(gameReady) { try { while(gameNotReady) gameReady.wait(); } catch(InterruptedException e) { return; } } while(!stopAll) { int rnd = (rand.nextInt() >> 8) % 5; shipX = -shipW; shipY = (int)(shipTop * (1 - rnd/32.)); synchronized(shotDone) { try { while(shotNotDone) shotDone.wait(); } catch(InterruptedException e) { return; } } shipHit = false; shipDrawn = true; for(int i = 1; i < w+shipW ; i++) { try { Thread.sleep(10 + rnd); } catch(InterruptedException e) { return; } shipX++; synchronized(drawReady) { drawNotReady = false; drawReady.notify(); } if(isFading || master.shipWasHit()) break; } shipDrawn = false; } } } // =============================================== Pocisk class ShotThread extends Thread { public ShotThread() { super.start(); } public void run() { shotNotDone = true; shotX = shotLeft; shotY = shotTop; for(int i = 1; i < h+shotH+1 ; i++) { try { Thread.sleep(10); } catch(InterruptedException e) { return; } if(stopAll) return; shotY--; synchronized(drawReady) { drawNotReady = false; drawReady.notify(); } if(master.shipWasHit()) break; } synchronized(shotDone) { shotDone.notify(); shotNotDone = false; } } } // ============================================ Kreślarz class DrawThread extends Thread { public DrawThread() { super.start(); } public void run() { // czeka na zakończenie paint synchronized(gameReady) { try { while(gameNotReady) gameReady.wait(); } catch(InterruptedException e) { return; } } while(!stopAll) { // czeka na zmianę pozycji synchronized(drawReady) { try { while(drawNotReady) drawReady.wait(); } catch(InterruptedException e) { return; } } // czyści bufor mDC.clearRect(0, 0, w, h); // wykreśla wyrzutnię mDC.drawLine( shotLeft + shotW/2, h, shotLeft + shotW/2, h-3 ); // wykreśla statek mDC.drawImage(shipImg, shipX, shipY, master); // wykreśla pocisk if(shotNotDone) mDC.drawImage(shotImg, shotX, shotY, master); // przenosi bufor na ekran if(!isFading) gDC.drawImage(img, 0, 0, master); drawNotReady = true; } } } // =============================================== Wybuch class FadeThread extends Thread { public FadeThread() { super.start(); } public void run() { explode.play(); for(int i = 0; i < 7 ; i++) { gDC.clearRect( shipPos-shipW/2, shipY-shipH/2, shipW*2, shipH*2 ); gDC.drawImage( fadeImg[i], shipPos-shipW/2, shipY-shipH/2, master ); try { Thread.sleep(200); } catch(InterruptedException e) { return; } } isFading = false; } } } Jan Bielecki Grafika 2-wymiarowa Pakiet JDK 1.2 (Java 2 Platform), zawiera szereg użytecznych klas i funkcji, rozszerzających możliwości graficzne pakietu JDK 1.1. Ponieważ wykreślacz jest obecnie obiektem klasy Graphics2D, pochodnej od Graphics, więc korzystanie z nowych metod wymaga użycia odnośnika typu Graphics2D. Natomiast korzystanie z dotychczasowych metod biblioteki AWT (Abstract Windowing Toolkit) może się odbywać za pomocą odnośnika typu Graphics. Uwaga: Metoda paint i update otrzymuje odnośnik do obiektu klasy Graphics2D, ale parametr metody pozostaje typu Graphics. Dlatego często dokonuje się konwersji parametru na odnośnik typu Graphics2D. Następujący program, pokazany na ekranie Stara i nowa grafika, ilustruje zastosowanie konwersji, która umożliwia użycie nowych metod graficznych. Ekran Stara i nowa grafika ### oldnew.gif =============================================== import java.applet.Applet; import java.awt.*; public class Master extends Applet { public void paint(Graphics gDC) { // cienki okrąg gDC.drawOval(0, 0, 100-1, 100-1); // cienka linia gDC.drawLine(100, 0, 100, 200); // konwersja Graphics2D gDC2 = (Graphics2D)gDC; // szerokość 10 pikseli gDC2.setStroke(new BasicStroke(10f)); // pogrubiony okrąg gDC2.drawOval(100, 100, 100-1, 100-1); // przywrócenie domniemań gDC2.setStroke(new BasicStroke()); // cienki prostokąt gDC.drawRect(0, 0, 200-1, 200-1); } } Układ współrzędnych Pulpit składa się pikseli, ale rzędne i odcięte układu współrzędnych znajdują się między pikselami. Podczas wykreślania linii, pióro przemieszcza się między punktami układu współrzędnych, wykreślając linię na pikselach znajdujących się z prawej i poniżej pióra. Natomiast podczas wypełniania obszaru, zmiany dotyczą tylko pikseli znajdujących się we wnętrzu ścieżki, wzdłuż której przemieszcza się pióro. Dlatego wykonanie metody drawRect(0, 0, 2, 2) spowoduje zamalowanie 8 pikseli, ale wykonanie metody fillRect(0, 0, 2, 2) spowoduje zamalowanie 4 pikseli (sic!). Definiowanie kształtu Definiowanie kształtu (shape) odbywa się za pomocą obiektów klas implementujących interfejs Shape. Takimi klasami są m.in. Rectangle2D.Double, Ellipse2D.Double oraz GeneralPath. Do wypełnienia kształtu służy metoda fill, a do obrysowania kształtu metoda draw. Uwaga: Obwiednie kształtów oraz kształty bez wnętrza (np. kształt Line2D.Double) są nazywane ścieżkami (path). Określenie wykreślanie stosuje się zarówno do wypełniania jak i do obrysowywania kształtów. Następujący program, pokazany na ekranie Definiowanie kształtu, ilustruje zasady definiowania i wykreślania linii. Ekran Definiowanie kształtu ### shapes2.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.geom.*; public class Master extends Applet { public void paint(Graphics gDC) { // konwersja parametru Graphics2D gDC2 = (Graphics2D)gDC; // zdefiniowanie kształtu GeneralPath path = new GeneralPath(GeneralPath.WIND_EVEN_ODD); path.moveTo(0f, 0f); path.lineTo(100f, 50f); path.quadTo(100f, 100f, 50f, 100f); path.closePath(); // wykreślenie kształtu gDC2.draw(path); // zdefiniowanie kształtu Rectangle2D rect = new Rectangle2D.Double(100, 100, 80-1, 80-1); path = new GeneralPath(rect); // wykreślenie kształtu gDC2.draw(path); } } Wykreślanie linii Wykreślanie linii odbywa się za pomocą pióra. Pióro jest obiektem klasy implementującej interfejs Stroke. Do definiowania piór służą konstruktory klasy BasicStroke. Umożliwiają one definiowanie szerokości, wyglądu i połączeń linii. Wygląd linii kreskowanej jest zdefiniowany w tablicy, której kolejne elementy określają na przemian długość kreski i długość występującego za nią odstępu. Połączenie szerokich linii może być zaostrzone, zaokrąglone albo spłaszczone. 1) Połączenie zaostrzone (JOIN_MITER) polega na przedłużeniu zewnętrznych krawędzi linii, aż do ich spotkania. 2) Połączenie zaokrąglone (JOIN_ROUND) polega na wykreśleniu koła o promieniu równym szerokości linii. 3) Połączenie spłaszczone (JOIN_BEVEL) polega na połączeniu i wypełnieniu zewnętrznych narożników linii. Zakończenie linii może być ścięte, półkoliste albo kwadratowe. 1) Zakończenie ścięte (CAP_BUTT) polega na pozostawieniu linii bez zmian. 2) Zakończenie półkoliste (CAP_ROUND) polega na dołączeniu wypełnionego półkola o średnicy równej szerokości linii. 3) Zakończenie kwadratowe (CAP_SQUARE) polega na dołączeniu wypełnionego kwadratu o boku równym połowie szerokości linii. Następujący program, pokazany na ekranie Rysowanie linii, ilustruje zastosowanie metody setStroke. Ekran Rysowanie linii ### stroke2.gif =============================================== import java.applet.Applet; import java.awt.*; public class Master extends Applet { public void paint(Graphics gDC) { // konwersja Graphics2D gDC2 = (Graphics2D)gDC; // zdefiniowanie pióra BasicStroke myStroke = new BasicStroke( 4f, // szerokość BasicStroke.CAP_ROUND, // zakończenie BasicStroke.JOIN_BEVEL, // połączenie 0f, // wydłużenie new float[] { 10f, 20f }, // kreskowanie 0f // odskoczenie ); // wstawienie pióra do wykreślacza gDC2.setStroke(myStroke); // wykreślenie prostokąta gDC2.drawRect(50, 50, 100-1, 100-1); } } Wypełnianie obszarów Obszarami są kształty i linie. Do wypełniania obszarów kolorem, gradientem albo teksturą służy pędzel (paint). Pędzel jest obiektem klasy implementującej interfejs Paint. Wstawienie go do wykreślacza odbywa się za pomocą metody setPaint (dla koloru także setColor). Uwaga: Składowe koloru i przezroczystości są liczbami z domkniętego przedziału 0..255. Graphics2D gDC2 = (Graphics2D)getGraphics(); // kolor niebieski, półprzezroczysty (alfa = 128) gDC2.setColor(new Color(0, 0, 255, 128)); // r g b a // pióro o szerokości 40 pikseli gDC2.setStroke(new BasicStroke(40)); // wykreślenie linii gDC2.drawLine(0, 0, 200, 200); Następujący program, pokazany na ekranie Wypełnianie gradientem, ilustruje wypełnianie kształtów i linii gradientami. Ekran Wypełnianie gradientem ### paint2.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.geom.*; public class Master extends Applet { public void paint(Graphics gDC) { // konwersja parametru Graphics2D gDC2 = (Graphics2D)gDC; // zdefiniowanie gradientu GradientPaint grad = new GradientPaint( 50f, 50f, // odkąd (x,y) Color.red, // od tego koloru 150f, 150f, // dokąd (x,y) Color.yellow // do tego koloru ); // wstawienie pędzla do wykreślacza gDC2.setPaint(grad); // zdefiniowanie kształtu Ellipse2D circle = new Ellipse2D.Double(50, 50, 100-1, 100-1); // wypełnienie koła gradientem gDC2.fill(circle); // ustawienie koloru gDC2.setPaint(Color.black); // wykreślenie okręgu gDC2.draw(circle); // wstawienie pióra do wykreślacza gDC2.setStroke(new BasicStroke(30f)); // zdefiniowanie wypełnienia gDC2.setPaint(grad); Dimension d = getSize(); int w = d.width; // wypełnienie linii gradientem gDC2.draw(new Line2D.Double(0, 20, w-1, 20)); } } Metoda setPaint może być użyta do wypełnienia obszaru teksturą. Element tekstury należy zdefiniować w obiekcie klasy BufferedImage. Obraz ten jest odwzorowywany na prostokąt, który staje się wzorcem tekstury. Następujący program, pokazany na ekranie Wypełnianie teksturą, ilustruje zastosowanie buforowania do zdefiniowania tekstury. Ekran Wypełnianie teksturą ### texture.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.geom.*; import java.awt.image.*; public class Master extends Applet { public void paint(Graphics gDC) { // konwersja parametru Graphics2D gDC2 = (Graphics2D)gDC; // zdefiniowanie bufora BufferedImage img = new BufferedImage( 8, 8, // rozmiary BufferedImage.TYPE_INT_RGB // model ); // utworzenie wykreślacza Graphics2D mDC2 = img.createGraphics(); // zdefiniowanie elementu tekstury mDC2.setPaint(Color.red); mDC2.drawLine(1, 1, 6, 6); mDC2.setPaint(Color.yellow); mDC2.drawLine(1, 6, 6, 1); // zdefiniowanie wzorca tekstury Rectangle2D.Double square = new Rectangle2D.Double(0, 0, 32, 32); TexturePaint textRect = new TexturePaint(img, square); // wstawienie wzorca do wykreślacza gDC2.setPaint(textRect); // zdefiniowanie kształtu Ellipse2D circle = new Ellipse2D.Double(10, 10, 180-1, 180-1); // wypełnienie koła teskturą gDC2.fill(circle); // ustawienie koloru gDC2.setPaint(Color.black); // obrysowanie koła gDC2.draw(circle); } } Przekształcanie obiektów Każdy obiekt graficzny można przemieścić, obrócić i rozciągnąć. Opis przekształcenia afinicznego, zawarty w obiekcie klasy AffineTransform, umieszcza się w wykreślaczu przed wykonaniem obrazowania. Przekształcenie afiniczne zachowuje równoległość i prostoliniowość. Oznacza to, że po wykonaniu przekształcenia linie równoległe pozostają równoległymi, a linie proste pozostają prostymi. Uwaga: Przekształceniom można poddawać także i napisy. Rozciągnięcie napisu odbywa się względem jego odcinka bazowego. Następujący program, pokazany na ekranie Przekształcenia afiniczne, definiuje kwadrat o boku 100 pikseli, przemieszcza jego lewy-górny narożnik do punktu (50,50), rozciąga w poziomie ze współczynnikiem 0.25 oraz obraca o 30o zgodnie z ruchem wskazówek zegara. Ekran Przekształcenia afiniczne ### affine.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.geom.*; public class Master extends Applet { final double Pi = Math.PI; public void paint(Graphics gDC) { // konwersja parametru Graphics2D gDC2 = (Graphics2D)gDC; // zdefiniowanie kwadratu Rectangle2D.Double square = new Rectangle2D.Double(0, 0, 100, 100); // zdefiniowanie przekształcenia AffineTransform affine = new AffineTransform(); // zdefiniowanie przemieszczenia affine.translate(50.0, 50.0); // zdefiniowanie rozciągnięcia affine.shear(0.25, 0.0); // zdefiniowanie obrotu affine.rotate(30 * Pi / 180); // wstawienie przekształcenia do wykreślacza gDC2.setTransform(affine); // wykreślenie kwadratu gDC2.draw(square); } } Nakładanie kolorów Wykreślenie kształtu albo obrazu może uwzględniać kolor podłoża. Opis sposobu nakładania koloru, zawarty w obiekcie klasy AlphaComposite umieszcza się w wykreślaczu przed wykonaniem obrazowania. Następujący program, pokazany na ekranie Mieszanie kolorów, definiuje 2 kwadraty o boku 100 pikseli, a następnie nakłada je na siebie. Ponieważ nakładanie zdefiniowano jako półprzezroczyste, więc poprzez wierzchni kwadrat prześwituje ten, który znajduje się na spodzie. Uwaga: Sposób nakładania koloru wyraża się liczbą z domkniętego przedziału 0.0 - 1.0. Liczba 0.0 oznacza nakładanie przezroczyste (transparent), a liczba 1.0 nakładanie nieprzezroczyste (opaque). Ekran Mieszanie kolorów ### mixing.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.geom.*; public class Master extends Applet { final double Pi = Math.PI; public void paint(Graphics gDC) { // ustawienie koloru gDC.setColor(Color.red); // wykreślenie czerwonego kwadratu gDC.fillRect(50, 50, 100, 100); // konwersja parametru Graphics2D gDC2 = (Graphics2D)gDC; // zdefiniowanie zielonego kwadratu Rectangle2D.Double square = new Rectangle2D.Double(-50, -50, 100, 100); // zdefiniowanie przekształcenia AffineTransform affine = new AffineTransform(); // zdefiniowanie przemieszczenia affine.translate(100.0, 100.0); // zdefiniowanie obrotu affine.rotate(45 * Pi / 180); // wstawienie przekształcenia do wykreślacza gDC2.setTransform(affine); // ustawienie koloru gDC2.setPaint(Color.green); // zdefiniowanie mieszacza AlphaComposite blend = AlphaComposite.getInstance( AlphaComposite.SRC_OVER, 0.5f ); // wstawienie mieszacza do wykreślacza gDC2.setComposite(blend); // wykreślenie zielonego kwadratu gDC2.fill(square); } } Rozpoznawanie trafień Rozpoznanie, czy operacja wykonana za pomocą myszki dotyczy konkretnego kształtu, można wykonać za pomocą metody hit. Określa ona, czy choć jeden punkt podanego prostokąta pokrywa się z podaną ścieżką. Następujący program, pokazany na ekranie Rozpoznawanie trafień, w zależności od tego, czy kursor znajduje się w obszarze, czy poza nim, zmienia kolor obszaru z zielonego na czerwony i odwrotnie. Ekran Rozpoznawanie trafień ### detect.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.geom.*; import java.awt.event.*; public class Master extends Applet { private Graphics2D gDC2; private GeneralPath path; private Rectangle box = new Rectangle(0, 0, 1, 1); private Color color, oldColor = Color.black; public void init() { gDC2 = (Graphics2D)getGraphics(); // zdefiniowanie kształtu Ellipse2D oval = new Ellipse2D.Double(50, 50, 100-1, 100-1); path = new GeneralPath(oval); addMouseMotionListener( new MouseMotionAdapter() { public void mouseMoved(MouseEvent evt) { int x = evt.getX(), y = evt.getY(); // przemieszczenie prostokąta box.translate(x, y); // ustawienie koloru if(gDC2.hit(box, path, true)) color = Color.red; else color = Color.green; if(!color.equals(oldColor)) { gDC2.setPaint(oldColor = color); // wykreślenie kształtu gDC2.fill(path); gDC2.setPaint(Color.black); gDC2.draw(path); } box.translate(-x, -y); } } ); } public void paint(Graphics gDC) { ((Graphics2D)gDC).draw(path); } } Definiowanie obszarów Obszary są reprezentowane w obiektach klasy Area. Obszar można zdefiniować na podstawie obszaru albo kształtu. Na obszarach można wykonywać takie same operacje jak na zbiorach punktów. Obwiednię obszaru można przekształcić w ścieżkę. Następujący program, pokazany na ekranie Operacje na obszarach, wykreśla kształt powstały z różnicy 2 obszarów. Uwaga: Różnicą zbiorów jest zbiór składający się z tych elementów pierwszego, które nie należą do drugiego. Ekran Operacje na obszarach ### areas2.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.geom.*; public class Master extends Applet { private int x = 50, y = 50, w = 100, h = 100; public void paint(Graphics gDC) { Graphics2D gDC2 =(Graphics2D)gDC; Rectangle2D square = new Rectangle2D.Double(x, y, w, h); Ellipse2D circle = new Ellipse2D.Double(x+1, x+1, w-2, h-2); // zdefiniowanie obszarów Area squareArea = new Area(square), circleArea = new Area(circle); // wykonanie operacji na obszarach Area area = new Area(squareArea); area.exclusiveOr(circleArea); // utworzenie obwiedni obszaru PathIterator outline = area.getPathIterator(new AffineTransform()); // utworzenie kształtu GeneralPath path = new GeneralPath(); path.append(outline, false); // ustawienie kolorów setBackground(Color.green); gDC2.setPaint(Color.red); // wypełnienie kształtu gDC2.fill(path); } } Obcinanie wykreśleń Wykreślanie obiektów graficznych może być ograniczone do obszaru obcinania. Domyślnym obszarem obcinania jest pulpit apletu, ale może nim być dowolny fragment pulpitu opisany przez obiekt implementujący interfejs Shape. Wstawienie obszaru obcinania do wykreślacza odbywa się za pomocą metody setClip. Uwaga: Skutek użycia nie-prostokątnego obszaru obcinania zależy od systemu. W systemie Windows nie zaobserwowano żadnych ograniczeń. Następujący program, pokazany na ekranie Obcinanie wykreśleń, ilustruje skutek użycia nieprostokątnego obszaru obcinania do wykreślenia w nim obrazu w formacie GIF. Ekran Obcinanie wykreśleń ### cutdraw.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.geom.*; import java.awt.font.*; import java.net.*; public class Master extends Applet { private String string = "Isa"; private Font font = new Font("Serif", 0, 170); private Image img; public void init() { // wczytanie obrazu MediaTracker tracker = new MediaTracker(this); URL url = getDocumentBase(); img = getImage(url, "Asterix.gif"); tracker.addImage(img, 0); try { tracker.waitForID(0); } catch(InterruptedException e) { } } public void paint(Graphics gDC) { // konwersja parametru Graphics2D gDC2 = (Graphics2D)gDC; // rozmiary apletu Dimension d = getSize(); int w = d.width, h = d.height; // opis obrazowania czcionki FontRenderContext frc = gDC2.getFontRenderContext(); // zdefiniowanie tekstu TextLayout layout = new TextLayout(string, font, frc); // zdefiniowanie obwiedni Rectangle2D bounds = layout.getBounds(); float xB = (float)bounds.getX(), yB = (float)bounds.getY(), wB = (float)bounds.getWidth(), hB = (float)bounds.getHeight(); // zdefiniowanie wyśrodkowania AffineTransform affine = new AffineTransform(); affine.translate(-xB + (w-wB)/2, -yB + (h-hB)/2); // zdefiniowanie kształtu Shape outline = layout.getOutline(affine); // ustawienie obszaru obcinania gDC2.setClip(outline); // wykreślenie obrazu w obszarze obcinania gDC2.drawImage(img, -20, -20, this); } } Przekształcanie napisów Do reprezentowania napisów używa się obiektów klasy TextLayout. Napis znajduje się w obrębie domyślnej obwiedni, której współrzędnych dostarcza metoda getBounds. Współrzędne lewego-górnego narożnika obwiedni są liczone względem punktu charakterystycznego, położonego z lewej strony domyślnego odcinka bazowego, na którym spoczywa napis. Metody getAscent i getDescent dostarczają uniesienie i obniżenie znaków napisu względem odcinka bazowego. Na ekranie Parametry napisów, utworzonym za pomocą następującego programu, ujawniono wzajemne położenie napisów i ich obwiedni. Uwaga: Punkty charakterystyczne otoczono kwadratami o boku 3 piksele. Ekran Parametry napisów ### layout2.gif =============================================== import java.applet.*; import java.awt.*; import java.awt.geom.*; import java.awt.font.*; public class Master extends Applet { private Font font = new Font("Serif", Font.ITALIC, 70); public void paint(Graphics gDC) { Graphics2D gDC2 = (Graphics2D)gDC; showText(gDC2, "kaja", new Point(50, 90 )); showText(gDC2, "ewa", new Point(240, 90)); showText(gDC2, "izabela", new Point(50, 180)); } public void showText(Graphics2D gDC2, String string, Point loc) { gDC2.setColor(Color.red); gDC2.drawLine(0, loc.y, 5, loc.y); gDC2.drawLine(loc.x, 0, loc.x, 5); // rozkład tekstu FontRenderContext frc = gDC2.getFontRenderContext(); TextLayout layout = new TextLayout(string, font, frc); // wyprowadzenie napisu gDC2.setColor(Color.blue); layout.draw(gDC2, (float)loc.getX(), (float)loc.getY()); gDC2.setColor(Color.red); // utworzenie obwiedni napisu Rectangle2D bounds = layout.getBounds(); // wykreślenie punktu charakterystycznego gDC2.fillRect(loc.x-1, loc.y-1, 3, 3); // wykreślenie obwiedni (uwaga na -1) bounds.setRect( bounds.getX()+loc.getX(), bounds.getY()+loc.getY(), bounds.getWidth() - 1, bounds.getHeight() - 1 ); gDC2.draw(bounds); // linia bazowa, uniesienie i obniżenie float asc = layout.getAscent(), dsc = layout.getDescent(); gDC2.setColor(Color.black); gDC2.drawLine( (int)bounds.getX(), (int)loc.y, (int)(bounds.getX() + bounds.getWidth()), (int)loc.y ); gDC2.drawLine( (int)loc.x, (int)(loc.y - asc), (int)(loc.x + 3), (int)(loc.y - asc) ); gDC2.drawLine( (int)loc.x, (int)(loc.y + dsc), (int)(loc.x + 3), (int)(loc.y + dsc) ); gDC2.drawLine( (int)loc.x, (int)(loc.y - asc), (int)loc.x, (int)(loc.y + dsc) ); } } Napisy reprezentowane w obiektach klasy TextLayout można poddawać przekształceniom. Następujący program, pokazany na ekranie Przekształcanie napisu, ilustruje wykonywanie takich operacji. Ekran Przekształcanie napisu ### process2.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.font.*; import java.awt.geom.*; public class Master extends Applet { private String string = "Bag"; private Font font = new Font("Serif", Font.BOLD, 80); public void paint(Graphics gDC) { Graphics2D gDC2 =(Graphics2D)gDC; Dimension d = getSize(); int w = d.width, h = d.height; Point2D loc = new Point(10, 10); float xL = (float)loc.getX(), yL = (float)loc.getY(); // opis obrazowania czcionki FontRenderContext frc = gDC2.getFontRenderContext(); // zdefiniowanie tekstu TextLayout layout = new TextLayout(string, font, frc); // zdefiniowanie obwiedni Rectangle2D bounds = layout.getBounds(); float xB = (float)bounds.getX(), yB = (float)bounds.getY(), wB = (float)bounds.getWidth(), hB = (float)bounds.getHeight(); // wykreślenie tekstu layout.draw(gDC2, -xB + xL, -yB + yL); // wykreślenie obwiedni tekstu gDC2.setPaint(Color.blue); bounds.setRect(xL, yL, wB, hB); gDC2.draw(bounds); // zdefiniowanie transformacji AffineTransform affine = new AffineTransform(); affine.translate(-xB + xL, -yB + h/2); affine.shear(0.5, 0.0); affine.rotate(15 * Math.PI / 180); // zdefiniowanie kształtu Shape outline = layout.getOutline(affine); // zdefiniowanie ścieżki GeneralPath path = new GeneralPath(outline); // utworzenie obwiedni ścieżki bounds = path.getBounds(); // wykreślenie obwiedni ścieżki gDC2.setPaint(Color.yellow); gDC2.fill(bounds); // ustawienie obszaru obcinania gDC2.setClip(path); // wykreślenie linii w obszarze obcinania int limit = 2 * (int)Math.max(w, h); for(int i = 0; i < limit ; i += 3) { gDC2.setPaint(Color.red); if(i % 2 == 0) gDC2.setPaint(Color.black); gDC2.drawLine(0, i, i, 0); } } } Wykreślanie glifów Glifem jest element zbioru kształtów używany do reprezentowania znaków czcionki. Zazwyczaj jeden znak jest reprezentowany przez jeden glif. Niekiedy jednak pojedynczy znak jest reprezentowany przez więcej niż jeden glif (jak w literze () albo para znaków jest reprezentowana przez jeden glif (jak w ligaturze fi) Następujący program, pokazany na ekranie Wykreślanie glifów, ilustruje użycie glifu litery Ś jako obszaru obcinania, w którym wykreślono obraz. Ekran Wykreślanie glifów ### glyphs.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.geom.*; import java.awt.font.*; import java.net.*; public class Master extends Applet { private Image img; public void init() { MediaTracker tracker = new MediaTracker(this); URL url = getDocumentBase(); img = getImage(url, "Asterix.gif"); tracker.addImage(img, 0); try { tracker.waitForID(0); } catch(InterruptedException e) { } } public void paint(Graphics gDC) { // konwersja parametru Graphics2D gDC2 = (Graphics2D)gDC; // czcionka Font font = new Font( "Courier New CE", Font.BOLD | Font.ITALIC, 200 ); // opis obrazowania czcionki FontRenderContext frc = gDC2.getFontRenderContext(); // wektor glifów GlyphVector glyphVec = font.createGlyphVector(frc, "Ś"); // kształt litery Ś GeneralPath path = new GeneralPath(glyphVec.getOutline(0, 190)); // ustawienie obszaru obcinania gDC2.setClip(path); // wykreślenie obrazu w obszarze gDC2.drawImage(img, 0, 0, this); } } Wykreślanie splajnów Splajnem jest linia zdefiniowana przez węzły i punkty sterujące. Linia przechodzi przez węzły, a domyślna prosta przechodząca przez węzeł i punkt sterujący oraz domyślna prosta przechodząca przez dwa węzły jest styczna do linii. Splajn 2. stopnia definiują 2 węzły i 1 punkt sterujący, a splajn 3. stopnia definiują 2 węzły i 2 punkty sterujące. Następujący program, pokazany na ekranie Linia Béziera, ilustruje użycie metod do wykreślania splajnów 3. stopnia. Ekran Linia Béziera ### bezier.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.geom.*; public class Master extends Applet { public void paint(Graphics gDC) { // konwersja parametru Graphics2D gDC2 = (Graphics2D)gDC; Dimension d = getSize(); int w = d.width, h = d.height, w18 = w/8, h18 = h/8, w78 = w * 7/8, h78 = h * 7/8; // zdefiniowanie linii 3. stopnia CubicCurve2D cubic = new CubicCurve2D.Double( 0, h, // punkt początkowy w18, h18, // punkt sterujący 1 w78, h78, // punkt sterujący 2 w, 0 // punkt końcowy ); // zdefiniowanie szerokości gDC2.setStroke(new BasicStroke(30f)); // wykreślenie linii gDC2.draw(cubic); // wykreślenie punktów sterujących gDC2.setStroke(new BasicStroke(3)); gDC2.drawLine(w18, h18, w18+1, h18+1); gDC2.drawLine(w78, h78, w78+1, h78+1); } } Lekkie komponenty Komponent lekki jest obiektem klasy pochodnej od Component albo Container. Ma on zazwyczaj nie-prostokątny kształt i często zawiera obszary przezroczyste. Lekki komponent można zdefiniować w taki sposób, aby wykonanie na nim akcji powodowało zajście zdarzenia odbieranego przez obiekty nasłuchujące. Następujący program, pokazany na ekranie Lekkie komponenty, ilustruje sposób zaprojektowania lekkiego komponentu, w którym może zajść zdarzenie action. Parametrem zdarzenia jest informacja o sposobie kliknięcia komponentu. Uwaga: Rozwiązano problem przeciągania lekkiego komponentu bez migotania. Zastosowana tu metoda wykorzystuje buforowanie oraz własną metodę update. Ekran Lekkie komponenty ### sprites.gif Założenia projektowe 1. Kształt komponentu określa argument konstruktora. Argumentem może być dowolny obiekt implementujący interfejs Shape. 2. Komponent można przeciągać po pulpicie. Wyeliminowanie migotania jest realizowane w klasie pojemnika. 3. Komponent umożliwia pokrywanie obrazem (argument klasy Image) i malowanie pędzlem (argument klasy Paint). Służą do tego metody setFilling. 4. Kliknięcie i wielo-kliknięcie komponentu powoduje zajście zdarzenia action z parametrem podającym licznik kliknięcia, który w metodzie actionPerformed jest dostarczany przez metodę getActionCommand. Uwaga: Dla każdego wielo-kliknięcia zachodzi tylko jedno zdarzenie action (np. dla trój-kliknięcia nie wysyła się zdarzeń z parametrami 1 i 2). Wymaga to zastosowania pomocniczego wątku. 5. Wywołanie na rzecz komponentu metody fireEvent, powoduje zajście takiego zdarzenia action, jakby wykonano kliknięcie albo wielo-kliknięcie. 6. Metoda dispose czyni komponent niewidocznym, usuwa z jego listy wszystkie obiekty nasłuchujące oraz zwalnia wszystkie jego własne zasoby. Stanowi to przygotowanie do zniszczenia komponentu. Użycie komponentu 1. Po trój-kliknięciu komponent wypełnia się przypadkowym kolorem jednorodnym, po dwu-kliknięciu teksturą, a po kliknięciu obrazem. 2. Komponent może być przeciągany po pulpicie apletu. Odbywa się to bez migotania. 3. Wykonanie cztero- i więcej-kliknięcia powoduje zniknięcie komponentu. 4. Operacjom na komponencie towarzyszą efekty dźwiękowe. Uwaga: Dla pewnych kształtów źle działa metoda biblioteczna GeneralPath.contains. W takim wypadku należy tak zmienić metodę onSprite, aby zawsze dostarczała true. =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; import java.util.Random; import java.util.Vector; import java.awt.image.BufferedImage; import java.awt.geom.*; import java.net.URL; import java.applet.AudioClip; public class Master extends Applet { private String backFile = "Asterix.gif", faceFile = "Candle2.gif", exitFile = "Gong.au"; private Point point = new Point(0, 0); private Sprite sprite; private Random random = new Random(); private URL url; private Image backImg, imgBuf, image; private AudioClip moveClip, exitClip; private GeneralPath path; private TexturePaint pattern; private Graphics mDC; private Toolkit kit = Toolkit.getDefaultToolkit(); private int w, h; public void paint(Graphics gDC) { gDC.drawImage(backImg, 0, 0, this); super.paint(gDC); } public void update(Graphics gDC) { mDC.clearRect(0, 0, w, h); paint(mDC); gDC.drawImage(imgBuf, 0, 0, this); } public void init() { setBackground(Color.green); } public void start() { url = getDocumentBase(); backImg = getImage(url, backFile); Dimension d = getSize(); w = d.width; h = d.height; imgBuf = createImage(w, h); mDC = imgBuf.getGraphics(); setLayout(null); // zdefiniowanie lekkiego komponentu ====== image = getImage(url, faceFile); MediaTracker tracker = new MediaTracker(this); tracker.addImage(image, 9); try { tracker.waitForID(0); } catch(InterruptedException e) { } exitClip = getAudioClip(url, exitFile); path = new GeneralPath(GeneralPath.WIND_NON_ZERO); path.append(new Ellipse2D.Double(0, 0, 48, 64), true); path.closePath(); sprite = new Sprite(path); sprite.setLocation(50, 50); sprite.setFilling(image, point); add(sprite); // ========================================= // zdefiniowanie tekstury ================== BufferedImage img = new BufferedImage( 8, 8, // rozmiary BufferedImage.TYPE_INT_RGB // model ); Graphics2D mDC2 = img.createGraphics(); mDC2.setPaint(Color.red); mDC2.drawLine(1, 1, 6, 6); mDC2.drawLine(1, 6, 6, 1); Rectangle2D.Double rect = new Rectangle2D.Double(0, 0, 16, 16); pattern = new TexturePaint(img, rect); // ========================================= // zdefiniowanie obsługi komponentu sprite.addActionListener( new ActionListener() { public void actionPerformed(ActionEvent evt) { String cmd = evt.getActionCommand(); int count = Integer.parseInt(cmd); switch(count) { case 1: sprite.setFilling(image, point); break; case 2: sprite.setFilling(pattern); break; case 3: int rand = random.nextInt(); Color color = new Color(rand); sprite.setFilling(color); break; default: exitClip.play(); sprite.dispose(); remove(sprite); sprite = null; return; } kit.beep(); } } ); Sprite sprite2 = new Sprite(new Ellipse2D.Double(0, 0, 50, 50)), sprite3 = new Sprite(new Rectangle2D.Double(0, 0, 50, 50)); sprite2.setFilling(pattern); add(sprite2); sprite3.setFilling(Color.yellow); sprite3.setLocation(100, 100); add(sprite3); } public void stop() { if(sprite != null) sprite.fireEvent(0); } // obsługa lekkiego komponentu =========== SpriteWatcher class SpriteWatcher extends MouseAdapter implements MouseMotionListener, Runnable { private Sprite sprite; private Point s, p; private boolean onSprite, wasDragged, isFired; private int countToFire, countFired; private Thread thread; private Object monitor = new Object(); public SpriteWatcher(Sprite sprite) { this.sprite = sprite; thread = new Thread(this); thread.start(); } public void mousePressed(MouseEvent evt) { if(evt.isMetaDown()) return; wasDragged = false; s = evt.getPoint(); onSprite = sprite.onSprite(s); } public void mouseDragged(MouseEvent evt) { if(evt.isMetaDown()) return; wasDragged = true; int x = evt.getX(), y = evt.getY(); p = sprite.getLocation(); if(onSprite) sprite.setLocation(p.x + x - s.x, p.y + y - s.y); sprite.getParent().repaint(); } public void mouseReleased(MouseEvent evt) { if(evt.isMetaDown()) return; if(!wasDragged) { synchronized(monitor) { int count = evt.getClickCount(); if(count <= countFired) countFired = 0; countToFire = count; if(!isFired) { countToFire -= countFired; isFired = true; monitor.notify(); } } } } public void mouseMoved(MouseEvent evt) { } public void run() { while(true) { synchronized(monitor) { try { if(!isFired) monitor.wait(); } catch(InterruptedException e) { return; } } try { Thread.sleep(500); } catch(InterruptedException e) { return; } synchronized(monitor) { sprite.fireEvent(countToFire); countFired = countToFire; isFired = false; } } } public void finalize() { thread.interrupt(); try { thread.join(); } catch(InterruptedException e) { } } public void dispose() { sprite = null; finalize(); } } // lekki komponent ============================== Sprite class Sprite extends Component { private SpriteWatcher spriteWatcher; private Sprite sprite; private Shape shape; private Paint paint; private Image image; private int w, h, x, y; private Point point00 = new Point(0,0); public Sprite(Shape shape) { sprite = this; this.shape = shape; Dimension d = shape.getBounds().getSize(); w = d.width; h = d.height; setSize(w+1, h+1); spriteWatcher = new SpriteWatcher(this); addMouseListener(spriteWatcher); addMouseMotionListener(spriteWatcher); } public void paint(Graphics gDC) { Graphics2D gDC2 = (Graphics2D)gDC; gDC2.setClip(shape); if(image != null) gDC2.drawImage(image, x, y, this); if(paint != null) { gDC2.setPaint(paint); gDC2.fill(shape); } } public void setFilling(Image image, Point point) { this.image = image; x = point.x; y = point.y; paint = null; repaint(); } public void setFilling(Image image) { setFilling(image, point00); } public void setFilling(Paint paint) { this.paint = paint; image = null; repaint(); } public void fireEvent(int count) { if(actionListener != null) { actionListener.actionPerformed( new ActionEvent( this, // źródło ActionEvent.ACTION_PERFORMED, "" + count // licznik ) ); } } public boolean onSprite(Point s) { return shape.contains(s); // por.: return true; } Vector listeners = new Vector(); ActionListener actionListener; synchronized void addActionListener(ActionListener lst) { actionListener = AWTEventMulticaster. add(actionListener, lst); listeners.add(lst); } synchronized void removeActionListener(ActionListener lst) { actionListener = AWTEventMulticaster. remove(actionListener, lst); listeners.remove(lst); } public void dispose() { listeners.removeAllElements(); spriteWatcher.dispose(); setVisible(false); } } } Studium programowe Następujący program, pokazany na ekranie Wykreślanie i animacja, ilustruje użycie nieprostokątnych obszarów obcinania do uzyskania nietrywialnej animacji. Ekran Wykreślanie i animacja ### rotate.gif Założenia projektowe 1) Na środku panelu wyświetla się okrąg o średnicy równej mniejszemu z rozmiarów panelu. 2) Symetrycznie względem środka i w obrębie okręgu wyświetla się napis (np. Isabel). 3) Dowolny napis (np. Isabel) jest wykreślany czcionką Serif o maksymalnym rozmiarze. 4) Domyślna linia pozioma, dzieląca panel na połowy, dzieli na połowy małe litery napisu, takie jak a (tj. bez obniżeń jak w g, ani bez uniesień jak w h). 5) Animacja polega na wypełnieniu koła obracającymi się połówkami, z których jedna jest czarna, a druga biała. 6) Ta część napisu, która wyświetla się na połówce czarnej jest biała, a ta która wyświetla się na białej jest czarna. 7) Podczas wykonywania programu nie przewiduje się zmiany rozmiarów apletu. Implementacja =============================================== import java.applet.Applet; import java.awt.*; import java.awt.geom.*; import java.awt.font.*; public class Master extends Applet implements Runnable { private String string = "Isabel"; private String typeFace = "Serif"; private int typeStyle = Font.BOLD | Font.ITALIC; private int Frames = 18; private double Pi = Math.PI; private int w, h, frame = -1, shift, drop; private Image img; private Graphics2D gDC2, mDC2; private Font font; private int x0, y0, r; public void paint(Graphics gDC) { gDC.setFont(font); gDC.drawString(string, shift, drop); gDC.drawOval(x0-r, y0-r, 2*r-1, 2*r-1); } public void init() { setBackground(Color.yellow); Dimension d = getSize(); x0 = (w = d.width) / 2; y0 = (h = d.height) / 2; r = Math.min(w, h) / 2; gDC2 = (Graphics2D)getGraphics(); // bufor obrazu img = createImage(w, h); mDC2 = (Graphics2D)img.getGraphics(); // największa czcionka napisu font = new Font( typeFace, typeStyle, getMaxSize(2*r, h) ); // wyznaczenie rozmiarów napisu FontRenderContext frc = gDC2.getFontRenderContext(); TextLayout layout = new TextLayout(string, font, frc); Rectangle2D bounds = layout.getBounds(); int width = (int)bounds.getWidth(), height = (int)bounds.getHeight(), ascent = -(int)bounds.getY(); // wyśrodkowanie napisu shift = (int)(w - width) / 2; drop = (h - height) / 2 + ascent; // wstawienie czcionki mDC2.setFont(font); // utworzenie wątku new Thread(this).start(); } public void run() { while(true) { frame = ++frame % Frames; double fi = frame * 360 / Frames * Pi / 180; int xA = (int)(x0 + r * Math.cos(fi)), xC = (int)(x0 - r * Math.cos(fi)), yA = (int)(y0 - r * Math.sin(fi)), yC = (int)(y0 + r * Math.sin(fi)); int xB = (int)(xA + r * Math.sin(fi)), xD = (int)(xC + r * Math.sin(fi)), yB = (int)(yA + r * Math.cos(fi)), yD = (int)(yC + r * Math.cos(fi)); int xE = (int)(xA - r * Math.sin(fi)), xF = (int)(xC - r * Math.sin(fi)), yE = (int)(yA - r * Math.cos(fi)), yF = (int)(yC - r * Math.cos(fi)); Ellipse2D.Double circle = new Ellipse2D.Double(x0-r, y0-r, 2*r-1, 2*r-1); int[] xPoints = { xA, xB, xD, xC }, yPoints = { yA, yB, yD, yC }; Polygon bounds = new Polygon(xPoints, yPoints, 4); // przecięcie obszarów Area clipArea = new Area(circle), boundsArea = new Area(bounds); clipArea.intersect(boundsArea); // ustawienie obcinania mDC2.setClip(clipArea); // wykreślenie w buforze mDC2.setPaint(Color.black); mDC2.fill(circle); mDC2.setPaint(Color.white); mDC2.drawString(string, shift, drop); xPoints = new int[] { xA, xE, xF, xC }; yPoints = new int[] { yA, yE, yF, yC }; bounds = new Polygon(xPoints, yPoints, 4); // przecięcie obszarów clipArea = new Area(circle); boundsArea = new Area(bounds); clipArea.intersect(boundsArea); // ustawienie obcinania mDC2.setClip(clipArea); // wykreślenie w buforze mDC2.fill(circle); mDC2.setPaint(Color.black); mDC2.drawString(string, shift, drop); // wykreślenie na pulpicie gDC2.drawImage(img, 0, 0, this); gDC2.drawOval(x0-r, y0-r, 2*r-2, 2*r-2); try { Thread.sleep(0); // do eksperymentów } catch(InterruptedException e) { } } } public int getMaxSize(int w, int h) { int width, sizeM, sizeL = 0, sizeR = 2*h; // opis obrazowania czcionki FontRenderContext frc = gDC2.getFontRenderContext(); do { // czcionka napisu sizeM = (sizeL + sizeR) / 2; font = new Font( typeFace, typeStyle, sizeM ); TextLayout layout = new TextLayout(string, font, frc); Rectangle2D bounds = layout.getBounds(); width = (int)bounds.getWidth(); if(width > w) sizeR = sizeM; else sizeL = sizeM; } while(sizeM > sizeL); return sizeM; } } Jan Bielecki Typy predefiniowane Typami predefiniowanymi są typy orzecznikowe, arytmetyczne i łańcuchowe. Typy arytmetyczne dzielą się na całkowite (byte, short, int, long), rzeczywiste (float, double) i znakowe (char). Typami łańcuchowymi są typy obiektowe: String i StringBuffer. Uwaga: Ponieważ typ znakowy jest typem arytmetycznym, więc na jego danych można wykonywać takie same operacje jak na danych całkowitych i rzeczywistych. Typ orzecznikowy Typem orzecznikowym jest typ boolean. Zmienne typu orzecznikowego mogą przybierać wartości true (prawda) i false (fałsz). Każde wyrażenie orzecznikowe exp może być przekształcone w wyrażenie całkowite typu int za pomocą operacji exp ? 1 : 0 a każde wyrażenie całkowite exp może być przekształcone w wyrażenie orzecznikowe za pomocą operacji exp != 0 Typy całkowite Typami całkowitymi są: byte, short, int i long. Zmienne typów całkowitych są byte 8-bitowe (od -128 do 127) short 16-bitowe (od -32768 do 32767) int 32-bitowe (od -2,147,483,648 do 2,147,483,647) long 64-bitowe (od -9,223,372,036,854,775,808 do +9,223,372,036,854,775,807) Dane przypisane zmiennym całkowitym są reprezentowane w systemie uzupełnień do 2. Ma on tę właściwość, że jeśli zaneguje się wszystkie bity danej, a następnie doda do niej 1, to skutek będzie taki, jakby znak danej zmieniono na przeciwny. Liczba zakończona literą L albo l jest typu long. Typy rzeczywiste Typami rzeczywistymi są: float i double. Zmienne typów rzeczywistych są float 32-bitowe (w przybliżeniu od -3.4e38 do 3.4e38) double 64-bitowe (w przybliżeniu od -1.8e308 do 1.8e308) Dane przypisane zmiennym rzeczywistym są reprezentowane w zapisie modułu-i-znaku, a wyniki nietypowych operacji, jak na przykład 1.0 / 0 (Double.POSITIVE_INFINITY) 1.0 /-0 (Double.NEGATIVE_INFINITY) 1.0 / 0 * 0 (Double.NaN tj. NotANumber) mają wartości, które można reprezentować przez symbole podane w nawiasach. Każda liczba z wąskiego przedziału wokół 0 jest reprezentowana przez zero-ze-znakiem (tj. ujemne-zero albo dodatnie-zero). Z punktu widzenia relacji równe i nie-równe oba te zera uznaje się za równe 0. Typ liczby rzeczywistej (np. liczby 12e2) wynika jednoznacznie z jej wartości. Jako kryterium wyboru przyjmuje się oszczędność reprezentacji. Liczba rzeczywista składa się z części całkowitej, kropki, części ułamkowej, litery e, wykładnika oraz litery d albo f (np. -12.34e-3d). Każdą z liter e, d, f można zapisać jako E, D, F. Niektóre elementy liczby rzeczywistej (np. literę e wraz z wykładnikiem) można pominąć. Po takich uproszczeniach liczba nie może mieć postaci liczby całkowitej. Liczba rzeczywista zakończona literą F albo f jest typu float. Pozostałe liczby rzeczywiste są typu double. Na przykład -2.4 oraz 3e2 jest typu double, ale -2e-3f jest typu float. W tabeli Liczby rzeczywiste podano przykłady kilku liczb o takiej samej wartości. Tabela Liczby rzeczywiste ### 12d 12. 12.0 1.2e1 120e-1 ### Typ znakowy Typem znakowym jest char. Zmiennym typu znakowego są przypisywane liczby reprezentujące 16-bitowe znaki Unikodu. Unikod umożliwia reprezentowanie znaków wszystkich języków europejskich oraz większości znaków pozostałych języków, w tym ideograficznych znaków Han używanych w krajach azjatyckich. Pierwszych 256 znaków Unikodu jest identycznych ze znakami kodu ASCII Latin-1 (kod ten zawiera m.in. większość znaków języków zachodnio-europejskich). Literały typu char mają postać 'c' gdzie c jest: znakiem widocznym (np. 'a'), symbolem znaku (np. '\n'), ósemkowym kodem znaku (np. '\141') albo czterocyfrowym szesnastkowym kodem znaku (np. '\u0061'). Kody wybranych znaków Unikodu przytoczono w tabeli Kody polskich liter. Tabela Kody polskich liter ### ą ć ę ł ń ó ś ź ż \u0105 \u0107 \u0119 \u0142 \u0144 \u00f3 \u015b \u017a \u017c Ą Ć Ę Ł Ń Ó Ś Ź Ż \u0104 \u0106 \u0118 \u0141 \u0143 \u00d3 \u015a \u0179 \u017b ### Typy łańcuchowe Predefiniowanymi typami łańcuchowymi są typy obiektowe zdefiniowane przez klasy String i StringBuffer. Zarówno String, jak i StringBuffer, są klasami finalnymi (final), to jest takimi, które nie mogą być dziedziczone. Właściwość ta zapewnia im efektywną implementację. Obiekty klasy String są niemodyfikowalne, a więc jeśli rezultat pewnej operacji jest klasy String, to nie musi być odrębnym obiektem. Obiekty klasy StringBuffer są modyfikowalne. Klasa StringBuffer jest używana przede wszystkim do tworzenia nowych obiektów klasy String. W szczególności, wyrażenie 4 + "sale" jest niejawnie przekształcane w wyrażenie new StringBuffer().append(4).append("sale").toString() Klasa String Metody klasy String są używane do porównywania, porządkowania, rozpoznawania, wycinania i przekształcania łańcuchów znaków. Uwaga: Znaki łańcucha są indeksowane od 0. Jeśli metoda ma dostarczyć indeks znaku, ale oczekiwanego znaku w łańcuchu nie ma, to dostarcza wartość -1. boolean equals(Object obj) Porównuje łańcuch z łańcuchem. Jeśli łańcuchy są równe, to dostarcza wartość true. W przeciwnym razie dostarcza wartość false. String hello = "Hello"; String world = "World"; boolean result = hello.equals(world); // false int compareTo(String string) Porównuje dwa łańcuchy. Jeśli są równe, to dostarcza 0. Jeśli pierwszy jest większy, to dostarcza liczbę dodatnią, a jeśli drugi, to ujemną. Porównanie na nierówność zastępuje się porównaniem pierwszej pary znaków różnych. Jeśli takiej pary nie ma, to za mniejszy uznaje się ten łańcuch, który jest krótszy. String hello = "Hello", world = "World", text; int value = hello.compareTo(world); if(value < 0) text = "less"; else if(value == 0) text = "equal"; else text = "greater"; boolean result = text.equals("less"); // true char charAt(int pos) Dostarcza znak występujący na podanej pozycji łańcucha. np. String string = "Hello"; char chr = string.charAt(4); // 'o' int indexOf(int chr) Dostarcza indeks pierwszego wystąpienia podanego znaku w łańcuchu. np. String string = "buffer"; int result = string.indexOf('u'); //1 int indexOf(String string) Dostarcza indeks pierwszego znaku stanowiącego podany podciąg łańcucha. np. String string = "remaining"; int pos = string.indexOf("main"); // 2 String substring(int from, int to) Dostarcza podciąg łańcucha składający się ze znaków występujących "między" znakami o podanych indeksach.(od from do to–1 włącznie). np. String string = "0123456789"; String result = string.substring(2, 6); // 2345 (sic!) String toUpperCase(String string) Dostarcza łańcuch powstały z oryginału przez zamianę każdej małej litery na dużą. np. String string = "Hello"; String result = string.toUpperCase(); // HELLO String toLowerCase(String string) Dostarcza łańcuch powstały z oryginału przez zamianę każdej dużej litery na małą. np. String string = "Hello World"; String result = string.toLowerCase(); // hello world Klasa StringBuffer Metody klasy StringBuffer są używane do przekształcania ciągów znaków. W odróżnieniu od metod klasy String, na ogół nie tworzą nowych obiektów, ale tylko je modyfikują. void setCharAt(int index, char chr) Zmienia znak na podanej pozycji łańcucha. np. StringBuffer string = new StringBuffer("Hello World"); string.setCharAt(5, '*'); String result = new String(string); // Hello*World StringBuffer append(char chr) Wydłuża łańcuch o podany znak. np. StringBuffer string = new StringBuffer("Hello"); string.append('*').append("World"); String result = new String(string); // Hello*World StringBuffer append(String string) Wydłuża łańcuch o podany łańcuch. np. StringBuffer string = new StringBuffer("Hello"); string.append("World"); String result = new String(string); // HelloWorld StringBuffer append(char arr[], int offset, int len) Wydłuża łańcuch o znaki podanego wycinka tablicy. np. char arr[] = { ' ', 'W', 'o', 'r', 'l', 'd' }; StringBuffer string = new StringBuffer("Hello"); string.append(arr, 1, 5); String result = new String(string); // HelloWorld StringBuffer append(Object obj) Wydłuża łańcuch o łańcuch utworzony z podanego obiektu, po wywołaniu metody toString jego klasy. np. Double value = new Double(2 - 3.4); StringBuffer string = new StringBuffer("x"); string.append(value); String result = new String(string); // x-1.4 StringBuffer insert(int offset, char chr) Wstawia w podanym miejscu łańcucha, podany znak. np. StringBuffer string = new StringBuffer("HelloWorld"); string.insert(5, '*'); String result = new String(string); // Hello*World" StringBuffer insert(int offset, String string) Wstawia w podanym miejscu łańcucha, podany łańcuch. np. StringBuffer string = new StringBuffer("HelloWorld"); string.insert(5, "***"); String result = new String(string); // Hello***World" StringBuffer insert(int offset, char arr[]) Wstawia w podanym miejscu łańcucha, znaki podanej tablicy. np. char arr[] = { '*', ' ', '*' }; StringBuffer string = new StringBuffer("HelloWorld"); string.insert(5, arr); String result = new String(string); // Hello* *World" StringBuffer insert(int offset, Object obj) Wstawia znaki łańcucha utworzonego z podanego obiektu, po wywołaniu na jego rzecz metody toString jego klasy. np. Integer value = new Integer(2); StringBuffer string = new StringBuffer("x"); string.insert(0, value); String result = new String(string); // 2x Typy kopertowe Typami kopertowymi są predefiniowane typy obiektowe Boolean, Integer, Long, Float, Double, Character, Byte i Short. Obiekt typu kopertowego stanowi otoczkę dla zmiennej odpowiadającego mu typu podstawowego: boolean, int, long, float, double, char, byte i short. Na zmiennych typu kopertowego można wykonywać operacje, których nie można wykonywać na zmiennych typu podstawowego. static Integer valueOf(String str) Dostarcza zmienną typu kopertowego zainicjowaną wartością liczby zawartej w podanym łańcuchu. np. Integer var = new Integer(0); var = Integer.valueOf("20"); System.out.print(var); // 20 static String toString(int val) Dostarcza odnośnik do obiektu zainicjowanego łańcuchem utworzonym z argumentu. np. Integer var = new Integer(20); String str = var.toString(); System.out.print(str); // 20 static int parseInt(String str) Dostarcza zmienną typu podstawowego, zainicjowaną wartością liczby zawartej w podanym łańcuchu. Łańcuch ma reprezentować liczbę w formacie dziesiętnym. np. int val = Integer.parseInt(str); Jan Bielecki Deklaracje i instrukcje Deklaracja jest opisem właściwości klas, zmiennych i funkcji, a instrukcja jest opisem czynności. Każdą instrukcję różną od instrukcji deklaracyjnej można poprzedzić etykietą albo więcej niż jedną etykietą. Etykieta ma postać identyfikatora, a od instrukcji (albo od etykiety) oddziela ją znak : (dwukropek). Uwaga: Identyfikatorem jest spójny ciąg literowo-cyfrowy, nie zaczynający się od cyfry. Za litery uznaje się także $ (dolar) i _ (podkreślenie). Na przykład void sub(int par) { Lab: // etykieta while(true) if(par++ < 0) break Lab; // odwołanie do etykiety } Instrukcja deklaracyjna Instrukcja deklaracyjna ma postać spec dcl, dcl, ... , dcl; w której spec jest zestawem specyfikatorów, a każde dcl jest deklaratorem zmiennej albo deklaratorem z inicjatorem. Specyfikatory Specyfikatory służą do określenia typu zmiennych i rezultatów funkcji, a ponadto określają dodatkowe atrybuty zmiennych, funkcji i klas, takie jak dostępność private, protected, public statyczność static rodzimość native ustaloność final synchroniczność synchronized nietrwałość transient Na przykład public final class Main { public static final double Pi = 3.14; // ... } Klasa Main jest publiczna (dostępna z wszystkich klas) i finalna (nie może być klasą bazową innej klasy). Zmienna Pi jest publiczna (dostępna z wszystkich klas), statyczna (istnieje od chwili załadowania do chwili wyładowania klasy) i ustalona (nie może być modyfikowana). Deklaratory Deklarator służy do określenia nazwy zmiennej. Jeśli zmienna jest odnośnikiem do tablicy, to deklarator zawiera informację o liczbie jej wymiarów. Uwaga: Sposób rozmieszczenia par nawiasów kwadratowych określających liczbę wymiarów tablicy jest dowolny. W obrębie nawiasów kwadratowych nie może wystąpić żaden napis. Na przykład int var; int[] vec; String[] arr[]; // String arr[][]; String [][]arr; int mtx[2][] // błąd (liczba w obrębie nawiasów) Deklaratorami są var, vec, arr[] i mtx. Identyfikator vec jest nazwą zmiennej typu int, a identyfikatory vec i arr są nazwami odnośników do tablic zmiennych, odpowiednio 1-wymiarowej i 2-wymiarowej (wynika to z liczby nawiasów kwadratowych). Inicjatory Inicjator deklaratora może mieć jedną z następujących postaci = exp // inicjator wyrażeniowy = { phr, phr, ... , phr } // inicjator klamrowy w których exp jest wyrażeniem, a każde phr jest wyrażeniem albo frazą inicjującą o postaci { phr, phr, ... , phr } Opracowanie instrukcji deklaracyjnej powoduje zadeklarowanie wymienionych w niej zmiennych oraz ewentualne zainicjowanie ich wartościami wyrażeń wymienionych w inicjatorach. Uwaga: Inicjator klamrowy może być użyty tylko do inicjowania tablic. Przykład Instrukcje deklaracyjne int varA; int varB = 10; int var1, var2 = 20, var3; int lotto[] = { 12, 45, 6, 32, 18, 5 }; String monolog[] = { "To", "be", "or", "not", "to", "be" }; int matrix[][] = { { 10, 20, 30, 40 }, { 20, 30, 40, 50 }, { 30, 40, 50, 60 } }; int var5(50); // błąd (niewłaściwy inicjator) int varC = { 10 }; // błąd (niewłaściwy inicjator) Zmienną matrix zainicjowano odnośnikiem do 2-wymiarowej tablicy o 3 wierszach i 4 kolumnach. Zakres deklaracji W punkcie bezpośrednio za deklaratorem zaczyna się zakres deklaracji zmiennej. Kończy się on w miejscu zakończenia najwęższego bloku, w którym wystąpiła ta deklaracja. Uwaga: Blokiem jest fragment programu zawarty między parą odpowiadających sobie nawiasów klamrowych. Na przykład import java.io.IOException; public class Main { public static void main(String args[]) throws IOException { if(args.length > 0) { String allArgs = ""; for(int i = 0; i < args.length ; i++) { System.out.println( "Argument #" + i + ": " + args[i] ); allArgs += args[i] + ' '; } System.out.println("All arguments: " + allArgs); } else System.out.println("Program was called " + "with no arguments!"); System.in.read(); } } W programie występują 4 wzajemnie zawierające się bloki. Wykonanie programu powoduje wyszczególnienie jego argumentów, na przykład Argument #0: Ewa Argument #1: Jan All arguments: Ewa Jan albo wyprowadzenie napisu Program was called with no arguments! Kolidowanie deklaracji W zakresie deklaracji zmiennej nie może wystąpić inna deklaracja, w której użyto takiego samego identyfikatora. Przykład Kolidowanie deklaracji void sub(int age) { System.out.println(age); { int age = 12; // błąd (kolizja z parametrem) // ... { int age = 20; // błąd (dodatkowa kolizja) String name = "Bob"; // ... } String name = "Tom"; // ... for(int i = 0; i < 10 ; i++) // ... for(int i = 10; i > 0 ; i--) // ... } } Odnośnik name identyfikujący obiekt zainicjowany łańcuchem "Bob" nie koliduje z odnośnikiem name identyfikującym obiekt zainicjowany łańcuchem "Tom". Nie występuje kolizja między identyfikatorami i występującymi w instrukcjach for. Identyfikowanie nazw Każdy identyfikator programu ma jednoznacznie określony typ, zakres i zasięg, określone za pomocą deklaracji identyfikatora. W następującym programie, prywatne pole Master.i jest typu "int". Zakresem jego deklaracji jest blok klasy Master, a zasięgiem jest cała funkcja main oraz ta część metody one, która nie należy do zakresu zmiennych sterujących instrukcji for. import java.io.*; public class Master { private int i = 10; public static void main(String args[]) throws IOException { new Master.one(); System.in.read(); } public void one() { for(int i = 0; i < 1 ; i++) System.out.println(i); // 0 System.out.println(i); // 10 for(int i = 0; i < 1 ; i++) System.out.println(i); // 0 } public void two(int i) { for(int i = 0; true ; ) // błąd ; } } Odszukanie deklaracji Odszukanie deklaracji identyfikatora zaczyna się w najwęższym bloku programu. Jeśli nie napotka się jej w żadnym z bloków zawierającej go metody, to poszukuje się jej w najwęższej zawierającej go klasie, a potem w jej kolejnych klasach bazowych. Poszukiwanie identyfikatorów poprzedzonych słowem kluczowym this zaczyna się od klasy bieżącej, a poszukiwanie identyfikatorów poprzedzonych słowem kluczowym super zaczyna się od klasy bazowej klasy bieżącej. Podczas takiego poszukiwania uwzględnia się przeciążenie metod. public class Main extends Outer { public static void main(String args[]) { new Main().show(); } public void show() { int i = 10; { show(i); // 10 show(j); // 20 show(this.i); // 30 show(super.i); // 40 show(super.j); // 50 new Inner(this); } } class Inner { public Inner(Main main) { main.show(i); // 30 main.show(j); // 20 main.show(main.i); // 30 main.show(main.k); // 60 main.show( ((Outer)main).i // 40 ); } } public int i = 30, j = 20; } class Outer { public int i = 40, j = 50, k = 60; public void show(int i) { System.out.println(i); } } Respektowanie dostępności Podczas poszukiwania identyfikatorów respektuje się ich dostępność, przyjmując że: 1) Składnik prywatny jest dostępny tylko w jego klasie macierzystej. 2) Składnik chroniony jest dostępny tylko w jego klasie macierzystej oraz w dowolnej jej klasie pochodnej. 3) Składnik pakietowy jest dostępny w całym pakiecie jego klasy macierzystej. 4) Składnik publiczny jest dostępny bez ograniczeń, to jest wszędzie. public class Main extends Outer { private int i = 10; // pole prywatne public static void main(String args[]) { new Main().show(); } public void show() { show(i); // 10 show(j); // 20 show(k); // 30 new Inner(this); } class Inner { public Inner(Main main) { main.show(i); // 10 main.show(j); // 20 main.show(k); // 30 } } } class Outer { protected int j = 20; // pole chronione int k = 30; // pole pakietowe void show(int i) { System.out.println(i); } } Składniki chronione Odwołania do odziedziczonych składników chronionych podlegają ważnemu ograniczeniu: Jeśli klasa zawiera składnik chroniony, to w jej klasie pochodnej, znajdującej się w innym pakiecie, nie wolno odwołać się do tego składnika poprzez obiekt jej klasy bazowej, chociaż wolno odwołać się do niego poprzez obiekt tej klasy pochodnej oraz poprzez obiekt jej klasy pochodnej. W szczególności, jeśli od klasy Primary wywodzi się klasa Derived, a klasa Derived jest zdefiniowana w innym pakiecie niż Primary, to w klasie Derived nie można odwołać się do składnika klasy Primary poprzez obiekt klasy Primary, ale można odwołać się do niego poprzez obiekt klasy Derived oraz poprzez obiekt dowolnej jej klasy pochodnej. // ============================================== package janb.bases; class Primary { protected int item = 10; // pole chronione } public class Derived extends Primary { } // ============================================== package janb.mains; import janb.bases.*; import java.io.*; public class Main extends Derived { public static void main(String args[]) throws IOException { Main main = new MainX(); System.out.println(main.item); // 10 Derived derived = new Derived(); System.out.println(derived.item); // błąd System.in.read(); } } class MainX extends Main { } Składniki statyczne Składnik statyczny nie ma żadnego związku z obiektami klasy, a metoda statyczna nie ma odnośnika this. Dlatego odwołanie do składnika statycznego nie wymaga odwołania obiektowego, a jeśli wystąpi, to będzie użyte tylko do określenia klasy składnika. import java.io.*; public class Main { static int i = 10; int j = 20; public static void main(String args[]) throws IOException { Inner inner = new Inner(); // odwołanie obiektowe show(Main.i); // 10 // odwołanie nieobiektowe show(i); // 10 // odwołania obiektowe show(inner.fun("i=").i); // i=10 show(new Main().j); // 20 // brak obiektu show(j); // błąd System.in.read(); } static void show(int value) { System.out.println(value); } static class Inner extends Main { Main fun(String text) { System.out.print(text); return new Main(); } } } Inicjowanie klas Inicjowanie klasy odbywa się podczas jej ładowania. Inicjator klasy ma postać static { // .... instrukcje inicjatora } Umieszcza się go w dowolnym miejscu, w którym można umieścić składnik klasy. Uwaga: Jeśli klasa zawiera więcej niż jeden inicjator, to wykonuje się wszystkie, w kolejności wystąpienia w klasie. public class Master extends java.applet.Applet { static { System.out.println("Master loaded"); } } Inicjowanie obiektów Inicjowanie obiektu odbywa się podczas jego konstruowania. Inicjator obiektu ma postać { // .... } Umieszcza się go w dowolnym miejscu, w którym można umieścić składnik klasy. Uwaga: Inicjator obiektu jest wykonywany bezpośrednio po wywołaniu konstruktora jego klasy bazowej. Jeśli inicjatorów jest więcej, to wykonuje się wszystkie, w kolejności wystąpienia w klasie. public class Master extends java.applet.Applet { static String str; static { str = "Hello"; } public void paint(java.awt.Graphics gDC) { gDC.drawString(str, 40, 40); // Hello World } { str += " World"; } } Inicjowanie zmiennych ustalonych Zmienne ustalone (final) nie muszą być inicjowane w trakcie deklarowania. Wymaga się jedynie, aby zostały zainicjowane przed ich użyciem. class Some { static final int Value; static { Value = 0; } int i = Value; public Some() { final int j; // bez inicjatora (sic!) System.out.println(i); // 1 j = 2; System.out.println(j); // 2 } { // inicjator obiektu i++; } } Inicjowanie tablic Inicjator tablicy może wystąpić nie tylko w deklaracji tablicy, ale również w fabrykatorze, w którym zaniechano określenia liczby elementów tablicy. W szczególności instrukcję int vec[] = { 10, 20 30 }; można zastąpić instrukcjami int vec[]; // ... vec = new int [] { 10, 20, 30 }; Instrukcja pusta Instrukcja pusta ma postać ; Jej wykonanie nie wywołuje żadnych skutków. Przykład Instrukcja pusta void fun() { int i = 10000; JustLabel: ; // instrukcja pusta while(i-- != 0) ; // instrukcja pusta Fin: // błąd (brak instrukcji pustej) } Instrukcja grupująca Instrukcja grupująca ma postać { Ins Ins ... Ins } w której każde Ins jest dowolną instrukcją. Wykonanie instrukcji grupującej składa się z sekwencyjnego wykonania zawartych w niej instrukcji Ins. Uwaga: Użycie instrukcji grupującej ma na celu utworzenie z sekwencji instrukcji dokładnie jednej instrukcji. Przykład Instrukcje grupujące int i = -100; do { System.out.println(i++); i++; } while(i < 0); do System.out.println(i++); i++; // błąd (brak instrukcji grupującej) while(i < 100); Między pierwszą parą słów kluczowych do i while występuje instrukcja grupująca. Instrukcja wyrażeniowa Instrukcja wyrażeniowa ma postać exp; w której exp jest wyrażeniem. Wykonanie instrukcji wyrażeniowej składa się z opracowania wyrażenia exp, a następnie zignorowania rezultatu tego opracowania. Uwaga: Wykonanie instrukcji wyrażeniowej powinno pociągać za sobą skutki uboczne, takie jak przypisywanie i przesyłanie danych. W przeciwnym razie jej użycie jest zbyteczne. Przykłady Instrukcje wyrażeniowe int var; // ... var = 10; // przypisanie var1 = var2 = 10; // przypisanie var = System.in.read(); // przesłanie System.in.read(); // przesłanie System.out.println(var); // przesłanie Instrukcja warunkowa Instrukcja warunkowa ma postać if(exp) InsT albo if(exp) InsT else InsF w których exp jest wyrażeniem orzecznikowym, a InsT oraz InsF jest instrukcją. Jeśli InsT jest instrukcją warunkową, to obowiązuje zasada, że każdej frazie else odpowiada najbliższa z lewej, poprzedzająca ją fraza if. W szczególności, instrukcja if(exp1) if(exp2) Ins1 else Ins2 jest równoważna instrukcji if(exp1) { if(exp2) Ins1 else Ins2 } a nie instrukcji if(exp1) { if(exp2) Ins1 } else Ins2 Wykonanie instrukcji warunkowej zaczyna się od wyznaczenia wartości wyrażenia exp. Jeśli wyrażenie ma wartość true, to jest wykonywana instrukcja InsT, a w przeciwnym razie instrukcja InsF (o ile występuje). Po wykonaniu tych czynności, wykonanie instrukcji warunkowej uznaje się za zakończone. Przykład Instrukcje warunkowe if(var < 5 && var != 0) System.out.println(var); if(flag || var >= 5) var = 0; else System.out.println(var); if(var >= 0) { if(var <= 9) System.out.println(var); } else var = -1; Instrukcja grupująca została użyta po to, aby przed frazą else nie wystąpiła instrukcja grupująca bez frazy else. Gdyby pominięto nawiasy klamrowe, to rozpatrywana instrukcja przybrałaby postać if(var >= 0) if(var <= 9) System.out.println(var); else var = -1; równoważną if(var >= 0) { if(var <= 9) System.out.println(var); else var = -1; } a więc istotnie różną od rozpatrzonej uprzednio. Instrukcje iteracyjne Instrukcje iteracyjne umożliwiają cykliczne wykonywanie objętych nimi instrukcji. Instrukcja iteracyjna powinna być skonstruowana w taki sposób, aby istniała gwarancja zakończenia jej wykonywania. Instrukcja while Instrukcja while ma postać while(exp) Ins w której exp jest wyrażeniem orzecznikowym, a Ins jest instrukcją. Wykonanie instrukcji while składa się z cyklicznego wykonywania następujących czynności 1. Opracowania i wyznaczenia wartości wyrażenia exp. 2. Jeśli wyrażenie exp ma wartość true, wykonania instrukcji Ins. Jeśli podczas kolejnego (w tym pierwszego) opracowania wyrażenia exp okaże się, że ma ono wartość false, to wykonanie instrukcji while zostanie zakończone. Przykład Instrukcja while int val = 3; while(val-- != 0) System.out.println(val); // 2 1 0 Opracowanie wyrażenia val-- != 0 ma skutek uboczny w postaci zmniejszenia wartości zmiennej val. Dzięki temu, po wykonaniu trzech obrotów pętli, nastąpi zakończenie wykonywania instrukcji while. Instrukcja for Instrukcja for ma postać for(Ins0 expC ; expI) Ins w której Ins0 jest instrukcją wyrażeniową albo deklaracyjną, expC jest wyrażeniem orzecznikowym, expI jest wyrażeniem, a Ins jest instrukcją. Wykonanie instrukcji for składa się z jednokrotnego wykonania instrukcji Ins0, a następnie z cyklicznego wykonywania następujących czynności 1. Opracowania i wyznaczenia wartości wyrażenia expC. 2. Jeśli wyrażenie expC ma wartość true, wykonania instrukcji Ins oraz instrukcji wyrażeniowej utworzonej z wyrażenia expI. Jeśli podczas kolejnego (w tym pierwszego) opracowania wyrażenia expC okaże się, że ma ono wartość false, to wykonanie instrukcji for zostanie zakończone. Uwaga: Należy odnotować, że wykonanie instrukcji for(Ins0 expC ; expI) Ins ma taki sam skutek jak wykonanie instrukcji { Ins0 while(expC) { Ins expI; } } (użycie zewnętrznych nawiasów klamrowych jest istotne!). Przykład Instrukcja for for(int var = 0; var < 3 ; var++) System.out.println(var); // 0 1 2 int var; for(var = 3; var > 0 ; var--) System.out.println(var); // 3 2 1 Instrukcja do Instrukcja do ma postać do Ins while(exp); w której Ins jest instrukcją, a exp jest wyrażeniem orzecznikowym. Wykonanie instrukcji do składa się z cyklicznego wykonywania następujących czynności 1. Wykonania instrukcji Ins. 2. Opracowania i wyznaczenia wartości wyrażenia exp. 3. Jeśli wyrażenie exp ma wartość true, ponownego wykonania podanych czynności. Jeśli podczas kolejnego (w tym pierwszego) opracowania wyrażenia exp okaże się, że ma ono wartość false, to wykonanie instrukcji do zostanie zakończone. Uwaga: Instrukcja Ins jest wykonywana co najmniej jeden raz. Przykład Instrukcja do int val = 3; do System.out.println(val); // 3 2 1 0 while(val-- != 0); Instrukcje zaniechania i kontynuowania We wnętrzu pętli mogą być użyte instrukcje break; oraz continue; Wykonanie instrukcji zaniechania (break) powoduje zakończenie wykonywania pętli, natomiast wykonanie instrukcji kontynuowania (continue) powoduje wykonanie takich czynności, jakby zakończyło się wykonywanie wszystkich instrukcji objętej pętlą (co spowoduje kontynuowanie wykonywania pętli). Przykład Instrukcje break i continue int sum = 0; for(int i = 1; i <= 9 ; i++) { if(i < 3) { sum--; continue; } else if(sum > 7) break; sum = sum + 2*i; } System.out.println("Sum = " + sum); Zmienna sum przyjmuje kolejno wartości: -1, -2, 4, 12. Zakończenie wykonywania pętli for następuje znacznie wcześniej niż wynikałoby z rozpatrzenia jej pierwszego wiersza. Użycie etykiet Instrukcje zaniechania i kontynuowania mogą mieć również postać break Lab; oraz continue Lab; w których Lab jest etykietą. W takim wypadku wykonanie instrukcji zaniechania powoduje zakończenie wykonywania instrukcji iteracyjnej poprzedzonej podaną etykietą, a wykonanie instrukcji kontynuowania powoduje kontynuowanie wykonania instrukcji iteracyjnej poprzedzonej podaną etykietą. Przykład Instrukcja zaniechania z etykietą int arr[][] = { { 1, 2, 3 }, { 4, 0, 5 }, { 1, 1, 0 } }; int sum = 0; Loop: for(int i = 0; i < 3 ; i++) for(int j = 0; j < 3 ; j++) if(arr[i][j] == 0) break Loop; else sum += arr[i][j]; System.out.println(sum); // 10 Pętla sumuje kolejne wiersze tablicy, ale kończy się w chwili napotkania elementu o wartości 0. Gdyby z instrukcji zaniechania usunięto etykietę, to nastąpiłoby wyprowadzenie liczby 12. Instrukcja decyzyjna Instrukcja decyzyjna ma postać switch(exp0) { Case Case ... Case Default } w której każde Case jest frazą o postaci case exp: Ins Ins ... break; a Default jest frazą o postaci default: Ins Ins ... Ins W takim zapisie, exp0 jest wyrażeniem całkowitym, każde exp jest wyrażeniem stałym całkowitym (zazwyczaj literałem albo identyfikatorem zmiennej ustalonej), a każde Ins jest instrukcją albo jest napisem pustym. Pominięcie frazy Default powoduje jej domniemanie w postaci default : break; Wykonanie instrukcji wyboru zaczyna się od opracowania i wyznaczenia wartości wyrażenia exp0. Następnie wartość tego wyrażenia jest porównywana z wartościami wyrażeń exp zawartych w kolejnych frazach Case, aż do stwierdzenia równości. W takim wypadku są wykonywane instrukcje danej frazy. W przeciwnym razie są wykonywane instrukcje frazy Default. Po zakończeniu tych czynności wykonanie instrukcji wyboru zostanie zakończone. Uwaga: Jeśli frazy Case nie kończy instrukcja zaniechania, to bezpośrednio po wykonaniu instrukcji frazy są wykonywane instrukcje następnych fraz, aż do napotkania instrukcji zaniechania bądź powrotu albo do końca instrukcji wyboru. Na przykład void sub(int par) throws IllegalArgumentException { switch(par) { case 0: System.out.print("0"); break; case -1: System.out.print("-1"); break; case +1: System.out.print("+1"); break; default: System.out.print("Wrong value"); throw new IllegalArgumentException(); } System.out.println(); } Instrukcje powrotu Instrukcja powrotu ma postać return; albo return exp; w której exp jest wyrażeniem. Wykonanie instrukcji powrotu powoduje zakończenie wykonywania zawierającej ją funkcji. Jeśli funkcja jest rezultatowa (jest typu różnego od void), to jej rezultat jest inicjowany wartością wyrażenia exp (po ewentualnej konwersji do typu rezultatu). Przykład Instrukcja powrotu static double fun(int par) { if(par > 0) return par * par; // return (double)(par * par); else return 0.0; } Jeśli funkcja fun zostanie wywołana w instrukcji System.out.print(fun(2)); to wartością jej rezultatu będzie 4.0. Instrukcja obsługi wyjątków Instrukcja obsługi wyjątków ma postać try Block Catch Finally w której Block jest instrukcją grupującą, Catch jest zestawem fraz catch(Dcl) Block w których Dcl jest deklaracją parametru anonimowej funkcji do obsługiwania wyjątków, a Finally jest frazą finally Block Uwaga: W instrukcji musi wystąpić co najmniej jedna fraza Catch albo Finally. Na przykład int len = 3, vec[]; try { vec = new int [len]; // ... } catch(OutOfMemoryError e) { // ... } catch(Exception e) { // ... } finally { vec = null; // ... } Wykonanie instrukcji try składa się z wykonania instrukcji grupującej występującej bezpośrednio po frazie try. Jeśli podczas jej wykonywania wystąpi sytuacja wyjątkowa (spowodowana na przykład brakiem pamięci na stercie), to wykonanie instrukcji grupującej zostanie zakończone, a wysłany wówczas wyjątek (obiekt klasy wyjątku) zostanie odebrany i obsłużony przez tę pierwszą frazę catch, której parametr można skojarzyć z wysłanym wyjątkiem. Niezależnie od tego jaki był przebieg wykonania instrukcji try, ale bezpośrednio przed jej zakończeniem, jest wykonywany blok frazy finally. Jeśli żadna z fraz catch instrukcji try nie jest w stanie odebrać wyjątku, to wysyła się go do najwęższej dynamicznie obejmującej ją instrukcji try. Jeśli takiej nie ma, to kończy się wykonywania programu. Uwaga: Jeśli wystąpienie sytuacji wyjątkowej spowoduje zaniechanie dalszego wykonywania jakiegokolwiek bloku programu, to nastąpi niejawne zniszczenie wszystkich jeszcze nie zniszczonych jego zmiennych lokalnych. Na przykład import java.io.IOException; public class Main { public static void main(String args[]) throws IOException { String str = fun("Hello"); // ... System.in.read(); } public static String fun(String str) { try { int fix = 12; // ... return getStr(str); } catch(OutOfMemoryError e) { System.out.println("Buy more RAM!"); System.exit(0); return null; // wymagane! } } public static String getStr(String str) throws OutOfMemoryError { String strRef; try { strRef = new String(str); System.out.println(strRef.charAt(0)); } catch(NullPointerException e) { System.exit(1); return null; } return strRef; } } Jeśli podczas wykonywania instrukcji strRef = new String(str); zostałby wysłany wyjątek klasy OutOfMemoryError, to ponieważ nie mógłby zostać odebrany przez frazę catch(NullPointerException e) wchodzącą w skład instrukcji try funkcji getStr, więc zostałby wysłany do dynamicznie obejmującej ją instrukcji try funkcji fun, gdzie zostałby przechwycony przez frazę catch(OutOfMemoryError e) Należy zwrócić uwagę, że tuż po rozpatrzeniu fraz catch instrukcji try należącej do funkcji getStr zostanie zniszczona zmienna strRef, a tuż przed rozpatrzeniem fraz catch instrukcji try należącej do funkcji fun zostanie zniszczona zmienna fix. Jan Bielecki Operacje i operatory Operacja jest frazą wchodzącą w skład wyrażenia. O kolejności wykonania operacji decyduje sposób użycia nawiasów oraz uwzględnienie priorytetów i wiązań operatorów. We wszystkich pozostałych przypadkach wyrażenia są opracowywane ściśle od-lewej-do-prawej (dotyczy to w szczególności kolejności opracowywania argumentów funkcji). Wyrażenia stałe W pewnych miejscach programu (na przykład w wyrażeniach fraz case), mogą wystąpić tylko wyrażenia stałe całkowite. W skład wyrażenia stałego mogą wchodzić jedynie literały, identyfikatory statycznych zmiennych ustalonych zainicjowanych wyrażeniami stałymi oraz konwersje do typu całkowitego. Na przykład class Any { protected static final int one = 1; protected static final double two = 2.9; public void sub(int par) { switch(par) { case one: // ... case (int)(two + 2): break; } } } Wyrażenie (int)(two + 2) jest wyrażeniem stałym o wartości 4 (sic!). Użycie nawiasów Jeśli wyrażenie w nawiasach okrągłych jest poprzedzone operatorem, to wykonanie operacji określonej przez ten operator nastąpi dopiero po opracowaniu wyrażenia w nawiasach. Na przykład a - (b + c) Operacja odejmowania zostanie wykonana dopiero po wykonaniu operacji dodawania. Priorytety Jeśli wyrażenie można uznać za argument dwóch operacji o różnym priorytecie, to najpierw wykonuje się operację określoną przez operator o wyższym priorytecie. Na przykład a + b * c; // a + (b * c); Najpierw zostanie wykonana operacja mnożenia, a dopiero po niej operacja dodawania. Wiązania Jeśli wyrażenie może być uznane za argument dwóch operacji o równym priorytecie, to kolejność wykonania operacji jest określona przez wiązanie. Jeśli wiązanie operatorów jest lewe, to najpierw jest wykonywana operacja określona przez operator znajdujący się z lewej strony wyrażenia, a jeśli jest prawe, to najpierw ta z prawej strony. Na przykład double num; int fix; num = fix = 4; Ponieważ operator przypisania ma wiązanie prawe, więc ostatnia instrukcja jest traktowana tak jak instrukcja num = (fix = 4); // a nie jak (num = fix) = 4; Promocje Operacje na zmiennych typu byte i short są wykonywane dopiero po poddaniu ich promocjom do typu int. A zatem faktycznym argumentem operacji na takich zmiennych jest pomocnicza zmienna tymczasowa typu int. byte b1 = 10; byte b2 = (byte)-b1; byte b3 = -b1; // błąd Operacje Do tworzenia zmiennych służą fabrykatory, a do wykonywania operacji na zmiennych służą operatory. Ponieważ napisy [] (nawias, nawias), . (kropka), () (nawias, nawias) i new nie są operatorami, więc indeksowanie tablicy, wybieranie składników klasy, wywoływanie funkcji oraz tworzenie obiektów wykonuje się inaczej niż w C++. Indeksowanie tablicy Operacja indeksowania tablicy ma postać vec[ind] w której vec jest odnośnikiem do tablicy, a ind jest wyrażeniem całkowitym. Wyrażenie vec[ind] jest nazwą tego elementu tablicy identyfikowanej przez vec, który ma indeks ind. Na przykład int tab[] = { 10, 20, 30 }; Zmienna tab jest odnośnikiem do 3-elementowego wektora. Wyrażenie tab[0] jest nazwą zerowego, a wyrażenie tab[2] jest nazwą ostatniego elementu tego wektora. Wybór składnika Operacja wyboru składnika ma postać obj.name w której obj jest odnośnikiem do obiektu, a name jest identyfikatorem jego składnika. Wyrażenie obj.name jest nazwą składnika name obiektu identyfikowanego przez obj. Na przykład class Child { protected String name; protected int age = 0; public Child(String name, int age) { this.name = name; this.age = age; } public void setAge(int name) { this.age = age; } public void sub() { Child tom = new Child("Thomas", 12), bob = new Child("Robert", 0) ; // ... System.out.println(tom.name); bob.setAge(12); // ... } } Wywołanie funkcji Operacja wywołania funkcji ma postać obj.fun(arg, arg, ... , arg) w której obj jest odnośnikiem identyfikującym obiekt klasy zawierającej funkcję fun, a każde arg jest argumentem wywołania. Rezultatem wywołania funkcji typu różnego od void jest zmienna zainicjowana wartością wyrażenia występującego w instrukcji powrotu. Na przykład class Master { protected int fix; public Master met() { return new Master(); } public void sub() { met().fix = 12; met() = null; // błąd } } Typ rezultatu metody met jest odnośnikowy. Mimo to, met() nie może wystąpić po lewej stronie operatora przypisania. Zarządzanie pamięcią Zarządzanie pamięcią operacyjną odbywa się za pomocą fabrykatora. Fabrykacja polega na przydzieleniu obszaru pamięci na stercie i utworzenie w nim obiektu. Zwolnienie obszaru dokona się automatycznie, ale nie wcześniej niż w chwili, gdy ani jeden odnośnik nie będzie już identyfikował obiektu utworzonego w tym obszarze. Fabrykowanie obiektu Operacja fabrykowania obiektu ma postać new Name(arg, arg, ... , arg) w której Name jest nazwą klasy, a każde arg jest argumentem jej konstruktora. Rezultatem operacji jest odnośnik do właśnie sfabrykowanego obiektu. Na przykład String ref = new String("Hello"); ref = new String(); Fabrykowanie tablicy Operacja fabrykowania tablicy ma postać new Type [exp][exp] ... [exp] w której Type jest identyfikatorem typu (np. int albo String), a każde exp jest wyrażeniem całkowitym. Jeśli pewne z wyrażeń exp jest puste, to wszystkie następujące po nim także muszą być puste. Rezultatem operacji jest odnośnik do właśnie sfabrykowanej tablicy. Na przykład int[] vec = new int [2]; vec = new int [4]; String[][] arr = new String [3][]; arr = new String [3][4]; Po wykonaniu instrukcji int[] vec = new int [4]; odnośnik vec identyfikuje 4-elementową tablicę zmiennych typu int. Po wykonaniu instrukcji String[][] arr = new String [3][]; odnośnik arr identyfikuje 3-elementową tablicę odnośników do tablic odnośników do obiektów klasy String (każdy z 3 odnośników tablicy ma wartość null). Po wykonaniu instrukcji arr = new String [3][4]; odnośnik arr identyfikuje 3-elementową tablicę odnośników do 4-elementowych tablic odnośników do obiektów klasy String (każdy z 3 odnośników ma wartość różną od null, ale każdy z pozostałych ma wartość null). Operatory Operatory służą do zapisywania operacji. Rezultatem operacji jest zmienna. Każde wyrażenie określone przez operację jest chwilową nazwą tej zmiennej. Uwaga: Wykaz operatorów języka, z podaniem ich priorytetów i wiązań podano w Dodatku A. Operatory konwersji Operatorem konwersji jest (Type). Wiązanie operatora konwersji jest prawe. Np. (double)(int)12.8 == (double)((int)12.8) Operacja konwersji ma postać (Type)exp w której Type jest opisem typu docelowego, a exp jest wyrażeniem poddawanym konwersji. Rezultatem operacji jest zmienna typu Type, zainicjowana wartością wyrażenia exp po przekształceniu jej do typu Type. Uwaga: Jeśli typem docelowym jest tablica odnośników, to w opisie typu nie mogą wystąpić rozmiary tablicy. Na przykład String str[] = { "Hello", "World" }; Object obj[] = (Object [])str; System.out.print((String)obj[0]); // Hello System.out.print(obj[1]); // World Konwersje nie-odnośnikowe Rezultatem konwersji nie-odnośnikowej (np. z typu double do int) jest zmienna typu Type, zainicjowana wartością wyrażenia exp, po przekształceniu jej do typu Type. Na przykład System.out.print(12.8); // 12.8 System.out.print((double)(int)12.8); // 12.0 Konwersje odnośnikowe Konwersja odnośnikowa jest poprawna tylko wówczas, gdy jest wykonalna. Rezultatem poprawnej konwersji odnośnikowej (np. z typu Vector do Object) jest odnośnik zainicjowany wartością argumentu. Konwersja odnośnikowa jest poprawna statycznie tylko wówczas, gdy jest tożsamościowa (np. z typu Vector do Vector) albo gdy polega na przekształceniu z klasy do jej klasy bazowej (np. z Vector do Object), z klasy do jej klasy pochodnej (np. z Object do Vector) albo z klasy do implementowanego przez nią interfejsu (np. z Vector do Cloneable). Konwersja odnośnikowa jest poprawna dynamicznie tylko wówczas, gdy: 1) jest poprawna statycznie, 2) wyrażenie poddawane konwersji identyfikuje obiekt, który jest typu docelowego. Konwersja jest wykonalna, gdy jest poprawna statycznie i dynamicznie. Jeśli jest wykonalna, to polega na skopiowaniu odnośnika. Jeśli jest niewykonalna, to z miejsca, w którym próbowano ją wykonać, wysyła się wyjątek klasy ClassCastException. Na przykład String city = "Warsaw"; Object obj = city; // konwersja niejawna city = (String)obj; // konwersja jawna Vector vec = (Vector)obj; // błąd wykonania Operatory zwiększenia i zmniejszenia Operatorem zwiększenia jest ++ (plus, plus), a operatorem zmniejszenia jest --(minus, minus). Każdy z nich może wystąpić w postaci przyrostkowej albo przedrostkowej. Wiązanie operatora przyrostkowego jest lewe, a przedrostkowego prawe. Argument operacji zwiększenia i zmniejszenia musi być nazwą zmiennej. Np. fix-- ++++fix == ++(++fix) // błąd Operacje przyrostkowe Przyrostkowa operacja zwiększenia ma postać var++ a przyrostkowa operacja zmniejszenia ma postać var-- w których var jest wyrażeniem arytmetycznym (m.in. typu char). Rezultatem operacji var++ jest zmienna zainicjowana wyrażeniem var. Skutkiem ubocznym operacji jest zwiększenie wartości zmiennej var o 1. Rezultatem operacji var-- jest zmienna zainicjowana wyrażeniem var. Skutkiem ubocznym operacji jest zmniejszenie wartości zmiennej var o 1. Uwaga: Przyrostkowe operacje zwiększenia i zmniejszenia dostarczają w miejscu ich użycia "starą" wartość zmiennej. Na przykład int fix = 12; int fix1 = fix--; System.out.print(fix); // 11 System.out.print(fix1); // 12 Operacje przedrostkowe Przedrostkowa operacja zwiększenia ma postać ++var a przedrostkowa operacja zmniejszenia ma postać --var w których var jest wyrażeniem arytmetycznym (m.in. typu char). Rezultatem operacji ++var jest zmienna var. Skutkiem ubocznym operacji jest zwiększenie wartości zmiennej var o 1. Rezultatem operacji --var jest zmienna var. Skutkiem ubocznym operacji jest zmniejszenie wartości zmiennej var o 1. Uwaga: Przedrostkowe operacje zwiększenia i zmniejszenia dostarczają w miejscu ich użycia "nową" wartość zmiennej. Na przykład int fix = 10; int fix1 = ++fix; System.out.print(fix); // 11 System.out.print(fix1); // 11 Operatory znakowe Operatorami znakowymi są + (plus) i - (minus). Ich wiązanie jest prawe. Np. -+var == -(+var) Operacja zachowania znaku ma postać +exp a operacja zmiany znaku ma postać -exp w których exp jest wyrażeniem arytmetycznym. Rezultat zachowania znaku ma taką samą wartość jak exp. Rezultat zmiany znaku ma wartość przeciwną do exp. Na przykład char fix = '2'; System.out.print(fix); // 2 System.out.print(+fix); // 50 (kod cyfry 2) Operatory czynnikowe Operatorami czynnikowymi są * (gwiazdka), / (skośnik) i % (procent). Operatory czynnikowe są używane w operacjach mnożenia (*), dzielenia (/) i reszty z dzielenia (%). Wiązanie operatorów czynnikowych jest lewe. Np. a / b * c == (a / b) * c Operator * Operacja mnożenia ma postać expL * expR w której expL i expR są wyrażeniami arytmetycznymi. Rezultatem mnożenia jest zmienna zainicjowana iloczynem argumentów. Na przykład System.out.print('#' * 2); // 70 Operator / Operacja dzielenia ma postać expL / expR w której expL i expR są wyrażeniami arytmetycznymi. Rezultatem dzielenia jest zmienna zainicjowana ilorazem lewego i prawego argumentu. Jeśli oba argumenty są całkowite, to rezultat także jest całkowity. Na przykład System.out.print(5.0 / 2); // 2.5 System.out.print(5 / 2); // 2 System.out.print(1 / 2 * 4); // 0 System.out.print(1. / 2 * 4); // 2.0 Operator % Operacja wyznaczenia reszty z dzielenia ma postać expL % expR w której expL i expR są wyrażeniami arytmetycznymi całkowitymi. Rezultatem reszty z dzielenia jest zmienna zainicjowana resztą z dzielenia lewego argumentu przez prawy. Znak reszty jest taki sam jak znak lewego argumentu. Przyjmuje się z definicji, że a % b == a - (a / b) * b Na przykład System.out.print(14 % 3); // 2 System.out.print(14 % -3); // 2 System.out.print(-14 % 3); // -2 Operatory składnikowe Operatorami składnikowymi są + (plus) i - (minus). Operatory składnikowe są używane w operacjach dodawania i odejmowania. Wiązanie operatorów składnikowych jest lewe. Np. a - b - c == (a - b) - c Operator + Operacja dodawania ma postać expL + expR w której expL i expR są wyrażeniami arytmetycznymi, znakowymi albo łańcuchowymi. Rezultatem dodawania jest zmienna zainicjowana sumą argumentów. Na przykład System.out.print(2 + 3); // 5 System.out.print('#' + 1); // 36 System.out.print('a' + 0); // 97 System.out.print('a' + "0"); // a0 Operator - Operacja odejmowania ma postać expL - expR w której expL i expR są wyrażeniami arytmetycznymi. Rezultatem odejmowania jest zmienna zainicjowana różnicą lewego i prawego argumentu. Na przykład System.out.print(2 - 3); // -1 System.out.print('z' - 'a'); // 25 Operator pochodzenia Operatorem pochodzenia jest instanceof. Wiązanie operatora pochodzenia jest lewe. Np. ((Object)new String()) instanceof String Operacja pochodzenia ma postać exp instanceof Name w której exp jest wyrażeniem odnośnikowym, a Name jest identyfikatorem klasy. Rezultatem operacji jest orzecznik o wartości orzeczenia: „odnośnik exp identyfikuje obiekt, który jest klasy Name albo jej klasy pochodnej”. Na przykład class Person { // ... } class Woman extends Person { // ... } class Master { public static void sub(Person person) { if(person instanceof Woman) { Woman woman = (Woman)person; // ... } } } Instrukcja Woman woman = (Woman)person; zostanie wykonana tylko wówczas, gdy odnośnik person identyfikuje obiekt klasy Woman. Tak się stanie, gdy funkcja sub zostanie wywołana za pomocą instrukcji Master.sub(new Woman()); ale nie stanie się, jeśli zostanie wywołana za pomocą instrukcji Master.sub(new Person()); Operatory porównania Operatorami porównania są: < (mniejsze), > (większe), <= (mniejsze, równe), >= (większe, równe), == (równe), != (nie równe). Dwa ostatnie mają priorytet niższy niż pozostałe. Wiązanie operatorów porównania jest lewe. Np. a < b < c == (a < b) < c Operacja porównania ma postać expL @ expR w której @ jest dowolnym operatorem porównania, a expL i expR są wyrażeniami: oba arytmetycznymi, oba orzecznikowymi albo oba odnośnikowymi. Rezultatem operacji jest zmienna orzecznikowa o wartości orzeczenia: „relacja określona przez porównanie jest prawdziwa”. Porównania arytmetyczne Porównanie zmiennych arytmetycznych (w tym zmiennych typu char) polega na porównaniu ich wartości liczbowych. Na przykład System.out.print(1.2 >= 1); // true Porównania orzecznikowe Porównanie zmiennych orzecznikowych polega na porównaniu ich wartości orzecznikowych. Zezwala się na użycie tylko operatorów równe (==) i nie-równe (!=). Na przykład boolean flag = true; System.out.print(flag != false); // true Porównania odnośnikowe Porównanie odnośników może być tylko na równość (==) i nie-równość (!=). Jeśli oba odnośniki identyfikują ten sam obiekt (w szczególności tablicę), to rezultat porównania na równość ma wartość true, a w przeciwnym razie ma wartość false. Analogicznie definiuje się porównanie na nie-równość. Na przykład String one = "Hello" + "World", two = "HelloWorld"; System.out.print(one == two); // true System.out.print(one + "" != one); // false Operatory orzecznikowe Operatorami orzecznikowymi są: ! (zaprzeczenie), && (koniunkcja) i || (dysjunkcja). Najwyższy priorytet ma zaprzeczenie, niższy koniunkcja, a najniższy dysjunkcja. Wiązanie operatora zaprzeczenia jest prawe, a pozostałych operatorów lewe. Np. a || !b && c == a || ((!b) && c) Operator ! Operacja zaprzeczenia ma postać !exp w której exp jest wyrażeniem orzecznikowym. Rezultatem operacji jest orzecznik. Ma on wartość true tylko wówczas, gdy jej argument ma wartość false. Na przykład boolean flag = true; System.out.print(!flag); // false System.out.print(!(12 > 3); // false Operator && Operacja koniunkcji ma postać expL && expR w której expL i expR są wyrażeniami orzecznikowymi. Rezultatem operacji jest orzecznik. Ma on wartość true tylko wówczas, gdy oba jej argumenty mają wartość true. Uwaga: Jeśli wyrażenie expL ma wartość false, to nie opracowuje się wyrażenia expR. Na przykład int vec[] = { 10, 20, 30 }; int chr = System.in.read(); boolean flag = chr >= '0' && chr <= '2'; if(flag && chr == vec[chr - '0']) System.out.println(); Operacja == jest wykonywana tylko wówczas, gdy zmienna flag ma wartość true. Operator || Operacja dysjunkcji ma postać expL || expR w której expL i expR są wyrażeniami orzecznikowymi. Rezultatem operacji jest orzecznik. Ma on wartość false tylko wówczas, gdy oba jej argumenty mają wartość false. Uwaga: Jeśli wyrażenie expL ma wartość true, to nie opracowuje się wyrażenia expR. Na przykład int vec[] = { 10, 20, 30 }; int chr = System.in.read(); boolean flag = chr < '0' && chr > '2'; if(flag || chr == vec[chr - '0']) System.out.println(); Operacja == jest wykonywana tylko wówczas, gdy zmienna flag ma wartość false. Operatory bitowe Operatorami bitowymi są: ~ (zanegowanie bitów), & (iloczyn bitów), | (suma bitów), ^ (suma modulo 2 bitów), << (przesunięcie bitów w lewo), >> (przesunięcie bitów w prawo), >>> (przesunięcie bitów w prawo-bez-znaku). Najwyższy priorytet ma zanegowanie, niższy przesunięcia, a potem kolejno: iloczyn, suma modulo 2 i suma. Wiązanie operatora zanegowania bitów jest prawe, a pozostałych lewe. Np. a || !b && c << d == a || ((!b) && (c << d)) Operator ~ Operacja zanegowania bitów ma postać ~exp w której exp jest wyrażeniem całkowitym. Rezultatem operacji jest zmienna takiego samego typu jak exp, po zanegowaniu każdego jej bitu. Uwaga: Negacją bitu 1 jest bit 0, a negacją bitu 0 jest bit 1. Na przykład int red = 1, green = 2, blue = 4; int hue = red | green; // 00 ... 011 (kolor żółty) hue = ~hue; // 11 ... 100 (kolor niebieski) Trzy najmniej znaczące bity zmiennej hue reprezentują jeden z 8 kolorów. Wykonanie operacji zanegowania bitów powoduje zmianę koloru na dopełniający. Operator & Operacja iloczynu bitów ma postać expL & expR w której expL i expR są wyrażeniami całkowitymi. W celu utworzenia rezultatu operacji, zmienne expL i expR poddaje się konwersjom do typu wspólnego, a następnie każdy bit rezultatu tworzy się z odpowiadających sobie bitów argumentów, wyznaczając ich iloczyn logiczny. Uwaga: Iloczyn logiczny pary bitów ma wartość 1 tylko wówczas gdy oba bity są jedynkowe. Na przykład int fix = 6; // 00 ... 110 int mask = '\u0003'; // 00 ... 011 fix = fix & ~mask; System.out.print(fix); // 4 (00 ... 100) Wykonanie operacji na zmiennej fix powoduje wyzerowanie tych wszystkich jej bitów, które w masce mask są jedynkowe. Operator ^ Operacja sumy modulo 2 bitów ma postać expL ^ expR w której expL i expR są wyrażeniami całkowitymi. W celu utworzenia rezultatu operacji, zmienne expL i expR poddaje się konwersjom do typu wspólnego, a następnie każdy bit rezultatu tworzy się z odpowiadających sobie bitów argumentów, wyznaczając ich sumę logiczną modulo 2. Uwaga: Suma logiczna modulo 2 pary bitów ma wartość 1 tylko wówczas gdy bity są różne. Na przykład int fix = 6; // 00 ... 110 int mask = '\u0003'; // 00 ... 011 fix = fix ^ mask; System.out.print(fix); // 5 (00 ... 101) Wykonanie operacji na zmiennej fix powoduje zanegowanie tych wszystkich jej bitów, które w masce mask są jedynkowe. Operator | Operacja sumy bitów ma postać expL | expR w której expL i expR są wyrażeniami całkowitymi. W celu utworzenia rezultatu, zmienne expL i expR poddaje się konwersjom do typu wspólnego, a następnie każdy bit rezultatu tworzy się z odpowiadających sobie bitów argumentów, wyznaczając ich sumę logiczną. Uwaga: Suma logiczna pary bitów ma wartość 0 tylko wówczas gdy oba bity są zerowe. Na przykład int fix = 5; // 00 ... 101 int mask = '\u0003'; // 00 ... 011 fix = fix | mask; System.out.print(fix); // 7 (00 ... 111) Wykonanie operacji na zmiennej fix powoduje ustawienie tych wszystkich jej bitów, które w mask są jedynkowe. Operator << Operacja przesunięcia bitów w lewo ma postać exp << N w której exp i N są wyrażeniami całkowitymi. Bity rezultatu tworzy się z bitów zmiennej exp, po przesunięciu ich o N pozycji w lewo. Uwaga: Podczas przesuwania w lewo bity najbardziej znaczące są odrzucane, a na pozycje najmniej znaczące wchodzą bity 0. Na przykład int fix = 7; // 00 ... 0111 fix = fix << 2; System.out.print(fix); // 28 (00 ... 011100) Bity zmiennej fix przesunięto o 2 pozycje w lewo. Operator >> Operacja przesunięcia bitów w prawo ma postać exp >> N w której exp i N są wyrażeniami całkowitymi. Bity rezultatu tworzy się z bitów zmiennej exp po przesunięciu ich o N pozycji w prawo. Uwaga: Podczas przesuwania w prawo bity najmniej znaczące są odrzucane, a bit najbardziej znaczący nie ulega zmianie. Na przykład int fix = 15; // 00 ... 01111 fix = fix >> 2; System.out.print(fix); // 3 (00 ... 00011) Bity zmiennej fix przesunięto o 2 pozycje w prawo. Operator >>> Operacja przesunięcia bitów w-prawo-bez-znaku ma postać exp >>> N w której exp i N są wyrażeniami całkowitymi. Bity rezultatu tworzy się z bitów zmiennej exp po przesunięciu ich o N pozycji w prawo. Uwaga: Podczas przesuwania w prawo bity najmniej znaczące są odrzucane, a bit najbardziej znaczący jest zerowany. Na przykład int fix = -1; // 111 ... 111 fix = fix >>> 30; System.out.print(fix); // 3 (000 ... 011) Bity zmiennej fix przesunięto o 30 pozycji w prawo. Operator warunku Operatorem warunku jest ?: (pytajnik, dwukropek). Wiązanie operatora warunku jest prawe. Np. a ? b : c ? d : e == a ? b : (c ? d : e) Operacja warunku ma postać exp ? expT : expF w której exp jest wyrażeniem orzecznikowym, a expT i expF są wyrażeniami dowolnego typu. Wyrażenie warunkowe jest nazwą zmiennej typu wspólnego Type wyrażeń expT i expF o wartości (Type)expT dla exp o wartości true (Type)expF dla exp o wartości false Uwaga: W każdym przypadku jest opracowywane wyrażenie exp oraz dokładnie jedno z wyrażeń expT i expF. Na przykład int fix, fix1 = 10, fix2 = 20; fix = System.in.read(); fix = (fix > 0 ? fix1 : fix2); fix = fix > 0 ? fix1 : fix2; // identyczne z poprzednim System.out.print(fix); // 10 (sic!) (fix > 0 ? fix1 : fix2) = 4; // błąd Operatory przypisania Operatorem przypisania jest = (równa się) oraz każdy operator o postaci @= w której @ jest jednym z następujących operatorów * / % + - << >> >>>= ^ & | Wiązanie operacji przypisania jest prawe. Np. a = b += c == a = (b += c) Operator = Prosta operacja przypisania ma postać var = exp w której var jest nazwą zmiennej, a exp jest wyrażeniem. Na przykład int fix = 10; // zainicjowanie fix = 20; // przypisanie ++(fix = 20); // błąd Jeśli var jest typu arytmetycznego, to exp musi być typu arytmetycznego. Jeśli jest typu odnośnikowego Name, to wyrażenie exp musi identyfikować obiekt klasy Name albo jej klasy pochodnej. Wykonanie prostej operacji przypisania składa się z wyznaczenia wartości wyrażenia exp, poddania jej ewentualnej konwersji do typu zmiennej var, a następnie przypisania zmiennej var. Uwaga: Jeśli typ wyrażenia exp jest różny od typu zmiennej var, to wymaga się, aby konwersja mogła być wykonana niejawnie. Na przykład int fix; fix = 12; fix = (int)4.8; fix = 4.8; // błąd (brak konwersji) String name = "Isabel"; Object obj; obj = name; // obj = (Object)name; name = (String)obj; name = obj; // błąd (brak konwersji) Operatory @= Rozszerzona operacja przypisania ma postać var @= exp Jest ona równoważna operacji var = var @ exp wykonanej tak, aby opracowanie wyrażenia var było jednokrotne. Na przykład int arr[] = { 10, 20 }, fix = 0; arr[fix += 1] += 3; System.out.print(fix); // 1 Instrukcja arr[fix += 1] += 3; // zwiększenie fix o 1 jest wykonywana tak jak fix += 1; arr[fix] = arr[fix] + 3; a więc nie jest równoważna instrukcji arr[fix += 1] = arr[fix += 1] + 3; // zwiększenie fix o 2 Jan Bielecki Java po C++ U źródeł akceptacji Javy leży jej podobieństwo do ANSI C++. Miało ono znaczący wpływ na szybki rozwój języka, który w ciągu zaledwie 3 lat zajął pozycję, jaką Fortran, Cobol, Pascal i C++ osiągały dopiero po ponad 10 latach. Z C++ zapożyczono większość składni. Zachowano hermetyzację, dziedziczenie i polimorfizm. Zrezygnowano z dyrektyw, struktur, unii, wyliczeń, wskaźników, wielodziedziczenia i złożonych konwersji. Z ANSI C++ wzięto typ orzecznikowy i wyjątki, ale zrezygnowano z list inicjacyjnych, argumentów domniemanych, szablonów i przestrzeni oraz z możliwości definiowania operatorów. Zapewniono przenośność programów między dowolnymi platformami sprzętowymi i systemowymi, umożliwiono zdalne wywoływanie metod, wbudowano w język mechanizmy współbieżności i odśmiecania oraz uniemożliwiono wyrządzanie szkód przez aplety pochodzące z niepewnych źródeł. Odnośniki Operacje na dynamicznych strukturach danych wykonuje się za pomocą odnośników. Jest to możliwe, ponieważ odnośnikom można przypisywać odniesienia. Dzięki temu, bez trudu można zapisać następujący program posługujący się jednokierunkową listą do implementowania stosu. Uwaga: Polecenie import oraz instrukcja zawierająca łańcuch Wait mają na celu umożliwienie zapoznania się z wynikami programu. import java.awt.*; class List { private Item head = null; public void push(int value) { Item item = new Item(value); item.setNext(head); head = item; } public int pop() { int val = head.getVal(); head = head.getNext(); return val; } class Item { private Item nextItem = null; private int val; public Item(int i) { val = i; } public Item getNext() { return nextItem; } public void setNext(Item item) { nextItem = item; } public int getVal() { return val; } } class Scan { private Item item; public Scan(List list) { item = list.head; } public Item nextItem() { Item theItem = item; if(theItem != null) item = item.getNext(); else item = head; return theItem; } } } public class Main { private static final int Limit = 10; public static void main(String args[]) { List list = new List(); for(int i = 0; i < Limit ; i++) { list.push(i); } for(int i = 0; i < Limit/2 ; i++) { int val = list.pop(); System.out.println(val); } list.pop(); System.out.println(); List.Scan scan = list.new Scan(list); List.Item item = null; while((item = scan.nextItem()) != null) { int val = item.getVal(); System.out.println(val); } new Frame("Wait").setVisible(true); } } Klasy Klasa jest opisem rodziny obiektów. Postać obiektów jest określona przez pola klasy, a operacje na obiektach są określone przez jej konstruktory i metody. Ponieważ nie ma list inicjacyjnych, więc inicjowanie pól obiektu odbywa się w ciele konstruktora. Poza polami, konstruktorami i metodami klasa może zawierać także pola i funkcje statyczne. Takie składniki klasy nie są związane z poszczególnymi obiektami, ale należą do całej klasy. Dostępność składnika klasy jest określana osobno dla każdego składnika. Jeśli dostępności składnika nie określi się jawnie, to będzie on dostępny w pakiecie, do którego należy jego klasa. public class Complex { // publiczna klasa protected double re; // chronione pole private double im; // prywatne pole public Complex (double re, double im) // publiczny konstruktor { this.re = re; this.im = im; } double abs() // pakietowa metoda { return Math.sqrt(re * re + im * im); } } Klasy i interfejsy Każda klasa, z wyjątkiem klasy Object, jest klasą pochodną dokładnie jednej klasy oraz może implementować dowolnie wiele interfejsów. Interfejs jest klasą abstrakcyjną, która zawiera tylko deklaracje metod i definicje zmiennych. Jeśli pewna klasa implementuje interfejs, a nie ma być klasą abstrakcyjną, to musi dostarczyć definicje wszystkich metod zadeklarowanych w tym interfejsie. Uwaga: Implementowanie interfejsu stosuje się najczęściej wówczas, gdy zestaw klas ma bardzo odległego albo niedostępnego przodka, ale gdy ma wspólną cechę, którą można wyrazić takimi słowami jak: skalowalna, przemieszczalna, przeliczalna, wykonywalna, itp. class Shape { // ... } interface Drawable { // ... public void draw(Graphics gDC); } class DrawableShape extends Shape implements Drawable { // ... public DrawableShape() { } public void draw(Graphics gDC) { // ... } } Zmienne i obiekty Zmienną jest obszar pamięci, w którym przechowuje się odniesienia albo dane typu predefiniowanego: byte, short, long, float, double, boolean i char. Rozmiar zmiennej jest ściśle określony. W szczególności zmienne typu int są 32-bitowe, a zmienne typu char są 16-bitowe. Obiektem jest zestaw zmiennych o typach określonych przez deklaracje pól opisującej go klasy. Tablicą jest zestaw zmiennych identycznego typu, określony w miejscu utworzenia tablicy. Zmienne proste tworzące obiekt albo element tablicy są ich elementami podstawowymi. Uwaga: Zmienną prostą jest zmienna jednego z nieobiektowych typów predefiniowanych (np. int i double, ale nie String). Każda tablica jest obiektem wyposażonym w pole length. Zmienna typu int opisana przez to pole ma wartość równą liczbie elementów tablicy. String[] names = { "Isa", "Eva", "Jan" }; System.out.println(names.length); // 3 int[][] values = { { 10 }, { 20, 30, 40 } }; System.out.println(values.length); // 2 System.out.println(values[1].length); // 3 Deklaracja obiektu i tablicy jest deklaracją odnośnika, w którym przechowuje się odniesienia do obiektów albo do tablic. Odnośnikom nie-interfejsowym typu Type można przypisywać odniesienia do obiektów klasy Type oraz odniesienia do obiektów dowolnej jej klasy pochodnej, a odnośnikom interfejsowym typu Type można przypisywać odniesienia do obiektów dowolnych klas implementujących interfejs Type. W szczególności Panel panel; jest deklaracją odnośnika typu Panel, któremu można przypisywać m.in. odniesienia do obiektów klasy Panel oraz Applet, a Serializable serial; jest deklaracją interfejsowego odnośnika typu Serializable, któremu można przypisywać m.in. odniesienia do obiektów klas Color i Cursor (ponieważ implementują interfejs Serializable). W zasięgu następujących deklaracji class String { private char value[]; private int offset, count; // ... } class Child { private String name; private int age; // ... } każdy obiekt klasy String składa się z odnośnika do tablicy o elementach podstawowych typu char oraz z 2 zmiennych typu int, a każdy obiekt klasy Child składa się z odnośnika typu String do obiektów klasy String oraz ze zmiennej typu int. Elementami podstawowymi obiektu klasy Child są: odnośnik typu char[] oraz 3 (sic!) zmienne typu int. Fabrykowanie obiektów Zadeklarowanie odnośnika nie powoduje utworzenia obiektu. W celu utworzenia obiektu należy użyć fabrykatora new TypObiektowy(Arg, Arg, ... , Arg) Jego rezultatem jest odnośnik zainicjowany odniesieniem do właśnie sfabrykowanej zmiennej. public void paint(Graphics gDC) { Point point; // deklaracja odnośnika point = new Point(10, 20); // przypisanie odniesienia gDC.drawLine(0, 0, point.x, point.y); } Zmienne lokalne Zakresem i jednocześnie zasięgiem deklaracji zmiennej lokalnej (w tym parametru) składowej jest cały blok w którym wystąpiła deklaracja, ale zasięgiem deklaracji zmiennych sterujących instrukcji for jest tylko ciało tej instrukcji. void Sub(int x, int y) { for(int i = 0; false ; ); for(int i = 1; false ; ); // dobrze (w ANSI C++ błąd!) int v = i; // błąd (nieznany inicjator) int j = 2; for(int j = 2; false ; ); // błąd (ponowna deklaracja) int y; // błąd (ponowna deklaracja) int z = 10; int v = 20; { int v = 30; // błąd (ponowna deklaracja) int u = 40; } { int u = 50; // dobrze! int z = 60; // błąd (ponowna deklaracja) } } Odnośnik this W ciele konstruktora i metody jest dostępny odnośnik this identyfikujący obiekt na rzecz którego wywołano ten konstruktor albo tę metodę. Ciało konstruktora rozpoczyna instrukcja this(Arg, Arg, ... , Arg); albo (jawna albo domniemana) instrukcja super(Arg, Arg, ... , Arg); W pierwszej jest wywoływany konstruktor danej klasy, a w drugiej konstruktor jej klasy bazowej. Jeśli w ciele konstruktora nie wystąpi żadna z tych instrukcji, to domniemywa się, że jego pierwszą instrukcją jest super(); Polimorfizm Każda metoda jest domyślnie wirtualna. Każde wywołanie metody, która nie jest prywatna albo finalna jest polimorficzne, a więc Niezależnie od typu odnośnika, wywołuje się metodę należącą do klasy tego obiektu, którego odniesienie jest przypisane odnośnikowi na rzecz którego odbywa się wywołanie. Podczas kompilowania programu typ odnośnika służy tylko do upewnienia się, że w klasie albo w interfejsie definiującym ten typ występuje deklaracja wywoływanej metody. Podczas wykonywania programu typ odnośnika nie jest już brany pod uwagę. Uwaga: Wywołanie zrealizowane za pomocą słowa kluczowego super (np. super.fun(3)) nie jest polimorficzne. Następujący aplet zawiera polimorficzne wywołania metod getAge i getId. =============================================== import java.applet.Applet; import java.awt.Graphics; abstract class Person { abstract int getAge(); } interface Citizen { String getId(); } class Mother extends Person { private int age; public Mother(int age) { this.age = age; } int getAge() { return age; } } class Father implements Citizen { private String id; public Father(String id) { this.id = id; } public String getId() { return id; } } public class Master extends Applet { private Mother mother = new Mother(24); private Father father = new Father("#12_137"); private Person pRef = mother; // odnośnik klasowy private Citizen cRef = father; // odnośnik interfejsowy public void paint(Graphics gDC) { gDC.drawString( "age = " + pRef.getAge() + // 24 " id = " + cRef.getId(), // #12_137 20, 40 ); } } Tablice Deklaracja tablicy jest w istocie deklaracją odnośnika do tablicy. Sama tablica jest tworzona podczas jawnego albo niejawnego opracowania fabrykatora. Uwaga: Każda tablica jest obiektem klasy pochodnej klasy Object i implementuje interfejs Cloneable. Obiekt tablicowy jest wyposażony w publiczne pole length określające liczbę elementów tablicy. Elementy predefiniowane W celu utworzenia tablicy o elementach typu predefiniowanego należy użyć fabrykatora new Typ [Rozmiar] Jego rezultatem jest odnośnik zainicjowany odniesieniem do właśnie utworzonej tablicy. W szczególności, wykonanie instrukcji int arr[]; // deklaracja odnośnika arr = new int [3]; // utworzenie tablicy for(int i = 0; i < arr.length ; i++) arr[i] = 0; powoduje utworzenie i zainicjowanie liczbą 0, wszystkich elementów tablicy identyfikowanej przez odnośnik arr. Elementy obiektowe W celu utworzenia tablicy o elementach typu obiektowego należy użyć fabrykatora new Typ [Rozmiar] Jego rezultatem jest odnośnik zainicjowany odniesieniem do wektora odnośników do elementów właśnie utworzonej tablicy. Na przykład, wykonanie instrukcji String arr[]; // deklaracja odnośnika arr = new String [3]; // utworzenie wektora odnośników for(int i = 0; i < arr.length ; i++) arr[i] = new String(); // utworzenie elementu podstawowego powoduje utworzenie i zainicjowanie (pustym łańcuchem) wszystkich elementów podstawowych tablicy identyfikowanej przez odnośnik arr. Domniemanie fabrykatora Domniemanie fabrykatora tablicy wynika z użycia inicjatora { fraza, fraza, ... , fraza } w którym każda fraza jest wyrażeniem skalarnym albo inicjatorem. W szczególności, następującą instrukcję, definiująca trójkątną tablicę identyfikowaną przez odnośnik vec int[][] vec = { { 10 } , { 20, 30 } , { 40, 50, 60 } }; można traktować jako skrót od int[][] vec = new int [3][]; vec[0] = new int [] { 10 }; vec[1] = new int [] { 20, 30 }; vec[2] = new int [] { 40, 50, 60 }; Konwersje Konwersja ma na celu przekształcenie zmiennej w zmienną innego typu. Do jej wykonania służy operacja (Type)exp w której Type jest nazwą typu docelowego. Uwaga: Konwersja (Type)exp, w której Type i exp są takiego samego typu jest konwersja tożsamościową. (double)(int)4.8 Wyrażenie jest nazwą zmiennej typu double o wartości 4.0. Konwersje arytmetyczne i odnośnikowe Konwersje dzielą się na arytmetyczne i odnośnikowe. Konwersją arytmetyczną jest przekształcenie zmiennej typu arytmetycznego w zmienną typu arytmetycznego, a konwersją odnośnikową jest przekształcenie odnośnika w odnośnik. long big = 1000000000; int small; small = (int)big; // konwersja arytmetyczna long[] vec = new long [2]; Object obj = (Object)vec; // konwersja odnośnikowa long[] vec2 = (long [])obj; // konwersja odnośnikowa int[] vec3 = (int [])obj; // błąd Konwersje standardowe i niestandardowe Konwersje dzielą się na standardowe i niestandardowe. Konwersja standardowa może być wykonana niejawnie. double val = 4; // równoważne double val = (double)4; long[] vec = new long [2]; Object obj = vec; // niejawna konwersja standardowa int val = 4.8; // błąd (brak niejawnej konwersji) Poprawność statyczna i dynamiczna Konwersja arytmetyczna zmiennej "węższego" typu arytmetycznego (np. byte) w "szerszy" (np. long) jest poprawna statycznie i dynamicznie. Konwersja odnośnikowa jest poprawna statycznie, jeśli jest konwersją odnośnika pewnego typu (np. Panel) na odnośnik jego typu bazowego (np. Container) albo pochodnego (np. Applet) albo gdy jest konwersją odnośnika do tablicy o elementach pewnego typu w odnośnik do tablicy o elementach, które są ich typu bazowego albo pochodnego. (Panel)new Applet() (Applet)new Panel() (Panel [])new Applet [20] (Applet [][])new Panel [20][30] Konwersja odnośnikowa (Type)exp wyrażenia exp do typu Type jest poprawna dynamicznie, gdy wyrażenie exp instanceof Type ma wartość true. Ma to miejsce tylko wówczas, gdy odnośnik exp identyfikuje obiekt klasy Type albo jej klasy pochodnej. String city = "Warsaw"; Object obj = city; // konwersja niejawna city = (String)obj; // konwersja jawna Vector vec = (Vector)obj; // konwersja niepoprawna dynamicznie Konwersja jest wykonalna, jeśli jest poprawna statycznie i dynamicznie. public String toString(Object obj) { if(obj instanceof String) return (String)obj; // konwersja wykonalna else return (String)obj; // konwersja niewykonalna } Obie konwersje są poprawne statycznie. Druga jest niewykonalna, ponieważ jest niepoprawna dynamicznie. Jan Bielecki Programowanie obiektowe Istotą programowania obiektowego jest [PPL1]łączne wykorzystanie hermetyzacji, dziedziczenia i polimorfizmu. Dobry styl programowania obiektowego polega na wyodrębnieniu rzeczowników opisujących elementy programu, a następnie wyrażeniu opisywanych przez nie pojęć za pomocą definicji klasy. W szczególności, jeśli program służy do wykreślania okręgów i kwadratów rejestrowanych w bazie danych, to powinien zawierać m.in. klasy Circle (okrąg), Square (kwadrat) i DataBase (baza danych). Ważnym elementem programowania obiektowego powinno być wykorzystanie polimorfizmu. Dzięki niemu unika się jawnego rozpoznawania obiektów, co umożliwia wykonywanie operacji na obiektach, których klasy jeszcze nie istniały podczas definiowania bazy danych. Następujący aplet, pokazany na ekranie Okręgi i kwadraty ilustruje istotę programowania obiektowego z użyciem polimorfizmu. Napisano go w taki sposób, że po naciśnięciu klawisza oznaczonego cyfrą 1 jest wykreślany okrąg, a po naciśnięciu klawisza oznaczonego cyfrą 2 jest wykreślany kwadrat. Dzięki zastosowaniu polimorfizmu, rozszerzenie programu na dodatkowe kształty wymaga jedynie dostarczenia definicji opisujących je klas oraz dokonania nieznacznej zmiany w obsłudze klawiatury. Ekran Okręgi i kwadraty ### cirsqr.gif Rozwiązanie obiektowe =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; import java.util.Vector; public class Master extends Applet { private final int LastKey = KeyEvent.VK_F2, r = 20, s = 40; private DataBase dataBase; private static Graphics gDC; private static int keyCode = 0; public void init() { gDC = getGraphics(); dataBase = new DataBase(); addMouseListener( new MouseAdapter() { public void mouseReleased(MouseEvent evt) { int x = evt.getX(), y = evt.getY(); Color c = new Color( x % 256, y % 256, (x+y) % 256 ); Figure figure; switch(keyCode) { case 1: figure = new Circle(x, y, r, c); break; case 2: figure = new Square(x, y, s, c); break; default: Toolkit.getDefaultToolkit().beep(); return; } dataBase.add(figure); figure.draw(gDC); } } ); addKeyListener( new KeyAdapter() { public void keyReleased(KeyEvent evt) { int keyCode = evt.getKeyCode(); if(keyCode < KeyEvent.VK_F1 || keyCode > LastKey) { Toolkit.getDefaultToolkit().beep(); return; } Master.keyCode = keyCode - KeyEvent.VK_F1 + 1; } } ); requestFocus(); } public void paint(Graphics gDC) { dataBase.drawAll(gDC); } } class DataBase extends Vector { public void add(Figure figure) { addElement(figure); } public void drawAll(Graphics gDC) { int count = size(); for(int i = 0; i < count ; i++) { Object object = elementAt(i); Figure figure = (Figure)object; figure.draw(gDC); } } } abstract class Figure { protected int x, y; protected Color c; public Figure(int x, int y, Color c) { this.x = x; this.y = y; this.c = c; } abstract public void draw(Graphics gDC); } class Circle extends Figure { protected int r; public Circle(int x, int y, int r, Color c) { super(x, y, c); this.r = r; } public void draw(Graphics gDC) { gDC.setColor(c); gDC.fillOval(x-r, y-r, r<<1, r<<1); } } class Square extends Figure { protected int s; public Square(int x, int y, int s, Color c) { super(x, y, c); this.s = s; } public void draw(Graphics gDC) { gDC.setColor(c); gDC.fillRect(x-(s>>1), y-(s>>1), s, s); } } Dla porównania przytoczono aplet napisany metodą nieobiektową. Jego kod wynikowy jest dłuższy, czytelność znacznie mniejsza, a podatność na zmiany bardzo ograniczona. Rozwiązanie nieobiektowe =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; import java.util.Vector; public class Master extends Applet { private final int LastKey = KeyEvent.VK_F2, r = 20, s = 40; private Vector dataBase; private static Graphics gDC; private static int keyCode = 0; public void init() { gDC = getGraphics(); dataBase = new Vector(); addMouseListener( new MouseAdapter() { public void mouseClicked(MouseEvent evt) { int x = evt.getX(), y = evt.getY(); Color c = new Color( x % 256, y % 256, (x+y) % 256 ); Figure figure; gDC.setColor(c); switch(keyCode) { case 1: figure = new Figure(1, x, y, r, c); gDC.fillOval(x-r, y-r, r<<1, r<<1); break; case 2: figure = new Figure(2, x, y, s, c); gDC.fillRect(x-(s>>1), y-(s>>1), s, s); break; default: Toolkit.getDefaultToolkit().beep(); return; } dataBase.addElement(figure); } } ); addKeyListener( new KeyAdapter() { public void keyReleased(KeyEvent evt) { int keyCode = evt.getKeyCode(); if(keyCode < KeyEvent.VK_F1 || keyCode > LastKey) { Toolkit.getDefaultToolkit().beep(); return; } Master.keyCode = keyCode - KeyEvent.VK_F1 + 1; } } ); requestFocus(); } public void paint(Graphics gDC) { int count = dataBase.size(); for(int i = 0; i < count ; i++) { Object object = dataBase.elementAt(i); Figure figure = (Figure)object; int x = figure.x, y = figure.y; Color c = figure.c; gDC.setColor(c); switch(figure.id) { case 1: int r = figure.rs; gDC.fillOval(x-r, y-r, r<<1, r<<1); break; case 2: int s = figure.rs; gDC.fillRect(x-(s>>1), y-(s>>1), s, s); break; default: return; } } } } class Figure { protected int id; protected int x, y; protected int rs; protected Color c; public Figure(int id, int x, int y, int rs, Color c) { this.id = id; this.x = x; this.y = y; this.rs = rs; this.c = c; } } Jan Bielecki Programowanie zdarzeniowe Następujący zestaw programów, z których każdy następny jest rozszerzeniem poprzedniego, ilustruje zasady programowania obiektowo-zdarzeniowego. W miarę zwiększania funkcjonalności, programy są wyposażane w coraz więcej cech, typowych dla właściwie napisanych programów obiektowych. Ostatni program, pokazany na ekranie Wersja docelowa, umożliwia wykreślanie i odtwarzanie z bazy danych: kół, kwadratów, napisów i krzywych reprezentowanych przez obiekty. Dzięki w pełni obiektowej strukturze może być łatwo modyfikowany i rozszerzany. Ekran Wersja docelowa ### laststep.gif Kolorowe koła Wykreślanie kolorowych kół o środku w punkcie kliknięcia. Kolor koła jest przypadkowy. Pulpit apletu nie jest odtwarzany. Liczba kół nie jest ograniczona. =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; import java.util.Random; public class Master extends Applet { protected Random rand = new Random(); protected Graphics gDC; protected final int r = 30; public void init() { gDC = getGraphics(); addMouseListener(new Watcher()); } class Watcher extends MouseAdapter { public void mouseReleased(MouseEvent evt) { int x = evt.getX(), y = evt.getY(); Color c = getColor(); gDC.setColor(c); gDC.fillOval(x-r, y-r, 2*r-1, 2*r-1); gDC.setColor(Color.black); gDC.drawOval(x-r, y-r, 2*r-1, 2*r-1); } public Color getColor() { int color = rand.nextInt(); return new Color(color); } } } Odtwarzanie z bazy danych Utworzenie bazy danych jako tablicy współrzędnych środków kół i kolorów. Ograniczenie liczby wykreślanych kół. Sygnalizowanie próby przekroczenia limitu. =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; import java.util.Random; public class Master extends Applet { protected Random rand = new Random(); protected Graphics gDC; protected final int r = 30; protected int count = 0; protected final int Size = 100; protected int xPos[], yPos[]; protected Color color[]; public void init() { gDC = getGraphics(); xPos = new int [Size]; yPos = new int [Size]; color = new Color [Size]; addMouseListener(new Watcher()); } public void paint(Graphics gDC) { for(int i = 0; i < count ; i++) { int x = xPos[i], y = yPos[i]; Color c = color[i]; gDC.setColor(c); gDC.fillOval(x-r, y-r, 2*r-1, 2*r-1); gDC.setColor(Color.black); gDC.drawOval(x-r, y-r, 2*r-1, 2*r-1); } } class Watcher extends MouseAdapter { public void mouseReleased(MouseEvent evt) { if(count == Size) { Toolkit kit = Toolkit.getDefaultToolkit(); kit.beep(); return; } int x = evt.getX(), y = evt.getY(); Color c = getColor(); gDC.setColor(c); gDC.fillOval(x-r, y-r, 2*r-1, 2*r-1); gDC.setColor(Color.black); gDC.drawOval(x-r, y-r, 2*r-1, 2*r-1); xPos[count] = x; yPos[count] = y; color[count++] = c; } public Color getColor() { int color = rand.nextInt(); return new Color(color); } } } Koła reprezentowane przez obiekty Reprezentowanie kół przez obiekty klasy Circle. Utworzenie bazy danych jako dynamicznie rozszerzanej tablicy odnośników do obiektów kół. Zniesienie ograniczenia co do liczby kół. =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; import java.util.Random; public class Master extends Applet { protected Random rand = new Random(); protected Graphics gDC; protected int count = 0; protected int Size = 2; protected Circle circles[]; public void init() { gDC = getGraphics(); circles = new Circle [Size]; addMouseListener(new Watcher()); } public void paint(Graphics gDC) { for(int i = 0; i < count ; i++) { Circle circle = circles[i]; circle.draw(gDC); } } class Circle { protected int x, y; protected Color c; protected final int r = 30; public Circle(int x, int y, Color c) { this.x = x; this.y = y; this.c = c; } public void draw(Graphics gDC) { gDC.setColor(c); gDC.fillOval(x-r, y-r, 2*r-1, 2*r-1); gDC.setColor(Color.black); gDC.drawOval(x-r, y-r, 2*r-1, 2*r-1); } } class Watcher extends MouseAdapter { public void mouseReleased(MouseEvent evt) { int x = evt.getX(), y = evt.getY(); Color c = getColor(); Circle circle = new Circle(x, y, c); circles[count++] = circle; circle.draw(gDC); if(count == Size) expand(); } public void expand() { Circle[] oldCircles = circles; Size *= 2; circles = new Circle [Size]; for(int i = 0; i < Size/2 ; i++) { circles[i] = oldCircles[i]; } } public Color getColor() { int color = rand.nextInt(); return new Color(color); } } } Baza w obiekcie klasy Vector Utworzenie bazy danych za pomocą obiektu klasy Vector, stanowiącego dynamiczną kolekcję obiektów klasy Circle. =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; import java.util.*; public class Master extends Applet { protected Random rand = new Random(); protected Graphics gDC; protected Vector objects; public void init() { gDC = getGraphics(); objects = new Vector(); addMouseListener(new Watcher()); } public void paint(Graphics gDC) { int count = objects.size(); for(int i = 0; i < count ; i++) { Object object = objects.elementAt(i); Circle circle = (Circle)object; circle.draw(gDC); } } class Circle { protected int x, y; protected Color c; protected final int r = 30; public Circle(int x, int y, Color c) { this.x = x; this.y = y; this.c = c; } public void draw(Graphics gDC) { gDC.setColor(c); gDC.fillOval(x-r, y-r, 2*r-1, 2*r-1); gDC.setColor(Color.black); gDC.drawOval(x-r, y-r, 2*r-1, 2*r-1); } } class Watcher extends MouseAdapter { public void mouseReleased(MouseEvent evt) { int x = evt.getX(), y = evt.getY(); Color c = getColor(); Circle circle = new Circle(x, y, c); objects.addElement(circle); circle.draw(gDC); } public Color getColor() { int color = rand.nextInt(); return new Color(color); } } } Wykreślanie kół i kwadratów Reprezentowanie kół i kwadratów przez obiekty klas Circle i Square, wywodzących się od klasy Figure. Utworzenie bazy danych jako obiektu klasy Vector. Wykreślanie kół po naciśnięciu klawisza F1, a wykreślanie kwadratów po naciśnięciu klawisza F2. =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; import java.util.*; public class Master extends Applet { protected Random rand = new Random(); protected final int Limit = 2; protected int keyId = 0; protected Graphics gDC; protected Vector objects; public void init() { requestFocus(); gDC = getGraphics(); objects = new Vector(); addKeyListener(new KeyWatcher()); addMouseListener(new MouseWatcher()); } public void paint(Graphics gDC) { int count = objects.size(); for(int i = 0; i < count ; i++) { Object object = objects.elementAt(i); Figure figure = (Figure)object; figure.draw(gDC); } } public void beep() { Toolkit kit = Toolkit.getDefaultToolkit(); kit.beep(); } abstract class Figure { protected int x, y; protected Color c; public Figure(int x, int y, Color c) { this.x = x; this.y = y; this.c = c; } public abstract void draw(Graphics gDC); } class Circle extends Figure { protected int r; public Circle(int x, int y, int r, Color c) { super(x, y, c); this.r = r; } public void draw(Graphics gDC) { gDC.setColor(c); gDC.fillOval(x-r, y-r, 2*r-1, 2*r-1); gDC.setColor(Color.black); gDC.drawOval(x-r, y-r, 2*r-1, 2*r-1); } } class Square extends Figure { protected int s; public Square(int x, int y, int s, Color c) { super(x, y, c); this.s = s; } public void draw(Graphics gDC) { gDC.setColor(c); gDC.fillRect(x-s/2, y-s/2, s-1, s-1); gDC.setColor(Color.black); gDC.drawRect(x-s/2, y-s/2, s-1, s-1); } } class KeyWatcher extends KeyAdapter { public void keyReleased(KeyEvent evt) { int code = evt.getKeyCode() - KeyEvent.VK_F1 + 1; if(code > 0 && code <= Limit) { keyId = code; } else { beep(); return; } } } class MouseWatcher extends MouseAdapter { public void mouseReleased(MouseEvent evt) { int x = evt.getX(), y = evt.getY(); Color c = getColor(); Figure figure; switch(keyId) { case 1: figure = new Circle(x, y, 30, c); break; case 2: figure = new Square(x, y, 50, c); break; default: beep(); return; } objects.addElement(figure); figure.draw(gDC); } public Color getColor() { int color = rand.nextInt(); return new Color(color); } } } Wykreślanie kół, kwadratów i napisów Reprezentowanie kół, kwadratów i napisów przez obiekty klas implementujących interfejs Drawable. Wykreślanie napisów po naciśnięciu klawisza F3. =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; import java.util.*; public class Master extends Applet { protected Random rand = new Random(); protected final int Limit = 3; protected int keyId = 0; protected Graphics gDC; protected Vector objects; public void init() { requestFocus(); gDC = getGraphics(); objects = new Vector(); addKeyListener(new KeyWatcher()); addMouseListener(new MouseWatcher()); } public void paint(Graphics gDC) { int count = objects.size(); for(int i = 0; i < count ; i++) { Object object = objects.elementAt(i); Drawable shape = (Drawable)object; shape.draw(gDC); } } public void beep() { Toolkit kit = Toolkit.getDefaultToolkit(); kit.beep(); } class Label implements Drawable { protected int x, y; protected String l; protected Color c; public Label(int x, int y, String l, Color c) { this.x = x; this.y = y; this.l = l; this.c = c; } public void draw(Graphics gDC) { gDC.setColor(c); gDC.drawString(l, x, y); } } interface Drawable { void draw(Graphics gDC); } abstract class Figure implements Drawable { protected int x, y; protected Color c; public Figure(int x, int y, Color c) { this.x = x; this.y = y; this.c = c; } } class Circle extends Figure { protected int r; public Circle(int x, int y, int r, Color c) { super(x, y, c); this.r = r; } public void draw(Graphics gDC) { gDC.setColor(c); gDC.fillOval(x-r, y-r, 2*r-1, 2*r-1); gDC.setColor(Color.black); gDC.drawOval(x-r, y-r, 2*r-1, 2*r-1); } } class Square extends Figure { protected int s; public Square(int x, int y, int s, Color c) { super(x, y, c); this.s = s; } public void draw(Graphics gDC) { gDC.setColor(c); gDC.fillRect(x-s/2, y-s/2, s-1, s-1); gDC.setColor(Color.black); gDC.drawRect(x-s/2, y-s/2, s-1, s-1); } } class KeyWatcher extends KeyAdapter { public void keyReleased(KeyEvent evt) { int code = evt.getKeyCode() - KeyEvent.VK_F1 + 1; if(code > 0 && code <= Limit) { keyId = code; } else { beep(); return; } } } class MouseWatcher extends MouseAdapter { public void mouseReleased(MouseEvent evt) { int x = evt.getX(), y = evt.getY(); Color c = getColor(); Drawable shape; switch(keyId) { case 1: shape = new Circle(x, y, 30, c); break; case 2: shape = new Square(x, y, 50, c); break; case 3: getLabel(x, y, c); return; default: beep(); return; } objects.addElement(shape); shape.draw(gDC); } public void getLabel(final int x, final int y, final Color c) { final Frame frame = new Frame(); TextField field = new TextField(20); frame.add(field, BorderLayout.CENTER); Dimension appletDim = getSize(); int aW = appletDim.width, aH = appletDim.height; Point p = getLocationOnScreen(); frame.pack(); Dimension frameDim = frame.getSize(); int fW = frameDim.width, fH = frameDim.height; frame.setLocation(p.x + (aW-fW)/2, p.y + (aH-fH)/2); frame.setVisible(true); field.addActionListener( new ActionListener() { public void actionPerformed(ActionEvent evt) { String cmd = evt.getActionCommand(); Label label = new Label(x, y, cmd, c); objects.addElement(label); frame.dispose(); label.draw(gDC); } } ); } public Color getColor() { int color = rand.nextInt(); return new Color(color); } } } Wykreślanie kół, kwadratów, napisów i krzywych Reprezentowanie krzywych (jako kolekcji odcinków klasy Line) przez obiekty klasy Curve. Wykreślanie krzywych po naciśnięciu klawisza F4. =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; import java.util.*; public class Master extends Applet { protected Random rand = new Random(); protected final int Limit = 4; protected int keyId = 0; protected Graphics gDC; protected Vector objects; protected Drawable shape; protected final int IsCurve = 4; protected Curve curve; public void init() { requestFocus(); gDC = getGraphics(); objects = new Vector(); addKeyListener(new KeyWatcher()); MouseWatcher mouseWatcher = new MouseWatcher(); addMouseListener(mouseWatcher); addMouseMotionListener(mouseWatcher); } public void paint(Graphics gDC) { int count = objects.size(); for(int i = 0; i < count ; i++) { Object object = objects.elementAt(i); Drawable shape = (Drawable)object; shape.draw(gDC); } } public void beep() { Toolkit kit = Toolkit.getDefaultToolkit(); kit.beep(); } class Curve extends Vector implements Drawable { protected Color c; public Curve(Color c) { this.c = c; } public void draw(Graphics gDC) { int count = size(); gDC.setColor(c); for(int i = 0; i < count; i++) { Object object = elementAt(i); Line line = (Line)object; line.draw(gDC); } } class Line { protected Point b, e; public Line(Point b, Point e) { this.b = b; this.e = e; } public void draw(Graphics gDC) { gDC.setColor(c); gDC.drawLine(b.x, b.y, e.x, e.y); } } } class Label implements Drawable { protected int x, y; protected String l; protected Color c; public Label(int x, int y, String l, Color c) { this.x = x; this.y = y; this.l = l; this.c = c; } public void draw(Graphics gDC) { gDC.setColor(c); gDC.drawString(l, x, y); } } interface Drawable { void draw(Graphics gDC); } abstract class Figure implements Drawable { protected int x, y; protected Color c; public Figure(int x, int y, Color c) { this.x = x; this.y = y; this.c = c; } } class Circle extends Figure { protected int r; public Circle(int x, int y, int r, Color c) { super(x, y, c); this.r = r; } public void draw(Graphics gDC) { gDC.setColor(c); gDC.fillOval(x-r, y-r, 2*r-1, 2*r-1); gDC.setColor(Color.black); gDC.drawOval(x-r, y-r, 2*r-1, 2*r-1); } } class Square extends Figure { protected int s; public Square(int x, int y, int s, Color c) { super(x, y, c); this.s = s; } public void draw(Graphics gDC) { gDC.setColor(c); gDC.fillRect(x-s/2, y-s/2, s-1, s-1); gDC.setColor(Color.black); gDC.drawRect(x-s/2, y-s/2, s-1, s-1); } } class KeyWatcher extends KeyAdapter { public void keyReleased(KeyEvent evt) { int code = evt.getKeyCode() - KeyEvent.VK_F1 + 1; if(code > 0 && code <= Limit) { keyId = code; } else { beep(); return; } } } class MouseWatcher extends MouseAdapter implements MouseMotionListener { protected Point oldPoint; public void mousePressed(MouseEvent evt) { if(keyId == IsCurve) { int x = evt.getX(), y = evt.getY(); oldPoint = new Point(x, y); Color c = getColor(); curve = new Curve(c); } } public void mouseDragged(MouseEvent evt) { if(keyId == IsCurve) { int x = evt.getX(), y = evt.getY(); Point newPoint = new Point(x, y); Curve.Line line = curve.new Line(oldPoint, newPoint); oldPoint = newPoint; curve.addElement(line); gDC.setColor(Color.black); line.draw(gDC); } } public void mouseMoved(MouseEvent evt) { } public void mouseReleased(MouseEvent evt) { if(keyId == IsCurve) { objects.addElement(curve); } } public void mouseClicked(MouseEvent evt) { int x = evt.getX(), y = evt.getY(); Color c = getColor(); switch(keyId) { case 1: shape = new Circle(x, y, 30, c); break; case 2: shape = new Square(x, y, 50, c); break; case 3: getLabel(x, y, c); return; case 4: // niczego nie wykreślono beep(); return; default: beep(); return; } objects.addElement(shape); shape.draw(gDC); } public void getLabel(final int x, final int y, final Color c) { final Frame frame = new Frame(); TextField field = new TextField(20); frame.add(field, BorderLayout.CENTER); Dimension appletDim = getSize(); int aW = appletDim.width, aH = appletDim.height; Point p = getLocationOnScreen(); frame.pack(); Dimension frameDim = frame.getSize(); int fW = frameDim.width, fH = frameDim.height; frame.setLocation(p.x + (aW-fW)/2, p.y + (aH-fH)/2); frame.setVisible(true); field.addActionListener( new ActionListener() { public void actionPerformed(ActionEvent evt) { String cmd = evt.getActionCommand(); Label label = new Label(x, y, cmd, c); objects.addElement(label); frame.dispose(); label.draw(gDC); } } ); } public Color getColor() { int color = rand.nextInt(); return new Color(color); } } } Jan Bielecki Projektowanie kolekcji Jeśli klasa jest wyposażona w funkcję "dołącz obiekt", to jest klasą kolekcyjną. Zazwyczaj wraz z klasą kolekcyjną jest definiowana klasa iteracyjna. Egzemplarzami klasy kolekcyjnej są kolektory, a egzemplarzami klasy iteracyjnej są iteratory. Kolektorów używa się do tworzenia, a iteratorów do przeglądania kolekcji. Kolekcję projektuje się zazwyczaj w taki sposób, że umożliwia ona dołączanie obiektów klas wywodzących się od określonej klasy albo implementujących określony interfejs. Wówczas klasę kolekcyjną można wyposażyć w funkcje do wykonywania operacji na dowolnych zbiorach jej obiektów, bez rozpatrywania ich typów. Następujący program, pokazany na ekranie Wyznaczanie powierzchni, w którym zdefiniowano klasę kolekcyjną Bundle i klasę iteracyjną BundleEnumerator, umożliwia tworzenie kolekcji, w skład których wchodzą obiekty dowolnych klas implementujących interfejs Measurable, w tym obiekty klas wywodzących się z klasy Figure. Ponieważ każdy z takich obiektów jest w stanie dostarczyć informacji o swoim polu powierzchni, więc klasę Bundle wyposażono w metodę fullArea wyznaczającą całkowite pole figur reprezentowanych przez obiekty znajdujące się w kolekcji. Ekran Wyznaczanie powierzchni ### fullarea.gif =============================================== import java.applet.Applet; import java.awt.Graphics; import java.awt.event.*; import java.util.NoSuchElementException; interface Measurable { double getArea(); // wyznaczenie powierzchni } class Bundle { private int noOfSlots = 4; // liczba miejsc private int freeSlot = 0; // pierwsze wolne miejsce private Measurable vec[]; // odnośniki do obiektów public Bundle() { vec = new Measurable [noOfSlots]; } public int freeSlot() { return freeSlot; } private int noOfSlots() { return noOfSlots; } public Measurable getAt(int pos) { return vec[pos]; } public int addFigure(Measurable figure) // dodaj kształt { if(freeSlot >= noOfSlots) { Measurable vecOld[] = vec; int noOfSlotsOld = noOfSlots; noOfSlots <<= 1; // podwój pojemność vec = new Measurable [noOfSlots]; System.arraycopy(vecOld, 0, vec, 0, vecOld.length); } vec[freeSlot] = figure; return freeSlot++; } public double fullArea() // całkowite pole { double total = 0; for(int i = 0; i < freeSlot ; i++) { Measurable figure = vec[i]; total += figure.getArea(); } return total; } class BundleEnumerator { private Bundle bundle; private int pos = 0; public BundleEnumerator(Bundle bundle) { this.bundle = bundle; } public boolean hasMore() { if(pos < bundle.freeSlot()) return true; pos = 0; return false; } public Measurable getNext() { if(hasMore()) return bundle.getAt(pos++); throw new NoSuchElementException( "BundleEnumerator" ); } } } abstract class Figure implements Measurable { protected int x = 0, y = 0; // współrzędne środka public Figure(int x, int y) { this.x = x; this.y = y; } public abstract void draw(Graphics gDC); } class Circle extends Figure { private static final double Pi = Math.PI; private int r; public Circle(int x, int y, int r) { super(x, y); this.r = r; } public void draw(Graphics gDC) { gDC.drawOval(x-r, y-r, r<<1, r<<1); } public void drawDot(Graphics gDC) { gDC.drawOval(x-3, y-3, 6, 6); } public double getArea() { return Pi * r * r; } } class Square extends Figure { private int s; public Square(int x, int y, int s) { super(x, y); this.s = s; } public void draw(Graphics gDC) { int s2 = s/2; gDC.drawRect(x-s, y-s, s, s); } public double getArea() { return (double)s * s; } } public class Master extends Applet { private Bundle bundle; private Graphics gDC; public void init() { gDC = getGraphics(); bundle = new Bundle(); addMouseListener(new Watcher()); } public void paint(Graphics gDC) { Bundle.BundleEnumerator scanner = bundle.new BundleEnumerator(bundle); while(scanner.hasMore()) ((Figure)scanner.getNext()).draw(gDC); } class Watcher extends MouseAdapter { private final int Rad = 10, Side = 20; public void mouseReleased(MouseEvent evt) { int x = evt.getX(), y = evt.getY(); int mods = evt.getModifiers(); if((mods & MouseEvent.SHIFT_MASK) == 0) { Figure figure; if((mods & MouseEvent.META_MASK) == 0) figure = new Circle(x, y, Rad); else figure = new Square(x, y, Side); bundle.addFigure(figure); figure.draw(gDC); double area = bundle.fullArea(); showStatus("Area = " + (int)area); } else { // obiekty w kolekcji Bundle.BundleEnumerator scanner = bundle.new BundleEnumerator(bundle); while(scanner.hasMore()) { Measurable figure = scanner.getNext(); if(figure instanceof Circle) ((Circle)figure).drawDot(gDC); } } } } } Jan Bielecki Aplety i aplikacje Programy dzielą się na aplety i aplikacje. Podział ten jest umowny, ponieważ bez trudu można napisać program, który jednocześnie jest [PPL2][PPL3]apletem i aplikacją. public class Master extends java.applet.Applet { // aplet public static void main(String args[]) // aplikacja { } } Aplety Apletem jest program składający się z zestawu definicji klas, z których przynajmniej jedna jest publiczna i wywodzi się od klasy Applet. W odróżnieniu od aplikacji, która jest programem wolnostojącym, wykonanie apletu wymaga użycia przeglądarki. Publiczna klasa apletowa zawiera w ogólnym wypadku metody init, start, paint, stop, destroy, wywoływane przez przeglądarkę w podanej kolejności. Metoda init oraz metoda destroy jest wywoływana jednokrotnie. Metody start, stop i paint mogą być wywoływane wielokrotnie. W najprostszym przypadku wystarcza zdefiniowanie w aplecie tylko metody paint. Jest ona wywoływana wkrótce po wykonaniu metody init (dokładnie: tuż po wykonaniu metody start), a także w każdej sytuacji, gdy zaistnieje potrzeba odtworzenia pulpitu apletu (na przykład po zasłonięciu i odsłonięciu go przez pewne okno). Aby przeglądarka mogła wyświetlić aplet, należy przekazać jej opis apletu. Uproszczony do minimum ma on postać w której Nazwa jest nazwą klasy apletowej, a Szerokość i Wysokość określają poziomy i pionowy rozmiar apletu. Następujący program, pokazany na ekranie Pozdrowienie z apletu, wykreśla na swoim pulpicie tradycyjny ciąg znaków. Ekran Pozdrowienie z apletu ### greethel.gif ============================================== import java.applet.Applet; import java.awt.*; public class Master extends Applet { private Font font; // czcionka public void init() { font = new Font( "Serif", // krój Font.BOLD | Font.ITALIC, // styl 24 // rozmiar ); } public void paint(Graphics gDC) { gDC.setFont(font); gDC.drawString("Hello, I am JanB.", 10, 100); } } Aplikacje Aplikacją jest program składający się z zestawu definicji klas, z których przynajmniej jedna jest publiczna i zawiera funkcję główną. Nagłówek funkcji głównej ma postać public static void main(String[] args) A zatem jest to funkcja publiczna, statyczna, bezrezultatowa, o jednym parametrze typu String[]. Liczbę argumentów aplikacji określa wyrażenie args.length, a jego kolejne argumenty występują w programie pod nazwami args[i] dla i = 0, 1, ... Następująca aplikacja wyprowadza na konsolę wszystkie dostarczone jej argumenty. public class Main { public static void main(String[] args) { int count = args.length; if(count == 0) System.out.println("No arguments!"); else { System.out.println("The arguments are: "); for(int i = 0; i < count ; i++) System.out.println(args[i]); } } } Ponieważ w środowisku graficznym, takim jak Windows 95/98/NT, zakończenie wykonywania funkcji głównej powoduje zakończenie wykonywania programu i zniknięcie okna konsoli, więc ostatnią instrukcją programu można uczynić System.in.read(); W takim wypadku program czeka na naciśnięcie klawisza Enter i do tego momentu okno konsoli nie znika. Ilustruje to następująca aplikacja, umożliwiająca zapoznanie się z ciągiem znaków wyprowadzonych na konsolę. import java.io.*; public class Main { public static void main(String[] args) throws IOException { System.out.println("Hello, I am JanB."); System.in.read(); } } Aplikacje graficzne Wyniki aplikacji graficznej są wyprowadzane do utworzonego przez nią okna graficznego. Zamknięcie okna nie jest na ogół jednoznaczne z zamknięciem aplikacji. Dlatego po zamknięciu okna wywołuje się zazwyczaj funkcję exit. Następujący program, pokazany na ekranie Pozdrowienie z aplikacji, wykreśla we własnym oknie ciąg znaków. Ekran Pozdrowienie z aplikacji ### apigraph.gif import java.awt.*; import java.awt.event.*; import java.awt.font.*; import java.awt.geom.*; import java.io.IOException; public class Main { private static String greet = "Hello, I am JanB."; private static Font font; public static void main(String[] args) throws IOException { // utworzenie okna graficznego Frame frame = new MyFrame("Greet Frame"); // określenie rozmiarów okna frame.setSize(200, 200); // wyświetlenie okna frame.setVisible(true); // utworzenie wykreślacza // komunikującego się z oknem Graphics2D gDC2 = (Graphics2D)frame.getGraphics(); // określenie wcięć okna Insets insets = frame.getInsets(); int top = insets.top, left = insets.left; // wybranie czcionki font = new Font("Serif", Font.BOLD, 14); // wyznaczenie uniesienia FontRenderContext frc = gDC2.getFontRenderContext(); TextLayout layout = new TextLayout(greet, font, frc); Rectangle2D bounds = layout.getBounds(); int asc = -(int)bounds.getY(); // wstawienie czcionki do wykreślacza gDC2.setFont(font); // wykreślenie tekstu gDC2.drawString(greet, 10+left, 20+top+asc); } } class MyFrame extends Frame { public MyFrame(String caption) { // przekazanie nazwy okna klasie Frame super(caption); // obsłużenie zamknięcia okna addWindowListener( new WindowAdapter() { public void windowClosing(WindowEvent evt) { setVisible(false); // ukrycie okna dispose(); // zniszczenie okna // zakończenie aplikacji System.exit(0); } } ); } } Jan Bielecki Opisy apletów Aplet jest programem, którego B-kod znajduje się w pliku z rozszerzeniem .class. Aplet wyświetla swoje wyniki w prostokątnej ramce utworzonej przez przeglądarkę. Rozmiary ramki są podawane w opisie apletu. Opis apletu umieszcza się w pliku z rozszerzeniem .html i nadaje mu najczęściej postać w której fullName jest pełną nazwą klasy opisującej aplet, codeURL jest lokalizatorem, a width i height są (wyrażonymi w pikselach) rozmiarami ramki przydzielonej apletowi. Uwaga: Jeśli nie użyto parametru codebase, a lokalizatorem katalogu, w którym znajduje się plik z opisem apletu jest url, to domniemywa się parametr codebase z takim właśnie lokalizatorem. Lokalizator codeURL można pobrać do programu za pomocą metody getCodeBase. Jeśli w opisie apletu nie ma parametru codebase, to wywołanie metody getCodeBase ma taki sam skutek jak wywołanie metody getDocumentBase (dostarczającej lokalizator pliku zawierającego opis apletu). Kompilacja klasy Name programu odbywa się w taki sposób, że jeśli należy ona do pakietu pack, a katalogiem wyjściowym jest output, to skompilowany B-kod klasy umieszcza się w pliku o poglądowej nazwie output/pack/Name.class W szczególności, jeśli następujący plik Master.java skompiluje się do katalogu wyjściowego C:\jbOutputs, to powstanie plik C:\jbOutputs\janb\hello\Master.class zawierający B-kod apletu Master. Plik Master.java package janb.hello; // specyfikacja pakietu import java.applet.Applet; // polecenia importu import java.awt.*; public class Master extends Applet { public void paint(Graphics gDC) { gDC.drawString("Hello", 20, 50); } } Jeśli przeglądarce dostarczy się lokalizator pliku HTML na przykład file:/C:/jbWeb/Hello/Index.html albo html://bolek.ii.pw.edu.pl/~jbl/jbWeb/Hello/Index.html w którym występuje opis to spowoduje to wykonanie apletu Master należącego do pakietu janb.hello. Poszukiwanie pliku Master.class apletu odbędzie się kolejno 1) W katalogach parametru classpath. 2) W katalogu określonym przez (jawny albo domniemany) parametr codebase. Uwaga: Poszukiwanie zakończy się w chwili znalezienia pliku. W szczególności, jeśli plik Master.class umieści się w katalogu C:\jbPacks\janb\hello a przeglądarce ustawi się parametr środowiska classpath=C:\jbPacks to plik zostanie znaleziony w C:\jbPacks\janb\hello, a lokalizator codeURL nie będzie wzięty pod uwagę (sic!). Jeśli w classpath nie poda się nazwy katalogu C:\jbPacks, to można użyć parametru codebase=file:/C:/jbPacks Należy zauważyć, że gdyby w pliku Master.java występowało odwołanie do własnej klasy Debug (por. Dodatek C), której B-kod umieszczono w katalogu C:\jbPacks\janb\debug to w pliku tym należałoby użyć polecenia import janb.debug.*; a parametrowi środowiska classpath należałoby nadać postać classpath=C:\jbPacks dla dociekliwych Opis apletu, zawarty w pliku HTML, ma w ogólnym przypadku postać ... Fraza code podaje pełną nazwę klasy apletowej. Frazy archive i codebase określają położenie katalogu, w którym znajduje się plik zawierający B-kod tej klasy, na przykład archive=jbClasses.jar codebase=file:/C:/jbPacks/jbHello albo archive="jars/Mary.jar,jars/John.jar" codebase=http://bolek.ii.edu.pl/~jbl/jbMatches a frazy width i height określają rozmiary ramki, w której przeglądarka wyświetli aplet. Fraza name określa pełną nazwę identyfikującą aplet, fraza align określa sposób rozmieszczenia apletu na stronie WWW (left, right, top, texttop, middle, absmiddle, baseline, bottom, absbottom), a frazy vspace i hspace określają (wyrażony w pikselach) pionowy i poziomy odstęp przed i po aplecie. Za pomocą fraz param można określić wartości parametrów przekazywanych do apletu. Jeśli użyto frazy archive, to plik z B-kodem apletu jest w pierwszej kolejności poszukiwany w podanych archiwach (lokalizowanych względem katalogu zawierającego plik HTML), a dopiero potem w katalogach określonych za pomocą parametru classpath i parametru codebase. W wypadku użycia frazy archive, ta sama kolejność poszukiwania pliku co dla klasy Master dotyczy wszystkich innych klas, które są ładowane podczas wykonywania apletu. W szczególności, gdyby opis apletu Master należącego do pakietu jbTools.jbAudio znajdował się w pliku D:/jbHTML/Index.html to plik z B-kodem apletu Master byłby poszukiwany kolejno w archiwach D:\jbHTML\jars\Mary.jar D:\jbHTML\jars\John.jar oraz w katalogu C:\jbPacks\jbTools\jbAudio W przypadku pominięcia frazy codebase, plik z B-kodem apletu Master byłby poszukiwany najpierw w podanych archiwach, a dopiero po tym w pliku C:\jbHTML\jbTools\jbAudio Następujący program ilustruje zastosowanie parametru name do komunikowania się apletów, nazwanych tu One i Two. Naciśnięcie przycisku myszki w obrębie pulpitu jednego z nich powoduje wykreślenie okręgu na pulpicie drugiego. Uwaga: Przeglądarka tworzy 2 aplety. Opisy apletów w dokumencie HTML są różne, ale B-kod obu apletów jest wspólny. ======================================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet { protected String name; protected Thread myThread = null; public void init() { name = getParameter("name"); Watcher watcher = new Watcher(); addMouseListener(watcher); } Applet otherApplet() { String other = null; if(name.equals("One")) other = "Two"; if(name.equals("Two")) other = "One"; AppletContext context = getAppletContext(); return context.getApplet(other); } class Watcher extends MouseAdapter { public void mousePressed(MouseEvent evt) { int x = evt.getX(), y = evt.getY(); Applet other = otherApplet(); Graphics gDC = other.getGraphics(); gDC.drawOval(x-20, y-20, 40-1, 40-1); } public void mouseReleased(MouseEvent evt) { otherApplet().repaint(); } } } Jan Bielecki Klasy wewnętrzne i inne Klasy dzielą się na zewnętrzne, wewnętrzne, lokalne i anonimowe. Definicje klas wewnętrznych umieszcza się wśród deklaracji składników klasy. Definicje klas lokalnych umieszcza się w ciele funkcji, a definicje klas anonimowych tuż za fabrykatorami. Klasy wewnętrzne Klasa jest wewnętrzna, jeśli zdefiniowano ją w miejscu, w którym mógłby wystąpić składnik obejmującej ją klasy zewnętrznej albo wewnętrznej. Następujący aplet zawiera definicję prywatnej klasy wewnętrznej Watcher, zawartej w publicznej klasie zewnętrznej Master. =============================================== import java.applet.Applet; import java.awt.Graphics; import java.awt.event.*; public class Master extends Applet { private final int Rad = 20; private Graphics gDC; private Watcher watcher; public void init() { gDC = getGraphics(); watcher = new Watcher(); addMouseListener(watcher); } private class Watcher extends MouseAdapter { public void mouseReleased(MouseEvent evt) { int x = evt.getX(), y = evt.getY(); gDC.drawOval(x-Rad, y-Rad, Rad<<1, Rad<<1); } } } Jeśli Inner jest klasą wewnętrzną, zawartą w klasie Outer, to w celu utworzenia obiektu klasy Inner należy użyć fabrykatora outer.new Inner(arg, arg, ... , arg) w którym outer jest odnośnikiem do obiektu klasy Outer (w tym jawnym albo domniemanym odnośnikiem this). =============================================== import java.applet.Applet; import java.awt.Graphics; public class Master extends Applet { public void paint(Graphics gDC) { Parent parent = new Parent(); // this.new Parent() Parent.Child child = parent.new Child(); gDC.drawString( child.getString(), // Hello 20, 40 ); } class Parent { class Child { public String getString() { return "Hello"; } } } } Klasy lokalne Klasa jest lokalna, jeśli zdefiniowano ją w ciele funkcji. Składniki klasy lokalnej mogą odwoływać się do dowolnych składników widocznych w miejscu wystąpienia definicji klasy, w tym do parametrów funkcji zawierającej definicję klasy lokalnej. =============================================== import java.applet.Applet; import java.awt.Graphics; public class Master extends Applet { public void paint(Graphics gDC) { class Child { public String getString() { return "Hello"; } } gDC.drawString( new Child().getString(), // Hello 20, 40 ); } } Klasy anonimowe Klasa jest anonimowa, jeśli jej definicja jest pozbawiona nagłówka (m.in. słowa kluczowego class oraz nazwy), a jej ciało występuje bezpośrednio po fabrykatorze, w którym użyto nazwy jej klasy bazowej albo nazwy interfejsu. Uwaga: Jeśli w fabrykatorze użyto nazwy interfejsu, to klasa anonimowa implementuje ten intefejs, a jej klasą bazową jest Object. Następujący aplet fabrykuje obiekt klasy anonimowej, pochodnej od klasy MouseAdapter. =============================================== import java.applet.Applet; import java.awt.Graphics; import java.awt.event.*; public class Master extends Applet { private final int Rad = 20; private Graphics gDC; public void init() { gDC = getGraphics(); addMouseListener( new MouseAdapter() { public void mouseReleased(MouseEvent evt) { int x = evt.getX(), y = evt.getY(); gDC.drawOval(x-Rad, y-Rad, Rad<<1, Rad<<1); } } ); } } Parametry finalne Jeśli parametr funkcji jest finalny, to składniki zadeklarowanej w niej klasy mogą odwoływać się do niego także po zakończeniu wykonywania tej funkcji. =============================================== import java.applet.Applet; import java.awt.Graphics; public class Master extends Applet { public void paint(Graphics gDC) { Parent parent = new Parent(); Parent.Child child = parent.new Child(); gDC.drawString( child.setString("Hello").getString(), // Hello 20, 40 ); } class Parent { class Child { public Child setString(final String string) { return new Child() { public String getString() { return string; } }; } public String getString() // niezbędne! { return "Anything"; } } } } Jan Bielecki Sytuacje wyjątkowe Podczas wykonywania programu mogą wystąpić sytuacje wyjątkowe: np. dzielenie całkowite przez 0, użycie danej o złym formacie albo napotkanie końca pliku podczas wprowadzania zmiennej (klasy DataInputStream i RandomAccessFile). Z miejsca wystąpienia sytuacji wyjątkowej jest wysyłany wyjątek: obiekt klasy Throwable opisujący zaistniałą sytuację. Dzięki temu, że wyjątek może być przechwycony, a sytuacja wyjątkowa obsłużona, powstaje szansa wyjścia z zaistniałej sytuacji i umożliwienie dalszego wykonywania programu. Uwaga: Klasa Throwable wywodzi się z klasy Object, a jej klasami pochodnymi są Exception i Error. Z klasy Exception wywodzi się m.in. klasa RuntimeException, a z niej takie klasy jak ArithmeticException, ArrayIndexOutOfBoundsException i NullPointerException. Wysyłanie wyjątków Do jawnego wysyłania wyjątków służy instrukcja throw, a do przechwytywania i obsługiwania wyjątków instrukcja try. Jeśli wywołanie funkcji może spowodować wysłanie wyjątku, to wymaga się, aby zawarto je w bloku instrukcji try albo aby w nagłówku funkcji wywołującej umieszczono frazę throws wyszczególniającą klasę albo klasą bazową tego wyjątku. To samo dotyczy instrukcji throw, ponieważ jej wykonanie zawsze powoduje wysłanie wyjątku. Wymaganie obsłużenia albo wyszczególnienia nie dotyczy wyjątków klasy Error, w tym wyjątków klas OutOfMemoryError (brak pamięci operacyjnej) StackOverflowError (przepełnienie stosu) ClassFormatError (błąd reprezentacji klasy) NoClassDefFoundError (brak definicji klasy) ponieważ na ogół nie wiadomo jak je obsłużyć, a także nie dotyczy wyjątków klasy RuntimeException, w tym wyjątków klas ArithmeticException (błąd operacji arytmetycznej) NumberFormatException (zły format liczby) IndexOutOfBoundsException (indeks poza zakresem) NegativeArraySizeException (ujemny rozmiar tablicy) NullPointerException (odwołanie przez odniesienie puste) ponieważ są objawem błędów, które z pewnością zostaną usunięte podczas uruchamiania programu. Wszystkie pozostałe wyjątki klasy Exception, w tym wyjątki klas EOFException (koniec pliku) FileNotFoundException (brak pliku) InterruptedIOException (przerwanie przesłania) pochodnych od IOException muszą być obsłużone albo wyszczególnione. Uwaga: Wyszczególnienie nazwy wyjątku we frazie throws metody przedefiniowującej jest dozwolone tylko wówczas, gdy taka nazwa występuje albo może wystąpić we frazie throws metody przedefiniowywanej. Dotyczy to m.in. przedefiniowania metody run klasy Thread. int readByte(String fileName) throws IOException { try { FileInputStream inp = new FileInputStream(fileName); try { return inp.read(); } catch(IOException e) { System.out.println("File " + fileName + " empty or read error"); throw new IOException(); } } catch(FileNotFoundException e) { System.out.println("File " + fileName + " does not exist"); return -1; } } Podczas wykonania operacji new FileInputStream(fileName) może zostać wysłany wyjątek klasy FileNotFoundException albo wyjątek klasy IOException. Pierwszy z nich jest obsłużony przez zewnętrzną frazę catch, a możliwość wysłania drugiego wyszczególnia fraza throws. Podczas wykonywania operacji inp.read() może być wysłany wyjątek klasy IOException. W takim wypadku zostanie obsłużony przez wewnętrzną frazę catch. Obsługiwanie wyjątków Zapamiętanie jakie wyjątki są wysyłane przez poszczególne funkcje jest dość uciążliwe, dlatego przypomnienie o konieczności obsłużenia albo wyszczególnienia wyjątku pozostawia się zazwyczaj kompilatorowi. W szczególności, kompilacja następującego programu ujawnia błąd polegający na nieuwzględnieniu tego, że funkcja sleep może wysłać wyjątek klasy InterruptedException. public class Main { public static void main(String args[]) { pause(100); } public static void pause(int time) { Thread.sleep(time); // błąd } } Błąd ten można usunąć, zawierając wywołanie funkcji sleep w bloku instrukcji try public class Main { public static void main(String args[]) { pause(1000); } public static void pause(int time) { try { Thread.sleep(time); } catch(InterruptedException e) { } } } albo wyszczególniając wyjątek w nagłówku funkcji pause i zawierając jej wywołanie w bloku instrukcji try public class Main { public static void main(String args[]) { try { pause(1000); } catch(InterruptedException e) { } } public static void pause(int time) throws InterruptedException { Thread.sleep(time); } } Można także całkowicie zrezygnować z użycia instrukcji try, nadając programowi postać public class Main { public static void main(String args[]) throws InterruptedException { pause(1000); } public static void pause(int time) throws InterruptedException { Thread.sleep(time); } } ale taki sposób postępowania nie jest zalecany. Stosowanie wyjątków Dobry styl programowania polega na pełnym obsługiwaniu wyjątków i reagowaniu nawet na te sytuacje wyjątkowe, których wystąpienie wydaje się mało prawdopodobne. Następujący program oblicza iloraz dostarczonych mu argumentów liczbowych. Przyjęto w nim, że argumenty przygotowano poprawnie i dlatego wbrew zasadom dobrego stylu programowania zrezygnowano z obsłużenia wyjątków klas NumberFormatError i IndexOutOfBoundsException. import java.io.IOException; public class Main { public static void main(String args[]) throws IOException { double dividend = Double.valueOf(args[0]).doubleValue(), divisor = Double.valueOf(args[1]).doubleValue(); System.out.println("Result = " + dividend / divisor); System.in.read(); } } Dopiero po wyposażeniu w pełną obsługę wyjątków, program staje się "odporny na dane". Mimo iż jest znacznie dłuższy, tylko w takiej wersji zasługuje na miano produktu. Uwaga: Wyjątek klasy ArrayIndexOutOfBoundsException może zostać wysłany podczas opracowywania odwołań args[0] i args[1], a wyjątek NumberFormatException może zostać wysłany podczas wykonywania funkcji valueOf. Ponieważ dzielenie nie-całkowite przez 0 nie wysyła wyjątku, więc w programie zastosowano instrukcję throw. import java.io.IOException; public class Main { public static void main(String args[]) throws IOException { try { double dividend = Double.valueOf(args[0]).doubleValue(); double divisor = Double.valueOf(args[1]).doubleValue(); if(divisor == 0) throw new ArithmeticException(); System.out.println( "Result = " + dividend / divisor ); } catch(ArrayIndexOutOfBoundsException e) { System.out.println("Program needs 2 arguments"); } catch(NumberFormatException e) { System.out.println( "Wrong argument " + e.getMessage() ); } catch(ArithmeticException e) { System.out.println("Division by 0"); } catch(Throwable e) { System.out.println("Something is wrong"); } finally { if(args.length > 2) System.out.println("Extra arguments dropped"); } System.in.read(); } } Jan Bielecki Obsługiwanie urządzeń Typowymi urządzeniami zewnętrznymi są myszka, klawiatura, głośnik i drukarka. Obsługiwanie myszki Do obsługiwania myszki deleguje się obiekty klas implementujących interfejs MouseListener i MouseMotionListener. Pierwszy z nich deklaruje metody mousePressed, mouseReleased, mouseClicked, mouseEntered, mouseExited, a drugi metody mouseMoved i mouseDragged. Współrzędnych punktu, w którym rozpoznano zdarzenie związane z myszką, dostarczają metody getX, getY i getPoint, a liczbę kliknięć metoda getClickCount. Do rozpoznania czy zdarzeniu towarzyszyło naciśnięcie klawiszy Ctrl, Shift, Alt służą metody isCtrlDown, isShiftDown, isAltDown, a do rozpoznania, czy zdarzenie dotyczyło prawego przycisku myszki służy metoda isMetaDown. W celu uproszczenia zapisu klasy, której obiekty są wykorzystywane do nasłuchiwania zdarzeń związanych z myszką, używa się klas adaptacyjnych: MouseAdapter i MouseMotionAdapter. addMouseListener( new MouseAdapter() { public void mouseReleased(MouseEvent evt) { int x = evt.getX(), y = evt.getY(), c = evt.getClickCount(); if(evt.isMetaDown() && evt.isShiftDown() && (c==2)) { // w punkcie o współrzędnych (x,y) // rozpoznano prawe-Shift-dwukliknięcie} // ... } // ... } } ); Następujący aplet, pokazany na ekranie Przyciski myszki, informuje o tym, czy operację wykonano za pomocą lewego, czy za pomocą prawego przycisku myszki. Ekran Przyciski myszki ### right.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet { public void init() { addMouseListener( new MouseAdapter() { public void mousePressed(MouseEvent evt) { int x = evt.getX(), y = evt.getY(); String button = "LEFT"; Graphics gDC = getGraphics(); if(evt.isMetaDown()) button = "RIGHT"; gDC.drawString( "You pressed " + button + " button!", x, y ); } public void mouseReleased(MouseEvent evt) { repaint(); } } ); } public void paint(Graphics gDC) { gDC.drawString("Press mouse button!", 10, 20); } } Obsługiwanie klawiatury Do obsługiwania klawiatury deleguje się obiekty klas implementujących interfejs KeyListener. Deklaruje on metody keyPressed, keyReleased i keyTyped. Wirtualny kod naciśniętego klawisza dostarcza metoda getKeyCode, a jego Unikod metoda getKeyChar. int getKeyCode() Zdarzenia KEY_PRESSED i KEY_RELEASED Dla dowolnego klawisza dostarcza jego kod wirtualny (m.in. identyczny dla Q i q oraz dla % i 5). Zdarzenie KEY_TYPED Dostarcza kod CHAR_UNDEFINED. char getKeyChar() Zdarzenia KEY_PRESSED i KEY_RELEASED Dla znaków zwykłych (np. q, T, 2, %) i nielicznych specjalnych (np. Tab, BkSp, Enter) dostarcza ich Unikod. Dla pozostałych (np. Shift, F2, Home, Del) dostarcza CHAR_UNDEFINED. Zdarzenie KEY_TYPED Dla znaków zwykłych (np. q, T, 2, %) i nielicznych specjalnych (np. Tab, BkSp, Enter) dostarcza ich Unikod. Dla pozostałych (np. Shift, F2, Home) nie zachodzi zdarzenie KEY_TYPED. Do rozpoznania czy zdarzeniu towarzyszyło naciśnięcie klawiszy Ctrl, Shift, Alt służą metody isCtrlDown, isShiftDown, isAltDown. W celu uproszczenia zapisu klasy, której obiekty są wykorzystywane do nasłuchiwania zdarzeń związanych z klawiaturą, używa się klasy adaptacyjnej KeyAdapter. addKeyListener( new KeyAdapter() { public void keyReleased(KeyEvent evt) { if(evt.isShiftDown()) { // rozpoznano Shift-zwolnienie // klawisza różnego od Shift // ... } // ... } } ); Następujący aplet, pokazany na ekranie Obsługiwanie klawiatury, ujawnia w okienku Debug sekwencję zdarzeń, jakie wystąpią po naciśnięciu klawisza A (małej litery a), a następnie klawisza Shift-A (dużej litery A). Ekran Obsługiwanie klawiatury ### typing.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; import janb.debug.Debug; public class Master extends Applet { public void start() { requestFocus(); // celownik na aplet } public void init() { new Debug(200, 280); addKeyListener( new KeyAdapter() { public void showKey(KeyEvent evt, String event) { char keyChar = evt.getKeyChar(); int code = evt.getKeyCode(); if(code == KeyEvent.VK_SHIFT) Debug.toFrame("Shift " + event); else { code = (int)keyChar; Debug.toFrame("pressed = " + code); } } public void keyPressed(KeyEvent evt) { showKey(evt, "pressed"); } public void keyReleased(KeyEvent evt) { showKey(evt, "released"); Debug.toFrame(""); } public void keyTyped(KeyEvent evt) { showKey(evt, "typed"); } } ); } public void paint(Graphics gDC) { gDC.drawString("Press a key!", 10, 20); } } Obsługiwanie głośnika Do zarządzania plikami dźwiękowymi apletu służą metody loop, play i stop zadeklarowane w interfejsie AudioClip pakietu java.applet. void loop() Cykliczne odtwarzanie podanego dźwięku, np. np. getAudioClip(URL, "greet/hello.au").loop(); void play() Odegranie podanego dźwięku, np. np. getAudioClip(URL, "greet/hello.au).play(); void stop() Zaniechanie dalszego odgrywania dźwięku, np. np. AudioClip hello = getAudioClip(URL, "greet/hello.au"); hello.play(); Thread.sleep(2000); hello.stop(); Następujący aplet odgrywa dwie melodie: jedną cyklicznie w tle (z pliku Music.au) i drugą co 1 s (z pliku Gong.au). =============================================== import java.applet.*; import java.awt.*; import java.net.*; public class Master extends Applet implements Runnable { protected AudioClip back; protected AudioClip fore; protected Thread player = null; protected boolean isVisible = true; public void init() { URL from = getDocumentBase(); back = getAudioClip(from, "Music.au"); fore = getAudioClip(from, "Gong.au"); } public void start() { isVisible = true; if(back != null) back.loop(); if(fore != null) { player = new Thread(this); player.start(); } } public void stop2() { isVisible = false; try { player.join(); } catch(InterruptedException e) { } if(back != null) { back.stop(); back = null; } } public void run() { while(isVisible) { if(fore != null) fore.play(); try { Thread.sleep(2000); } catch(InterruptedException e) { } } } } Obsługiwanie drukarki Do zarządzania drukarką służą metody klas PrintJob i PrinterJob. Drukowanie polega na wysłaniu do drukarki oblicza komponentu albo zawartości pojemnika. Istotę postępowania ilustruje następujący schemat. // utworzenie odnośnika do obiektu wydruku PrintJob printJob; // uzyskanie obiektu wydruku Toolkit kit = Toolkit.getDefaultToolkit(); printJob = kit.getPrintJob( frame, // okno macierzyste dialogu "Print Job", // tytuł okna dialogu null // obiekt parametrów wydruku ); if(printJob != null) { // utworzenie wykreślacza Graphics pDC = printJob.getGraphics(); if(pDC != null) { // wykreślenie komponentu // identyfikowanego przez toPrint toPrint.printAll(pDC); // wyrzucenie strony pDC.dispose(); // zakończenie drukowania printJob.end(); } } Drukowanie komponentu odbywa się za pomocą jego metody paint. W celu rozstrzygnięcia, czy wywołano ją podczas wyświetlania, czy podczas drukowania, można posłużyć się operatorem instanceof. public void paint(Graphics gDC) { // ... if(gDC instanceof PrintGraphics) { // drukowanie } else { // wykreślanie } // ... } Dzielenie wydruku na strony i drukowanie numerów stron należy wykonać we własnym zakresie. Rozmiar strony oraz rozdzielczość wydruku można otrzymać za pomocą metod Dimension getPageDimension() oraz int getPageResolution() wywołanych na rzecz obiektu klasy PrintJob. Uwaga: Drukowanie odbywa się z rozdzielczością około 100 pikeli / cal (a więc zbliżoną do rozdzielczości ekranowej, która wynosi: 50 p/cal w trybie 640 x 480 i 70 p/cal w trybie 800 x 600). Z przeprowadzonych eksperymentów wynika, że liczba ta nie zależy od faktycznej rozdzielczości drukarki, która na przykład dla HP LasetJet IIID wynosi 300 p/cal; dla Cannon BJC 4200 wynosi 360 p/cal. Następujący program, pokazany na ekranie Drukowanie komponentów, reaguje na kliknięcie przycisku Print wykreśleniem zawartości pulpitu ramki: przycisku oraz żuków. Ekran Drukowanie komponentów ### bugs.gif import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Main extends Frame { protected static Main main; protected String face[] = { "North", "South", "East", "West" }; protected Image[] bugs = new Image [4]; protected Bug[] bug = new Bug [4]; public static void main(String[] args) { main = new Main("Bugs"); } public Main(String caption) { super(caption); MediaTracker tracker = new MediaTracker(this); Toolkit kit = Toolkit.getDefaultToolkit(); for(int i = 0; i < 4 ; i++) { char f = face[i].charAt(0); bugs[i] = kit.getImage("Bug" + f + ".gif"); tracker.addImage(bugs[i], 0); try { tracker.waitForID(0); } catch(InterruptedException e) { } bug[i] = new Bug(bugs[i]); add(bug[i], face[i]); } Button print = new Button("Print"); add(print, BorderLayout.CENTER); pack(); setVisible(true); print.addMouseListener( new MouseAdapter() { public void mouseClicked(MouseEvent evt) { show(main, main); } } ); addWindowListener( new WindowAdapter() { public void windowClosing(WindowEvent evt) { System.exit(0); } } ); } public void show(Main parent, Component toPrint) { PrintJob printJob; Toolkit kit = Toolkit.getDefaultToolkit(); printJob = kit.getPrintJob(parent, "Bugs", null); if(printJob != null) { Graphics pDC = printJob.getGraphics(); toPrint.printAll(pDC); pDC.dispose(); printJob.end(); } } class Bug extends Panel { protected Image bug; public Bug(Image bug) { this.bug = bug; } public void paint(Graphics gDC) { gDC.drawImage(bug, 8, 8, this); } public Dimension getPreferredSize() { return new Dimension(50, 50); } } } Jan Bielecki Współrzędne pulpitu Współrzędne pulpitu określa się względem lewego-górnego narożnika obszaru ograniczającego. W przypadku ramek i okien należy wziąć pod uwagę wcięcia obszaru. Następujący program, pokazany na ekranie Współrzędne pulpitu, ujawnia współrzędne kursora udostępniane metodom mousePressed i mouseMoved. Pokazuje on również, w jaki sposób należy posługiwać się współrzędnymi pulpitu okna, aby móc wykreślać obiekty w całości widoczne w obszarze wykreślania. Ekran Współrzędne pulpitu ### coordin.gif import java.awt.*; import java.awt.event.*; public class Main extends Frame { private int w = 200, h = 200; private static Main main; private static int d = 50; public static void main(String[] args) { main = new Main("Coordinates"); MenuBar menuBar = new MenuBar(); Menu fileMenu = new Menu("File"); menuBar.add(fileMenu); main.setMenuBar(menuBar); } Label xLab = new MyLabel(" x "), yLab = new MyLabel(" y "), cLab = new MyLabel(" "); public Main(String caption) { super(caption); main = this; setLayout(new FlowLayout()); add(xLab); add(yLab); add(cLab); setSize(w, h); show(); addMouseListener(new MouseHandler()); addMouseMotionListener(new MouseMotionHandler()); addWindowListener( new WindowAdapter() { public void windowClosing(WindowEvent evt) { dispose(); System.exit(0); } } ); } public void paint(Graphics gDC) { gDC.fillOval(0, 0, d-1, d-1); gDC.translate( main.getInsets().left, main.getInsets().top ); int lr = main.getInsets().left + main.getInsets().right; gDC.fillOval(w-lr-d-1, 0, d-1, d-1); } class MouseHandler extends MouseAdapter { public void mousePressed(MouseEvent evt) { int x = evt.getX(), y = evt.getY(); cLab.setText(" (" + x + "," + y + ") "); cLab.setVisible(true); cLab.invalidate(); Graphics gDC = main.getGraphics(); gDC.drawOval(x-10, y-10, 20-1, 20-1); } public void mouseReleased(MouseEvent evt) { cLab.setVisible(false); } } class MouseMotionHandler extends MouseMotionAdapter { public void mouseMoved(MouseEvent evt) { int x = evt.getX(), y = evt.getY(); xLab.setText(" x=" + x); yLab.setText(" y=" + y); } } } class MyLabel extends Label { public MyLabel(String label) { super(label); } public void paint(Graphics gDC) { super.paint(gDC); Dimension dim = getSize(); int w = dim.width, h = dim.height; gDC.drawRect(0, 0, w-1, h-1); } } Jan Bielecki Wykreślanie figur Do wykreślania obiektów graficznych służy wykreślacz. Umożliwia on sporządzanie wykresów na pulpicie, w pamięci albo na papierze. Odnośnik do wykreślacza otrzymuje się poprzez parametr metody paint i update. Odnośnik do własnego wykreślacza dostarcza metoda getGraphics. Każde jej wywołanie powoduje utworzenie odrębnego wykreślacza. Z wykreślaczem jest związana czcionka, kolor, tryb wykreślania i obszar obcinania. Wykreślaczowi można wydawać polecenia wykreślania i usuwania obiektów graficznych. void drawString(String str, int x, int y) Wykreślenie tekstu str spoczywającego na domyślnym, poziomym odcinku bazowym, którego lewy koniec ma współrzędne (x,y). void drawLine(xA, yA, xZ, yZ) Wykreślenie odcinka łączącego punkty o współrzędnych (xA, yA) i (xZ, yZ). void drawRect(int x, int y, int w, int h) Wykreślenie prostokąta o współrzędnych lewego-górnego wierzchołka (x,y) i rozmiarach w x h pikseli (w - szerokość, h - wysokość). void drawOval(int x, int y, int w, int h) Wykreślenie owalu (okręgu albo elipsy) wpisanego w domyślny prostokąt, o współrzędnych lewego-górnego wierzchołka (x,y) i rozmiarach w x h pikseli. void drawArc(int x, int y, int w, int h, int f, int t) Wykreślenie łuku wpisanego w domyślny prostokąt, o współrzędnych lewego-górnego wierzchołka (x,y) i rozmiarach w x h pikseli, od kąta początkowego f do kąta końcowego t. void drawPolygon(int x[], int y[], int n) Wykreślenie linii łamanej łączącej n punktów o współrzędnych (x[i], y[i]). void fillRect(int x, int y, int w, int h) Wykreślenie wypełnionego prostokąta (por. drawRect). void fillOval(int x, int y, int w, int h) Wykreślenie wypełnionego owalu (por. drawOval). void fillPolygon(int x[], int y[], int n) Wykreślenie wypełnionego wielokąta (por. drawPolygon). void clearRect(int x, int y, int w, int h) Wykreślenie wypełnionego prostokąta kolorem tła. Uwaga: W celu wykreślenia prostokąta i owalu podaje się współrzędne lewego-górnego narożnika domyślnego prostokąta, w który jest wpisany obiekt oraz rozmiary tego prostokąta zmniejszone o 1. W celu wykreślenia odcinka podaje się współrzędne jego końców. Następujący program, pokazany na ekranie Figury geometryczne, ilustruje użycie podanych metod. Ekran Figury geometryczne ### figures.gif =============================================== import java.applet.Applet; import java.awt.*; public class Master extends Applet { protected int w, h; public void init() { Dimension dim = getSize(); w = dim.width; h = dim.height; } public void paint(Graphics gDC) { gDC.drawLine(0, 0, w-1, h-1); gDC.drawRect(0, 0, w-1, h-1); gDC.setColor(Color.blue); gDC.fillRect(0, 50, 60, 50); gDC.drawRoundRect(100, 100, 60, 60, 15, 25); gDC.drawOval(50, 0, 50, 70); gDC.setColor(Color.magenta); gDC.fillOval(90, 40, 50, 50); gDC.drawArc(80, 80, 40, 40, 0, 270); int[] xVec = { 50, 120, 70, 60, }, yVec = { 100, 150, 150, 180, }; gDC.fillPolygon(xVec, yVec, 4); } } Układ współrzędnych Współrzędne komponentu graficznego: płótna, apletu, ramki, itp. są określane względem jego lewego-górnego narożnika. Współrzędne x zwiększają się w prawo, a współrzędne y do dołu. Lewy-górny narożnik komponentu ma współrzędne (0, 0). Uwaga: Współrzędne komponentu umieszczonego w pojemniku są określane względem lewego-górnego narożnika pojemnika, a współrzędne udostępniane w miejscu obsługi zdarzenia związanego z komponentem są określane względem lewego-górnego narożnika komponentu. Następujący aplet, pokazany na ekranie Współrzędne obiektów, ilustruje sposób wykreślenia tekstu i jego odcinka bazowego oraz widocznego w całości, największego prostokąta jaki można wykreślić na pulpicie apletu. Ekran Współrzędne obiektów ### coords.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.font.*; import java.awt.geom.*; public class Master extends Applet { private final String greet = "Hello"; private final int xPos = 20, yPos = 80; private int w, h; private Font font; public void init() { Dimension dim = getSize(); w = dim.width; h = dim.height; } public void paint(Graphics gDC) { font = new Font("Serif", Font.BOLD, 48); Graphics2D gDC2 = (Graphics2D)gDC; FontRenderContext frc = gDC2.getFontRenderContext(); TextLayout layout = new TextLayout(greet, font, frc); Rectangle2D bounds = layout.getBounds(); int ww = (int)bounds.getWidth(); gDC.setFont(font); gDC.setColor(Color.blue); gDC.drawString(greet, xPos, yPos); gDC.setColor(Color.red); gDC.drawLine(xPos, yPos, xPos+ww-1, yPos); gDC.setColor(Color.black); gDC.drawRect(0, 0, w-1, h-1); } } Kolory Domyślnym modelem koloru jest RGB. W modelu RGB każdy kolor można przedstawić jako trójkę składników RGB w której R (red), G (green), B (blue) określają ile w kolorze jest składnika czerwonego, zielonego i niebieskiego. W szczególności, kolor RGB(255,255,0), w którym występuje maksymalna ilość czerwieni i zieleni, ale nie ma składnika niebieskiego, jest kolorem żółtym. Modelem alternatywnym do RGB jest HSB. W modelu tym H określa odcień (hue), S określa nasycenie (saturation), a B określa jaskrawość (brightness). W programie wynikowym kolor jest reprezentowany przez czwórkę bajtów aRGB w której a jest składnikiem alfa, określającym przeźroczystość koloru, a R, G, B są składnikami koloru o wartościach z przedziału 0..255 włącznie. Jeśli składnik alfa ma wartość 0x00, to kolor jest całkowicie przeźroczysty (transparent), a jeśli ma wartość 0xff, to jest całkowicie nieprzezroczysty (opaque). Możliwe są wartości pośrednie. Domniemaną wartością alfa jest 0xff. Wybrane kolory mają oznaczenia symboliczne podane w tabeli Symbole kolorów Tabela Symbole kolorów ### Color.black Color.blue Color.cyan Color.darkGray Color.gray Color.green Color.lightGray Color.magenta Color.orange Color.pink Color.red Color.white Color.yellow ### Kolor bieżący Z każdym wykreślaczem jest związany kolor bieżący. Ustawienie i pobranie koloru bieżącego odbywa się za pomocą metod setColor i getColor. void setColor(Color color) Ustawienie koloru bieżącego na podany. Color getColor() Dostarczenie odnośnika do odrębnego obiektu opisującego kolor bieżący. Kolor lica i kolor tła Z każdym komponentem jest związany kolor lica (foreground) i kolor tła (background). Do zarządzania nimi służą metody getForeground i setForeground oraz metody getBackground i setBackground. void setForeground(Color color) Ustawia kolor lica komponentu na podany. void setBackground(Color color) Ustawia kolor tła komponentu na podany. Color getForeground() Dostarcza kolor lica komponentu. Color getBackground() Dostarcza kolor tła komponentu. Tryb XOR Do wykreślenia i wytarcia wykreślonego obiektu, ale bez naruszenia tła służy wykreślanie w trybie XOR. Dwukrotne wykreślenie tego samego obiektu w trybie XOR przywraca pierwotny stan tła. void setXORMode(Color color) Dostosowuje wykreślacza do wykreślania w trybie XOR z podanym kolorem. Kolor wynikowy jest tworzony z podanego koloru, koloru tła i koloru bieżącego. Reprezentacje tych 3 kolorów są sumowane pozycyjnie, modulo-2. void setPaintMode() Dostosowuje wykreślacz do wykreślania w zwykłym trybie. Następujący aplet, pokazany na ekranie Odtwarzanie tła, umożliwia przeciąganie okręgu po aplecie, bez naruszenia kolorowego napisu stanowiącego tło. Ekran Odtwarzanie tła ### xormode.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet { private final int r = 20; private Font font; private int xOld = -1000, yOld = -1000; public void init() { setBackground(Color.green); font = new Font("Serif", Font.BOLD, 60); addMouseMotionListener( new MouseMotionAdapter() { public void mouseDragged(MouseEvent evt) { int x = evt.getX(), y = evt.getY(); drawRing(xOld, yOld); drawRing(xOld = x, yOld = y); } } ); } public void paint(Graphics gDC) { gDC.setFont(font); gDC.setColor(Color.red); gDC.drawString("Drag over me!", 10, 60); } public void drawRing(int x, int y) { Graphics gDC = getGraphics(); gDC.setColor(Color.red); gDC.setXORMode(Color.green); int l = x-r, t = y-r, w = 2*r, h = 2*r; for(int i = 0; i < 5 ; i++) gDC.drawOval(l+i, t+i, w-2*i, h-2*i); } } Czcionki Czcionkę charakteryzuje rozmiar, krój i styl. Na przykład tytuł niniejszego podrozdziału jest napisany 14-punktową czcionką Times New Roman CE, w stylu Bold. Mając na względzie przenośność wyglądu czcionki zaleca się posługiwać tylko czcionkami o nazwach podanych w tabeli Czcionki (dla porównania podano ich odpowiedniki w systemie Windows). Uwaga: Czcionka domyślna ma rozmiar 12 punktów i styl zwykły. Tabela Czcionki ### Java Windows 95/98/NT Serif Times New Roman SansSerif Arial Monospaced Courier New Dialog MS Sans Serif DialogInput ### Do zapisania stylu można posłużyć się symbolami podanymi w tabeli Style czcionki. Tabela Style czcionki ### Styl Symbol zwykły Font.PLAIN kursywa Font.ITALIC pogrubiony Font.BOLD pogrubiona kursywa Font.BOLD | Font.ITALIC ### Czcionka jest reprezentowana przez obiekt klasy Font. W wywołaniu jej konstruktora podaje się nazwę kroju, styl i rozmiar czcionki. Font(String name, int style, int size) Utworzenie obiektu czcionki o podanym kroju, stylu i rozmiarze. Jeśli w danym Systemie taka czcionka nie istnieje, to zostanie utworzona czcionka maksymalnie zbliżona do wymaganej. Następujący program, pokazany na ekranie Kroje i style, wyświetla zestaw 12-punktowych czcionek standardowych o różnych krojach i stylach. Ekran Kroje i style ### styles.gif =============================================== import java.applet.Applet; import java.awt.*; public class Master extends Applet { String sample = "Żółć\u27a5\u00a3\u00be"; String string; String typeFace[] = { "Serif", "SansSerif", "Monospaced", "Dialog", "DialogInput", }; int style[] = { Font.PLAIN, Font.BOLD, Font.ITALIC, Font.BOLD + Font.ITALIC }; String styleName[] = { " plain", " bold", " italic", " bold-italic" }; public void init() { // przypisanie kodu znaku char chr = 0x2720; // to samo co ’\u2720’ sample += chr; } public void paint(Graphics gDC) { for(int n = 1, i = 0; i < typeFace.length ; i++) { String fontName = typeFace[i]; for(int j = 0; j < style.length ; j++) { int fontStyle = style[j]; Font font = new Font(fontName, fontStyle, 14); gDC.setFont(font); string = fontName + styleName[j] + " " + sample; gDC.drawString(string, 10, 10 + 15 * n++); } n++; } } } Metryka Kompletny zestaw właściwości czcionki opisuje jej metryka. Metryka czcionki jest zawarta w obiekcie klasy LineMetrics. Ważnymi parametrami metryki są uniesienie (ascent) odległość między liną bazową, a szczytem znaku, jak w literze Ś obniżenie (descent) odległość między linią bazową, a podstawą znaku, jak w literze g światło (leading) odstęp poniżej podstawy znaku wysokość (height) suma uniesienia, obniżenia i światła int getAscent() Dostarcza uniesienie w pikselach. int getDescent() Dostarcza obniżenie w pikselach. int getLeading() Dostarcza światło w pikselach. int getHeight() Dostarcza wysokość czcionki w pikselach. Następujący aplet, pokazany na ekranie Metryka czcionki, ilustruje posługiwanie się ważniejszymi parametrami czcionki. Ekran Metryka czcionki ### metrics.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.geom.*; import java.awt.font.*; public class Master extends Applet { protected Font font = new Font("Serif", Font.BOLD, 80); protected String string = "Śmigło"; protected int xBase = 10, yBase, strWidth; protected float strAscent, strDescent, strHeight, strLeading; public void init() { // utworzenie wykreślacza Graphics2D gDC2 = (Graphics2D)getGraphics(); // utworzenie opisu obrazowania FontRenderContext frc = gDC2.getFontRenderContext(); // zdefiniowanie tekstu TextLayout layout = new TextLayout(string, font, frc); // zdefiniowanie obwiedni Rectangle2D bounds = layout.getBounds(); int strWidth = (int)bounds.getWidth(); // utworzenie metryki LineMetrics mtx = font.getLineMetrics("a", frc); strAscent = mtx.getAscent(); strDescent = mtx.getDescent(); strHeight = mtx.getHeight(); strLeading = strHeight - (strAscent + strDescent); yBase = (int)strHeight + 10; } public void paint(Graphics gDC) { Graphics2D gDC2 = (Graphics2D)gDC; gDC.setFont(font); gDC.drawString(string, xBase + 50, yBase); draw(gDC, "BaseLine", 0); draw(gDC, "AscentLine", (int)strAscent); draw(gDC, "DescentLine", (int)-strDescent); draw(gDC, "", (int)(-strDescent-strLeading)); } void draw(Graphics gDC, String str, int h) { gDC.drawLine(xBase, yBase-h, xBase+ 50 + 10 + strWidth, yBase-h); Font oldFont = gDC.getFont(), newFont = new Font("Serif", Font.ITALIC, 10); gDC.setFont(newFont); gDC.drawString(str, xBase, yBase-h); gDC.setFont(oldFont); } } Obszar wykreślania Częścią obszaru ograniczającego pojemnik jest jego obszar wykreślania nazywany pulpitem. W obszarze tym ujawniają się wykreślane obiekty graficzne. W przypadku płócien, apletów i paneli, obszar wykreślania pokrywa się z obszarem pojemnika, natomiast w przypadku okien i ramek jest zmniejszony o wcięcia. W systemie Windows domniemane wcięcia dla ramki mają wartości 4, 23, 4, 4. Dimension getSize() Dostarcza odnośnik do obiektu, którego pola width i height określają poziomy i pionowy rozmiar obszaru pojemnika. Insets getInsets() Dostarcza odnośnik do obiektu, którego pola left (lewe), top (górne), right (prawe) i bottom (dolne) określają wcięcia krawędzi pulpitu względem krawędzi obszaru pojemnika. Następujący aplet, pokazany na ekranach Pulpit apletu i Pulpit ramki, napisano w taki sposób, aby rozmiar pulpitu ramki był identyczny z rozmiarem pulpitu apletu. W celu uwidocznienia obu pulpitów, wykreślono na nich prostokąty o maksymalnych rozmiarach. Uwaga: Umieszczenie przycisku na pulpicie oraz wykreślenie odcinka łączącego punkt kliknięcia ze środkiem ramki wymaga uwzględnienia wcięć. Ekran Pulpit apletu ### insets1.gif Ekran Pulpit ramki ### insets2.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet { private Frame frame; private Button hello, world; private int w, h; public void init() { setLayout(null); add(hello = new Button("Hello")); hello.setBounds(1, 1, 60, 40); addMouseListener( new MouseAdapter() { public void mouseReleased(MouseEvent evt) { Dimension dim = getSize(); int w = dim.width, h = dim.height; Graphics gDC = getGraphics(); int xC = w/2, yC = h/2, x = evt.getX(), y = evt.getY(); gDC.drawLine(x, y, xC, yC); } } ); frame = new MyFrame("Frame View"); Insets ins = frame.getInsets(); frame.add(world = new Button("World")); int l = ins.left, // 4 t = ins.top, // 23 r = ins.left, // 4 b = ins.bottom; // 4 world.setBounds(l+1, t+1, 60, 40); Dimension d = getSize(); // 160 x 100 int w = d.width + (l + r), h = d.height + (t + b); frame.setSize(w, h); } public void paint(Graphics gDC) { Dimension dim = getSize(); Insets ins = getInsets(); int w = dim.width - (ins.left + ins.right), h = dim.height - (ins.top + ins.bottom); gDC.drawRect(0, 0, w-1, h-1); } } class MyFrame extends Frame { public MyFrame(String caption) { super(caption); addMouseListener( new MouseAdapter() { public void mouseReleased(MouseEvent evt) { Dimension dim = getSize(); Insets ins = getInsets(); int lr = ins.left + ins.right, tb = ins.top + ins.bottom, w = dim.width - lr, h = dim.height - tb; Graphics gDC = getGraphics(); int xC = w/2, yC = h/2, x = evt.getX(), y = evt.getY(); gDC.drawLine(x, y, xC, yC); } } ); addWindowListener( new WindowAdapter() { public void windowClosing(WindowEvent evt) { System.exit(0); } } ); show(); } public void paint(Graphics gDC) { Dimension dim = getSize(); Insets ins = getInsets(); int lr = ins.left + ins.right, tb = ins.top + ins.bottom, w = dim.width - lr, h = dim.height - tb; gDC.drawRect(ins.left, ins.top, w-1, h-1); } } Obszar obcinania Wykreślanie pikseli na komponencie odbywa się tylko w jego obszarze obcinania. Domyślnym obszarem obcinania komponentu jest jego obszar ograniczający. Polecenie wykreślenia piksela poza obszarem obcinania jest ignorowane. Do określania obszaru obcinania związanego z wykreślaczem komponentu służy metoda setClip. void setClip(int x, int, y, int width, int height) Definiuje jako obszar obcinania, prostokąt o współrzędnych narożnika (x,y) i rozmiarach w x h. Następujący aplet, pokazany na ekranie Obszar obcinania, wykreśla prostokąt, a następnie zajęty przezeń obszar definiuje jako obszar obcinania. Dlatego wykres jednego z okręgów jest obcięty do wybranego prostokąta. Ekran Obszar obcinania ### clip.gif =============================================== import java.applet.Applet; import java.awt.*; public class Master extends Applet { public void paint(Graphics gDC) { gDC.drawRect(25, 25, 50-1, 50-1); // zmiana domniemanego obszaru obcinania gDC.setClip(25, 25, 50, 50); gDC.drawOval(0, 25, 50-1, 50-1); Dimension dim = getSize(); int w = dim.width, h = dim.height; // przywrócenie obszaru obcinania gDC.setClip(0, 0, w-1, h-1); gDC.drawOval(50, 25, 50-1, 50-1); } } Jan Bielecki Lokalizowanie zasobów Położenie zasobu określa jego lokalizator. W ogólnym przypadku lokalizator ma postać prot://host:port/~user/file#ref w której prot jest nazwą protokołu komunikacyjnego (np. file, http, ftp, telnet), host jest nazwą komputera-gospodarza (np. www.sun.com), port jest numerem portu (dla http domyślnie 80), user jest nazwą użytkownika (np. jbl), file jest nazwą pliku albo katalogu (np. Bell.gif), a ref jest odnośnikiem do miejsca w pliku HTML (np. Chapter2). Uwaga: Nazwę katalogu od nazwy pliku odróżnia w lokalizatorze to, że kończy ją skośnik (/). W szczególności jeśli zasobem jest lokalny plik Bubbles.gif, to jego lokalizatorem może być file:/C:/jbJava/jbGifs/Bubbles.gif a jeśli jest nielokalny plik Index.html, to jego lokalizatorem może być http://www.sun.com:80/~jack/applets/hello/Index.html#pos2 Do zarządzania lokalizatorami służy klasa java.net.URL. Jej konstruktory akceptują kompletny lokalizator oraz umożliwiają utworzenie lokalizatora na podstawie protokołu, gospodarza, portu i pliku. new URL("http://www.sun.com/Index.html") new URL("http", "www.sun.com", "Index.html") new URL("http", "www.sun.com", 80, "Index.html") Lokalizatory są dostarczane przez metody: getDocumentBase i getCodeBase. Pierwsza dostarcza lokalizator pliku HTML z opisem apletu, a druga dostarcza lokalizator katalogu użytego w jawnym albo domniemanym parametrze codebase. Uwaga: W wielu tekstach można przeczytać, że lokalizator użyty w parametrze codebase dotyczy katalogu, w którym znajduje się B-kod apletu. Tak jest tylko wówczas, gdy plik z B-kodem apletu nie został znaleziony w żadnym z katalogów wymienionych w parametrze środowiska classpath. Następujący aplet, pokazany na ekranie Lokalizator katalogu, ilustruje odwołanie do parametru codebase z domniemanym lokalizatorem określonym na podstawie lokalizatora tego pliku, w którym znajduje się opis apletu. Ekran Lokalizator katalogu ### folder.gif =============================================== import java.applet.Applet; import java.awt.*; import java.net.*; public class Master extends Applet { private String fileName = "Asterix.gif"; private MediaTracker tracker = new MediaTracker(this); private Image img; public void init() { URL codeBase = getCodeBase(); String fileBase = codeBase.getFile(); URL fileURL = null; try { // file:/D:/JDK12Run/src fileURL = new URL("file:" + fileBase); } catch(MalformedURLException e) { } img = getImage(fileURL, fileName); tracker.addImage(img, 0); try { tracker.waitForID(0); } catch(InterruptedException e) { } } public void paint(Graphics gDC) { gDC.drawImage(img, 0, 0, this); } } Jan Bielecki Ładowanie obrazów Wbrew mylącej nazwie, wywołanie metody getImage, powoduje jedynie sprawdzenie, czy istnieje plik, z którego ma być załadowany obraz. Jeśli przed wywołaniem metody drawImage nie wywoła się metody prepareImage albo nie skorzysta się z usług nadzorcy mediów, to ładowanie obrazu zacznie się dopiero przed przystąpieniem do jego wykreślenia. Uwaga: Jeśli obraz nie istnieje, to metoda getImage dostarcza null. Rolę nadzorcy mediów pełni obiekt klasy MediaTracker. Po określeniu miejsca, z którego obraz pochodzi i utworzeniu odnośnika typu Image, można powierzyć nadzorcy zadanie załadowania obrazu. Nie spowoduje to wstrzymania programu. Wstrzymanie, do chwili załadowania obrazu, nastąpi dopiero po wywołaniu metody waitForID. =============================================== import java.applet.Applet; import java.awt.*; import java.net.*; public class Master extends Applet { protected Image img; public void init() { URL where = getDocumentBase(); img = getImage(where, "Duke.gif"); MediaTracker tracker = new MediaTracker(this); tracker.addImage(img, 0); try { tracker.waitForID(0); } catch(InterruptedException e) { } } public void paint(Graphics gDC) { gDC.drawImage(img, 0, 0, this); } } Następujący aplet, pokazany na ekranie Nadzorowanie ładowania, ilustruje użycie nadzorcy mediów do przygotowania kadrów animacji. Ekran Nadzorowanie ładowania ### medtrack.gif =============================================== import java.applet.Applet; import java.awt.*; import java.net.*; public class Master extends Applet implements Runnable { private int count; private Image photos[]; private static MediaTracker tracker; private Thread thread; private Graphics gDC; private int width, height; public void init() { tracker = new MediaTracker(this); URL where = getDocumentBase(); try { String count = getParameter("Count"); this.count = Integer.parseInt(count); } catch(NumberFormatException e) { this.count = 0; } photos = new Image [count+1]; String prefix = getParameter("Prefix"); for(int i = 1; i < count+1 ; i++) { String parName = prefix + i, fileName = getParameter(parName); photos[i] = getImage(where, fileName); tracker.addImage(photos[i], i); } int failCount = 0; for(int i = 1; i < count+1 ; i++) { try { tracker.waitForID(i); } catch(InterruptedException e) { } } gDC = getGraphics(); thread = new Thread(this); thread.start(); } public void run() { for(int c = 0; c < 10 ; c++) { for(int i = 1; i < count+1 ; i++) { gDC.drawImage(photos[i], 0, 0, this); try { thread.sleep(100); } catch(InterruptedException e) { } } } } } Jan Bielecki Buforowanie wykreśleń Buforowanie wykreśleń polega na utworzeniu bufora w pamięci operacyjnej, skompletowaniu obrazu w buforze, a następnie skopiowaniu bufora na ekran. Głównym zastosowaniem buforowania jest animacja. Image createImage(int w, int h) Dostarcza odnośnik do obiektu reprezentującego obraz o rozmiarach w x h pikseli. Uwaga: Metoda createImage jest składową klasy Component, a więc m.in. może być wywołana na rzecz obiektu klasy Applet oraz Frame. Następujący aplet, pokazany na ekranie Kompletowanie obrazu, wyświetla animowane koło, które poruszając się poziomym ruchem wahadłowym, zawsze znajduje się nad czerwonymi i pod zielonymi kolumnami. Ekran Kompletowanie obrazu ### complete.gif =============================================== import java.applet.Applet; import java.awt.*; public class Master extends Applet implements Runnable { private final int Count = 10, Delta = 1; private int w, h; private Thread thread; private Image buffer; private int dx = Delta, ww, hh; private Column column; private Graphics gDC, mDC; public void init() { Dimension dim = getSize(); w = dim.width; h = dim.height; ww = w / (2*Count+1); hh = 2 * h / 3; column = new Column(ww/2, hh); gDC = getGraphics(); setBackground(Color.yellow); // utworzenie bufora buffer = createImage(w, h); // utworzenie wykreślacza mDC = buffer.getGraphics(); thread = new Thread(this); thread.start(); } public void run() { int x = 0, y = h>>1; while(true) { // skompletowanie obrazu w buforze mDC.clearRect(0, 0, w, h); for(int i = 0; i < (Count>>1) ; i++) column.draw(mDC, 2*ww + 4*i*ww, (h-hh)>>1, Color.red); mDC.setColor(Color.cyan); mDC.fillOval(x, y, 50, 50); mDC.setColor(Color.black); mDC.drawOval(x, y, 50, 50); for(int i = 0; i < (Count>>1) ; i++) column.draw(mDC, 4*ww + 4*i*ww, (h-hh)>>1, Color.green); // skopiowanie bufora na ekran gDC.drawImage(buffer, 0, 0, this); try { thread.sleep(20); } catch(InterruptedException e) { } x += dx; if(x > w-50 || x < 0) dx = -dx; } } } class Column { protected int r, h; protected Color c; public Column(int r, int h) { this.r = r; this.h = h; } public void draw(Graphics gDC, int x, int y, Color c) { gDC.setColor(c); gDC.fillOval(x-r, y-(r>>1)+h, r<<1, r); gDC.setColor(Color.black); gDC.drawOval(x-r, y-(r>>1)+h, r<<1, r); gDC.setColor(c); gDC.fillRect(x-r, y, r<<1, h); gDC.setColor(Color.black); gDC.drawLine(x-r, y, x-r, y+h); gDC.drawLine(x+r, y, x+r, y+h); gDC.setColor(Color.black); gDC.fillOval(x-r, y-(r>>1), r<<1, r); } } Jan Bielecki Pakietowanie klas Pakiet jest zestawem klas, pakietów i interfejsów. W szczególności, w pakiecie java.awt znajdują się m.in. klasy java.awt.Graphics i java.awt.Color oraz pakiet java.awt.event, a w pakiecie java.awt.event znajduje się m.in. klasa java.awt.event.AWTEventMulticaster i interfejs ActionListener. Przynależność do pakietu, klas i interfejsów zdefiniowanych w pliku źródłowym, określa się za pomocą specyfikacji pakietu. Specyfikacja pakietu ma postać package Name; w której Name jest nazwą pakietu. Uwaga: Jeśli przynależność do pakietu nie zostanie wyrażona jawnie, to klasy oraz interfejsy należą do pakietu domyślnego. W opisie apletu należy podać pełną nazwę klasy apletowej, uwzględniając jej przynależność do pakietu. Klasa z B-kodem apletu jest wówczas poszukiwana w katalogu określonym za pomocą parametrów classpath i codebase oraz w katalogu wyjściowym (domyślnie bieżącym). Ilustruje to następujący aplet. ====================================== package janb.greet; import java.applet.Applet; import java.awt.*; public class Master extends Applet { private String greet = "Hello"; public void paint(Graphics gDC) { gDC.drawString(greet, 10, 20); } } Polecenia importu Polecenia importu bierze się pod uwagę podczas opracowywania nazw klas. Dla dowolnej klasy Name nie wolno użyć więcej niż jednego polecenia importu kończącego się na Name. Po wszystkich jawnych poleceniach importu niejawnie dodaje się polecenie import java.lang.*; Uwaga: Jeśli klasa Name znajduje się w hierarchii pakietów, to powstały z niej plik Name.class musi znajdować się w analogicznej do niej hierarchii katalogów. Niespełnienie tego warunku w odniesieniu do kodu apletu powoduje wyprowadzenie komunikatów Applet not initialized (na pasku stanu apletu) load: class Master.class not found (w okienku komunikatów) Jeśli polecenie importu ma postać import xxx.yyy. ... .zzz.Name; wyraża ono życzenie, aby pełną nazwę klasy: xxx.yyy. ... .zzz.Name można było skrócić do Name. Jeśli polecenie importu ma postać import xxx.yyy. ... .zzz.*; to wyraża ono życzenie, aby pełną nazwę klasy Name należącej do pakietu xxx.yyy. ... .zzz można było skrócić do Name. W szczególności oznacza to, że polecenia importu są używane tylko dla wygody, a na przykład program public class Master extends java.applet.Applet { public void paint(java.awt.Graphics gDC) { gDC.setColor(java.awt.Color.red); gDC.drawString("Hello", 20, 20); } } jest równoważny programowi import java.applet.Applet; import java.awt.*; public class Master extends Applet { public void paint(Graphics gDC) { gDC.setColor(Color.red); gDC.drawString("Hello", 20, 20); } } Parametr classpath Poszukiwanie pliku zawierającego kod klasy uwzględnia łącznie polecenia importu i parametr classpath. Jeśli parametr środowiska classpath ma postać classpath=path;path;path a odwołanie do klasy Name odbywa się w pakiecie Pack (m.in. w pakiecie domyślnym), to dla każdej ścieżki path klasy Name najpierw szuka się w bieżącym pakiecie, a dopiero po tym, w pakietach wymienionych w poleceniach importu. Jeśli dla danej klasy, przeszukanie ścieżki zakończy się pomyślnie, to dla tej klasy dalsze ścieżki nie są już brane pod uwagę. Jeśli w ramach tej samej ścieżki path, klasę Name znajdzie się w więcej niż jednym pakiecie, a nie użyto ani jednego polecenia importu kończącego się na Name, ta wymaga się, aby klasa należała do pakietu bieżącego. Jeśli kończącego się na Name użyto, to polecenia importu przegląda się tylko do znalezienia klasy. W szczególności, jeśli przed kompilacją następującego programu package janb.pack; // pakiet bieżący import janb.packs.packA.*; import janb.packs.packB.*; import Name; public class Master { Name obj = new Name(); // ... } ustawiono classpath=c:\jbClasses;e:\jbClasses.zip;d:\jbClasses; to klasa Name będzie poszukiwana kolejno w c:\jbClasses\janb\pack c:\jbClasses\janb\packs\packA c:\jbClasses\janb\packs\packB d:\jbClasses.zip\janb\pack // poglądowo d:\jbClasses.zip\janb\packs\packA // poglądowo d:\jbClasses.zip\janb\packs\packB // poglądowo d:\jbClasses\janb\pack d:\jbClasses\janb\packs\packA d:\jbClasses\janb\packs\packB Gdyby z programu usunięto ostatnie polecenie importu, a klasę Name znaleziono w dwóch miejscach, to podczas kompilacji zostałby zasygnalizowany błąd class janb.pack.Name not found in type declaration Kolizje nazw Nie wyklucza się możliwości definiowania własnych klas o nazwach identycznych z nazwami klas pakietu java.lang (np. Integer). W takim wypadku własna klasa systemowa musi należeć do pakietu bieżącego. Uwaga: Jeśli skompilowaną, własną klasę o nazwie z pakietu java.lang, spróbuje się (za pomocą classpath) podstawić w miejsce klasy predefiniowanej (czyli przed ścieżką do classes.zip), to System nie dopuści do wykonania programu. Jan Bielecki Udostępnianie zdarzeń W chwili zajścia zdarzenia jest tworzony obiekt zdarzeniowy, a następnie udostępniany metodzie klasy nasłuchującej, przewidzianej do jego obsługi. W szczególności, w chwili zajścia zdarzenia action spowodowanego naciśnięciem przycisku, jest tworzony obiekt klasy ActionEvent udostępniony poprzez parametr metody actionPerformed, a w chwili zajścia zdarzenia window spowodowanego zamknięciem okna, jest two[PPL4]rzony obiekt klasy WindowEvent, udostępniony poprzez parametr metody windowClosing. Następujący aplet, pokazany na ekranie Zamknięcie okna, ilustruje wykorzystanie metod zdarzeniowych do zamknięcia okna utworzonego przez aplet. Ekran Zamknięcie okna ### closewin.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet implements ActionListener { private Button close; private Frame frame; public void init() { close = new Button("Close"); add(close); frame = new Frame("Frame"); frame.setSize(100, 150); frame.setVisible(true); close.addActionListener(this); frame.addWindowListener( new WindowAdapter() { public void windowClosing(WindowEvent evt) { frame.setVisible(false); frame.dispose(); } } ); } public void actionPerformed(ActionEvent evt) { frame.setVisible(false); frame.dispose(); } } Poniżej opisano zdarzenia predefiniowane oraz wyszczególniono metody klas zdarzeniowych, używane do rozpoznawania zdarzeń. W pierwszej kolejności opisano klasy abstrakcyjne AWTEvent i InputEvent, od których bezpośrednio albo pośrednio wywodzą się wszystkie pozostałe klasy zdarzeniowe. Klasa AWTEvent Klasa deklaruje m.in. metody umożliwiające rozpoznanie źródła oraz identyfikatora zdarzenia. Object getSource() źródło zdarzenia int getID() identyfikator zdarzenia Uwaga: Źródłem zdarzenia jest obiekt, w którym zaszło zdarzenie. Natomiast identyfikatorem zdarzenia jest liczba całkowita określająca rodzaj zdarzenia (np. MOUSE_PRESSED, MOUSE_RELEASED, itp.). W klasie AWTEvent zdefiniowano maski wykorzystywane w metodzie enableEvent. Wymieniono je w tabeli Maski zdarzeń. Uwaga: Wywołanie metody enableEvent na rzecz obiektu klasy Component powoduje, że obsługuje on zdarzenia nawet wówczas, gdy nie został zarejestrowany jako obiekt nasłuchujący. Tabela Maski zdarzeń ### ACTION_EVENT_MASK ADJUSTMENT_EVENT_MASK COMPONENT_EVENT_MASK CONTAINER_EVENT_MASK FOCUS_EVENT_MASK ITEM_EVENT_MASK KEY_EVENT_MASK MOUSE_EVENT_MASK MOUSE_MOTION_EVENT_MASK TEXT_EVENT_MASK WINDOW_EVENT_MASK ### Klasa InputEvent Klasa deklaruje m.in. metody umożliwiające rozpoznawanie naciśnięcia klawiszy Alt, Ctrl, Shift i Meta oraz metodę umożliwiającą określenie chwili zajścia zdarzenia. Component getComponent() źródło zdarzenia int getModifiers() modyfikatory boolean isAltDown() klawisz Alt wciśnięty boolean isCtrlDown() klawisz Ctrl wciśnięty boolean isShiftDown() klawisz Shift wciśnięty boolean isMetaDown() klawisz Meta wciśnięty long getWhen() chwila zajścia zdarzenia (w ms) W klasie InputEvent zdefiniowano maski umożliwiające rozpoznanie rezultatu dostarczonego przez metodę getModifiers: ALT_MASK, CTRL_MASK, SHIFT_MASK, META_MASK, itp. Zdarzenie action Zdarzenie action wysyłają m.in. komponenty klas Button, List, TextField, MenuItem Rejestracja obiektu nasłuchującego, implementującego interfejs ActionListener, z metodą void actionPerformed(ActionEvent evt) odbywa się za pomocą metody void addActionListener(ActionListener lst) Obiekt zdarzeniowy jest klasy ActionEvent wywodzącej się od AWTEvent, z której pochodzą m.in. metody getSource i getID. Dodatkową metodą klasy ActionEvent jest String getActionCommand() nazwa zdarzenia Metoda dostarcza argument określony za pomocą metody setActionCommand (dla przycisku i polecenia menu jest to domyślnie ich opis, a dla klatki jej zawartość). Uwaga: Metoda getID dostarcza identyfikator ACTION_PERFORMED. W klasie ActionEvent zdefiniowano maski umożliwiające rozpoznanie rezultatu dostarczonego przez metodę getModifiers: ALT_MASK, CTRL_MASK, SHIFT_MASK, META_MASK, itp. Zdarzenie adjustment Zdarzenie adjustment wysyła m.in. komponent klasy Scrollbar Rejestracja obiektu nasłuchującego, implementującego interfejs AdjustmentListener, z metodą void adjustmentValueChanged(AdjustmentEvent evt) odbywa się za pomocą metody void addAdjustmentListener(AdjustmentListener lst) Obiekt zdarzeniowy jest klasy AdjustmentEvent wywodzącej się od AWTEvent, z której pochodzą m.in. metody getSource i getID. Dodatkowymi metodami klasy AdjustmentEvent są Adjustable getAdjustable() źródło zdarzenia int getAdjustmentType() rodzaj zdarzenia int getValue() nowa wartość W klasie AdjustmentEvent zdefiniowano symbole umożliwiające rozpoznanie rezultatu dostarczonego przez metodę getAdjustmentType: UNIT_INCREMENT, UNIT_DECREMENT, BLOCK_INCREMENT, BLOCK_DECREMENT, TRACK, itp. Zdarzenie component Zdarzenie component wysyłają m.in. komponenty klasy Component Rejestracja obiektu nasłuchującego, implementującego interfejs ComponentListener, z metodami void componentHidden(ComponentEvent evt) void componentMoved(ComponentEvent evt) void componentResized(ComponentEvent evt) void componentShown(ComponentEvent evt) odbywa się za pomocą metody void addComponentListener(ComponentListener lst) Obiekt zdarzeniowy jest klasy ComponentEvent wywodzącej się od AWTEvent, z której pochodzą m.in. metody getSource i getID. Dodatkową metodą klasy ComponentEvent jest Component getComponent() źródło zdarzenia Metoda dostarcza odnośnik do komponentu, który został przemieszczony, przeskalowany, pokazany albo ukryty. W klasie ComponentEvent zdefiniowano symbole umożliwiające rozpoznanie rezultatu dostarczonego przez metodę getID: COMPONENT_MOVED, COMPONENT_RESIZED, COMPONENT_SHOWN, COMPONENT_HIDDEN, itp. Zdarzenie container Zdarzenie container wysyłają m.in. komponenty klasy Container Rejestracja obiektu nasłuchującego, implementującego interfejs ContainerListener, z metodami void componentAdded(ContainerEvent evt) void componentRemoved(ContainerEvent evt) odbywa się za pomocą metody void addContainerListener(ContainerListener lst) Obiekt zdarzeniowy jest klasy ContainerEvent wywodzącej się od ComponentEvent, z której pochodzą m.in. metody getSource, getID i getComponent. Dodatkowymi metodami klasy ContainerEvent są Component getChild() dodany/usunięty komponent Container getContainer() dodany/usunięty pojemnik W klasie ContainerEvent zdefiniowano symbole umożliwiające rozpoznanie rezultatu dostarczonego przez metodę getID: COMPONENT_ADDED, COMPONENT_REMOVED, itp. Zdarzenie focus Zdarzenie focus wysyłają m.in. komponenty klasy Component Rejestracja obiektu nasłuchującego, implementującego interfejs FocusListener, z metodami void focusGained(FocusEvent evt) void focusLost(FocusEvent evt) odbywa się za pomocą metody void addFocusListener(FocusListener lst) Obiekt zdarzeniowy jest klasy FocusEvent wywodzącej się od ComponentEvent, z której pochodzi m.in. metoda getComponent. Dodatkową metodą klasy FocusEvent jest boolean isTemporary() tymczasowość celownika Metoda dostarcza informacji, czy utrata celownika jest chwilowa (np. spowodowana jego utratą przez pojemnik komponentu). W klasie FocusEvent zdefiniowano symbole umożliwiające rozpoznanie rezultatu dostarczonego przez metodę getID: FOCUS_GAINED, FOCUS_LOST, itp. Zdarzenie item Zdarzenie item wysyłają m.in. komponenty klasy Choice, List, Checkbox, CheckboxMenuItem Rejestracja obiektu nasłuchującego, implementującego interfejs ItemListener, z metodami void itemStateChanged(ItemEvent evt) odbywa się za pomocą metody void addItemListener(ItemListener lst) Obiekt zdarzeniowy jest klasy ItemEvent wywodzącej się od AWTEvent, z której pochodzą m.in. metody getSource i getID. Dodatkowymi metodami klasy ItemEvent są Object getItem() wybrany/nie wybrany obiekt ItemSelectable getItemSelectable() źródło zdarzenia int getStateChange() nowy stan obiektu W klasie ItemEvent zdefiniowano symbole umożliwiające rozpoznanie rezultatu dostarczonego przez metodę getStateChange: SELECTED, DESELECTED. Uwaga: Metoda getID dostarcza rezultat ITEM_STATE_CHANGED. Zdarzenie key Zdarzenie key wysyłają m.in. komponenty klasy Component Rejestracja obiektu nasłuchującego, implementującego interfejs KeyListener, z metodami void keyPressed(KeyEvent evt) void keyReleased(KeyEvent evt) void keyTyped(KeyEvent evt) odbywa się za pomocą metody void addKeyListener(KeyListener lst) Obiekt zdarzeniowy jest klasy KeyEvent wywodzącej się od InputEvent, z której pochodzą m.in. metody getComponent, getModifiers, isKeyDown (Alt, Ctrl, Shift, Meta) i getWhen. Dodatkowymi metodami klasy KeyEvent są char getKeyChar() znak unikodu int getKeyCode() wirtualny kod klawisza boolean isActionKey() klawisz niedrukowalny W klasie KeyEvent zdefiniowano symbole umożliwiające rozpoznanie rezultatu dostarczonego przez metodę getID: KEY_PRESSED, KEY_RELEASED, KEY_TYPED oraz symbole klawiszy wirtualnych: VK_A do VK_Z, VK_0 do VK_9, VK_F1 do VK_F12, VK_HOME, VK_END, VK_ENTER, itp. (por. Dodatek B). Uwaga: Symboli klawiszy wirtualnych należy używać w przypadku obsługi zdarzeń KEY_PRESSED i KEY_RELEASED, wspomaganej metodą getKeyCode. Jeśli rozpoznano zdarzenie KEY_TYPED, a wprowadzony znak nie jest znakiem Unikodu, to metoda getKeyChar dostarcza CHAR_UNDEFINED. Zdarzenia mouse i mouseMotion Zdarzenie mouse wysyłają m.in. komponenty klasy Component Rejestracja obiektu nasłuchującego, implementującego interfejs MouseListener, z metodami void mouseClicked(MouseEvent evt) void mouseEntered(MouseEvent evt) void mouseExited(MouseEvent evt) void mousePressed(MouseEvent evt) void mouseReleased(MouseEvent evt) oraz obiektu nasłuchującego, implementującego interfejs MouseMotionListener, z metodami void mouseDragged(MouseEvent evt) void mouseMoved(MouseEvent evt) odbywa się (odpowiednio) za pomocą metod void addMouseListener(MouseListener lst) void addMouseMotionListener(MouseListener lst) Obiekt zdarzeniowy jest klasy MouseEvent wywodzącej się od InputEvent, z której pochodzą m.in. metody getComponent, getModifiers, isKeyDown (Alt, Ctrl, Shift, Meta) i getWhen. Dodatkowymi metodami klasy MouseEvent są int getClickCount() liczba kliknięć Point getPoint() (x,y) punktu kliknięcia int getX() x punktu kliknięcia int getY() y punktu kliknięcia boolean isPopupTrigger() kliknięcie wyzwalające void translatePoint(int dx, int dy) przemieszczenie punktu (x,y) W klasie MouseEvent zdefiniowano symbole umożliwiające rozpoznanie rezultatu dostarczonego przez metodę getID: MOUSE_PRESSED, MOUSE_RELEASED, MOUSE_CLICKED, MOUSE_DRAGGED, MOUSE_MOVED, MOUSE_ENTERED, MOUSE_EXITED, itp. Zdarzenie text Zdarzenie text wysyłają m.in. komponenty klas TextField, TextArea Rejestracja obiektu nasłuchującego, implementującego interfejs TextListener, z metodami void textValueChanged(TextEvent evt) odbywa się za pomocą metody void addTextListener(TextListener lst) Obiekt zdarzeniowy jest klasy TextEvent wywodzącej się od AWTEvent, z której pochodzą m.in. metody getSource i getID. Uwaga: Metoda getID dostarcza rezultat TEXT_VALUE_CHANGED. Zdarzenie window Zdarzenie window wysyłają m.in. komponenty klasy Dialog, Frame Rejestracja obiektu nasłuchującego, implementującego interfejs WindowListener, z metodami void windowActivated(WindowEvent evt) void windowClosed(WindowEvent evt) void windowClosing(WindowEvent evt) void windowDeactivated(WindowEvent evt) void windowDeiconified(WindowEvent evt) void windowIconified(WindowEvent evt) void windowOpened(WindowEvent evt) odbywa się za pomocą metody void addWindowListener(WindowListener lst) Obiekt zdarzeniowy jest klasy WindowEvent wywodzącej się od ComponentEvent, z której pochodzą m.in. metody getSource, getID i getComponent. Dodatkową metodą klasy WindowEvent jest Window getWindow() źródło zdarzenia W klasie WindowEvent zdefiniowano symbole umożliwiające rozpoznanie rezultatu dostarczonego przez metodę getID: WINDOW_OPENED, WINDOW_CLOSING, WINDOW_CLOSED, WINDOW_ICONIFIED, WINDOW_DEICONIFIED, WINDOW_ACTIVATED, WINDOW_DEACTIVATED, itp. Jan Bielecki Projektowanie zdarzeń Predefiniowany zestaw klas zdarzeniowych jest dość obszerny, ale niekiedy zachodzi potrzeba utworzenia własnych zdarzeń. Wymaga to zdefiniowania klas, interfejsów i metod analogicznych do ActionEvent, ActionEventListener i actionPerformed. W celu przybliżenia zasad definiowania własnych zdarzeń, zaprojektowano klasę zegarową Timer. Jej sposób użycia jest następujący 1) Utworzenie obiektu zegarowego, z określeniem kwantu czasu (w ms), po upływie którego są odpalane metody timeElapsed obiektów nasłuchujących. Timer timer = new Timer(rate); 2) Zarejestrowanie / wyrejestrowanie obiektu nasłuchującego. timer.addTimerListener(listener); timer.removeTimerListener(listener); 3) Aktywowanie odpalań. timer.start(); 4) Wstrzymanie / przywrócenie odpalań. timer.off(); timer.on(); 5) Zatrzymanie zegara. timer.stop(); Uwaga: W chwili zatrzymania zegara zachodzi ostatnie zdarzenie. Jeśli parametrem metody timeElapsed jest evt, to wyrażenie evt.lastEvent ma wówczas wartość true. Następujący aplet, pokazany na ekranie Zdarzenia zegarowe, posługuje się klasą Timer. Naciśnięcie przycisku On/Off powoduje włączenie / wyłączenie migotania zielonego pulpitu apletu. Po naciśnięciu klawisza Stop migotanie zostaje wstrzymane, a kolor pulpitu zmienia się na czerwony. Ekran Zdarzenia zegarowe ### timers.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; import janb.timer.*; public class Master extends Applet implements TimerListener, ActionListener { private Timer timer = new Timer(200); private Color color = Color.green; private Button onOff, stop; private boolean lighter = true, freezed = false; public void init() { add(onOff = new Button("On/Off")); add(stop = new Button("Stop")); timer.addTimerListener(this); setBackground(color); onOff.addActionListener(this); stop.addActionListener(this); timer.start(); } public void actionPerformed(ActionEvent evt) { if(evt.getSource() == onOff) { if(freezed) timer.on(); else timer.off(); freezed = !freezed; } else timer.stop(); } public void timeElapsed(TimerEvent evt) { if(evt.lastEvent) { setBackground(Color.black); return; } if(lighter) setBackground(color.darker()); else setBackground(color); lighter = !lighter; } } Implementacja Pokazano sposób implementowania klasy Time i TimerEvent oraz interfejsu TimerListener. Utworzone z nich pliki *.class należy umieścić w podkatalogu o nazwie folder\janb\timer, w której folder jest dowolnym katalogiem wyszczególnionym w zmiennej classpath. Klasa Timer package janb.timer; // Timer package // Copyright Jan Bielecki // 1999.02.03 /* Usage: Timer timer = new Timer(rate) create timer object for firing every "rate" ms timer.on() start/resume firing timer.off() suspend firing timer.stop() stop firing Note: register / unregister Timer listeners with timer.addTimerListener(listener) timer.removeTimerListener(listener) */ package janb.timer; import java.util.*; public class Timer implements Runnable { private Thread thread; private int rate; private Vector listeners = new Vector(); private boolean running = false, isStopped = false; public Timer(int rate) { this.rate = rate; running = true; thread = new Thread(this); } public void run() { while(running) { try { Thread.sleep(rate); } catch(InterruptedException e) { } synchronized(thread) { while(isStopped) { try { thread.wait(); } catch(InterruptedException e) { } } } TimerEvent event = new TimerEvent(this, false); if(listeners != null) fireEvent(event); } } public synchronized void start() { thread.start(); running = true; isStopped = false; } public synchronized void stop() { if(running) { running = false; isStopped = false; // (sic!) thread.interrupt(); try { thread.join(); } catch(InterruptedException e) { } thread = null; } TimerEvent event = new TimerEvent(this, true); if(listeners != null) fireEvent(event); } public synchronized void on() { if(running && isStopped) { synchronized(thread) { isStopped = false; thread.notify(); } } } public synchronized void off() { if(running && !isStopped) { synchronized(thread) { isStopped = true; } } } public synchronized void addTimerListener(TimerListener lst) { if (listeners != null) listeners.addElement(lst); } public synchronized void removeTimerListener(TimerListener lst) { if (listeners != null) listeners.removeElement(lst); } private void fireEvent(TimerEvent evt) { Vector listeners2 = (Vector)listeners.clone(); int size = listeners2.size(); for(int i = 0; i < size ; i++) { TimerListener listener = (TimerListener)listeners2.elementAt(i); listener.timeElapsed(evt); } } } Klasa TimerEvent package janb.timer; import java.util.*; public class TimerEvent extends EventObject { public boolean lastEvent; public TimerEvent(Object source, boolean lastEvent) { super(source); this.lastEvent = lastEvent; } } Interfejs TimerListener package janb.timer; import java.util.*; public abstract interface TimerListener extends EventListener { public abstract void timeElapsed(TimerEvent evt); } Jan Bielecki Układanie komponentów Układanie komponentów odbywa się za pomocą zarządców rozkładu. Można zdefiniować własnego zarządcę albo posłużyć się zarządcą predefiniowanym. Zarządca predefiniowany umożliwia wybranie rozkładu ciągłego, brzegowego, siatkowego, torebkowego albo kartowego. Uwaga: Wywołanie metody setLayout z argumentem null powoduje zrezygnowanie z usług zarządców i zastosowanie rozkładu pustego. Rozkład pusty Rozmieszczenie komponentów wymaga jawnego określenia ich położenia i rozmiarów, to jest wywołania metody setBounds albo metod setLocation i setSize. Metody getPreferredSize i getMinimumSize nie są brane pod uwagę. Ekran Rozkład pusty ### null.gif Panel panelN = new MyPanel(); panelN.setLayout(null); Button b = new Button("Hello"); b.setBounds(0, 0, 40, 40); panelN.add(b); b = new Button("World"); b.setBounds(30, 30, 40, 40); panelN.add(b); Rozkład ciągły Komponenty rozmieszcza się wierszami. Rozmiar komponentu określa metoda getPreferredSize. public FlowLayout() public FlowLayout(int align) public FlowLayout(int align, int hGap, int vGap) Uwaga: Za pomocą argumentu align można określić sposób rozmieszczenia komponentów w wierszu: do-lewej (LEFT), do-prawej (RIGHT) albo środkująco (CENTER), a za pomocą argumentów hGap i vGap można określić poziomy odstęp między parami sąsiadujących komponentów oraz pionowy odstęp między parami sąsiadujących wierszy. Ekran Rozkład ciągły ### flow.gif Panel panelF = new Panel(); panelF.setLayout(new FlowLayout()); Panel p = new Panel(); p.add(new Button("A")); p.add(new Button("Z")); panelF.add(p); Rozkład brzegowy Komponenty rozmieszcza się na brzegach i w środku pojemnika. Rozmiar komponentu określają metody getPreferredSize i getMinimumSize. public BorderLayout() Uwaga: Jeśli w zestawie obszarów West, Center, East nie użyje się pewnego obszaru, to zostanie skonsumowany przez pozostałe. Podobnej konsumpcji podlega nie użyty obszar North lub South. Ekran Rozkład brzegowy ### border.gif Panel panelB = new Panel(); panelB.setLayout(new BorderLayout()); Button b = new Button("N"); panelB.add(b, BorderLayout.NORTH); b = new Button("S"); panelB.add(b, BorderLayout.SOUTH); b = new Button("W"); panelB.add(b, BorderLayout.WEST); b = new Button("E"); panelB.add(b, BorderLayout.EAST); Panel c = new Panel(); c.add(new Button("X")); c.add(new Button("Y")); panelB.add(c, BorderLayout.CENTER); Rozkład siatkowy Komponenty rozmieszcza się wierszami, w prostokątnym układzie komórek o identycznych rozmiarach. Jeśli poda się liczbę wierszy siatki, to ignoruje się ewentualnie podaną liczbę kolumn. Jeśli poda się tylko liczbę kolumn, to ignoruje się liczbę wierszy. public GridLayout(int rows, int cols) public GridLayout(int rows, int cols, int hGap, int vGap) Uwaga: Za pomocą argumentów hGap i vGap można określić poziomy oraz pionowy odstęp między parami sąsiadujących komórek siatki. Ekran Rozkład siatkowy ### grid.gif Panel panelG = new Panel(); panelG.setLayout(new GridLayout(3,4)); for(int k = 0; k < 12 ; k++) panelG.add(new Button("" + k)); Rozkład torebkowy Komponenty rozmieszcza się wierszami, w prostokątnym układzie torebek. Rozmiary torebek nie muszą być identyczne. Sposób rozmieszczenia komponentów w torebkach jest określony przez wymuszenia (constraints). Uwaga: Każda torebka składa się z prostokątnego układu komórek. Każdy komponent zajmuje jedną torebkę. Rozmiary torebek nie muszą być identyczne. Rozmiar komponentu w torebce określa metoda getMinimumSize. public GridBagLayout() Wymuszenia dotyczące poszczególnych torebek siatki umieszcza się w obiekcie klasy GridBagConstraints. public GridBagConstraints() Poszczególne wymuszenia określają współrzędne lewego-górnego narożnika, rozmiary torebki, sposób umieszczenia komponentu w torebce oraz rangę torebki. GridBagConstraints gbc = new GridBagConstraints(); Panel panel = new Panel(); GridBagLayout gbl = new GridBagLayout(); panel.setLayout(gbl); // ... Button button = new Button("Greet"); gbl.setConstraints(button, gbc); panel.add(button); Wymuszenia są reprezentowane w obiektach klasy GridBagConstraints. Każde pole obiektu przechowuje jedno wymuszenie. int gridx, int gridy Wymuszenia gridx i gridy określają współrzędne komórki zajętej przez lewy-górny narożnik torebki. Domyślna wartość GridBagConstraints.RELATIVE oznacza, że torebka ma być umieszczona bezpośrednio za albo bezpośrednio pod poprzednią torebką. int gridwidth, int gridheight Wymuszenia gridwidth i gridheight określają liczbę komórek jakie torebka zajmuje w poziomie albo w pionie (domyślnie 1). Wartość GridBagConstraints.REMAINDER oznacza, że dana torebka jest ostatnia w wierszu albo w kolumnie. int fill Wymuszenie fill określa, jak należy postąpić, jeśli torebka nie zostanie w całości wypełniona przez komponent. Domyślna wartość GridBagConstraints.NONE oznacza, że komponent nie rozprzestrzeni się na pozostałe komórki torebki. int ipadx, int ipady Wymuszenia ipadx i ipady (domyślnie 0) określają odstępy na brzegach komponentu, zwiększające jego rozmiary określone za pomocą metody getPreferredSize albo getMinimumSize. Insets insets Wymuszenie insets określa marginesy wyrażone w pikselach (domyślnie 0), występujące wokół komponentu: l (z lewej), r (z prawej), t (z góry), b (z dołu). int anchor Wymuszenie anchor określa sposób rozmieszczenia komponentu, którego rozmiary są mniejsze od rozmiarów torebki. Domyślna wartość GridBagConstraints.CENTER oznacza wyśrodkowanie, a każdy z pozostałych symboli kierunkowych (np. NORTHWEST) wyznacza dosunięcie komponentu w układzie geograficznym (np. dla NORTHWEST dosunięcie do lewego-górnego narożnika torebki). double weightx, double weighty Wymuszenia weightx i weighty (domyślnie 0) określają rangę komponentu względem pozostałych komponentów wiersza albo kolumny torebek. Uwaga: Im wyższa ranga komponentu, tym więcej komórek zajmuje on w pionie albo w poziomie (np. z dwóch komponentów o tych samych rozmiarach określonych przez getPreferredSize, komponent o randze 0.6 zajmuje dwa razy większy obszar niż komponent o randze 0.3). Ekran Rozkład torebkowy ### gridbag.gif Panel panelX = new Panel(); GridBagLayout gbl = new GridBagLayout(); panelX.setLayout(gbl); make("A", 0, 0, 1, 2, 0.0, 0.0); make("B", 1, 0, 4, 1, 2.0, 0.0); make("C", 1, 1, 2, 1, 0.0, 0.0); make("D", 3, 1, 2, 2, 0.0, 2.0); make("E", 0, 2, 2, 2, 0.0, 0.0); make("F", 1, 4, 1, 1, 0.5, 0.5); make("G", 2, 2, 1, 3, 0.0, 0.0); make("H", 3, 4, 1, 1, 0.0, 0.0); make("I", 4, 3, 1, 1, 1.0, 0.5); // ... void make(String name, int x, int y, int w, int h, double wx, double wy) { GridBagLayout gbl = (GridBagLayout)panelX.getLayout(); GridBagConstraints gbc = new GridBagConstraints(); gbc.fill = GridBagConstraints.BOTH; gbc.gridx = x; gbc.gridy = y; gbc.gridwidth = w; gbc.gridheight = h; gbc.weightx = wx; gbc.weighty = wy; Button button; panelX.add(button = new Button(name)); gbl.setConstraints(button, gbc); } Rozkład kartowy Rozkład kartowy polega na rozmieszczeniu komponentów na wzajemnie zasłaniających się kartach. Każda karta ma unikalną, identyfikującą ją nazwę. Za każdym razem gdy do pojemnika z rozkładem kartowym jest wstawiany nowy komponent, staje się on jego nową kartą. Istnieją metody umożliwiające wyświetlanie wybranych kart pojemnika, w tym pierwszej, następnej i ostatniej. Następujący program, z którego pochodzą przytoczone powyżej ekrany, ilustruje zastosowanie rozkładów predefiniowanych. Wyświetlenie następnej karty odbywa się przez kliknięcie przycisku Next. =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet { protected MyCard cardN, cardF, cardB, cardG, cardX; protected Panel panelN, panelF, panelB, panelG, panelX; protected CardLayout cardLayout = new CardLayout(); protected Panel cards = new Panel(); protected Button next = new Button("Next"), b; public void init() { setLayout(new BorderLayout()); add(next, BorderLayout.SOUTH); next.addActionListener( new ActionListener() { public void actionPerformed(ActionEvent evt) { cardLayout.next(cards); } } ); // rozkład pusty panelN = new MyPanel(); panelN.setLayout(null); b = new Button("Hello"); b.setBounds(0, 0, 40, 40); panelN.add(b); b = new Button("World"); b.setBounds(30, 30, 40, 40); panelN.add(b); // rozkład ciągły panelF = new Panel(); panelF.setLayout(new FlowLayout()); Panel p = new Panel(); p.add(new Button("A")); p.add(new Button("Z")); panelF.add(p); // rozkład brzegowy panelB = new Panel(); panelB.setLayout(new BorderLayout()); b = new Button("N"); panelB.add(b, BorderLayout.NORTH); b = new Button("S"); panelB.add(b, BorderLayout.SOUTH); b = new Button("W"); panelB.add(b, BorderLayout.WEST); b = new Button("E"); panelB.add(b, BorderLayout.EAST); Panel c = new Panel(); c.add(new Button("X")); c.add(new Button("Y")); panelB.add(c, BorderLayout.CENTER); // rozkład siatkowy panelG = new Panel(); panelG.setLayout(new GridLayout(3,4)); for(int k = 0; k < 12 ; k++) panelG.add(new Button("" + k)); // rozkład torebkowy panelX = new Panel(); GridBagLayout gbl = new GridBagLayout(); panelX.setLayout(gbl); make("A", 0, 0, 1, 2, 0.0, 0.0); make("B", 1, 0, 4, 1, 2.0, 0.0); make("C", 1, 1, 2, 1, 0.0, 0.0); make("D", 3, 1, 2, 2, 0.0, 2.0); make("E", 0, 2, 2, 2, 0.0, 0.0); make("F", 1, 4, 1, 1, 0.5, 0.5); make("G", 2, 2, 1, 3, 0.0, 0.0); make("H", 3, 4, 1, 1, 0.0, 0.0); make("I", 4, 3, 1, 1, 1.0, 0.5); cardN = new MyCard(0, panelN, "Null Layout"); cardF = new MyCard(1, panelF, "Flow Layout"); cardB = new MyCard(2, panelB, "Border Layout"); cardG = new MyCard(3, panelG, "Grid Layout"); cardX = new MyCard(4, panelX, "GridBag Layout"); cards.setLayout(cardLayout); cards.add("cardN", cardN); cards.add("cardF", cardF); cards.add("cardB", cardB); cards.add("cardG", cardG); cards.add("cardX", cardX); add(cards, BorderLayout.CENTER); } void make(String name, int x, int y, int w, int h, double wx, double wy) { GridBagLayout gbl = (GridBagLayout)panelX.getLayout(); GridBagConstraints gbc = new GridBagConstraints(); gbc.fill = GridBagConstraints.BOTH; gbc.gridx = x; gbc.gridy = y; gbc.gridwidth = w; gbc.gridheight = h; gbc.weightx = wx; gbc.weighty = wy; Button button; panelX.add(button = new Button(name)); gbl.setConstraints(button, gbc); } } class MyCard extends Panel { private int no; private Panel p; private String name; public MyCard(int no, Panel p, String name) { this.no = no; this.p = p; this.name = name; setLayout(new BorderLayout()); Canvas canvas = new MyCanvas(no, name); Panel panel = new MyPanel(); panel.add(p); add(canvas, BorderLayout.NORTH); add(panel, BorderLayout.CENTER); } } class MyPanel extends Panel { public Dimension getPreferredSize() { return new Dimension(100, 200); } } class MyCanvas extends Canvas { private int no; private String name; public MyCanvas(int no, String name) { this.no = no; this.name = name; } public Dimension getPreferredSize() { return new Dimension(40, 80); } public void paint(Graphics gDC) { int b=60, dx=40, dy=5, r=15, c=0, d; for(int i = 0; i < no+1 ; i++) gDC.drawLine(i*dx, b, c=(i+1)*dx, b); gDC.drawArc(c-r, b-2*r, 2*r, 2*r, 270, 90); gDC.drawLine(c+r, b-r, c+r, b-r-dy); gDC.drawArc(c+r, d=b-2*r-dy, 2*r, 2*r, 90, 90); c += 2*r; gDC.drawLine(c, d, getSize().width, d); gDC.drawString(name, c, b-r); } } Jan Bielecki Obsługiwanie sterowników Predefiniowane sterowniki są przystosowane do rejestrowania obiektów klas nasłuchujących. Każdy sterownik, z wyjątkiem CheckboxMenuItem i MenuItem, może wysyłać zdarzenia key, mouse, mouseMotion, focus i component, a jeśli jest pojemnikiem, to także zdarzenia container. Zdarzenie action może być wysłane tylko przez sterowniki Button, CheckboxMenuItem, List, MenuItem i TextField. Zdarzenie adjustment może być wysłane tylko przez sterownik Scrollbar. Zdarzenie item może być wysłane tylko przez sterowniki Checkbox, CheckboxMenuItem, Choice i List. Zdarzenie text może być wysłane tylko przez sterowniki TextArea i TextField. Zdarzenie window może być wysłane tylko przez sterowniki Dialog, Frame i Window. Sterownik Button Zdarzeniem wyróżniającym sterownik jest action. Zdarzenia action są obsługiwane przez metodę actionPerformed interfejsu ActionListener. Ekran Przycisk ### buttonc.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet implements ActionListener { protected Button button = new Button("Beep"); public void init() { add(button); button.addActionListener(this); } public void actionPerformed(ActionEvent evt) { Toolkit kit = Toolkit.getDefaultToolkit(); kit.beep(); } } Sterownik Canvas Zdarzeniami wyróżniającymi sterownik są mouse i mouseMotion. Zdarzenia mouse są obsługiwane przez metody mousePressed, mouseReleased, mouseClicked, mouseEntered, mouseExited interfejsu MouseListener, a zdarzenia mouseMotion są obsługiwane przez metody mouseDragged, mouseMoved interfejsu MouseMotionListener. Ekran Płótno ### canvasc.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet { protected Canvas canvas = new MyCanvas(); protected Color color = Color.yellow; protected int x0, y0; class StateWatcher extends MouseAdapter { public void mouseEntered(MouseEvent evt) { canvas.setBackground(Color.green); } public void mouseExited(MouseEvent evt) { canvas.setBackground(color); } } class MotionWatcher implements MouseMotionListener { public void mouseMoved(MouseEvent evt) { x0 = evt.getX(); y0 = evt.getY(); } public void mouseDragged(MouseEvent evt) { int x = evt.getX(), y = evt.getY(); Graphics gDC = canvas.getGraphics(); gDC.drawLine(x0, y0, x0=x, y0=y); } } public void init() { canvas.setBackground(color); add(canvas); canvas.addMouseListener(new StateWatcher()); canvas.addMouseMotionListener(new MotionWatcher()); } } class MyCanvas extends Canvas { public void paint(Graphics gDC) { Dimension size = getSize(); int w = size.width, h = size.height; gDC.drawRect(0, 0, w-1, h-1); } public Dimension getPreferredSize() { return new Dimension(100, 100); } } Sterownik Checkbox Zdarzeniem wyróżniającym sterownik jest item. Zdarzenia item są obsługiwane przez metodę itemStateChanged interfesju ItemListener. Ekran Nastawa ### checkbc.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet { protected Checkbox checkbox[] = new Checkbox [3];; protected Button color = new Button(""); protected int rgb[] = { 0, 0, 0 }; public void init() { for(int i = 0; i < 3 ; i++) { checkbox[i] = new Checkbox(); add(checkbox[i]); checkbox[i].addItemListener( new ItemListener() { public void itemStateChanged(ItemEvent evt) { setColor(); } } ); } add(color); } public void setColor() { for(int i = 0; i < 3 ; i++) rgb[i] = checkbox[i].getState() ? 255 : 0; color.setBackground( new Color(rgb[0], rgb[1], rgb[2]) ); } } Sterownik CheckboxMenuItem Zdarzeniami wyróżniającymi sterownik są item i action. Zdarzenia item są obsługiwane przez metodę itemStateChanged interfejsu ItemListener, a zdarzenia action są obsługiwane przez metodę actionPerformed interfejsu ActionListener. Ekran Polecenie ### checkmen.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet { public void init() { Frame frame = new MyFrame("CheckboxMenuItem"); frame.pack(); frame.setVisible(true); } } class MyFrame extends Frame implements ItemListener { protected CheckboxMenuItem checkbox[]; protected String colors[] = { "Red", "Green", "Blue" }; public MyFrame(String caption) { super(caption); MenuBar menuBar = new MenuBar(); Menu menu = new Menu("Color"); checkbox = new CheckboxMenuItem [3]; for(int i = 0; i < 3 ; i++) { checkbox[i] = new CheckboxMenuItem(colors[i]); menu.add(checkbox[i]); checkbox[i].setActionCommand("" + i); checkbox[i].addItemListener(this); } menuBar.add(menu); setMenuBar(menuBar); } public void itemStateChanged(ItemEvent evt) { CheckboxMenuItem source = (CheckboxMenuItem)evt.getSource(); String cmd = source.getActionCommand(); int val = Integer.valueOf(cmd).intValue(); String label = source.getLabel(); int stateChange = evt.getStateChange(); if(stateChange == ItemEvent.SELECTED) label += " selected"; else label += " deselected"; setTitle(label); int rgb[] = new int [3]; for(int i = 0; i < 3 ; i++) rgb[i] = checkbox[i].getState() ? 255 : 0; Color color = new Color(rgb[0], rgb[1], rgb[2]); setBackground(color); repaint(); } public Dimension getPreferredSize() { return new Dimension(150, 150); } } Sterownik Choice Zdarzeniem wyróżniającym sterownik jest item. Zdarzenia item są obsługiwane przez metodę itemStateChanged interfejsu ItemListener. Ekran Wybór ### choicec.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet { protected Choice choice; protected Color colors[] = { Color.red, Color.green, Color.blue }; public void init() { choice = new Choice(); choice.add("Red"); choice.add("Green"); choice.add("Blue"); choice.addItemListener( new ItemListener() { public void itemStateChanged(ItemEvent evt) { Object source = evt.getSource(); Choice choice = (Choice)source; int index = choice.getSelectedIndex(); setBackground(colors[index]); } } ); add(choice); } } Sterownik Dialog Zdarzeniem wyróżniającym sterownik jest window. Zdarzenia window są obsługiwane przez metody windowActivated, windowClosed, windowClosing, windowDeactivated, windowDeiconified, windowIconified, windowOpened interfejsu WindowListener. Ekran Dialog ### dialogc.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet { public void init() { Frame frame = new MyFrame("Frame"); frame.pack(); frame.setVisible(true); } } class MyFrame extends Frame { protected Dialog dialog; MyFrame(String caption) { super(caption); dialog = new MyDialog(this, "Dialog", false); dialog.pack(); dialog.addWindowListener(new Closer(dialog)); dialog.setVisible(true); } public Dimension getPreferredSize() { return new Dimension(150, 150); } } class Closer extends WindowAdapter { protected Dialog dialog; public Closer(Dialog dialog) { this.dialog = dialog; } public void windowClosing(WindowEvent evt) { dialog.setVisible(false); dialog.dispose(); } } class MyDialog extends Dialog implements ActionListener { public MyDialog(Frame parent, String caption, boolean modal) { super(parent, caption, modal); Button button = new Button("Close"); button.addActionListener(this); add(button); } public Dimension getPreferredSize() { return new Dimension(150, 150); } public void actionPerformed(ActionEvent evt) { setVisible(false); dispose(); } } Sterownik Frame Zdarzeniem wyróżniającym sterownik jest window. Zdarzenia window są obsługiwane przez metody windowActivated, windowClosed, windowClosing, windowDeactivated, windowDeiconified, windowIconified, windowOpened interfejsu WindowListener. Ekran Ramka ### framec.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet { public void init() { Frame frame = new MyFrame("Frame"); frame.pack(); frame.setVisible(true); } } class MyFrame extends Frame { protected Dialog dialog; public MyFrame(String caption) { super(caption); Label label = new Label(""); label.setAlignment(Label.CENTER); add(label); addWindowListener(new Watcher(label)); } public Dimension getPreferredSize() { return new Dimension(150, 150); } } class Watcher extends WindowAdapter { protected Label label; public Watcher(Label label) { this.label = label; } public void windowActivated(WindowEvent evt) { label.setText("Activated"); } public void windowDeactivated(WindowEvent evt) { label.setText("Deactivated"); } } Sterownik Label Sterownik nie ma zdarzeń, które by go wyróżniały. Ekran Etykieta ### labelc.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet { protected Label label; protected Button button; public void init() { label = new Label("Click here -->"); button = new Button(""); add(label); add(button); button.addActionListener( new ActionListener() { public void actionPerformed(ActionEvent evt) { button.setVisible(false); label.setText("Done!"); } } ); } } Sterownik List Zdarzeniami wyróżniającymi sterownik są item i action. Zdarzenia item są obsługiwane przez metodę itemStateChanged interfejsu ItemListener, a zdarzenia action są obsługiwane przez metodę actionPerformed interfejsu ActionListener. Ekran Lista ### listc.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet { protected List list; protected Label label; protected Button west, east; class Watcher extends Object implements ActionListener, ItemListener { public void actionPerformed(ActionEvent evt) { List source = (List)evt.getSource(); int index = source.getSelectedIndex(); Color colors[] = { Color.red, Color.magenta, Color.green, Color.blue, Color.cyan, Color.yellow }; west.setBackground(colors[index]); east.setBackground(colors[index]); } public void itemStateChanged(ItemEvent evt) { List source = (List)evt.getSource(); int index = source.getSelectedIndex(); String name = list.getItem(index); label.setText(name + " selected"); } } public void init() { list = new List(3); label = new Label("", Label.CENTER); String names[] = { "Red", "Magenta", "Green", "Blue", "Cyan", "Yellow" }; int len = names.length; for(int i = 0; i < len ; i++) { list.add(names[i]); } setLayout(new BorderLayout()); add("Center", list); add("South", label); add("West", west = new Button("")); add("East", east = new Button("")); Watcher watcher = new Watcher(); list.addActionListener(watcher); list.addItemListener(watcher); } } Sterownik MenuItem Zdarzeniem wyróżniającym sterownik jest action. Zdarzenia action są obsługiwane przez metodę actionPerformed interfejsu ActionListener. Ekran Polecenie ### menuc.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet { public void init() { Frame frame = new MyFrame("MenuItem"); frame.pack(); frame.setVisible(true); } } class MyFrame extends Frame implements ActionListener { protected MenuItem menuItem[]; protected String names[] = { "Red", "Green", "Blue" }; protected Color colors[] = { Color.red, Color.green, Color.blue }; public MyFrame(String caption) { super(caption); MenuBar menuBar = new MenuBar(); Menu menu = new Menu("Color"); menuItem = new MenuItem [3]; for(int i = 0; i < 3 ; i++) { menuItem[i] = new MenuItem(names[i]); menu.add(menuItem[i]); menuItem[i].setActionCommand("" + i); menuItem[i].addActionListener(this); } menuBar.add(menu); setMenuBar(menuBar); } public void actionPerformed(ActionEvent evt) { MenuItem source = (MenuItem)evt.getSource(); String cmd = source.getActionCommand(); int index = Integer.valueOf(cmd).intValue(); Color color = colors[index]; setBackground(color); repaint(); } public Dimension getPreferredSize() { return new Dimension(150, 150); } } Sterownik Panel Zdarzeniami wyróżniającymi sterownik są mouse i mouseMotion. Zdarzenia mouse są obsługiwane przez metody mousePressed, mouseReleased, mouseClicked, mouseEntered, mouseExited interfejsu MouseListener, a zdarzenia mouseMotion są obsługiwane przez metody mouseDragged, mouseMoved interfejsu MouseMotionListener. Ekran Panel ### panelc.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet { protected Panel panel; protected Graphics gDC; class Watcher extends MouseAdapter { public void mousePressed(MouseEvent evt) { gDC = panel.getGraphics(); panel.update(gDC); gDC.setColor(Color.green); gDC.drawOval(0, 0, 50, 50); } public void mouseClicked(MouseEvent evt) { gDC = panel.getGraphics(); gDC.setColor(Color.red); gDC.fillOval(0, 0, 50, 50); } } class MyPanel extends Panel { public Dimension getPreferredSize() { return new Dimension(100, 100); } public void paint(Graphics gDC) { Dimension size = getSize(); int w = size.width, h = size.height; gDC.drawRect(0, 0, w-1, h-1); } } public void init() { panel = new MyPanel(); panel.addMouseListener(new Watcher()); add(panel); } } Sterownik Scrollbar Zdarzeniem wyróżniającym sterownik jest adjustment. Zdarzenia adjustment są obsługiwane przez metodę adjustmentValueChanged interfejsu AdjustmentListener. Ekran Suwak ### scrollc.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet { protected Scrollbar scrollbar; public void init() { scrollbar = new Scrollbar(Scrollbar.VERTICAL); scrollbar.setMinimum(0); scrollbar.setMaximum(255); scrollbar.setBlockIncrement(32); add(scrollbar); Watcher watcher = new Watcher(this); scrollbar.addAdjustmentListener(watcher); } } class Watcher implements AdjustmentListener { protected Applet applet; public Watcher(Applet applet) { this.applet = applet; } public void adjustmentValueChanged(AdjustmentEvent evt) { int newValue = evt.getValue(); Color color = new Color(255, newValue, 255); applet.setBackground(color); } } Sterownik Scrollpane Sterownik nie ma zdarzeń, które by go wyróżniały. Ekran Przewijak ### scrolpan.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet { protected ScrollPane scrollPane; protected Panel panel; protected Adjustable hAdj, vAdj; public void init() { scrollPane = new ScrollPane( ScrollPane.SCROLLBARS_AS_NEEDED ); class MyPanel extends Panel { public void paint(Graphics gDC) { Dimension size = getSize(); int w = size.width, h = size.height; gDC.drawLine(0, 0, w, h); gDC.drawLine(0, h, w, 0); } public Dimension getPreferredSize() { return new Dimension(500, 500); } } panel = new MyPanel(); scrollPane.add(panel); add(scrollPane); hAdj = scrollPane.getHAdjustable(); vAdj = scrollPane.getVAdjustable(); hAdj.setUnitIncrement(10); vAdj.setUnitIncrement(10); hAdj.setBlockIncrement(50); vAdj.setBlockIncrement(50); scrollPane.setSize(100, 100); } } Sterownik TextArea Zdarzeniem wyróżniającym sterownik jest text. Zdarzenia text są obsługiwane przez metodę textValueChanged interfejsu TextListener. Ekran Notatnik ### areac.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet implements TextListener { protected TextArea area; protected boolean hash = false; public void init() { area = new TextArea(5, 20); area.setText("HelloWorld"); add(area); area.setCaretPosition(5); area.requestFocus(); area.addTextListener(this); } public void textValueChanged(TextEvent evt) { if(!hash) { int pos = area.getCaretPosition(); area.append("#"); area.setCaretPosition(pos); } hash = !hash; } } Sterownik TextField Zdarzeniami wyróżniającymi sterownik są action i text. Zdarzenia action są obsługiwane przez metodę actionPerformed interfejsu ActionListener, a zdarzenia text są obsługiwane przez metodę textValueChanged interfejsu TextListener. Ekran Klatka ### fieldc.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet implements TextListener, ActionListener { protected TextField field; public void init() { field = new TextField(20); field.setText("0"); add(field); field.requestFocus(); field.addTextListener(this); field.addActionListener(this); } public void textValueChanged(TextEvent evt) { try { String text = field.getText(); int value = Integer.valueOf(text).intValue(); } catch(NumberFormatException e) { Toolkit kit = Toolkit.getDefaultToolkit(); kit.beep(); } } public void actionPerformed(ActionEvent evt) { boolean wrongField = false; int value = 0; try { String text = field.getText(); value = Integer.valueOf(text).intValue(); } catch(NumberFormatException e) { wrongField = true; } Color color; if(wrongField || value < 0 || value > 255) color = Color.black; else color = new Color(0, 0, value); setBackground(color); field.setText("0"); } } Sterownik Window Sterownik nie ma zdarzeń, które by go wyróżniały. Ekran Okno ### windowc.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet { protected Window window; protected Button button; protected boolean isDestroyed = false; public void destroy() { isDestroyed = true; // ważne! } public void init() { Frame frame = new Frame("Frame"); frame.setVisible(true); window = new MyWindow(frame); window.setBounds(100, 100, 200, 200); window.setVisible(true); add(button = new Button("Destroy")); button.addActionListener( new ActionListener() { public void actionPerformed(ActionEvent evt) { window.setVisible(false); window.dispose(); } } ); window.addWindowListener( new WindowAdapter() { public void windowClosed(WindowEvent evt) { Graphics gDC = getGraphics(); if(!isDestroyed) gDC.drawString( "Window closed!", 35, 80 ); } } ); } class MyWindow extends Window { public MyWindow(Frame frame) { super(frame); } public void paint(Graphics gDC) { Dimension d = getSize(); int w = d.width, h = d.height; gDC.drawRect(0, 0, w-1, h-1); } } } Jan Bielecki Projektowanie oblicza Najbardziej znanym panelem jest aplet. Nie zawsze jednak rozmieszczenie komponentów na pulpicie apletu spełnia wszystkie oczekiwania. Dlatego wygodnym sposobem projektowania oblicza apletu jest użycie paneli. Najczęściej postępuje się w taki sposób, że komponenty umieszcza się na panelu, a panel wstawia do apletu. Ilustruje to następujący aplet, pokazany na ekranie Projektowanie oblicza. Jego przyciski umieszczono w rozkładzie ciągłym na pulpicie panelu, a panel, po narzuceniu mu rozkładu brzegowego, wstawiono na środek apletu. Przyciski oprogramowano w taki sposób, że kliknięcie dowolnego z nich powoduje wyświetlenie obrazu GIF pobranego z pliku określonego za pomocą parametrów apletu. Ekran Projektowanie oblicza ### interfac.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; import java.net.*; public class Master extends Applet { private int count; private Image img[]; private Panel panel; private Canvas canvas; private Applet applet; public void init() { applet = this; try { String str = getParameter("Count"); count = Integer.parseInt(str); } catch(NumberFormatException e) { count = 0; } panel = new Panel(); panel.setLayout(new FlowLayout()); img = new Image[count]; String prefix = getParameter("Gifs"); URL docBase = getDocumentBase(); MediaTracker tracker = new MediaTracker(this); for(int i = 0; i < count ; i++) { String str = getParameter(prefix + i); Button button = new Button(str); button.setActionCommand("" + i); panel.add(button); button.addActionListener( new ActionListener() { public void actionPerformed(ActionEvent evt) { Graphics gDC = canvas.getGraphics(); String idStr = evt.getActionCommand(); try { int id = Integer.parseInt(idStr); Dimension d = applet.getSize(); int w = d.width, h = d.height; gDC.drawImage( img[id], 0, 0, w, h, applet); } catch(NumberFormatException e) { } } } ); img[i] = getImage(docBase, str + ".gif"); tracker.addImage(img[i], 0); try { tracker.waitForID(0); } catch(InterruptedException e) { } } setLayout(new BorderLayout()); add(panel, BorderLayout.NORTH); canvas = new Canvas(); add(canvas, BorderLayout.CENTER); } } Bardziej złożone oblicze ilustruje następujący program pokazany na ekranie Przegląd sterowników. Ekran Przegląd sterowników ### overview.gif =============================================== import java.applet.Applet; import java.awt.*; public class Master extends Applet { protected Label label = new Label("Label"); protected Button button = new Button("Button"); protected Panel panel = new MyPanel(); protected Canvas canvas = new MyCanvas(); protected CheckboxGroup group = new CheckboxGroup(); protected Checkbox checkbox1 = new Checkbox("Man", true), checkbox2 = new Checkbox("Sex", group, true); protected Scrollbar scrollbar = new Scrollbar(Scrollbar.HORIZONTAL); protected List list = new List(3); protected Choice choice = new Choice(); protected TextField field = new TextField("Mr."); protected TextArea area = new MyArea("\n\nJan Bielecki, Ph.D."); protected ScrollPane pane = new ScrollPane(ScrollPane.SCROLLBARS_ALWAYS); public void init() { add(label); add(button); panel.setBackground(Color.red); add(panel); canvas.setBackground(Color.green); add(canvas); add(checkbox1); add(checkbox2); add(scrollbar); add(field); list.add("Mr."); list.add("Jan"); list.add("Andrzej"); list.add("Bielecki"); add(list); choice.add("Prof."); choice.add("Dr."); add(choice); add(area); Panel panel = new Panel(); Scrollbar scrollbar = new Scrollbar(Scrollbar.VERTICAL); Button button = new Button("Click"); panel.add(scrollbar); panel.add(button); pane.add(panel); add(pane); } } class MyPanel extends Panel { public MyPanel() { CheckboxGroup radio = new CheckboxGroup(); Checkbox check1 = new Checkbox("X", true), check2 = new Checkbox("Y", radio, true); add(check1); add(check2); } public Dimension getPreferredSize() { return new Dimension(40, 60); } } class MyCanvas extends Canvas { public Dimension getPreferredSize() { return new Dimension(40, 60); } public void paint(Graphics gDC) { gDC.drawOval(0, 0, 30, 30); } } class MyArea extends TextArea { MyArea(String text) { super(text); } public Dimension getPreferredSize() { return new Dimension(200, 120); } } Jan Bielecki Obsługiwanie okien Istnieje kilka sposobów zamykania ramek i okien. Najprostszy polega na wydelegowaniu obiektu, w którym występuje obsługa zdarzenia window zrealizowana w metodzie windowClosing. addWindowListener(new Closer(this)); class Closer extends WindowAdapter { private Frame frame; public Closer(Frame frame) { this.frame = frame; } public void windowClosing(WindowEvent evt) { frame.setVisible(false); frame.dispose(); // ... } } Można również, wprost w klasie okienkowej, umieścić definicję metody processWindowEvent, do obsłużenia zamknięcia okna. public void processWindowEvent(WindowEvent evt) { int id = evt.getID(); if(id == WindowEvent.WINDOW_CLOSING) { setVisible(false); dispose(); // ... } else { // ... super.processWindowEvent(evt); } } Można również, także w klasie okienkowej, użyć wywołania metody enableEvent i dostarczyć metodę processEvent. Zastosowano to w następującej aplikacji, pokazanej na ekranie Zamykanie okna. Napisano ją w taki sposób, że wprowadzenie z klawiatury dowolnej litery powoduje wyświetlenie jej dokładnie w środku okna. Ekran Zamykanie okna ### clowind.gif import java.awt.*; import java.awt.event.*; import java.awt.geom.*; import java.awt.font.*; public class Main extends Frame { private Font font = new Font("Serif", Font.BOLD, 150); private String string = ""; private int x, y; private Graphics2D gDC2; public static void main(String[] args) { new Main("Chars"); } public Main(String caption) { super(caption); setSize(200, 200); addKeyListener(new Typer(this)); enableEvents(AWTEvent.WINDOW_EVENT_MASK); show(); gDC2 = (Graphics2D)getGraphics(); } public void paint(Graphics gDC) { gDC.setFont(font); gDC.drawString(string, x, y); } class Typer extends KeyAdapter { private Frame frame; public Typer(Frame frame) { this.frame = frame; } public void keyReleased(KeyEvent evt) { int key = evt.getKeyCode(); if(key < KeyEvent.VK_A || key > KeyEvent.VK_Z) key = '?'; char chr = Character.toUpperCase((char)key); string = "" + chr; Insets insets = frame.getInsets(); int lt = insets.left, tp = insets.top, rt = insets.right, bt = insets.bottom; FontRenderContext frc = gDC2.getFontRenderContext(); TextLayout layout = new TextLayout(string, font, frc); Rectangle2D bounds = layout.getBounds(); int chrW = (int)bounds.getWidth(), chrH = (int)bounds.getHeight(), asc = -(int)bounds.getY(); Dimension d = frame.getSize(); int frmW = d.width - lt - rt, frmH = d.height - tp - bt; x = (frmW - chrW) / 2 + lt; y = (frmH - chrH) / 2 + tp + asc; frame.repaint(); } } public void processEvent(AWTEvent evt) { int id = evt.getID(); if(id == WindowEvent.WINDOW_CLOSING) { setVisible(false); dispose(); System.exit(0); } else super.processEvent(evt); } } Jan Bielecki Projektowanie menu Menu można zdefiniować jako opadające albo wyskakujące. Menu opadające musi być związane z oknem i jest wówczas wyświetlane bezpośrednio pod paskiem menu. Menu wyskakujące może być związane z dowolnym pojemnikiem, a jego pozycję określa się we współrzędnych pojemnika. Menu opadające Zestaw opadających menu (np. File, Edit) jest wyświetlany na pasku menu. Każde menu składa się z poleceń albo z menu niższego poziomu. Obsługiwanie menu odbywa się tak, jak obsługiwanie zdarzeń action wysyłanych przez przyciski. Następujący program, pokazany na ekranie Opadające menu, ilustruje typowe sposoby obsługiwania poleceń menu. Ekran Opadające menu ### dropdown.gif import java.awt.*; import java.awt.event.*; public class Main extends Frame implements ActionListener { protected static Main frame; protected static MenuItem exitItem; public Main(String caption) { super(caption); MenuBar menuBar = new MenuBar(); Menu fileMenu = new Menu("File"); MenuItem newItem = new MenuItem("New"); fileMenu.add(newItem); exitItem = new MenuItem("Exit"); fileMenu.add(exitItem); Menu colorMenu = new Menu("Color"); MenuItem redItem = new MyMenuItem("Red", Color.red); colorMenu.add(redItem); MenuItem greenItem = new MyMenuItem("Green", Color.green); colorMenu.add(greenItem); MenuItem blueItem = new MyMenuItem("Blue", Color.blue); colorMenu.add(blueItem); Menu helpMenu = new Menu("Help"); MenuItem aboutItem = new MenuItem("About"); helpMenu.add(aboutItem); menuBar.add(fileMenu); menuBar.add(colorMenu); menuBar.add(helpMenu); setMenuBar(menuBar); newItem.addActionListener(this); exitItem.addActionListener(this); aboutItem.addActionListener( new ActionListener() { public void actionPerformed(ActionEvent evt) { Graphics gDC = getGraphics(); gDC.drawString("JanB", 50, 100); } } ); } public void actionPerformed(ActionEvent evt) { String cmd = evt.getActionCommand(); if(cmd.equals("New")) { setBackground(Color.white); return; } Object source = evt.getSource(); if(source == exitItem) System.exit(0); } public static void main(String[] args) { frame = new Main("Frame Menu"); frame.setSize(200, 200); frame.setVisible(true); frame.addWindowListener( new WindowAdapter() { public void windowClosing(WindowEvent evt) { frame.dispose(); } } ); } class MyMenuItem extends MenuItem implements ActionListener { protected Color color; public MyMenuItem(String label, Color color) { super(label); this.color = color; addActionListener(this); } public void actionPerformed(ActionEvent evt) { setBackground(color); } } } Menu wyskakujące Następujący aplet, pokazany na ekranie Wyskakujące menu, ilustruje zasadę tworzenia wyskakującego menu składającego się z poleceń Red, Green, Blue i Exit. Ekran Wyskakujące menu ### popmenu.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet implements ActionListener { protected PopupMenu popup; protected MenuItem mi; public void init() { popup = new PopupMenu("Colors"); mi = new MenuItem("Red"); mi.addActionListener(this); popup.add(mi); mi = new MenuItem("Green"); mi.addActionListener(this); popup.add(mi); mi = new MenuItem("Blue"); mi.addActionListener(this); popup.add(mi); popup.addSeparator(); mi = new MenuItem("Exit"); mi.addActionListener(this); popup.add(mi); add(popup); // dodaj do apletu addMouseListener( new MouseAdapter() { public void mouseClicked(MouseEvent evt) { int x = evt.getX(), y = evt.getY(); popup.show( evt.getComponent(), x, y ); } } ); } public void actionPerformed(ActionEvent evt) { String cmd = evt.getActionCommand(); if(cmd.equals("Red")) setBackground(Color.red); else if(cmd.equals("Green")) setBackground(Color.green); else if(cmd.equals("Blue")) setBackground(Color.blue); else System.exit(0); } } Ponieważ wyświetlenie wyskakującego menu jest zazwyczaj wyzwalane przez p-kliknięcie, przytoczony aplet można przekształcić do następującej postaci, w której jest rozpoznawana akcja wyzwalająca. Uwaga: Rozpoznanie akcji wyzwalającej odbywa się za pomocą metody isPopupTrigger i musi wystąpić w metodzie processEvent albo processMouseEvent, bezpośrednio po rozpoznaniu zdarzenia MOUSE_RELEASED. =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet implements ActionListener { protected PopupMenu popup; protected boolean rClick; protected MenuItem mi; protected String rgbNames[] = { "Red", "Green", "Blue", null, "Exit" }; protected Color rgbColors[] = { Color.red, Color.green, Color.blue }; public void init() { popup = new PopupMenu("Colors"); for(int i = 0; i < 5 ; i++) { if(i != 3) { mi = new MenuItem(rgbNames[i]); mi.setActionCommand("" + i); mi.addActionListener(this); popup.add(mi); } else popup.addSeparator(); } add(popup); // dodaj do apletu addMouseListener( new MouseAdapter() { public void mouseClicked(MouseEvent evt) { int x = evt.getX(), y = evt.getY(); if(rClick) popup.show( evt.getComponent(), x, y ); } } ); } public void processMouseEvent(MouseEvent evt) { if(evt.getID() == MouseEvent.MOUSE_RELEASED) rClick = evt.isPopupTrigger(); super.processMouseEvent(evt); } public void actionPerformed(ActionEvent evt) { String cmd = evt.getActionCommand(); int value = new Integer(cmd).intValue(); if(value == 4) System.exit(0); Color color = rgbColors[value]; setBackground(color); } } Jan Bielecki Projektowanie dialogów Dialogi dzielą się na trwałe (modal) i nietrwałe (non-modal). Bezpośrednio po wyświetleniu dialogu trwałego i aż do chwili jego zamknięcia, celownik aplikacji jest nastawiony na ten dialog. Projektowanie dialogu odbywa się w sposób analogiczny do projektowania ramki. Konstruktor dialogu oczekuje podania odnośnika do okna macierzystego, określenia napisu na pasku tytułowym oraz podania czy dialog jest trwały czy nietrwały. Następujący program, pokazany na ekranie Dialog komunikatowy, kończy się m.in. po kliknięciu ikony zamknięcia, ale wyświetla pytanie, czy istotnie ma nastąpić zakończenie wykonywania. Ekran Dialog komunikatowy ### dialog.gif import java.awt.*; import java.awt.event.*; import java.io.*; public class Main extends Frame { protected static Main frame = new Main("Dialog"); public static void main(String[] args) { frame.setSize(200, 200); frame.setVisible(true); frame.addWindowListener( new WindowAdapter() { public void windowClosing(WindowEvent evt) { MsgDialog dlg = new MsgDialog(frame, "Closing!"); if(dlg.wasOK()) { frame.dispose(); System.exit(0); } } } ); } public Main(String caption) { super(caption); } } class MsgDialog extends Dialog implements ActionListener { protected boolean wasOK; protected Button ok, cancel; public MsgDialog(Frame parent, String caption) { super(parent, caption, true); setLayout(new FlowLayout()); add(new Label("Ready to close?")); add(ok = new Button("OK")); add(cancel = new Button("Cancel")); ok.addActionListener(this); cancel.addActionListener(this); pack(); show(); } public void actionPerformed(ActionEvent evt) { wasOK = evt.getSource() == ok; dispose(); } public boolean wasOK() { return wasOK; } } Jan Bielecki Projektowanie przycisków Gdy zaistnieje potrzeba wykreślenia komponentu na podstawie obrazu zapisanego w formacie GIF, to można użyć programu Paint Shop Pro 5.0, a jeden z kolorów zdefiniować jako przezroczysty. Umożliwi to rozpoznawanie pikseli należących do obrazu. Wykorzystano to w następującym programie, pokazanym na ekranie Przyciski obrazowe. Uwaga: Obrazy trójwymiarowych przycisków są przezroczystymi obrazami GIF o rozmiarach 24 x 24 piksele. Ekran Przyciski obrazowe ### buttons3.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; import java.awt.image.*; import java.net.*; public class Master extends Applet implements ActionListener { protected Image img; protected Graphics mDC; protected int w, h; protected BallButton rBall, yBall, gBall; protected Color color = Color.gray; public void init() { rBall = new BallButton(this, "Magenta"); add(rBall); rBall.addActionListener(this); yBall = new BallButton(this, "Yellow"); add(yBall); yBall.addActionListener(this); gBall = new BallButton(this, "Green"); add(gBall); gBall.addActionListener(this); w = Integer.parseInt(getParameter("width")); h = Integer.parseInt(getParameter("height")); } public void paint(Graphics gDC) { if(img == null) { img = createImage(w, h); mDC = img.getGraphics(); } mDC.setColor(color); for(int i = 0; i < h/6 ; i++) for(int j = 0; j < 3 ; j++) mDC.drawLine(0, i*6 + j, w-1, i*6 + j); super.paint(mDC); // wykreśla lekkie komponenty gDC.drawImage(img, 0, 0, this); } public void actionPerformed(ActionEvent evt) { String cmd = evt.getActionCommand(); if(cmd.equals("Magenta")) color = Color.magenta; else if(cmd.equals("Yellow")) color = Color.yellow; else if(cmd.equals("Green")) color = Color.green; repaint(); } } class BallButton extends Component { protected Image img; protected byte[] pixels; protected IndexColorModel model; protected int tPix; protected String name; public void load() { Toolkit kit = Toolkit.getDefaultToolkit(); img = kit.getImage(name + "Ball.gif"); PixelGrabber grab; grab = new PixelGrabber( img, 0, 0, -1, -1, false ); try { grab.grabPixels(); } catch(InterruptedException e) { } Object pix = grab.getPixels(); if(pix instanceof byte[]) pixels = (byte[])pix; model = (IndexColorModel)grab.getColorModel(); tPix = model.getTransparentPixel(); } protected BallButton source; protected Applet applet; public BallButton(Applet applet, String name) { this.applet = applet; this.name = name; Watcher watcher = new Watcher(); addMouseListener(watcher); source = this; load(); } public Dimension getPreferredSize() { return new Dimension(24, 24); } Image getImage(String fileName) { URL imageURL = null, docBase = null; try { docBase = applet.getDocumentBase(); imageURL = new URL(docBase, fileName); } catch(MalformedURLException e) { } MediaTracker tracker; Image image = applet.getImage(imageURL); tracker = new MediaTracker(this); tracker.addImage(image, 1); try { tracker.waitForID(1); } catch(InterruptedException e) { } return image; } public void paint(Graphics gDC) { gDC.drawImage(img, 0, 0, this); } ActionListener actionListener; synchronized void addActionListener(ActionListener lst) { actionListener = AWTEventMulticaster. add(actionListener, lst); } synchronized void removeActionListener(ActionListener lst) { actionListener = AWTEventMulticaster. remove(actionListener, lst); } class Watcher extends MouseAdapter { public void mouseClicked(MouseEvent evt) { int x = evt.getX(), y = evt.getY(), p = pixels[y * 24 + x] & 0xff; boolean onBall = p != tPix; if(onBall && actionListener != null) { actionListener.actionPerformed( new ActionEvent( source, ActionEvent.ACTION_PERFORMED, name ) ); } } } } Jan Bielecki Programowanie współbieżne Do zapisania algorytmu postępowania służy język programowania. Wyrażony w nim algorytm jest programem, a jego wykonanie jest procesem. Proces jest realizowany przez jeden albo więcej wątków. Wykonanie wątku polega na przepływie sterowania przez instrukcje programu. Na najniższym poziomie sprowadza się to do pobierania i wykonywania rozkazów procesora, wraz z towarzyszącą temu aktualizacją licznika rozkazów. Uwaga: Każdy proces wykonuje się w odrębnej przestrzeni adresowej. Natomiast każdy z wątków procesu wykonuje się w tej samej przestrzeni adresowej. Dzięki temu zarządzanie wątkami jest prostsze, a komunikacja między nimi znacznie szybsza. Dlatego wątki można nazywać „lekkimi procesami”. Bezpośrednio po odpaleniu programu jest aktywny tylko wątek pierwotny. Wątek pierwotny może tworzyć dodatkowe wątki. Poszczególne wątki są wykonywane współbieżnie z pozostałymi. Wykonanie współbieżne polega na takim przydzielaniu procesora poszczególnym wątkom, aby każdemu z nich umożliwić przepływ sterowania przez instrukcje programu. Współbieżne wykonywanie wątków może być równoległe albo emulowane. W systemie z dostateczną liczbą procesorów współbieżne wykonywanie wątków może być w pełni równolegle. W systemie z jednym procesorem równoległość wykonywania wątków jest emulowana. Projektowanie i uruchamianie programów wielowątkowych jest o rząd wielkości trudniejsze niż projektowanie programów sekwencyjnych. Wynika to z konieczności synchronizowania dostępu do zasobów dzielonych przez wątki oraz unikania zjawiska impasu i zagłodzenia wątków. Nagrodą za te wysiłki jest program czytelniejszy i szybszy oraz wrażliwszy (responsive) na zdarzenia zewnętrzne. Uwaga: Wbrew powszechnym stereotypom, główną zaletą programowania wielowątkowego nie jest zwiększenie szybkości, ale zwiększenie wrażliwości programu. Stany wątków Wątek istnieje od chwili utworzenia do chwili zniszczenia W czasie jego istnienia (isAlive) wątek może być aktywny albo nieaktywny. Wątek aktywny przebiega kolejne instrukcje programu. Wątek nieaktywny jest zatrzymany, wstrzymany (wait) albo uśpiony (sleep). Obiekty wątków Z każdym wątkiem jest związany odrębny obiekt wątku. Obiekt wątku jest klasy Thread albo jej klasy pochodnej. W każdej chwili tylko jeden wątek może być związany z tym samym obiektem wątku. Argumentem jednoparametrowego konstruktora klasy Thread jest odnośnik do obiektu klasy implementującej interfejs Runnable. Zobowiązuje to ją do dostarczenia metody run, przez której instrukcje przepływa sterowanie wątku. Zarządzanie wątkami Wątek zostaje utworzony bezpośrednio po wykonaniu na rzecz obiektu wątku metody start, a zostaje zniszczony bezpośrednio po zakończeniu wykonywania metody run. Wykonanie na rzecz obiektu wątku metody interrupt powoduje, że wątek uśpiony albo wstrzymany staje się aktywny. W każdym z tych przypadków, z miejsca uśpienia albo wstrzymania wątku jest wysyłany wyjątek InterruptedException. Jeśli podczas tworzenia obiektu wątku użyto bezparametrowego konstruktora klasy Thread, to wywołanie metody start spowoduje wykonanie metody run klasy Thread. Ponieważ jej ciało jest puste, metodę tę z reguły przedefiniowuje się w klasie pochodnej klasy Thread (patrz poniżej klasa Drawer) Uwaga: Wywoływanie metod init, start, stop i destroy apletu odbywa się w ramach wątku systemowego. W innym wątku systemowym są wykonywane metody paint i update oraz metody do obsługiwania zdarzeń: actionPerformed, mouseClicked, keyTyped, itp. Następujący aplet, pokazany na ekranie Zarządzanie wątkami, ilustruje typowe sposoby tworzenia wątków. Ekran Zarządzanie wątkami ### threads.gif =============================================== import java.applet.Applet; import java.awt.*; public class Master extends Applet implements Runnable { private Thread beepThread, backThread, drawThread; public Color backColor = null; public void init() { Beeper beeper = new Beeper(); beepThread = new Thread(beeper); beepThread.start(); backThread = new Thread(this); backThread.start(); drawThread = new Drawer(this); } public void paint(Graphics gDC) { Dimension dim = getSize(); int w = dim.width; int h = dim.height; gDC.drawRect(0, 0, w-1, h-1); } public void run() { Color colors[] = { Color.cyan, Color.green }; int flag = 0; while(true) { flag = 1 - flag; backColor = colors[flag]; try { Thread.sleep(3000); } catch(InterruptedException e) { } } } class Drawer extends Thread { private Applet applet; private int x, y, w, h; public Drawer(Applet applet) { this.applet = applet; start(); } public void run() { Graphics gDC = applet.getGraphics(); int dx = 1, dy = 0, r = 30; Dimension dim = applet.getSize(); x = (w = dim.width) / 2; y = (h = dim.height) / 2; while(true) { if(backColor != null) { gDC.setPaintMode(); gDC.setColor(backColor); gDC.fillRect(0, 0, w, h); gDC.setColor(Color.black); backColor = null; } gDC.setXORMode(Color.white); gDC.drawOval(x-r, y-r, r<<1, r<<1); try { Thread.sleep(10); } catch(InterruptedException e) { } gDC.drawOval(x-r, y-r, r<<1, r<<1); x += dx; y += dy; if(x > w-r || x < r) dx = -dx; if(y > h-r || y < r) { dy = -dy; } } } } } class Beeper implements Runnable { public void run() { while(true) { try { Thread.sleep(1000); } catch(InterruptedException e) { } Toolkit.getDefaultToolkit().beep(); } } } Synchronizowanie wątków Wątki posługujące się wspólnym zasobem muszą być synchronizowane. W przeciwnym razie stan zasobu może stać się nieokreślony. Aby się przekonać o konieczności synchronizowania dostępu do wspólnego zasobu, wystarczy rozpatrzyć sytuację, gdy rolę wątków pełnią dwaj kasjerzy, którzy mają nie-synchronizowany dostęp do bazy kont oszczędnościowych. Jeśli na wspólnym koncie dwóch osób jest na przykład $100, a każda z nich wpłaca w osobnym okienku $20, to może zaistnieć następująca sytuacja Pierwszy kasjer sprawdza konto i odnotowuje jego stan ($100), ale zostaje oderwany do telefonu. Drugi kasjer sprawdza konto, odnotowuje jego stan ($100), dodaje $20 i aktualizuje konto (do $120). Pierwszy kasjer kończy rozmowę, dodaje $20 do odnotowanej sumy i aktualizuje bazę (do $120). W następstwie nie-synchronizowanego dostępu do bazy kont, następuje zwiększenie stanu konta nie o $40, ale o $20. Uwaga: Synchronizowaniu muszą podlegać również operacje, które wydają się atomowe, jak na przykład account++. Wynika to stąd, że program współbieżny powinien wykonywać się poprawnie nie tylko w komputerze, którego procesor wykonuje operację ++ za pomocą nieprzerywalnej instrukcji "dodaj do pamięci", ale i na takim, który tę operację wykonuje za pomocą przerywalnej sekwencji: "pobierz", "dodaj", "zapamiętaj". Następujący aplet, pokazany na ekranie Brak synchronizacji, ilustruje skutek niesynchronizowanego dostępu do wspólnego zasobu wątków oneThread i twoThread jakim jest obiekt identyfikowany przez p. Mimo iż współrzędne x i y punktu p są modyfikowane "jednocześnie", dochodzi do sytuacji, gdy współrzędne te są różne. Objawia się to wykreślaniem odcinków nie leżących na głównej przekątnej apletu. Uwaga: Dodatkowym, nie synchronizowanym zasobem apletu jest wykreślacz identyfikowany przez gDC. Ponadto, ponieważ metoda drawLine nie jest synchronizowana, można obawiać się skutków jej jednoczesnego wykonania z różnych wątków. Ekran Brak synchronizacji ### nosync.gif =============================================== import java.applet.Applet; import java.awt.*; public class Master extends Applet implements Runnable { private Point p = new Point(0, 0); private Thread oneThread, twoThread; private Graphics gDC; public void init() { gDC = getGraphics(); oneThread = new Thread(this); twoThread = new Thread(this); oneThread.start(); twoThread.start(); } public void run() { int c = 0; while(true) { int x = Math.abs(p.x), y = Math.abs(p.y); gDC.drawLine(0, 0, x, y); p.x = (x+1) % 100; p.y = (y+1) % 100; if(++c % 100000 == 0) { p.x = p.y = 0; repaint(); } } } } Sekcje krytyczne Do synchronizowania wątków służą: instrukcja synchronizująca i funkcje synchronizujące (zadeklarowane ze specyfikatorem synchronized). Każda z nich wyznacza sekcję krytyczną. Instrukcja synchronizująca zapewnia, że w danej chwili tylko jeden z synchronizowanych przez nią wątków może wykonywać instrukcje jej sekcji krytycznej. Dwa kolejne aplety, pokazane na ekranie Dostęp synchronizowany, ilustrują użycie sekcji krytycznych do wykluczenia jednoczesnego dostępu do wspólnego zasobu wątków. Uwaga: Aby uniknąć synchronizowania dostępu do wykreślacza, utworzono go odrębnie dla każdego z wątków. Dla uproszczenia przyjęto (co na ogół nie jest prawdą!), że wywołanie nie-synchronizowanej metody drawLine nie spowoduje zawieszenia systemu Ekran Dostęp synchronizowany ### section.gif Użycie instrukcji synchronizującej =============================================== import java.applet.Applet; import java.awt.*; public class Master extends Applet implements Runnable { private Point p = new Point(0, 0); private Thread oneThread, twoThread; private Boolean lock = new Boolean(false); public void init() { oneThread = new Thread(this); twoThread = new Thread(this); oneThread.start(); twoThread.start(); } public void run() { Graphics gDC = getGraphics(); while(true) { int x, y; // instrukcja synchronizująca synchronized(lock) { x = Math.abs(p.x); y = Math.abs(p.y); } gDC.drawLine(0, 0, x, y); // instrukcja synchronizująca synchronized(lock) { p.x = (x+1) % 100; p.y = (y+1) % 100; } } } } Użycie funkcji synchronizującej =============================================== import java.applet.Applet; import java.awt.*; public class Master extends Applet implements Runnable { private Point p = new Point(0, 0); private Thread oneThread, twoThread; public void init() { Graphics gDC = getGraphics(); oneThread = new Thread(this); twoThread = new Thread(this); oneThread.start(); twoThread.start(); } // funkcja synchronizująca public synchronized void incXY(int x, int y) { p.x = (x+1) % 100; p.y = (y+1) % 100; } public void run() { Graphics gDC = getGraphics(); while(true) { int x, y; synchronized(this) { x = Math.abs(p.x); y = Math.abs(p.y); } gDC.drawLine(0, 0, x, y); incXY(x, y); } } } Instrukcja synchronizująca Instrukcja synchronizująca ma postać synchronized(obj) block w której obj jest odnośnikiem do synchronizatora, a block jest blokiem. Jeśli w chwili podjęcia wykonywania instrukcji synchronizującej stwierdzi się, że obiekt identyfikowany przez obj nie jest przydzielony obcemu wątkowi, to przydzieli się go bieżącemu wątkowi na czas wykonywania bloku instrukcji synchronizującej albo do chwili, gdy w tym bloku zostanie wywołana metoda wait. W przeciwnym razie, wątek zostaje zatrzymany. Uwaga: Opracowanie nagłówka tej samej instrukcji synchronizującej przez różne wątki może dotyczyć różnych obiektów identyfikowanych przez obj. W tym rzadkim przypadku nie nastąpi zatrzymanie wątku. Jeśli wątkowi przydzielono ten sam obiekt wielokrotnie synchronized(lock) { // ... synchronized(lock) { // ... } // ... } to zwolnienie obiektu nastąpi dopiero wówczas, gdy zakończy się wykonywanie najszerszej instrukcji synchronizującej albo gdy w ciele dowolnej z nich zostanie wywołana metoda wait. Jeśli zakończenie wykonywania instrukcji synchronizującej spowoduje zwolnienie synchronizatora, to jeden z wątków zatrzymanych na tym obiekcie zostanie ożywiony, tj. dopuszczony do kontynuowania zawierającej go instrukcji albo funkcji synchronizującej. Następujący aplet, pokazany na ekranie Instrukcje synchronizujące, ilustruje zastosowanie synchronizacji na skutek której nie uda się spowodować, aby wyprowadzenie napisu Action nastąpiło między napisami Before sleep i After sleep. Ekran Instrukcje synchronizujące ### syncins.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet implements Runnable { private Integer lock = new Integer(0); private Button button = new Button("Action"); private TextArea area = new TextArea(); private Thread thread; public void init() { setLayout(new BorderLayout()); add(button, BorderLayout.WEST); add(area, BorderLayout.CENTER); button.addActionListener( new ActionListener() { public void actionPerformed(ActionEvent evt) { show("Action"); } } ); thread = new Thread(this); thread.start(); } public void run() { while(true) try { synchronized(lock) { show("\nBefore sleep"); thread.sleep(3000); show("After sleep"); } } catch(InterruptedException e) { } } public void show(String string) { synchronized(lock) { area.append(string + "\n"); } } } Funkcje synchronizujące Funkcją synchronizującą jest funkcja zadeklarowana ze specyfikatorem synchronized. Jeśli jest metodą, to wykonuje się ją tak, jakby jej ciało zawarto w instrukcji zaczynającej się od synchronized(this) a jeśli jest funkcją statyczną klasy Name, to wykonuje się ją tak, jakby jej ciało zawarto w instrukcji zaczynającej się od synchronized(Name.class) w której Name.class jest nazwą unikalnego synchronizatora klasy Name. Ma to taki skutek, że na czas wykonywania funkcji synchronizującej, zostanie zatrzymany każdy inny wątek, który podejmie próbę wykonania dowolnej funkcji synchronizującej jej klasy, posługującej się tym samym synchronizatorem. Następujący aplet, pokazany na ekranie Funkcje synchronizujące, ilustruje zastosowanie synchronizacji na skutek której nie uda się spowodować, aby napis Action został wyprowadzony między napisami Before sleep i After sleep. Ekran Funkcje synchronizujące ### syncfun.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet implements Runnable { private Button button = new Button("Action"); private TextArea area = new TextArea(); private Thread thread; public void init() { setLayout(new BorderLayout()); add("West", button); add("Center", area); button.addActionListener( new ActionListener() { public void actionPerformed(ActionEvent evt) { show("Action"); } } ); thread = new Thread(this); thread.start(); } public synchronized void show(String string) { area.append(string + "\n"); } public synchronized void sleep() { try { show("\nBefore sleep"); thread.sleep(3000); show("After sleep"); } catch(InterruptedException e) { } } public void run() { while(true) sleep(); } } Metody wait Wywołanie metody wait ma postać obj.wait() obj.wait(ms) obj.wait(ms, ns) // ms + 0.001*ns w której obj jest odnośnikiem do obiektu, a ms i ns są liczbami określającymi czas w milisekundach. Wykonanie metody wait może wystąpić tylko w bloku takiej instrukcji synchronizującej, w której do synchronizacji użyto obiektu identyfikowanego przez obj. W przeciwnym razie, z miejsca wykonania metody, zostanie wysłany wyjątek klasy IllegalMonitorStateException. Wątek wywołujący bezparametrową metodę wait jest wstrzymywany na czas nieokreślony. Jeśli wywoła jedną z pozostałych metod, to zostanie wstrzymany na czas nie dłuższy niż określony za pomocą argumentów. A zatem otoczenie wywołania metody wait przybiera zazwyczaj postać synchronized(lock) { // ... try { lock.wait(); } catch(IllegalMonitorStateException e) { // ... } catch(InterruptedException e) { // ... } } Jeśli wywołania metody wait nie poprzedzono nazwą obiektu, a wywołanie znajduje się w ciele metody, to domyślnie przyjmuje się, że chodzi o wywołanie na rzecz obiektu identyfikowanego przez this. A zatem poprawna jest m.in. metoda synchronized void inc(Fixed fix) throws IllegalMonitorStateException, InterruptedException { if(fix == -1) wait(); fix.add(1); } ponieważ można ją przedstawić w postaci void inc(Fixed fix) throws IllegalMonitorStateException, InterruptedException { synchronized(this) { if(fix == -1) this.wait(); fix.add(1); } } Uwaga: Wywołanie metody wait w ciele funkcji statycznej jest niejawnie kwalifikowane odnośnikiem do unikalnego obiektu opisującego klasę, w której zawarto tę funkcję. Metody notify Wywołanie metod notify ma postać obj.notify() obj.notifyAll() w której obj jest odnośnikiem do obiektu. Wykonanie metody notify może wystąpić tylko w bloku takiej instrukcji synchronizującej, w której do synchronizacji użyto obiektu identyfikowanego przez obj. W przeciwnym razie, z miejsca wykonania metody, zostanie wysłany wyjątek klasy IllegalMonitorStateException. Wykonanie metody notify powoduje uwolnienie jednego (notify) albo wszystkich wątków (notifyAll) wstrzymanych na na skutek wykonania metody wait na rzecz obiektu identyfikowanego przez obj. Uwolnienie polega na przekształceniu wątku wstrzymanego w zatrzymany. Uwolniony wątek zatrzymany może zostać ożywiony nie wcześniej, niż po zakończeniu wykonywania wszystkich sekcji krytycznych zawierających uwalniającą go instrukcję notify albo notifyAll. Następujący aplet, pokazany na ekranie Metody wait i notify, tworzy zestaw współbieżnych wątków. W notatniku umieszczonym na pulpicie apletu są wyświetlane informacje o wystartowaniu i zatrzymaniu wątku. Po zatrzymaniu wszystkich wątków jest wyprowadzany napis Done!. Ekran Metody wait i notify ### waitnoti.gif W tabeli Przebieg wykonania przytoczono przykładowy zestaw komunikatów wysłanych przez program. Tabela Przebieg wykonania ### Thread 2 starting Thread 1 starting Thread 3 starting all starting to die! some may be alive some may be alive some may be alive Thread 1 stopping Thread 2 stopping some may be alive Thread 3 stopping all are Dead! ### =============================================== import java.applet.Applet; import java.awt.*; import java.util.Vector; public class Master extends Applet implements Runnable { int noOfRuns = 3; int id, count; TextArea area; Vector threads; Integer monitor = new Integer(0); public void start() { setLayout(new BorderLayout()); area = new TextArea(); add("Center", area); id = 0; threads = new Vector(); synchronized(monitor) { for(int i = 0; i < noOfRuns ; i++) { Thread thread = new Thread(this); threads.addElement(thread); thread.start(); } count = noOfRuns; } synchronized(monitor) { if(count != 0) try { monitor.wait(); } catch(InterruptedException e) { } } show("all starting to die!"); boolean allDead = false; while(!allDead) { show(" some may be alive"); allDead = true; for(int i = 0; i < threads.size() ; i++) { Object thread = threads.elementAt(i); allDead &= !((Thread)thread).isAlive(); } } show("all are Dead!"); } public void stop() { for(int i = 0; i < threads.size() ; i++) { Thread thread = (Thread)threads.elementAt(i); thread.interrupt(); try { thread.join(); } catch(InterruptedException e) { } } } public void run() { synchronized(monitor) { } int id = getId(); show("starting", id); try { Thread.sleep(200); } catch(InterruptedException e) { return; } decCount(); // eksperytmentuj z czasem snu try { Thread.sleep(30); } catch(InterruptedException e) { show("stopping", id); } } synchronized int getId() { return ++id; } void decCount() { synchronized(monitor) { count--; if(count == 0) monitor.notify(); } } synchronized void show(String string, int id) { area.append("Thread " + id + " " + string + "\n"); } synchronized void show(String string) { area.append("\n" + string + "\n"); } } Ponieważ instrukcja thread.start(); wchodzi w skład instrukcji synchronizującej, więc każdy nowy wątek zostanie zatrzymany na instrukcji synchronized(monitor) { } rozpoczynającej metodę run. Dopiero po wykonaniu instrukcji count = noOfRuns; oraz zwolnieniu obiektu, wątki otrzymają szansę dalszego wykonywania. A zatem (w środowisku wieloprocesorowym!) będą się wykonywać jednocześnie. Wykonanie instrukcji synchronized(monitor) { if(count != 0) try { monitor.wait(); } catch(InterruptedException e) { } } ma na celu wstrzymanie wykonywania głównego wątku do chwili zakończenia wszystkich pozostałych wątków. O ile to jeszcze nie nastąpiło, zostanie wykonana instrukcja wait, a wątek główny zostanie wstrzymany do chwili wykonania sekcji krytycznej zawierającej uwalniającą go instrukcję notify. synchronized(monitor) { count--; if(count == 0) monitor.notify(); // uwolnienie } Ponieważ wyprowadzanie napisów do ramki odbywa się z różnych wątków, a metoda append klasy TextArea nie jest synchronizowana, więc w celu uniknięcia wyprowadzenia bezsensownie przemieszanych napisów, obie metody show implementowano jako synchronizowane! Synchronizowanie wątków Wywołanie metody notify ma zazwyczaj na celu uwolnienie wątku wstrzymanego na skutek wykonania funkcji wait. Może się jednak zdarzyć, że funkcja notify zostanie wywołana zanim wątek zostanie wstrzymany. W takim przypadku wywołanie funkcji notify nie będzie miało żadnego skutku, co może doprowadzić do zawieszenia programu. Ilustruje to następujący program Producent - Konsument. Uwaga: Program nie jest poprawny, gdyż może się zawieszać. Warto się zastanowić, co jest tego przyczyną. =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet { final int Count = 5000, Empty = -1, Last = 0; int theItem = Empty; Boolean buffer = new Boolean(false); public void init() { Consumer consumer = new Consumer(); new Thread(consumer).start(); Producer producer = new Producer(); new Thread(producer).start(); } class Producer implements Runnable { public void run() { for(int i = 1; i <= Count+1 ; i++) { synchronized(buffer) { // producing if(i < Count+1) { int item = i; theItem = item; System.out.println("P: " + item); } else theItem = Last; buffer.notify(); try { buffer.wait(); } catch(InterruptedException e) { } } } } } class Consumer implements Runnable { public void run() { while(true) { synchronized(buffer) { try { buffer.wait(); } catch(InterruptedException e) { } // consuming int item = theItem; theItem = Empty; if(item == Last) break; System.out.println("C: " + item); buffer.notify(); } } System.out.println("Done!"); } } } W celu usunięcia przyczyny zawieszania powyższego i podobnych do niego programów, należy 1) Posłużyć się pomocniczą zmienną orzecznikową. 2) W dowolnym miejscu sekcji zawierającej wywołanie metody notify nadać aktualną wartość zmiennej orzecznikowej. 3) W ramach podejmowania decyzji o wykonaniu metody wait zbadać wartość zmiennej orzecznikowej. 4) Po powrocie ze wstrzymania przywrócić pierwotną wartość zmiennej orzecznikowej. Taki sposób postępowania ilustruje następujący program. =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet { final int Count = 5000, Empty = -1, Last = 0; int theItem = Empty; Boolean buffer = new Boolean(false); boolean bufferIsEmpty = true; boolean bufferIsFull = false; public void init() { Producer producer = new Producer(); new Thread(producer).start(); Consumer consumer = new Consumer(); new Thread(consumer).start(); } class Producer implements Runnable { public void run() { for(int i = 1; i <= Count+1 ; i++) { synchronized(buffer) { if(!bufferIsEmpty) { try { buffer.wait(); } catch(InterruptedException e) { } } // producing if(i < Count+1) { int item = i; theItem = item; System.out.println("P: " + item); } else theItem = Last; bufferIsFull = true; bufferIsEmpty = false; buffer.notify(); } } } } class Consumer implements Runnable { public void run() { while(true) { synchronized(buffer) { if(!bufferIsFull) { try { buffer.wait(); } catch(InterruptedException e) { } } // consuming int item = theItem; theItem = Empty; if(item == Last) break; System.out.println("C: " + item); bufferIsEmpty = true; bufferIsFull = false; buffer.notify(); } } System.out.println("Done!"); } } } Ponieważ między chwilą uwolnienia wątku i chwilą przydzielenia mu sekcji krytycznej może zostać przywrócony warunek wstrzymania, zaleca się, aby zamiast instrukcji if używano instrukcji while, to jest aby instrukcję synchronized(buffer) { if(!bufferIsEmpty) { try { buffer.wait(); } catch(InterruptedException e) { } } // ... bufferIsEmpty = false; buffer.notify(); } zastąpiono instrukcją synchronized(buffer) { while(!bufferIsEmpty) { try { buffer.wait(); } catch(InterruptedException e) { } } // ... bufferIsEmpty = false; buffer.notify(); } Przypadek ten ilustruje następujący program, w którym 1) Zastąpienie funkcji notifyAll funkcją notify czyni program błędnym, gdyż może prowadzić do impasu (deadlock), to jest trwałego zawieszenia programu. Wystąpi to wówczas, gdy w stanie wstrzymania wątku producenta, wywołanie metody notify przez jednego z konsumentów (po skonsumowaniu bufora) uwolni wątek drugiego konsumenta (a nie producenta!), co spowoduje, że po wykonaniu badań w instrukcji while i stwierdzeniu, że bufor jest pusty, zostaną wstrzymane także i wątki konsumentów. 2) Pozostawienie funkcji notifyAll, ale zastąpienie instrukcji while instrukcją if czyni program błędnym. Ujawni się to wówczas, gdy bezpośrednio po uwolnieniu wątków konsumenta, jeden z nich wejdzie do sekcji krytycznej i skonsumuje bufor, po czym do sekcji krytycznej wejdzie drugi wątek konsumenta, który przystąpi do konsumpcji, mimo iż nie ma czego konsumować. Uwaga: Zastąpienie instrukcji buffer.notifyAll(); instrukcją buffer.notify(); oraz dodatkowo umieszczenie jej przed instrukcją buffer.wait(); zapobiega trwałemu zawieszeniu programu, ale może powodować aktywny impas (livelock), tutaj: marnotrawne wstrzymywanie i uwalnianie wątków konsumenta, gdy postęp wykonania programu jest możliwy tylko po uwolnieniu wątku producenta. =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet { final int Count = 5000, Empty = -1, Last = 0; int theItem = Empty; Boolean buffer = new Boolean(false); boolean bufferIsEmpty = true; boolean bufferIsFull = false; public void init() { Consumer consumer1 = new Consumer(1); new Thread(consumer1).start(); Producer producer = new Producer(); new Thread(producer).start(); Consumer consumer2 = new Consumer(2); new Thread(consumer2).start(); } class Producer implements Runnable { public void run() { for(int i = 1; i <= Count+2 ; i++) { synchronized(buffer) { while(!bufferIsEmpty) { try { buffer.wait(); } catch(InterruptedException e) { } } // producing if(i < Count+1) { int item = i; theItem = item; System.out.println("P: " + item); } else theItem = Last; bufferIsFull = true; bufferIsEmpty = false; buffer.notify(); } } } } class Consumer implements Runnable { protected int id; public Consumer(int id) { this.id = id; } public void run() { while(true) { synchronized(buffer) { while(!bufferIsFull) { try { // próbuj: buffer.notify(); buffer.wait(); } catch(InterruptedException e) { } } // consuming int item = theItem; theItem = Empty; if(item == Last || item == Empty) break; System.out.println("C" + id + ": " + item); bufferIsEmpty = true; bufferIsFull = false; buffer.notifyAll(); // próbuj: notify() } } Toolkit.getDefaultToolkit().beep(); System.out.println("C" + id + " Done!"); } } } Aby nie powstało wrażenie, że użycie funkcji notifyAll jest niezbędne, a także dla pokazania sposobu synchronizowania wątków opartego na więcej niż jednej sekcji krytycznej, opracowano następujący program Producent - Konsumenci, w którym każda dana jest odbierana przez tylko jednego konsumenta. Uwaga: W celu ułatwienia eksperymentów, program wyposażono w rozbudowany system wydruków kontrolnych, wykorzystujący zdefiniowaną w Dodatku C, klasę Debug. =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; import janb.debug.*; public class Master extends Applet implements Runnable { protected final boolean isDebug$ = false; protected final boolean showAll = false; protected final int Count = 1000, Eaters = 3, // liczba konsumentów Empty = -1, Last = 0; public void show(String str, boolean always) { if(always || showAll) { synchronized(this) { if(isDebug$) Debug.toFrame(str); else System.out.println(str); } } } public void show(String str) { show(str, false); } public void run() { while(isActive) { try { Thread.sleep(1000); } catch(InterruptedException e) { } String totals = ""; for(int i = 0; i < Eaters ; i++) totals = totals + (i+1) + ": " + consumer[i].getTotal() + " "; show(totals, true); } } protected Consumer[] consumer = new Consumer[Eaters]; protected int theItem = Empty; protected Boolean bufferFull = new Boolean(false); protected boolean bufferIsFull = false; protected Boolean bufferEmpty = new Boolean(false); protected boolean bufferIsEmpty = true; protected boolean isActive = true; public void init() { if(isDebug$) new Debug(); Producer producer = new Producer(); new Thread(producer).start(); for(int i = 0; i < Eaters ; i++) { consumer[i] = new Consumer(i+1); new Thread(consumer[i]).start(); } new Thread(this).start(); } class Producer implements Runnable { public void run() { for(int i = 1; i <= Count+Eaters ; i++) { synchronized(bufferEmpty) { while(!bufferIsEmpty) { try { show(" P waits"); bufferEmpty.wait(); show(" P wakes"); } catch(InterruptedException e) { } } // producing if(i < Count+1) { int item = i; theItem = item; // show(" P: " + item, true); } else theItem = Last; bufferIsFull = true; bufferIsEmpty = false; } synchronized(bufferFull) { show(" P notifies"); bufferFull.notify(); } } synchronized(bufferFull) { show(" P notifies last"); bufferFull.notify(); } show(" P Done! ", true); isActive = false; } } class Consumer implements Runnable { protected int id, total = 0; public Consumer(int id) { this.id = id; } public int getTotal() { return total; } public void run() { show("C" + id + " starts"); while(true) { synchronized(bufferFull) { while(!bufferIsFull) { try { show("C" + id + " waits"); bufferFull.wait(); show("C" + id + " wakes"); } catch(InterruptedException e) { } } // consuming int item = theItem; theItem = Empty; bufferIsEmpty = true; bufferIsFull = false; // show("C" + id + ": " + item, true); if(item == Last || item == Empty) break; total++; } synchronized(bufferEmpty) { show("C" + id + " notifies"); bufferEmpty.notify(); } } synchronized(bufferEmpty) { show("C" + id + " notifies last"); bufferEmpty.notify(); } Toolkit.getDefaultToolkit().beep(); show("C" + id + " Done! " + total, true); } } } Pozostaje jeszcze pytanie, czy konsumenci o takim samym priorytecie mają równe szanse otrzymywania danych. Odpowiedź jest negatywna: może się zdarzyć, że w systemie z emulacją współbieżności, niektóre wątki konsumenta będą faworyzowane, a inne zostaną zagłodzone (starved). Następujący program rozwiązuje problem sprawiedliwego dostępu do bufora. Zastosowana w nim metoda jest prosta, ale jeśli nie chce się tworzyć własnego dyspozytora, to okazuje się całkiem skuteczna. =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet implements Runnable { protected final int Count = 6000, Eaters = 3, // liczba konsumentów Empty = -1, Last = 0; public void show(String str) { synchronized(this) { System.out.println(str); } } protected String oldTotals = ""; public void run() { while(isActive) { try { Thread.sleep(500); } catch(InterruptedException e) { } String totals = ""; for(int i = 0; i < Eaters ; i++) totals = totals + (i+1) + ": " + consumer[i].getTotal() + " "; if(!totals.equals(oldTotals)) show(totals); oldTotals = totals; } } protected Consumer[] consumer = new Consumer[Eaters]; protected int theItem = Empty; protected Boolean bufferFull = new Boolean(false); protected boolean bufferIsFull = false; protected Boolean bufferEmpty = new Boolean(false); protected boolean bufferIsEmpty = true; protected boolean isActive = true; public void init() { Producer producer = new Producer(); new Thread(producer).start(); for(int i = 0; i < Eaters ; i++) { consumer[i] = new Consumer(i+1); new Thread(consumer[i]).start(); } new Thread(this).start(); } class Producer implements Runnable { public void run() { for(int i = 1; i <= Count+Eaters ; i++) { synchronized(bufferEmpty) { while(!bufferIsEmpty) { try { bufferEmpty.wait(); } catch(InterruptedException e) { } } // producing if(i < Count+1) { int item = i; theItem = item; // show(" P: " + item); } else theItem = Last; bufferIsFull = true; bufferIsEmpty = false; } // wybór poszkodowanego int min = consumer[0].getTotal(), id = 0; for(int j = 1; j < Eaters ; j++) { int total = consumer[j].getTotal(); if(total < min) { min = total; id = j; } } consumer[id].setMinFlag(); synchronized(bufferFull) { // uwolnienie wszystkich konsumentów, // ale w istocie tylko poszkodowanego bufferFull.notifyAll(); } } isActive = false; } } class Consumer implements Runnable { protected int id, total = 0; protected boolean minFlag = false; public Consumer(int id) { this.id = id; } public int getTotal() { return total; } public void setMinFlag() { minFlag = true; } public void run() { while(true) { synchronized(bufferFull) { while(!(bufferIsFull && minFlag)) { try { bufferFull.wait(); } catch(InterruptedException e) { } } minFlag = false; // consuming int item = theItem; theItem = Empty; bufferIsEmpty = true; bufferIsFull = false; // show("C" + id + ": " + item); if(item == Last || item == Empty) break; total++; } synchronized(bufferEmpty) { bufferEmpty.notify(); } } } } } Metoda interrupt Wywołanie metody interrupt ma postać obj.interrupt() w której obj jest odnośnikiem do obiektu wątku. Jeśli metoda interrupt zostanie wywołana na rzecz obiektu wstrzymanego albo uśpionego wątku, to w najbliższej chwili gdy wątek ten zostanie ożywiony, z miejsca jego ożywienia, a więc tuż po sleep albo wait, zostanie wysłany wyjątek klasy InterruptedException. Następujący program ilustruje użycie metody interrupt. Naciśnięcie przycisku Push powoduje zmianę koloru tła apletu na czerwony. =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet implements Runnable { public void init() { final Thread waiter = new Thread(this); waiter.start(); Button push = new Button("Push"); add(push); push.addActionListener( new ActionListener() { public void actionPerformed(ActionEvent evt) { waiter.interrupt(); } } ); } public void run() { try { wait(); } catch(InterruptedException e) { setBackground(Color.red); return; } setBackground(Color.green); } } Metoda join Wywołanie metody join ma postać obj.join() w której obj jest odnośnikiem do obiektu wątku. Wykonanie metody powoduje zawieszenie wykonywania wątku do chwili, gdy zakończy się wykonywanie wątku opartego na obiekcie identyfikowanym przez obj. Metoda isAlive Wywołanie metody isAlive ma postać obj.isAlive() w której obj jest odnośnikiem do obiektu wątku. Metoda dostarcza orzecznik o wartości logicznej „istnieje wątek oparty na obiekcie identyfikowanym przez obj”. Priorytety W warunkach współbieżności emulowanej, przydzielanie procesora wątkom odbywa się „w zasadzie” na podstawie priorytetów. W praktyce oznacza to, że nie można wykluczyć sytuacji, kiedy wątek o najwyższym priorytecie zmonopolizuje procesor. Ale nie można również założyć, że w systemie z wywłaszczaniem (preemptive) czas procesora będzie dzielony między wątkami sprawiedliwie. Dlatego należy postępować tak, aby program mógł się poprawnie wykonać zarówno w systemie jedno- i wieloprocesorowym jak i w systemie wywłaszczającym i nie-wywłaszczającym. Osądzenie, czy jest to zadanie łatwe, pozostawia się bez odpowiedzi. Uwaga: W żadnym wypadku nie wolno zastąpić synchronizowania wątków operacjami na ich priorytetach. W szczególności nie wolno przyjmować, że przyznanie wątkowi najwyższego priorytetu gwarantuje w systemie jednoprocesorowym monopolizację procesora. Następujący program, mimo iż może wielu systemach wykonuje się poprawnie, jest w niektórych systemach błędny, ponieważ do synchronizowania wątków posługuje się priorytetami. Uwaga: Program byłby błędny wszędzie (bo wykreślane przez niego piksele nie leżałyby na prostej), gdyby usunięto z niego instrukcje zawierające wywołania funkcji setPriority. =============================================== import java.applet.Applet; import java.awt.*; public class Master extends Applet implements Runnable { private Point p = new Point(0, 0); private Thread oneThread, twoThread; private Boolean lock = new Boolean(false); public void init() { oneThread = new Thread(this); twoThread = new Thread(this); oneThread.start(); oneThread.setPriority(Thread.MIN_PRIORITY); twoThread.start(); twoThread.setPriority(Thread.MIN_PRIORITY); } public void run() { Graphics gDC = getGraphics(); Thread thread = Thread.currentThread(); while(true) { int x, y; // niedozwolona synchronizacja thread.setPriority(Thread.MAX_PRIORITY); x = Math.abs(p.x); y = Math.abs(p.y); thread.setPriority(Thread.MIN_PRIORITY); gDC.drawLine(0, 0, x, y); // niedozwolona synchronizacja thread.setPriority(Thread.MAX_PRIORITY); p.x = (x+1) % 100; p.y = (y+1) % 100; thread.setPriority(Thread.MIN_PRIORITY); } } } Klasa monitorowa Zapis programu współbieżnego można znacznie uprościć, posługując się klasą monitorową. public class Monitor { /** Klasa monitorowa Copyright © Jan Bielecki 1999.01.03 */ private boolean monitorFlag; private Boolean monitor = new Boolean(false); public Monitor(boolean flag) { monitorFlag = flag; } public void jbWait() throws InterruptedException { synchronized(monitor) { while(!monitorFlag) monitor.wait(); monitorFlag = false; } } public void jbNotify() { synchronized(monitor) { monitorFlag = true; monitor.notify(); } } // wersja nieprzerywalna, zbędne try public void jbWait2() { synchronized(monitor) { while(!monitorFlag) { try { monitor.wait(); } catch(InterruptedException e) { } } monitorFlag = false; } } public void jbPause() { synchronized(monitor) { } } } Następujący program ilustruje użycie klasy monitorowej do rozwiązania problemu Producent - Konsument. =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet { final int Count = 500, Last = 0; class Buffer { private final int Empty = -1; private int theItem = Empty; synchronized void setBuffer(int value) { theItem = value; } synchronized void setEmpty() { setBuffer(Empty); } synchronized int getBuffer() { return theItem; } } // bufor Buffer buffer = new Buffer(); // obiekty monitorowe Monitor bufferIsEmpty = new Monitor(true), bufferIsFull = new Monitor(false); public void init() { Producer producer = new Producer(); new Thread(producer).start(); Consumer consumer = new Consumer(); new Thread(consumer).start(); bufferIsEmpty.jbNotify(); } class Producer implements Runnable { public void run() { for(int i = 1; i <= Count+1 ; i++) { // czekanie na pusty bufor bufferIsEmpty.jbWait2(); // produkcja if(i < Count+1) { int item = i; buffer.setBuffer(item); System.out.println("P: " + item); } else buffer.setBuffer(Last); bufferIsFull.jbNotify(); } System.out.println("Done!"); } } class Consumer implements Runnable { public void run() { while(true) { // czekanie na pełny bufor bufferIsFull.jbWait2(); // konsumpcja int item = buffer.getBuffer(); buffer.setEmpty(); if(item == 0) break; System.out.println("C: " + item); bufferIsEmpty.jbNotify(); } } } } Studium programowe Przytoczone tu programy rozwiązują następujący problem 1) Na pulpicie apletu pojawia się napis informacyjny: Press letters, then Enter. 2) Zezwala się na naciskanie tylko klawiszy literowych oraz klawisza Enter. 3) Naciśnięcie klawisza literowego wyświetla literę w miejscu napisu informacyjnego. 4) Naciśnięcie niedozwolonego klawisza jest ignorowane i generuje sygnał dźwiękowy. 5) Naciśnięcie klawisza Enter wyświetla informację o tym, ilokrotnie naciskano poszczególne klawisze literowe oraz podaje statystykę naciśnięć. W szczególności, jeśli naciśnięto klawisze B, A, B, A, Z, to w miejscu napisu informacyjnego wyświetli się napis pokazany na ekranie Podsumowanie. A: 2, B: 2, Z: 1, Total = 5 6) Po naciśnięciu klawisza Enter wszystkie naciśnięcia klawiszy są ignorowane. Ekran Podsumowanie ### sumup.gif Założenia projektowe 1) Każde naciśnięcie klawisza jest delegowane do tego samego obiektu. 2) Z każdym klawiszem literowym jest związany odrębny wątek. 3) Dodatkowy wątek jest związany z ogółem dozwolonych klawiszy. 3) Wszystkie wątki są tworzone w metodzie init. Po utworzeniu wątku klawisza, zatrzymuje się go do chwili utworzenia wątków wszystkich pozostałych klawiszy. 4) Wątek klawisza literowego czeka na pobudzenie z metody keyReleased. Po pobudzeniu zwiększa licznik naciśnięć swojego klawisza. 5) Wątek klawiszy dozwolonych czeka na pobudzenie z metody keyReleased. Po pobudzeniu zwiększa licznik naciśnięć klawiszy literowych, a dla klawisza Enter podaje statystykę naciśnięć. Rozwiązanie bez monitora =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet implements Runnable { protected final int Runners = 26, Letter_A = KeyEvent.VK_A; protected Runner[] runner = new Runner[Runners]; protected Object allMonitor = new Object(); protected Object[] oneMonitor = new Object[Runners]; protected Object runMonitor = new Object(); protected boolean isEnter = false, theLetter[] = new boolean [Runners], allTyping = false; protected int total = 0; protected String result = "Press letters, then Enter"; protected Toolkit kit = Toolkit.getDefaultToolkit(); public void init() { requestFocus(); synchronized(runMonitor) { for(int i = 0; i < Runners ; i++) { oneMonitor[i] = new Object(); runner[i] = new Runner(i); theLetter[i] = false; } new Thread(this).start(); } addKeyListener( new KeyAdapter() { public void keyReleased(KeyEvent evt) { if(isEnter) return; int code = evt.getKeyCode(); boolean isLetter = code >= Letter_A && code <= Letter_A + Runners-1; isEnter = code == KeyEvent.VK_ENTER; int id = code - Letter_A; if(isLetter) { synchronized(oneMonitor[id]) { result = "" + (char)(id + 'A'); repaint(); theLetter[id] = true; oneMonitor[id].notify(); } } synchronized(allMonitor) { if(isLetter || isEnter) { allTyping = true; allMonitor.notify(); } else kit.beep(); } } } ); } public void waitForEnter() { synchronized(allMonitor) { while(!allTyping) { try { allMonitor.wait(); if(isEnter) return; } catch(InterruptedException e) { } allTyping = false; total++; } } } public void run() { waitForEnter(); result = ""; for(int i = 0; i < Runners ; i++) { runner[i].interrupt(); try { runner[i].join(); } catch(InterruptedException e) { } int count = runner[i].getCount(); if(count != 0) { result = result + (char)('A'+i) + ": " + count + ", "; } } result = result + "Total = " + total; repaint(); } public synchronized void paint(Graphics gDC) { gDC.drawString(result, 10, 20); } class Runner extends Thread { protected int id; protected int count = 0; public Runner(int id) { this.id = id; start(); } public void run() { synchronized(runMonitor) { } while(true) { synchronized(oneMonitor[id]) { while(!theLetter[id]) { try { oneMonitor[id].wait(); } catch(InterruptedException e) { // w Symantec Cafe 1.8 // należy użyć (sic!) // następującej instrukcji //for(int i = 0; i < 0 ; ); return; } } theLetter[id] = false; count++; } } } public int getCount() { return count; } } } Rozwiązanie z monitorem Należy pamiętać o dołączeniu klasy monitorowej! =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet implements Runnable { protected final int Runners = 26, Letter_A = KeyEvent.VK_A; protected Runner[] runner = new Runner[Runners]; protected Monitor allMonitor = new Monitor(false); protected Monitor[] oneMonitor = new Monitor[Runners]; protected Monitor runMonitor = new Monitor(false); protected boolean isEnter = false; protected int total = 0; protected String result = "Press letters, then Enter"; protected Toolkit kit = Toolkit.getDefaultToolkit(); public void init() { synchronized(runMonitor) { for(int i = 0; i < Runners ; i++) { oneMonitor[i] = new Monitor(false); runner[i] = new Runner(i); } new Thread(this).start(); } addKeyListener( new KeyAdapter() { public void keyReleased(KeyEvent evt) { if(isEnter) return; int code = evt.getKeyCode(); boolean isLetter = code >= Letter_A && code <= Letter_A + Runners-1; isEnter = code == KeyEvent.VK_ENTER; int id = code - Letter_A; if(isLetter) oneMonitor[id].jbNotify(); if(isLetter || isEnter) allMonitor.jbNotify(); else kit.beep(); } } ); requestFocus(); } public void waitForEnter() { while(true) { allMonitor.jbWait2(); if(isEnter) return; total++; } } public void run() { waitForEnter(); result = ""; for(int i = 0; i < Runners ; i++) { runner[i].interrupt(); try { runner[i].join(); } catch(InterruptedException e) { } int count = runner[i].getCount(); if(count != 0) result = result + (char)('A'+i) + ": " + count + ", "; } result = result + "Total = " + total; repaint(); } public void paint(Graphics gDC) { gDC.drawString(result, 10, 20); } class Runner extends Thread { protected int id; protected int count = 0; public Runner(int id) { this.id = id; start(); } public void run() { runMonitor.jbPause(); while(true) { try { oneMonitor[id].jbWait(); } catch(InterruptedException e) { return; } count++; result = "" + (char)(id + 'A'); repaint(); } } public int getCount() { return count; } } } Rozwiązanie alternatywne Obiekty synchronizujące przeniesiono do wnętrza klasy Runner. Należy pamiętać o dołączeniu klasy monitorowej! =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet implements Runnable { protected final int Runners = 26, Letter_A = KeyEvent.VK_A; protected Runner[] runner = new Runner[Runners]; protected Monitor allMonitor = new Monitor(false), runMonitor = new Monitor(false); protected boolean isEnter = false; protected int total = 0; protected String result = "Press letters, then Enter"; protected Toolkit kit = Toolkit.getDefaultToolkit(); public void init() { synchronized(runMonitor) { for(int i = 0; i < Runners ; i++) runner[i] = new Runner(i); new Thread(this).start(); } addKeyListener( new KeyAdapter() { public void keyReleased(KeyEvent evt) { if(isEnter) return; int code = evt.getKeyCode(); boolean isLetter = code >= Letter_A && code <= Letter_A + Runners-1; isEnter = code == KeyEvent.VK_ENTER; int id = code - Letter_A; if(isLetter) runner[id].jbNotify(); if(isLetter || isEnter) allMonitor.jbNotify(); else kit.beep(); } } ); requestFocus(); } public void waitForEnter() { while(true) { allMonitor.jbWait2(); if(isEnter) return; total++; } } public void run() { waitForEnter(); result = ""; for(int i = 0; i < Runners ; i++) { runner[i].interrupt(); try { runner[i].join(); } catch(InterruptedException e) { } int count = runner[i].getCount(); if(count != 0) result = result + (char)('A'+i) + ": " + count + ", "; } result = result + "Total = " + total; repaint(); } public void paint(Graphics gDC) { gDC.drawString(result, 10, 20); } class Runner extends Thread { protected Monitor monitor; protected int id; protected int count = 0; public Runner(int id) { this.id = id; monitor = new Monitor(false); start(); } public void run() { runMonitor.jbPause(); while(true) { try { monitor.jbWait(); } catch(InterruptedException e) { return; } count++; result = "" + (char)(id + 'A'); repaint(); } } public void jbNotify() { monitor.jbNotify(); } public int getCount() { return count; } } } Jan Bielecki Przetwarzanie plików W każdym współczesnym systemie występują pliki i katalogi. Katalog jest logicznym pojemnikiem plików i katalogów, a plik jest pojemnikiem danych. Katalog, który nie jest zawarty w innym katalogu jest katalogiem głównym. Pliki dzielą się na sekwencyjne i wyrywkowe. Otwarcie pliku powoduje utworzenie strumienia. Strumień jest wewnątrz-programową reprezentacją pliku. Plik sekwencyjny składa się z sekwencji bajtów albo znaków, a plik wyrywkowy składa się z zestawu zmiennych typu predefiniowanego (m.in. int, Boolean, Double). Bajty i znaki mogą być przetwarzane tylko sekwencyjnie, natomiast zmienne mogą być przetwarzane sekwencyjnie i wyrywkowo, to jest niezależnie od położenia uprzednio przetworzonej zmiennej. Pliki sekwencyjne mogą być buforowane. Buforowanie polega na wykorzystaniu pośredniczącego obszaru pamięci operacyjnej do przyspieszenia sekwencyjnego przesyłania danych. Z utworzonego już strumienia można utworzyć strumień pochodny o dodatkowych właściwościach. W szczególności ze strumienia typu FileInputStream można utworzyć strumień typu BufferedInputStream, a z niego strumień DataInputStream. Wykonanie operacji zamknięcia strumienia powinno dotyczyć tylko ostatniego ze strumieni pochodnych. Uwaga: Ze względu na przenośność między systemami Unix i Windows, w nazwach plików zaleca się używać skośnika (/), a nie ukośnika (\). import java.io.*; class Some { public void anyFun() { String fileName = "C:/jbJava/Source"; // strumień plikowy FileInputStream inpF = null; try { inpF = new FileInputStream(fileName); } catch(FileNotFoundException e) { } // buforowany strumień plikowy BufferedInputStream inpB = new BufferedInputStream(inpF); // buforowany, plikowy strumień zmiennych DataInputStream inpD = new DataInputStream(inpB); // ... try { long var = inpD.readLong(); // ... inpD.close(); } catch(IOException e) { } } } W celu przetworzenia wszystkich elementów strumienia sekwencyjnego można stosować następujące schematy final int EOF = -1; int chr = inp.read(); while(chr != EOF) { // ... chr = inp.read(); } final int EOF = -1; int chr; while((chr = inp.read()) != EOF) { // ... } Uwaga: Jeśli podczas wykonywania metody read zostanie wysłany wyjątek klasy IOException, to nie wynika to z napotkania końca pliku, ale jest skutkiem wystąpienia błędu. Strumienie bajtowe Strumień bajtowy jest sekwencją bajtów. Następujący program kopiuje zawartość pliku źródłowego do docelowego. Użyty w nim strumień wejściowy zrealizowano jako buforowany. import java.io.*; public class Main { public static void main(String args[]) throws IOException { if(args.length != 2) { System.out.println("Wrong arguments"); System.in.read(); return; } String srcName = args[0], trgName = args[1]; FileInputStream inpF = null; FileOutputStream out = null; try { inpF = new FileInputStream(srcName); out = new FileOutputStream(trgName); } catch(FileNotFoundException e) { System.out.println( "File \"" + srcName + "\" not found" ); System.in.read(); return; } BufferedInputStream inp = new BufferedInputStream(inpF); final int EOF = -1; int chr = inp.read(); while(chr != EOF) { out.write(chr); chr = inp.read(); } out.close(); // zbyteczne } } Wyprowadzanie bajtów Wyprowadzanie bajtów odbywa się za pomocą metod klasy FileOutputStream. Do otwarcia wyjściowego strumienia bajtowego można stosować następujący schemat String fileName = "C:/jbJava/Target"; FileOutputStream out = new FileOutputStream(fileName); Na tak utworzonym obiekcie strumieniowym można wykonywać następujące operacje void write(int chr) throws IOException Wyprowadza do strumienia jeden bajt o podanej wartości. void write(byte[] buf) throws IOException Wyprowadza do strumienia wszystkie bajty podanej tablicy. void close() throws IOException Zamyka strumień i plik. Wprowadzanie bajtów Wprowadzanie bajtów odbywa się za pomocą metod klasy FileInputStream. Do otwarcia wejściowego strumienia bajtowego można stosować następujący schemat String fileName = "C:/jbJava/Source"; FileInputStream inp = new FileInputStream(fileName); Na tak utworzonym obiekcie strumieniowym można wykonywać następujące operacje int read() throws IOException Wprowadza ze strumienia jeden bajt i dostarcza jego wartość. Jeśli strumień znajduje się w pozycji tuż przed końcem pliku, to dostarcza -1. int read(byte[] buf) throws IOException Wprowadza ze strumienia sekwencję bajtów i umieszcza je w podanej tablicy. Dostarcza liczbę faktycznie wprowadzonych znaków (z pozycji przed końcem pliku podaje 0). long skip(long drop) Pomija podaną liczbę kolejnych bajtów. Dostarcza liczbę faktycznie pominiętych bajtów. void close() throws IOException Zamyka strumień i plik. Strumienie znakowe Strumień znakowy jest sekwencją znaków w określonym systemie kodowania (np. 8859-1, Cp1250, Cp1251). Znak wprowadzony ze strumienia jest poddawany niejawnemu przekształceniu na znak Unikodu. Podczas wyprowadzania znaku jest wykonywana konwersja odwrotna. Taki sposób postępowania umożliwia oszczędne reprezentowanie znaków w pliku (8 bitów) i wygodne reprezentowanie ich w programie (16 bitów). Uwaga: W Windows 95 dla ustawienia regionalnego Angielski (USA) domyślnym systemem kodowania jest 8859-1, a dla ustawień Polski i Rosyjski odpowiednio Cp1250 i Cp1251. Następujący program trzykrotnie kopiuje zawartość znakowego pliku źródłowego do docelowego: 1) bez zmian, 2) z zamianą dużych liter na małe, 3) z zamianą małych liter na duże. Każda kopia zaczyna się od nowego wiersza. import java.io.*; public class Main { public static void main(String args[]) throws IOException { try { copy3(args[0], args[1]); System.out.println("Done!"); } catch(Exception e) { System.out.println("Error"); e.printStackTrace(); } finally { System.in.read(); } } static void copy3(String src, String trg) throws Exception { FileWriter out = new FileWriter(trg); for(int i = 0; i < 3 ; i++) { FileReader inp = new FileReader(src); while(inp.ready()) { char chr = (char)inp.read(); switch(i) { case 1: chr = Character.toLowerCase(chr); break; case 2: chr = Character.toUpperCase(chr); } out.write(chr); } inp.close(); // niezbędne String sep = System.getProperty( "line.separator" ); out.write(sep); } out.close(); // zbyteczne } } Wyprowadzanie znaków Wyprowadzanie znaków odbywa się za pomocą metod klasy FileWriter. Do otwarcia strumienia wyjściowego można stosować następujący schemat String fileName = "C:/jbJava/Target.txt"; FileWriter out = new FileWriter(fileName); Na tak utworzonym obiekcie strumieniowym można wykonywać następujące operacje void write(int chr) throws IOException Wyprowadza do strumienia znak o podanym kodzie. void write(String str) Wyprowadza do strumienia podany łańcuch znaków. void write(char[] buf) Wyprowadza do strumienia znaki podanej tablicy. void close() throws IOException Zamyka strumień i plik. Wprowadzanie znaków Wyprowadzanie znaków odbywa się za pomocą metod klasy FileReader. Do otwarcia strumienia wejściowego można stosować następujący schemat String fileName = "C:/jbJava/Source.txt"; FileReader inp = new FileReader(fileName); Na tak utworzonym obiekcie strumieniowym można wykonywać następujące operacje int read() throws IOException Wprowadza ze strumienia jeden znak i dostarcza jego Unikod. Jeśli strumień znajduje się w pozycji tuż przed końcem pliku, to dostarcza -1. int read(char[] buf) Wprowadza ze strumienia znaki do podanej tablicy. Dostarcza liczbę wprowadzonych znaków. void close() throws IOException Zamyka strumień i plik. Wyprowadzanie wierszy Niekiedy jest wygodnie wyprowadzać dane do pliku wierszami. Do tego celu można użyć klasy BufferedWriter i metody newLine. Do otwarcia strumienia wyjściowego można stosować następujący schemat String fileName = "C:/jbJava/Target.txt"; BufferedWriter out = new BufferedWriter(fileName); Na tak utworzonym obiekcie strumieniowym można wykonywać operacje wyprowadzania wierszy. void newLine() throws IOException Wyprowadza zakończenie wiersza, w postaci dostosowanej do platformy. Następujący program tworzy plik C:/jbJava/Lang.txt. Jego kolejne wiersze powstają z elementów tablicy source. import java.io.*; public class Main { private static String fileName = "C:/jbJava/Lang.txt"; private static String source[] = { "C++ is cool", "Java is cooler", }; public static void main(String args[]) throws IOException { FileWriter outR = new FileWriter(fileName); BufferedWriter out = new BufferedWriter(outR); int len = source.length; for(int i = 0; i < len ; i++) { out.write(source[i]); out.newLine(); } out.close(); System.in.read(); } } Wprowadzanie wierszy Niekiedy jest wygodnie przetwarzać plik wierszami. Do tego celu można użyć klasy BufferedReader i metody readLine. Do otwarcia strumienia wejściowego można stosować następujący schemat String fileName = "C:/jbJava/Source.txt"; FileReader inpR = new FileReader(fileName); BufferedReader inp = new BufferedReader(inpR); Na tak utworzonym obiekcie strumieniowym można wykonywać operacje wprowadzania wierszy. String readLine() throws IOException Dostarcza kolejny wiersz, pozbawiony znaków '\r' i '\n'. Z pozycji tuż przed końcem pliku dostarcza null. Następujący program kopiuje na konsolę wszystkie znaki zawarte w pliku C:/config.sys. import java.io.*; public class Main { public static void main(String args[]) throws IOException { FileReader inpR = new FileReader("C:/autoexec.bat"); BufferedReader inp = new BufferedReader(inpR); String line; while((line = inp.readLine()) != null) { int len = line.length(); for(int i = 0; i < len ; i++) System.out.print(line.charAt(i)); System.out.println(); } System.in.read(); } } Wyprowadzanie leksemów Wyprowadzanie leksemów odbywa się za pomocą metod klasy PrintWriter. Do otwarcia strumienia wejściowego można stosować następujący schemat String fileName = "C:/jbJava/Target.txt"; FileWriter outF = new FileWriter(fileName); PrintWriter out = new PrintWriter(outF); Na tak utworzonym obiekcie strumieniowym można wykonywać następujące operacje boolean checkError() Wymiata strumień i dostarcza orzecznik o wartości „wystąpił błąd strumienia”. void print(boolean b) void print(char c) void print(int i) void print(long l) void print(float f) void print(double d) void print(String s) void print(Object o) void print(char a[]) Wyprowadza argument po przetworzeniu go na ciąg znaków. Uwaga: Zastąpienie identyfikatora print identyfikatorem println powoduje dodatkowo wyprowadzenie znaku końca wiersza. void println() Wyprowadza znak końca wiersza. void flush() Wymiata bufor wyjściowy. void close() Zamyka strumień (wykonywane domyślnie po zakończeniu wykonywania programu). Wykonanie następującego programu powoduje umieszczenie napisu c:\mouse.exe /q nc w pliku C:/jbJava/autoexec.bat. import java.io.*; public class Main { public static void main(String args[]) throws IOException { FileWriter outF = new FileWriter("c:/jbJava/autoexec.bat"); PrintWriter out = new PrintWriter(outF); out.println("c:\\mouse.exe /q"); out.println("nc"); out.close(); System.in.read(); } } Wprowadzanie leksemów Do wprowadzania z pliku leksemów: symboli, liczb i znaków służy klasa StreamTokenizer. Za pomocą jej konstruktora określa się strumień z którego mają być wprowadzone znaki, a za pomocą metody nextToken wprowadza kolejne leksemy. Do otwarcia strumienia wejściowego można stosować następujący schemat String fileName = "C:/jbJava/Target.txt"; FileReader inpR = new FileReader(fileName); StreamTokenizer inp = new StreamTokenizer(inpR); Na tak utworzonym obiekcie strumieniowym można wykonywać następujące operacje void quoteChar(int quote) Określa kod znaku, który ma być użyty jako obustronny ogranicznik łańcucha znaków zawierającego odstępy (domyślnie nie ma takiego ogranicznika). int nextToken() Jeśli wprowadzono słowo, to dostarcza symbol TT_WORD, a odnośnik do słowa umieszcza w sval. Jeśli wprowadzono liczbę, to dostarcza symbol TT_NUMBER, a liczbę umieszcza w nval. Jeśli wprowadzono łańcuch, to dostarcza kod ogranicznika łańcucha (por. quoteChar), a odnośnik do niego umieszcza w sval. W pozostałych przypadkach dostarcza znak. void eolIsSignificant(boolean itIs) Określa, że mają być rozpoznawane i dostarczane znaki końca wiersza (w przeciwnym razie znak końca wiersza jest traktowany tak jak inne odstępy). Uwaga: Symbole TT_EOF i TT_EOL oznaczają odpowiednio: koniec pliku i koniec wiersza. Biorąc powyższe pod uwagę, typowe rozpoznanie leksemów zawartych w pliku przybiera postać static final int EOF = StreamTokenizer.TT_EOF, WORD = StreamTokenizer.TT_WORD, NUMBER = StreamTokenizer.TT_NUMBER; // ... FileReader inpR = new FileReader(name); StreamTokenizer inp = new StreamTokenizer(inpR); int what; while((what = inp.nextToken()) != EOF) { switch(what) { case WORD: // użycie inp.sval case NUMBER: // użycie inp.nval default: // użycie what } } Następujący program analizuje plik C:/jbJava/Source.txt, wyprowadzając zawarte w nim leksemy oraz łańcuchy zawarte między parami znaków # (hash). Jeśli plik zawiera napis 12 Isa -4.2e2 #John -4.2e2 Mary# 127 Isa#bell#13 $%& to nastąpi wyprowadzenie napisu 12.0 Isa -4.2 e2 John -4.2e2 Mary 127.0 End of line #1 Isa bell 13.0 $ % & End of line #2 Na uwagę zasługuje sposób potraktowania „liczby” -4.2e2 (inaczej w łańcuchu, niż poza nim). import java.io.*; public class Main { static String fileName = "C:/kill77.txt"; static final int EOF = StreamTokenizer.TT_EOF, WORD = StreamTokenizer.TT_WORD, NUMBER = StreamTokenizer.TT_NUMBER, EOL = StreamTokenizer.TT_EOL; public static void main(String args[]) throws IOException { FileReader inpR; try { inpR = new FileReader(fileName); } catch(FileNotFoundException e) { System.out.println("File " + fileName + " not found"); System.in.read(); return; } StreamTokenizer inp; inp = new StreamTokenizer(inpR); int what; inp.eolIsSignificant(true); inp.quoteChar('#'); int lineNo = 1; while((what = inp.nextToken()) != EOF) { switch(what) { case EOL: System.out.print( "\nEnd of line #" + lineNo++ + '\n' ); break; case NUMBER: System.out.print(inp.nval + " "); break; case WORD: System.out.print(inp.sval + " "); break; case '#': System.out.print(inp.sval + " "); break; default: System.out.print((char)what + " "); } } System.out.println(); System.in.read(); } } Strumienie zmiennych Strumienie zmiennych służą do przenośnego wyprowadzania i wprowadzania zmiennych typów podstawowych (np. long, ale nie Long). Następujący program wyprowadza, a następnie wprowadza i porównuje z oryginałem, obiekt klasy Child, wraz z jego „przyległościami”. import java.io.*; public class Main { private static Child isa = new Child("Isabel", 15); private static String tmp = "C:/jbJava/Temp"; public static void main(String args[]) throws IOException { FileOutputStream outF = new FileOutputStream(tmp); DataOutputStream out = new DataOutputStream(outF); isa.write(out); out.close(); FileInputStream inpF = new FileInputStream(tmp); DataInputStream inp = new DataInputStream(inpF); Child isa2 = new Child(); isa2.read(inp); System.out.println(isa2.equals(isa)); System.in.read(); } } class Child { private String name; private int age; public Child() { name = ""; age = 0; } public Child(String name, int age) { this.name = name; this.age = age; } public boolean equals(Child child) { return name.equals(child.name) && age == child.age; } public void write(DataOutputStream out) throws IOException { out.writeInt(name.length()); out.writeChars(name); out.writeInt(age); } public void read(DataInputStream inp) throws IOException { StringBuffer name = new StringBuffer(); int len = inp.readInt(); for(int i = 0; i < len ; i++) name.append(inp.readChar()); this.name = new String(name); age = inp.readInt(); } } Wyprowadzanie zmiennych Wyprowadzanie zmiennych odbywa się za pomocą metod klasy DataOutputStream. Do otwarcia strumienia wyjściowego można stosować następujący schemat String fileName = "C:/jbJava/Target"; FileOutputStream outF = new FileOutputStream(fileName); DataOutputStream out = new DataOutputStream(outF); Na tak utworzonym obiekcie strumieniowym można wykonywać następujące operacje void write(int b) Wyprowadza bajt o podanej wartości. void write(byte[] buf) Wyprowadza wszystkie bajty podanej tablicy. void writeType(type var) np. void writeInt(int var) Wyprowadza reprezentację podanej zmiennej typu Type: Boolean, Byte, String, Char, Double, Float, Int, Long, Short. void writeChars(String str) Wyprowadza reprezentację wszystkich znaków podanej zmiennej. Wprowadzanie zmiennych Wprowadzanie zmiennych odbywa się za pomocą metod klasy DataInputStream. Do otwarcia strumienia wejściowego można stosować następujący schemat String fileName = "C:/jbJava/Source"; FileInputStream inpF = new FileInputStream(fileName); DataInputStream inp = new DataInputStream(inpF); Na tak utworzonym obiekcie strumieniowym można wykonywać następujące operacje int read(byte[] buf) Umieszcza w podanej tablicy reprezentacje kolejnych bajtów i dostarcza ich liczbę. type readType() np. int readInt() Dostarcza reprezentację zmiennej typu Type: Boolean, Byte, String, Char, Double, Float, Int, Long, Short. int skipBytes(int drop) Pomija podaną liczbę bajtów. Dostarcza faktycznie pominiętą liczbę bajtów. Pliki wyrywkowe Plik wyrywkowy składa się z zestawu zmiennych typów podstawowych. Wyprowadzanie i wprowadzanie zmiennych odbywa się za pomocą metod klasy RandomAccessFile. Do pliku wyrywkowego można stosować następujący schemat String fileName = "C:/jbJava/DataBase", mode = "rw"; // albo mode = "r" RandomAccessFile rio = new RandomAccessFile(fileName, mode); Na tak utworzonym obiekcie można wykonywać identyczne operacje jak na obiektach klas DataOutputStream i DataInputStream. Ponadto można posługiwać się następującymi metodami zapewniającymi dostęp wyrywkowy long getFilePointer() throws IOException Dostarcza pozycję pliku (pozycją początkową jest 0). long length() throws IOException Dostarcza liczbę bajtów pliku (także pozycję końcową). void seek(long pos) throws IOException Ustawia plik w podanej pozycji. Następujący program posługuje się "tablicą" obiektów typu double utworzoną w pliku o podanej nazwie. import java.io.*; public class Main { private static String fileName = "C:/jbJava/Array"; private static final int Last = 10; public static void main(String args[]) throws IOException { Array array = new Array(fileName, 100); for(int i = 0; i < Last ; i++) array.write(i * i, i); for(int i = Last-1 ; i >= 0 ; i--) System.out.println(array.read(i)); System.in.read(); } } class Array { private RandomAccessFile file; public Array(String name, int size) throws IOException { file = new RandomAccessFile(name, "rw"); for(int i = 0; i < size ; i++) file.writeDouble(0); file.seek(0); } public void write(double num, long pos) throws IOException { file.seek(8 * pos); file.writeDouble(num); } public double read(long pos) throws IOException { file.seek(8 * pos); return file.readDouble(); } } Klasa plikowa Równie często jak przesyłanie danych, występuje potrzeba wykonania operacji na pliku albo katalogu. Do tego celu doskonale nadaje się klasa plikowa File. Metody klasy File umożliwiają realizowanie zapytań o właściwości plików i katalogów. Jeśli program jest aplikacją, to metoda delete umożliwia usuwanie plików i katalogów. Do utworzenia obiektu klasy File można stosować następujący schemat String fileName = "C:/jbJava/ReadMe.txt"; File file = new File(fileName); Na tak utworzonym obiekcie można wykonywać następujące operacje boolean exists() Orzeka, czy istnieje plik albo katalog identyfikowany przez obiekt plikowy. public boolean isFile() Orzeka, czy obiekt plikowy identyfikuje plik. boolean isDirectory() Orzeka, czy obiekt plikowy identyfikuje katalog. boolean canRead() Orzeka, czy plik albo katalog może być odczytany. boolean canWrite() Orzeka, czy do pliku albo katalogu można dokonać zapisu. boolean mkDir() Tworzy katalog (wraz z nadkatalogami) o nazwie określonej przez obiekt plikowy. boolean renameTo(File trg) Zmienia nazwę pliku albo katalogu na podaną. public boolean delete() Usuwa plik. Następujący program wyprowadza zawartość pliku albo katalogu. Jeśli zostanie wywołany z argumentami określającymi ścieżkę (np. C:/jbJava) i nazwę katalogu (np. Lake), to poda nazwy wszystkich plików tego katalogu, a jeśli zostanie wywołany ze ścieżką (np. C:/jbJava/Lake) i nazwą pliku (np. Master.java) to poda zawartość tego pliku. import java.io.*; public class Main { public static void main(String args[]) throws IOException { String path, name; if(args.length != 2) { System.out.println("Please supply Path & Name"); return; } path = args[0]; name = args[1]; String pathName = path + "/" + name; File file = new File(pathName); if(!file.exists()) throw new FileNotFoundException(pathName); if(file.isFile()) { if(!file.canRead()) { System.out.println( "File " + pathName + " is not readable" ); return; } else { FileInputStream source = new FileInputStream(pathName); byte buffer[] = new byte[1024]; while(true) { int byteCount = source.read(buffer); if(byteCount == -1) break; System.out.write(buffer, 0, byteCount); } } } else { System.out.println( "Directory " + pathName + " contains\n"); String list[] = file.list(); for(int i = 0; i < list.length ; i++) { String fileName = list[i]; System.out.print(fileName); File fullName = new File(pathName + "/" + fileName); if(fullName.isDirectory()) System.out.print("\t(directory)"); System.out.println(); } } System.in.read(); } } Jan Bielecki Komponenty JavaBeans Komponentem JavaBeans jest egzemplarz klasy zgodnej ze specyfikacją JavaBeans. Opisem komponentu jest definicja klasy. Każdy komponent cechuje ustalony zestaw właściwości, metod i zdarzeń. Właściwości są opisane przez pola klasy, metody umożliwiają przetwarzanie właściwości, a zdarzenia są wysyłane po wykonaniu operacji na komponencie. Właściwości i metody Właściwości komponentu są implementowane przez metody klasy. W celu umożliwienia dostępu do właściwości swobodnej X, klasę komponentu wyposaża się w metody getX i setX, a w przypadku właściwości typu orzecznikowego, w metodę isX, na przykład getColor, setColor, isNegative, itp. Następująca klasa jest opisem komponentu ColorBean, wyposażonego we właściwość color. public class ColorBean extends Component { private Color color; public ColorBean(Color color) { this.color = color; } public Color getColor() { return color; } public void setColor(Color color) { this.color = color; } } Właściwości skalarne i indeksowane Z każdą właściwością skalarną Prop typu Type jest związany zestaw metod public void setProp(Type val) ustawienie public Type getProp() pobranie a z każdą właściwością indeksowaną są związane dwa zestawy metod dla tablicy public void setProp(Type[] val) ustawienie public Type[] getProp() pobranie dla elementu public void setProp(int index, Type val) public Type getProp(int index) Uwaga: Jeśli właściwość skalarna jest typu boolean, to metodę getProp można zastąpić metodą isProp. Następująca klasa opisuje komponent z właściwością indeksowaną Item, opartą na komponencie java.awt.List. Zdefiniowanie metody getItem(int) jest zbyteczne, ponieważ występuje ona w klasie bazowej. public class ListBean extends List { public String[] getItem() { return getItems(); } public synchronized void setItem(String[] item) { for(int i=0; i < item.length ; i++) add(item[i]); } public void setItem(int index, String item) { replaceItem(item, index); } } Właściwości swobodne Właściwość komponentu jest swobodna, jeśli jej zmiana nie ma bezpośredniego wpływu na inne obiekty programu. Zmiana właściwości swobodnej musi być rozpoznana jawnie (przez wywoływanie metody). Następujący aplet, pokazany na ekranie Właściwości swobodne, posługuje się komponentem ColorSelector, wyposażonym we właściwość Color implementowaną przez pola red, green i blue. Ponieważ właściwość jest swobodna, aktualizowanie sterownika display odbywa się za pomocą odrębnego wątku, który co 100 ms bada stan komponentu klasy ColorSelector. Ekran Właściwości swobodne ### free.gif plik Index.html plik Master.java import java.applet.Applet; import java.awt.*; public class Master extends Applet implements Runnable { private ColorSelector rgb; private Display display = new Display(); private Thread update; private boolean stopRun; public void init() { display.setBackground(Color.red); display.setSize(50, 50); rgb = new ColorSelector(Color.red); add(rgb); add(display); } class Display extends Canvas { public void paint(Graphics gDC) { Dimension size = getSize(); int w = size.height, h = size.width; gDC.drawRect(0, 0, w-1, h-1); } } public void start() { stopRun = false; update = new Thread(this); update.start(); } public void stop() { stopRun = true; try { update.join(); } catch(InterruptedException e) { } } public void run() { while(!stopRun) { try { Thread.sleep(100); } catch(InterruptedException e) { } Color color = rgb.getColor(); display.setBackground(color); } } } plik ColorSelector.java import java.awt.*; public class ColorSelector extends Panel { private Box red, green, blue; public ColorSelector() { this(Color.red); } public ColorSelector(Color color) { setLayout(new GridLayout(3, 1)); red = new Box("Red"); green = new Box("Green"); blue = new Box("Blue"); setColor(color); add(red); add(green); add(blue); } class Box extends Checkbox { public Box(String caption) { super(caption, false); } public Dimension getPreferredSize() { return new Dimension(80, 30); } } public Color getColor() { int r = red.getState() ? 255 : 0, g = green.getState() ? 255 : 0, b = blue.getState() ? 255 : 0; return new Color(r, g, b); } public void setColor(Color color) { red.setState(color.getRed() != 0); green.setState(color.getGreen() != 0); blue.setState(color.getBlue() != 0); } } Właściwości związane W odróżnieniu od właściwości swobodnych, zmiana właściwości związanej umożliwia niejawne wywołanie metody propertyChange obiektów klas nasłuchujących implementujących interfejs PropertyChangeListener. W celu umożliwienia rejestracji należy wyposażyć klasę komponentu w dodatkowe pole klasy PropertyChangeSupport, zainicjowane odnośnikiem do obiektu klasy zawierającej metody addPropertyChangeListener i removePropertyChangeListener. class ColorSelector extends Component { // ... PropertyChangeSupport changes = new PropertyChangeSupport(this); // ... public void addPropertyChangeListener( PropertyChangeListener lst ) { changes.addPropertyChangeListener(lst); } public void removePropertyChangeListener( PropertyChangeListener lst ) { changes.removePropertyChangeListener(lst); } } Zarejestrowanie obiektu klasy nasłuchującej powoduje, że każde wykonanie metody firePropertyChange na rzecz obiektu changes, na przykład changes.firePropertyChange( "Color", oldColor, newColor ) powoduje wywołanie metody void propertyChange(PropertyChangeEvent evt) w każdym z obiektów nasłuchujących, zarejestrowanych za pomocą metody addPropertyChangeListener. Uwaga: Jeśli nowa wartość właściwości nie różni się od starej, to metoda propertyChange nie zostanie wywołana. Następujący aplet, pokazany na ekranie Właściwości związane, posługuje się komponentem klasy ColorSelector z właściwością związaną implementowaną przez pole color (komponentu użyto do automatycznego aktualizowania koloru sterowników display1 i display2). Ekran Właściwości związane ### bound.gif plik Index.html plik Master.java import java.applet.Applet; import java.awt.*; import java.awt.event.*; import java.beans.*; public class Master extends Applet { private ColorSelector rgb; private Display display1, display2; public void init() { rgb = new ColorSelector(); add(rgb); display1 = new Display(rgb); display2 = new Display(rgb); display1.setSize(50, 50); display2.setSize(50, 50); add(display1); add(display2); rgb.addPropertyChangeListener(display1); rgb.addPropertyChangeListener(display2); } class Display extends Canvas implements PropertyChangeListener { public Display(ColorSelector rgb) { setBackground(rgb.getColor()); } public void propertyChange(PropertyChangeEvent evt) { String propertyName; propertyName = evt.getPropertyName(); if(!propertyName.equals("Color")) return; else { Color color = rgb.getColor(); setBackground(color); } } public void paint(Graphics gDC) { Dimension size = getSize(); int w = size.height, h = size.width; gDC.drawRect(0, 0, w-1, h-1); } } } plik ColorSelector.java import java.awt.*; import java.awt.event.*; import java.beans.*; public class ColorSelector extends Panel implements ItemListener { private Box red, green, blue; private Color color; private PropertyChangeSupport changes; public ColorSelector() { this(Color.magenta); } public ColorSelector(Color color) { changes = new PropertyChangeSupport(this); Panel panel = new Panel(); GridBagLayout gbl = new GridBagLayout(); panel.setLayout(gbl); red = new Box("Red"); green = new Box("Green"); blue = new Box("Blue"); GridBagConstraints gbc = new GridBagConstraints(); gbc.insets = new Insets(0, 10, 0, 0); gbc.gridx = 0; gbc.gridy = 0; gbl.setConstraints(red, gbc); gbc.gridy = 1; gbl.setConstraints(green, gbc); gbc.gridy = 2; gbl.setConstraints(blue, gbc); panel.add(red); panel.add(green); panel.add(blue); panel.setBackground(Color.green); add(panel); this.color = color; setColor(color); red.addItemListener(this); green.addItemListener(this); blue.addItemListener(this); } public void itemStateChanged(ItemEvent e) { Object source = e.getItem() ; int r = red.getState() ? 255 : 0, g = green.getState() ? 255 : 0, b = blue.getState() ? 255 : 0 ; setColor(new Color(r, g, b)); } class Box extends Checkbox { public Box(String caption) { super(caption, false); } public Dimension getPreferredSize() { return new Dimension(60, 30); } } public Color getColor() { return color; } public void setColor(Color color) { red.setState(color.getRed() != 0); green.setState(color.getGreen() != 0); blue.setState(color.getBlue() != 0); Color oldColor = this.color, newColor = color; this.color = newColor; // wywołanie metod propertyChange // obiektów nasłuchujących // zarejestrowanych // za pośrednictwem odnośnika changes changes.firePropertyChange( "Color", oldColor, newColor ); } public void addPropertyChangeListener( PropertyChangeListener lst ) { changes.addPropertyChangeListener(lst); } public void removePropertyChangeListener( PropertyChangeListener lst ) { changes.removePropertyChangeListener(lst); } public void paint(Graphics gDC) { Dimension size = getSize(); int w = size.width, h = size.height; gDC.setColor(Color.red); for(int i = 0; i < 5; i++) { int t = 1+2*i; gDC.drawRoundRect(i, i, w-t, h-t, 20, 20); } } } Właściwości nadzorowane Właściwości związane mogą być nadzorowane. Nadzór polega na sprawdzeniu przez obiekt nasłuchujący, czy oczekiwana zmiana właściwości jest dopuszczalna. Jeśli nie jest dopuszczalna, to obiekt nasłuchujący wysyła wyjątek klasy PropertyVetoException, co powinno uniemożliwić wywołanie metody firePropertyChange. Podobnie jak nasłuch, tak i nadzór właściwości musi być zarejestrowany. W celu umożliwienia rejestracji należy wyposażyć klasę komponentu w pole klasy VetoableChangeSupport, zainicjowane odnośnikiem do obiektu klasy zawierającej metody addVetoableChangeListener i removeVetoableChangeListener, na przykład class ColorSelector extends Component { // ... VetoableChangeSupport vetos = new VetoableChangeSupport(this); // ... public void addVetoableChangeListener( VetoableChangeListener lst ) { vetos.addVetoableChangeListener(lst); } public void removeVetoableChangeListener( VetoableChangeListener lst ) { vetos.removeVetoableChangeListener(lst); } } Zarejestrowanie obiektu klasy nadzorującej ma taki skutek, że każde wywołanie metody fireVetoableChange, na przykład changes.fireVetoableChange( "Color", oldColor, newColor ) powoduje wywołanie metody void vetoableChange(PropertyChangeEvent evt) throws PropertyVetoException w każdym z obiektów zarejestrowanych za pomocą metody addVetoableChangeListener. Metodę vetoableChange wywołuje się na rzecz kolejnych obiektów nadzorujących, aż do obsłużenia wszystkich tych obiektów albo do chwili, gdy jeden z nich wyśle wyjątek klasy PropertyVetoException. Jeśli taka sytuacja wystąpi, to metodę wywołuje się ponownie, tym razem dla wszystkich obiektów zarejestrowanych. W tym drugim przebiegu wywołanie metody getOldValue dostarcza wartość jaką w pierwszym przebiegu dostarczała metoda getNewValue (i odwrotnie). Zazwyczaj nadzór i nasłuch występują łącznie. W takim wypadku sekwencja wykonanych czynności przybiera na przykład postać try { // wywołanie metod vetoableChange // obiektów nadzorujących vetos.fireVetoableChange( "Color", oldColor, newColor ); } catch(PropertyVetoException e) { // jeśli zawetowano return; } // zmiana właściwości związanej color = newColor; // wywołanie metod propertyChange // obiektów nasłuchujących changes.firePropertyChange( "Color", oldColor, newColor ); Następujący aplet, pokazany na ekranie Właściwości nadzorowane, posługuje się komponentem klasy ColorSelector z właściwością związaną implementowaną przez pole color (komponentu użyto do automatycznego aktualizowania koloru sterowników display1 i display2). Ekran Właściwości nadzorowane ### constr.gif Uwaga: Nadzorowanie polega na tym, że przycisk vetoer1 nie dopuszcza do zmiany koloru sterowników na biały, a przycisk vetoer2 nie dopuszcza do zmiany ich koloru na czarny. Próba dokonania zabronionej zmiany właściwości powoduje chwilową zmianę koloru przycisku nadzorującego na czerwony. Na ekranie Zmiana zabroniona pokazano wygląd apletu podczas próby niedopuszczalnej zmiany koloru. Ekran Zmiana zabroniona ### veto.gif plik Index.html plik Master.java import java.applet.Applet; import java.awt.*; import java.awt.event.*; import java.beans.*; public class Master extends Applet { private ColorSelector rgb; private Display display1, display2; private VetoingButton vetoer1, vetoer2; class VetoingButton extends Button implements VetoableChangeListener { public VetoingButton(String caption) { super(caption); } public void vetoableChange(PropertyChangeEvent evt) throws PropertyVetoException { Object newValue = evt.getNewValue(), oldValue = evt.getOldValue(); boolean scan2 = rgb.getColor().equals(newValue); if(scan2) return; String propertyName; propertyName = evt.getPropertyName(); if(!propertyName.equals("Color")) return; if(getLabel().equals("1") && newValue.equals(Color.black) || getLabel().equals("2") && newValue.equals(Color.white)) { setBackground(Color.red); try { Thread.sleep(300); } catch(InterruptedException e) { } setBackground(Color.lightGray); throw new PropertyVetoException( "Prohibited change", evt ); } } public Dimension getPreferredSize() { return new Dimension(60, 30); } } public void init() { rgb = new ColorSelector(); add(rgb); display1 = new Display(rgb); display2 = new Display(rgb); display1.setSize(50, 50); display2.setSize(50, 50); add(display1); add(display2); vetoer1 = new VetoingButton("1"); vetoer2 = new VetoingButton("2"); add(vetoer1); add(vetoer2); rgb.addPropertyChangeListener(display1); rgb.addPropertyChangeListener(display2); rgb.addVetoableChangeListener(vetoer1); rgb.addVetoableChangeListener(vetoer2); } class Display extends Canvas implements PropertyChangeListener { public Display(ColorSelector rgb) { setBackground(rgb.getColor()); } public void propertyChange(PropertyChangeEvent evt) { String propertyName; propertyName = evt.getPropertyName(); if(!propertyName.equals("Color")) return; else { Color color = rgb.getColor(); setBackground(color); } } public void paint(Graphics gDC) { Dimension size = getSize(); int w = size.height, h = size.width; gDC.drawRect(0, 0, w-1, h-1); } } } plik ColorSelector.java import java.awt.*; import java.awt.event.*; import java.beans.*; public class ColorSelector extends Panel implements ItemListener{ private Box red, green, blue; private Color color; private PropertyChangeSupport changes; private VetoableChangeSupport vetos; public ColorSelector() { this(Color.magenta); } public ColorSelector(Color color) { changes = new PropertyChangeSupport(this); vetos = new VetoableChangeSupport(this); Panel panel = new Panel(); GridBagLayout gbl = new GridBagLayout(); panel.setLayout(gbl); red = new Box("Red"); green = new Box("Green"); blue = new Box("Blue"); GridBagConstraints gbc = new GridBagConstraints(); gbc.insets = new Insets(0, 10, 0, 0); gbc.gridx = 0; gbc.gridy = 0; gbl.setConstraints(red, gbc); gbc.gridy = 1; gbl.setConstraints(green, gbc); gbc.gridy = 2; gbl.setConstraints(blue, gbc); panel.add(red); panel.add(green); panel.add(blue); panel.setBackground(Color.green); add(panel); setColor(color); red.addItemListener(this); green.addItemListener(this); blue.addItemListener(this); } public void itemStateChanged(ItemEvent e) { Object source = e.getSource() ; int r = red.getState() ? 255 : 0, g = green.getState() ? 255 : 0, b = blue.getState() ? 255 : 0; setColor(new Color(r, g, b)); } class Box extends Checkbox { public Box(String caption) { super(caption, false); } public Dimension getPreferredSize() { return new Dimension(60, 30); } } public Color getColor() { return color; } public void setColor(Color color) { red.setState(color.getRed() != 0); green.setState(color.getGreen() != 0); blue.setState(color.getBlue() != 0); Color oldColor = this.color, newColor = color; try { // wywołanie metod vetoableChange // obiektów nadzorujących vetos.fireVetoableChange( "Color", oldColor, newColor ); } catch(PropertyVetoException e) { // przywrócenie wyglądu komponentu restore(oldColor); return; } // zmiana właściwości związanej this.color = newColor; // wywołanie metod propertyChange // obiektów nasłuchujących changes.firePropertyChange( "Color", oldColor, newColor ); } void restore(Color color) { red.setState(color.getRed() != 0); green.setState(color.getGreen() != 0); blue.setState(color.getBlue() != 0); } public void addPropertyChangeListener( PropertyChangeListener lst ) { changes.addPropertyChangeListener(lst); } public void removePropertyChangeListener( PropertyChangeListener lst ) { changes.removePropertyChangeListener(lst); } public void addVetoableChangeListener( VetoableChangeListener lst ) { vetos.addVetoableChangeListener(lst); } public void removeVetoableChangeListener( VetoableChangeListener lst ) { vetos.removeVetoableChangeListener(lst); } public void paint(Graphics gDC) { Dimension size = getSize(); int w = size.width, h = size.height; gDC.setColor(Color.red); for(int i = 0; i < 5; i++) { int t = 1+2*i; gDC.drawRoundRect(i, i, w-t, h-t, 20, 20); } } } Pakowanie komponentu Przetestowany komponent można umieścić w pliku JAR oraz określić miejsce, w którym plik ten zostanie wygenerowany. W opisie apletu posługującego się takim komponentem należy umieścić frazę archive określającą nazwę pliku definiującego komponent. Uwaga: Wygodnym miejscem do umieszczenia pliku JAR jest katalog, w którym znajduje się plik zawierający opis apletu. Następujący aplet ilustruje użycie komponentu ColorSelector umieszczonego w pliku Selector.jar. ============================= import java.applet.Applet; import java.awt.*; import java.awt.event.*; import java.beans.*; import jbBeans.selector.ColorSelector; public class Master extends Applet { private ColorSelector rgb; private Display display1, display2; private VetoingButton vetoer1, vetoer2; class VetoingButton extends Button implements VetoableChangeListener { public VetoingButton(String caption) { super(caption); } public void vetoableChange(PropertyChangeEvent evt) throws PropertyVetoException { Object newValue = evt.getNewValue(), oldValue = evt.getOldValue(); boolean scan2 = rgb.getColor().equals(newValue); if(scan2) return; String propertyName; propertyName = evt.getPropertyName(); if(!propertyName.equals("Color")) return; if(getLabel().equals("1") && newValue.equals(Color.black) || getLabel().equals("2") && newValue.equals(Color.white)) { setBackground(Color.red); try { Thread.sleep(300); } catch(InterruptedException e) { } setBackground(Color.lightGray); throw new PropertyVetoException( "Prohibited change", evt ); } } public Dimension getPreferredSize() { return new Dimension(60, 30); } } public void init() { rgb = new ColorSelector(); add(rgb); display1 = new Display(rgb); display2 = new Display(rgb); display1.setSize(50, 50); display2.setSize(50, 50); add(display1); add(display2); vetoer1 = new VetoingButton("1"); vetoer2 = new VetoingButton("2"); add(vetoer1); add(vetoer2); rgb.addPropertyChangeListener(display1); rgb.addPropertyChangeListener(display2); rgb.addVetoableChangeListener(vetoer1); rgb.addVetoableChangeListener(vetoer2); } class Display extends Canvas implements PropertyChangeListener { public Display(ColorSelector rgb) { setBackground(rgb.getColor()); } public void propertyChange(PropertyChangeEvent evt) { String propertyName; propertyName = evt.getPropertyName(); if(!propertyName.equals("Color")) return; else { Color color = rgb.getColor(); setBackground(color); } } public void paint(Graphics gDC) { Dimension size = getSize(); int w = size.height, h = size.width; gDC.drawRect(0, 0, w-1, h-1); } } } Jan Bielecki Programowanie wizualne Programowanie wizualne polega na tworzeniu programu wykonalnego metodą kliknij-i-przeciągnij, to jest z wyeliminowaniem kodowania instrukcji źródłowych. Na obecnym etapie rozwoju, programowanie wizualne musi być wspomagane przez tradycyjne programowanie manualne. W środowisku Visual Cafe projektowanie oblicza programu oraz większości oddziaływań można całkowicie wykonać metodą wizualną. Bardziej złożone oddziaływania muszą być zakodowane ręcznie. Pomocne w tym jest przełożenie operacji wizualnych na automatycznie generowany kod źródłowy. Na ekranie Operacje na danych pokazano aplet utworzony metodą wizualną. Działa on w taki sposób, że 1. Wprowadzenie tekstu do klatki i naciśnięcie klawisza Enter powoduje skopiowanie zawartości klatki do listy, bez naruszenia zawartości klatki. 2. Naciśnięcie przycisku powoduje przeniesienie zawartości klatki do listy, wyczyszczenie klatki i nastawienie celownika na klatkę. 3. Dwukliknięcie elementu listy powoduje usunięcie go z listy, przeniesienie do klatki i nastawienie celownika na klatkę. 4. Wykonanie operacji na pustej klatce jest odrzucane i generuje sygnał dźwiękowy. Ekran Operacje na danych ### dataopr.gif Utworzenie apletu W celu utworzenia apletu należy wydać polecenie File / New Project // Basic Applet. Spowoduje to wygenerowanie szkieletu apletu. W osobnym oknie zostanie wyświetlony formularz do projektowania oblicza. Po wykonaniu p-kliknięcia i wybraniu polecenia Properties można zmienić nazwę apletu i formularza, na przykład na Master. import java.applet.Applet; import java.awt.*; public class Master extends Applet { public void init() { // Take out this line if you don't use // symantec.itools.net.RelativeURL or // symantec.itools.awt.util.StatusScroller // symantec.itools.lang.Context.setApplet(this); // This code is automatically generated // by Visual Cafe //{{INIT_CONTROLS setLayout(null); setSize(426,266); //}} } //{{DECLARE_CONTROLS //}} } Utworzenie oblicza Nanoszenie komponentów na formularz odbywa się całkowicie metodą wizualną. Polega ono na kliknięciu ikony sterownika widocznej na pasku narzędziowym, a następnie przeciągnięciu kursora myszki po formularzu. Do rozmieszczenia sterowników można użyć poleceń menu Layout, a do określenia ich nazw i właściwości użyć okienka Property List, wyświetlanego po p-kliknięciu sterownika i wybraniu polecenia Properties. Po wykonaniu tych czynności, formularzowi o postaci docelowej odpowiada następujący program źrodłowy. import java.applet.Applet; import java.awt.*; public class Master extends Applet { public void init() { //{{INIT_CONTROLS setLayout(null); setSize(311,277); list = new java.awt.List(0); add(list); list.setBounds(33,45,106,126); data = new java.awt.TextField(); data.setBounds(33,222,106,35); add(data); move = new java.awt.Button(); move.setLabel("Move"); move.setBounds(189,136,106,35); move.setBackground(new Color(12632256)); add(move); listLabel = new java.awt.Label("List"); listLabel.setBounds(12,21,65,22); add(listLabel); dataLabel = new java.awt.Label("Data"); dataLabel.setBounds(12,198,65,22); add(dataLabel); //}} } //{{DECLARE_CONTROLS java.awt.List list; java.awt.TextField data; java.awt.Button move; java.awt.Label listLabel; java.awt.Label dataLabel; //}} } Kodowanie oddziaływań Większość oddziaływań, jak na przykład "po kliknięciu przycisku skopiuj zawartość klatki na koniec listy", można kodować wizualnie. Pozostałe, jak na przykład "wydaj sygnał dźwiękowy" należy zakodować ręcznie. Następujący opis wyszczególnia czynności jakie należy wykonać, aby zrealizować wymagane funkcje apletu. Przed każdą z nich naciśnięto ikonę oddziaływań i przeciągnięto kursor ze źródła do celu oddziaływania (np. z klatki do listy). Uwaga: Jeśli akcja dotyczy tylko źródła zdarzenia, to po p-kliknięciu źródła należy wydać polecenie Add interaction. 1. Wprowadzenie tekstu do klatki i naciśnięcie klawisza Enter powoduje skopiowanie zawartości klatki do listy, bez naruszenia zawartości klatki. Oddziaływanie klatka - lista a. Start an interaction for "data" enterHit b. Select the item you want to interact with list c. Choose what you want to happen add a string to the List ... d. Dalej e. Add a string to the List using information from ... Another item data By... Get the contents of the TextField Spowoduje to wygenerowanie kodu import java.applet.Applet; import java.awt.*; public class Master extends Applet { public void init() { //{{INIT_CONTROLS setLayout(null); setSize(311,277); list = new java.awt.List(0); add(list); list.setBounds(33,45,106,126); data = new java.awt.TextField(); data.setBounds(33,222,106,35); add(data); move = new java.awt.Button(); move.setLabel("Move"); move.setBounds(189,136,106,35); move.setBackground(new Color(12632256)); add(move); listLabel = new java.awt.Label("List"); listLabel.setBounds(12,21,65,22); add(listLabel); dataLabel = new java.awt.Label("Data"); dataLabel.setBounds(12,198,65,22); add(dataLabel); //}} //{{REGISTER_LISTENERS SymAction lSymAction = new SymAction(); data.addActionListener(lSymAction); //}} } //{{DECLARE_CONTROLS java.awt.List list; java.awt.TextField data; java.awt.Button move; java.awt.Label listLabel; java.awt.Label dataLabel; //}} class SymAction implements java.awt.event.ActionListener { public void actionPerformed(java.awt.event.ActionEvent event) { Object object = event.getSource(); if (object == data) data_EnterHit(event); } } void data_EnterHit(java.awt.event.ActionEvent event) { // to do: code goes here. //{{CONNECTION // Add a string to the List ... // Get the contents of the TextField list.add(data.getText()); //}} } } 2. Naciśnięcie przycisku powoduje przeniesienie zawartości klatki do listy, wyczyszczenie klatki i nastawienie celownika na klatkę. Oddziaływanie przycisk - lista a. Start an interaction for "move". actionPerformed b. Select the item you want to interact with ... list c. Choose what you want to happen ... Add a string to the List ... d. Dalej e. Add a string to the list using information from ... data By ... Get the contents of the TextField Oddziaływanie przycisk - klatka a. Start an interaction for "move". actionPerformed b. Select the item you want to interact with ... data c. Choose what you want to happen ... Clear the text for TextField Oddziaływanie przycisk - klatka a. Start an interaction for "move". actionPerformed b. Select the item you want to interact with ... data c. Choose what you want to happen ... Request the focus 3. Dwukliknięcie elementu listy powoduje usunięcie go z listy, przeniesienie do klatki i nastawienie celownika na klatkę. Oddziaływanie lista - lista a. Start an interaction for "list". dblClicked b. Select the item you want to interact with ... list c. Choose what you want to happen ... Delete an item from the List ... d. Dalej e. Delete an item from the List using information from ... Another item list By ... Get the current item index Oddziaływanie lista - klatka a. Start an interaction for "list". dblClicked b. Select the item you want to interact with ... data c. Choose what you want to happen ... Request focus 4. Wykonanie operacji na pustej klatce jest odrzucane i generuje sygnał dźwiękowy. Przytoczona właściwość apletu musi być dodana ręcznie. Zrealizowano ją za pomocą metody boolean notEmpty(TextField field) { boolean empty = field.getText().equals(""); if(empty) Toolkit.getDefaultToolkit().beep(); return !empty; } Jej wywołania dodano do obsługi klatki i przycisku. Po tych zabiegach, aplet przyjmie następującą postać import java.applet.Applet; import java.awt.*; public class Master extends Applet { public void init() { //{{INIT_CONTROLS setLayout(null); setSize(311,277); list = new java.awt.List(0); add(list); list.setBounds(33,51,106,126); data = new java.awt.TextField(); data.setBounds(33,222,106,35); add(data); move = new java.awt.Button(); move.setLabel("Move"); move.setBounds(189,138,106,35); move.setBackground(new Color(12632256)); add(move); listLabel = new java.awt.Label("List"); listLabel.setBounds(12,21,65,22); add(listLabel); dataLabel = new java.awt.Label("Data"); dataLabel.setBounds(12,198,65,22); add(dataLabel); //}} //{{REGISTER_LISTENERS SymAction lSymAction = new SymAction(); data.addActionListener(lSymAction); move.addActionListener(lSymAction); list.addActionListener(lSymAction); //}} } boolean notEmpty(TextField field) { boolean empty = field.getText().equals(""); if(empty) Toolkit.getDefaultToolkit().beep(); return !empty; } //{{DECLARE_CONTROLS java.awt.List list; java.awt.TextField data; java.awt.Button move; java.awt.Label listLabel; java.awt.Label dataLabel; //}} class SymAction implements java.awt.event.ActionListener { public void actionPerformed(java.awt.event.ActionEvent event) { Object object = event.getSource(); if (object == data) data_EnterHit(event); else if (object == move) move_ActionPerformed(event); else if (object == list) list_DblClicked(event); } } void data_EnterHit(java.awt.event.ActionEvent event) { // to do: code goes here. //{{CONNECTION // Add a string to the list ... // Get the contents of the TextField if(notEmpty(data)) list.add(data.getText()); //}} } void move_ActionPerformed (java.awt.event.ActionEvent event) { // to do: code goes here. //{{CONNECTION // Add a string to the list ... // Get the contents of the TextField if(notEmpty(data)) list.add(data.getText()); //}} //{{CONNECTION // Clear the text for TextField data.setText(""); //}} //{{CONNECTION // Request the focus data.requestFocus(); //}} } void list_DblClicked(java.awt.event.ActionEvent event) { // to do: code goes here. //{{CONNECTION // Request the focus data.requestFocus(); //}} //{{CONNECTION // Delete an item from the list ... // Get the current item index list.remove(list.getSelectedIndex()); //}} } } Uwaga: Symantec Visual Cafe 2.5 generuje wywołanie zdeaktualizowanej metody delItem. Zmieniono je na wywołanie metody remove. Jan Bielecki Wykorzystywanie kostek Kostką jest komponent JavaBeans przystosowany do programowania wizualnego. Jako taki jest uzupełniony informacjami umożliwiającymi posługiwanie się nim w taki sam sposób jak predefiniowanymi sterownikami oblicza graficznego (m.in. klatkami, listami i przyciskami). Przekształcenie komponentu w kostkę wymaga dostarczenia klasy opisującej właściwości, metody i zdarzenia komponentu, a ponadto dostarczenia plików graficznych określających wygląd ikon kostki, wykorzystywanych przez budowniczego podczas programowania wizualnego. W szczególności, dla komponentu ColorSelector należy dostarczyć klasę ColorSelectorBeanInfo oraz pliki ColorSelector32.gif, ColorSelector16.gif i ColorSelectorMono.gif Zdefiniowanie kostki Następujący zestaw klas, uzupełniony plikami GIF, jest definicją kostki pokazanej na ekranie Kostka ColorSelector. Ekran Kostka ColorSelector ### selector.gif Opis komponentu ColorSelector package jbBeans.selector; import java.awt.*; import java.awt.event.*; import java.beans.*; public class ColorSelector extends Panel implements ItemListener{ private Box red, green, blue; private Color color; private PropertyChangeSupport changes; private VetoableChangeSupport vetos; public ColorSelector() { this(Color.magenta); } public ColorSelector(Color color) { changes = new PropertyChangeSupport(this); vetos = new VetoableChangeSupport(this); Panel panel = new Panel(); GridBagLayout gbl = new GridBagLayout(); panel.setLayout(gbl); red = new Box("Red"); green = new Box("Green"); blue = new Box("Blue"); GridBagConstraints gbc = new GridBagConstraints(); gbc.insets = new Insets(0, 10, 0, 0); gbc.gridx = 0; gbc.gridy = 0; gbl.setConstraints(red, gbc); gbc.gridy = 1; gbl.setConstraints(green, gbc); gbc.gridy = 2; gbl.setConstraints(blue, gbc); panel.add(red); panel.add(green); panel.add(blue); panel.setBackground(Color.green); add(panel); setColor(color); red.addItemListener(this); green.addItemListener(this); blue.addItemListener(this); } public void itemStateChanged(ItemEvent e) { Object source = e.getSource() ; int r = red.getState() ? 255 : 0, g = green.getState() ? 255 : 0, b = blue.getState() ? 255 : 0; setColor(new Color(r, g, b)); } class Box extends Checkbox { public Box(String caption) { super(caption, false); } public Dimension getPreferredSize() { return new Dimension(60, 30); } } public Color getColor() { return color; } public void setColor(Color color) { red.setState(color.getRed() != 0); green.setState(color.getGreen() != 0); blue.setState(color.getBlue() != 0); Color oldColor = this.color, newColor = color; try { // wywołanie metod vetoableChange // obiektów nadzorujących vetos.fireVetoableChange( "Color", oldColor, newColor ); } catch(PropertyVetoException e) { // przywrócenie wyglądu kostki restore(oldColor); return; } // zmiana właściwości związanej this.color = newColor; // wywołanie metod propertyChange // obiektów nasłuchujących changes.firePropertyChange( "Color", oldColor, newColor ); } void restore(Color color) { red.setState(color.getRed() != 0); green.setState(color.getGreen() != 0); blue.setState(color.getBlue() != 0); } public void addPropertyChangeListener( PropertyChangeListener lst ) { changes.addPropertyChangeListener(lst); } public void removePropertyChangeListener( PropertyChangeListener lst ) { changes.removePropertyChangeListener(lst); } public void addVetoableChangeListener( VetoableChangeListener lst ) { vetos.addVetoableChangeListener(lst); } public void removeVetoableChangeListener( VetoableChangeListener lst ) { vetos.removeVetoableChangeListener(lst); } public void paint(Graphics gDC) { Dimension size = getSize(); int w = size.width, h = size.height; gDC.setColor(Color.red); for(int i = 0; i < 5; i++) { int t = 1+2*i; gDC.drawRoundRect(i, i, w-t, h-t, 20, 20); } } } Opis kostki ColorSelector package jbBeans.selector; import java.awt.*; import java.awt.event.*; import java.beans.*; import jbBeans.selector.Selector; public class ColorSelectorBeanInfo extends SimpleBeanInfo { Class SELECTOR = jbBeans.selector.Selector.class; public Image getIcon(int kind) { Image img; if(kind == BeanInfo.ICON_COLOR_16x16) img = loadImage("ColorSelector16.gif"); else if(kind == BeanInfo.ICON_COLOR_32x32) img = loadImage("ColorSelector32.gif"); else img = loadImage("ColorSelectorMono.gif"); return img; } public BeanDescriptor getBeanDescriptor() { BeanDescriptor beanDS = new BeanDescriptor( SELECTOR ); beanDS.setDisplayName("Selector \u00a9JanB"); return beanDS; } public PropertyDescriptor[] getPropertyDescriptors() { PropertyDescriptor colorPD, blockedPD; try { colorPD = new PropertyDescriptor( "Color", SELECTOR ); colorPD.setShortDescription( "Selector color" ); } catch(IntrospectionException e) { return super.getPropertyDescriptors(); } PropertyDescriptor[] allPD = { colorPD, }; return allPD; } public int getDefaultPropertyIndex() { return 0; } public MethodDescriptor[] getMethodDescriptors() { Class argsVoid[], argsColor[]; argsVoid = new Class [] { }; argsColor = new Class [] { Color.class }; MethodDescriptor getColor, setColor; try { getColor = new MethodDescriptor( SELECTOR. getMethod("getColor", argsVoid) ); setColor = new MethodDescriptor( SELECTOR. getMethod("setColor", argsColor) ); } catch(NoSuchMethodException e) { return super.getMethodDescriptors(); } MethodDescriptor[] methods = { getColor, setColor, }; return methods; } public EventSetDescriptor[] getEventSetDescriptors() { EventSetDescriptor change, veto; try { change = new EventSetDescriptor( SELECTOR, "propertyChange", PropertyChangeListener.class, "propertyChange" ); veto = new EventSetDescriptor( SELECTOR, "vetoableChange", VetoableChangeListener.class, "vetoableChange" ); } catch (IntrospectionException e) { return super.getEventSetDescriptors(); } EventSetDescriptor[] events = { change, veto, }; return events; } } Utworzenie kostki Kostkę JavaBeans dystrybuuje się pod postacią pliku JAR. W celu utworzenia go, należy w Visual Cafe 2.5 wydać polecenie Project / JAR. W wyświetlonym wówczas oknie należy pozostawić nazwy plików związanych z tworzoną kostką ColorSelector.class JavaBean ColorSelector$Box.class ColorSelector.BeanInfo.class Design Time Only uzupełnić je nazwami plików GIF określających wygląd ikon ColorSelector16.gif ColorSelector32.gif ColorSelectorMono.gif (z podaniem tego samego pakietu co dla plików *.class) oraz określić miejsce, w którym zostanie wygenerowany plik JAR. Po wykonaniu tych czynności należy wydać polecenie Insert / Component into Library. Spowoduje to umieszczenie kostki w bibliotece. Uwaga: Ponieważ podczas wykonywania instrukcji img = loadImage("ColorSelector32.gif"); poszukiwanie pliku ColorSelector32.gif odbywa się względem katalogu, w którym znajduje się plik ColorSelectorBeanInfo.class, zaleca się umieszczenie plików GIF oraz plików .class kostki w tym samym katalogu. Zarządzanie paletą kostek Kostki JavaBeans znajdują się w bibliotece kostek. W celu zapoznania się z biblioteką należy wydać polecenie View / Component Library. Po wykonaniu p-kliknięcia w oknie biblioteki, można w niej utworzyć nową grupę kostek (Insert Group) albo dodać grupę do palety (Add to Palette). Po p-kliknięciu w obrębie paska palety i wydaniu polecenia Customize Palette, wyświetla się okno do zarządzania paletą. Umożliwia ono rozmieszczenie kostek w grupach palety. Programowanie wizualne Następujący aplet, utworzony metodą wizualną, posługuje się kostką biblioteczną ColorSelector. Uwaga: Dla uproszczenia przykładu zrezygnowano z nadzorowania zmiany właściwości color. =============================================== import java.applet.Applet; import java.awt.*; import jbBeans.selector.ColorSelector; public class Master extends Applet { public void init() { //{{INIT_CONTROLS setLayout(null); setSize(200, 200); colorSelector = new jbBeans.selector.ColorSelector(); colorSelector.setBounds(33, 60, 84, 99); add(colorSelector); setBackground(colorSelector.getColor()); //}} //{{REGISTER_LISTENERS SymPropertyChange lSymPropertyChange = new SymPropertyChange(); colorSelector. addPropertyChangeListener(lSymPropertyChange); //}} } //{{DECLARE_CONTROLS jbBeans.selector.ColorSelector colorSelector; //}} class SymPropertyChange implements java.beans.PropertyChangeListener { public void propertyChange(java.beans.PropertyChangeEvent event) { Object object = event.getSource(); if (object == colorSelector) colorSelector_propertyChange(event); } } void colorSelector_propertyChange (java.beans.PropertyChangeEvent event) { //{{CONNECTION // Set the Background Color... getColor { setBackground(colorSelector.getColor()); } //}} } } Studium programowe Studium dotyczy kostek Chooser, Watcher i Blocker. Kostka Chooser służy do wybrania koloru, kostka Watcher do obserwowania zmiany koloru, a kostka Blocker do blokowania zmiany koloru. Następujący aplet, pokazany na ekranie Testowanie, ilustruje użycie kostek. Na ekranie Introspekcja uwidoczniono napisy wyprowadzone do okna Debug. Kody źródłowe kostek podano po aplecie. Ekran Testowanie ### tests.gif Ekran Introspekcja ### intro.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; import java.beans.*; import jbBeans.chooser.Chooser; import jbBeans.watcher.Watcher; import jbBeans.blocker.Blocker; public class Master extends Applet implements ActionListener { private Chooser rgb; private Watcher watcher1 = new Watcher(), watcher2 = new Watcher(); private Blocker blocker = new Blocker(), blocker1 = new Blocker(), blocker2 = new Blocker(); private VetoingButton vetoer1 = new VetoingButton("1"), vetoer2 = new VetoingButton("2"); class VetoingButton extends Button implements VetoableChangeListener { public VetoingButton(String caption) { super(caption); } public void vetoableChange(PropertyChangeEvent evt) throws PropertyVetoException { Object newValue = evt.getNewValue(), oldValue = evt.getOldValue(); boolean scan2 = rgb.getColor().equals(newValue); if(scan2) return; String propertyName; propertyName = evt.getPropertyName(); if(!propertyName.equals("Color")) return; if(getLabel().equals("1") && newValue.equals(Color.black) || getLabel().equals("2") && newValue.equals(Color.white)) { setBackground(Color.red); try { Thread.sleep(300); } catch(InterruptedException e) { } setBackground(Color.lightGray); throw new PropertyVetoException( "Prohibited change", evt ); } } public Dimension getPreferredSize() { return new Dimension(60, 30); } } void showAll(String caption, Class beanClass) { Debug.toFrame("\n" + caption + "\n======="); BeanInfo bi = null; try { bi = Introspector.getBeanInfo(beanClass); } catch(IntrospectionException e) { Debug.toFrame("Introspection exception!"); return; } PropertyDescriptor pd[] = bi.getPropertyDescriptors(); show("Properties", pd); MethodDescriptor md[] = bi.getMethodDescriptors(); show("Methods", md); EventSetDescriptor ed[] = bi.getEventSetDescriptors(); show("Events", ed); } void show(String header, FeatureDescriptor fd[]) { Debug.toFrame("\n" + header + "\n==="); int len = fd.length; for(int i = 0; i < len ; i++) Debug.toFrame(fd[i].getName()); } public void init() { new Debug(); showAll("Chooser", Chooser.class); showAll("Watcher", Watcher.class); showAll("Blocker", Blocker.class); watcher1.setSize(50, 50); watcher2.setSize(50, 50); add(watcher1); add(blocker1); add(watcher2); add(blocker2); rgb = new Chooser(); add(rgb); add(blocker); blocker.addPropertyChangeListener(rgb); blocker1.addPropertyChangeListener(watcher1); blocker2.addPropertyChangeListener(watcher2); add(vetoer1); add(vetoer2); rgb.addPropertyChangeListener(watcher1); rgb.addPropertyChangeListener(watcher2); rgb.addVetoableChangeListener(vetoer1); rgb.addVetoableChangeListener(vetoer2); rgb.setColor(Color.blue); rgb.addActionListener(this); } public void actionPerformed(ActionEvent evt) { Color color = rgb.getColor(); Toolkit.getDefaultToolkit().beep(); setBackground(color); } } Kostka Chooser Kostka ma wygląd jak na ekranie Kostka Chooser. Implementuje nadzorowaną właściwość Color, metody getColor i setColor oraz zdarzenia propertyChange i vetoableChange. Ekran Kostka Chooser ### chooser.gif package jbBeans.chooser; import java.awt.*; import java.awt.event.*; import java.beans.*; public class Chooser extends Container implements ItemListener, PropertyChangeListener { private Box red, green, blue; private Color color; private boolean blocked = false; private static Box theBox; private PropertyChangeSupport changes; private VetoableChangeSupport vetos; private ActionListener actionListener; public Chooser() { this(Color.red); } public Chooser(Color color) { setLayout(null); enableEvents( AWTEvent.MOUSE_EVENT_MASK ); changes = new PropertyChangeSupport(this); vetos = new VetoableChangeSupport(this); Panel panel = new Panel(); panel.setBounds(15, 7, 70, 87); panel.setLayout(null); red = new Box("Red", 0); green = new Box("Green", 30); blue = new Box("Blue", 60); panel.add(red); panel.add(green); panel.add(blue); panel.setBackground(Color.green); add(panel); this.color = Color.blue; setColor(color); red.addItemListener(this); green.addItemListener(this); blue.addItemListener(this); } public void itemStateChanged(ItemEvent e) { Object source = e.getSource() ; int r = red.getState() ? 255 : 0 ; int g = green.getState() ? 255 : 0 ; int b = blue.getState() ? 255 : 0 ; setColor(new Color(r, g, b)); } class Box extends Checkbox { public Box(String caption, int pos) { super(caption); setBounds(10, pos, 60, 30); } } private void restoreBean(Color color) { red.setState(color.getRed() != 0); green.setState(color.getGreen() != 0); blue.setState(color.getBlue() != 0); } public synchronized Color getColor() { return color; } public synchronized void setColor(Color color) { Color oldColor = this.color, newColor = color; if(blocked) { restoreBean(oldColor); return; } try { // wywołanie metod vetoableChange // obiektów nadzorujących if(vetos != null) { vetos.fireVetoableChange( "Color", oldColor, newColor ); } } catch(PropertyVetoException e) { restoreBean(oldColor); return; } red.setState(newColor.getRed() != 0); green.setState(newColor.getGreen() != 0); blue.setState(newColor.getBlue() != 0); // zmiana właściwości związanej this.color = newColor; repaint(); // jeśli tego nie zawetowano, // wywołanie metod propertyChange // obiektów nasłuchujących if(changes != null) { changes.firePropertyChange( "Color", oldColor, newColor ); } } public Boolean getBlocked() { return new Boolean(blocked); } public void setBlocked(Boolean blocked) { this.blocked = blocked.booleanValue(); } public void addPropertyChangeListener( PropertyChangeListener lst ) { changes.addPropertyChangeListener(lst); } public void removePropertyChangeListener( PropertyChangeListener lst ) { changes.removePropertyChangeListener(lst); } public void addVetoableChangeListener( VetoableChangeListener lst ) { vetos.addVetoableChangeListener(lst); } public void removeVetoableChangeListener( VetoableChangeListener lst ) { vetos.removeVetoableChangeListener(lst); } public Dimension getPreferredSize() { return new Dimension(100, 100); } public void paint(Graphics gDC) { Dimension size = getPreferredSize(); int w = size.width, h = size.height; gDC.setColor(Color.red); for(int i = 0; i < 5; i++) { int t = 1+2*i; gDC.drawRoundRect(i, i, w-t, h-t, 20, 20); } gDC.setColor(color); gDC.fillOval(0, 0, 14, 14); if(color.equals(Color.black)) gDC.setColor(Color.white); else gDC.setColor(Color.black); gDC.drawOval(0, 0, 14, 14); } public synchronized void addActionListener(ActionListener lst) { actionListener = AWTEventMulticaster. add(actionListener, lst); } public synchronized void removeActionListener(ActionListener lst) { actionListener = AWTEventMulticaster. remove(actionListener, lst); } int ACTION = ActionEvent.ACTION_PERFORMED; protected void processMouseEvent(MouseEvent evt) { if(evt.getID() != MouseEvent.MOUSE_CLICKED) return; int x = evt.getX() - 7, y = evt.getY() - 7; // clicking on circle if(x*x + y*y < 7 * 7) { if(actionListener != null) { actionListener. actionPerformed( new ActionEvent( this, ACTION, "" ) ); } } else super.processMouseEvent(evt); } public void propertyChange(PropertyChangeEvent evt) { String name = evt.getPropertyName(); if(name.equals("Blocking")) { Boolean value = (Boolean)evt.getNewValue(); setBlocked(value); } } } // ======================================================== package jbBeans.chooser; import java.awt.*; import java.awt.event.*; import java.beans.*; import jbBeans.chooser.Chooser; public class ChooserBeanInfo extends SimpleBeanInfo { Class CHOOSER = jbBeans.chooser.Chooser.class; public Image getIcon(int kind) { Image img; if(kind == BeanInfo.ICON_COLOR_16x16) img = loadImage("Chooser16.gif"); else if(kind == BeanInfo.ICON_COLOR_32x32) img = loadImage("Chooser32.gif"); else img = loadImage("ChooserMono.gif"); return img; } public BeanDescriptor getBeanDescriptor() { BeanDescriptor beanDS = new BeanDescriptor( CHOOSER ); beanDS.setDisplayName("Chooser \u00a9JanB"); return beanDS; } public PropertyDescriptor[] getPropertyDescriptors() { PropertyDescriptor colorPD, blockedPD; try { colorPD = new PropertyDescriptor( "Color", CHOOSER ); colorPD.setShortDescription( "Chooser color" ); blockedPD = new PropertyDescriptor( "Blocked", CHOOSER ); blockedPD.setShortDescription( "Chooser blocked" ); } catch(IntrospectionException e) { return super.getPropertyDescriptors(); } PropertyDescriptor[] allPD = { colorPD, blockedPD, }; return allPD; } public int getDefaultPropertyIndex() { return 0; } public MethodDescriptor[] getMethodDescriptors() { Class argsVoid[], argsColor[], argsBool[]; argsVoid = new Class [] { }; argsColor = new Class [] { Color.class }; argsBool = new Class [] { Boolean.class }; MethodDescriptor getColor, setColor, getBlocked, setBlocked; try { getColor = new MethodDescriptor( CHOOSER. getMethod("getColor", argsVoid) ); setColor = new MethodDescriptor( CHOOSER. getMethod("setColor", argsColor) ); getBlocked = new MethodDescriptor( CHOOSER. getMethod("getBlocked", argsVoid) ); setBlocked = new MethodDescriptor( CHOOSER. getMethod("setBlocked", argsBool) ); } catch(NoSuchMethodException e) { return super.getMethodDescriptors(); } MethodDescriptor[] methods = { getColor, setColor, getBlocked, setBlocked, }; return methods; } public EventSetDescriptor[] getEventSetDescriptors() { EventSetDescriptor action, change, veto; try { action = new EventSetDescriptor( CHOOSER, "actionPerformed", ActionListener.class, "actionPerformed" ); change = new EventSetDescriptor( CHOOSER, "propertyChange", PropertyChangeListener.class, "propertyChange" ); veto = new EventSetDescriptor( CHOOSER, "vetoableChange", VetoableChangeListener.class, "vetoableChange" ); } catch (IntrospectionException e) { return super.getEventSetDescriptors(); } EventSetDescriptor[] events = { action, change, veto, }; return events; } } Kostka Watcher Kostka ma wygląd jak na ekranie Kostka Watcher. Implementuje nadzorowaną właściwość Color, metody getColor i setColor oraz zdarzenie propertyChange. Ekran Kostka Watcher ### watcher.gif package jbBeans.watcher; import java.awt.*; import java.awt.event.*; import java.beans.*; public class Watcher extends Component implements PropertyChangeListener { private final int Size = 100, Last = 13; // properties private Color color = Color.red; private boolean blocked = false; private PropertyChangeSupport changes; private VetoableChangeSupport vetos; private Color paintColor; public Watcher() { changes = new PropertyChangeSupport(this); vetos = new VetoableChangeSupport(this); paintColor = color; } public synchronized Color getColor() { return color; } public synchronized void setColor(Color color) { Color oldColor = this.color, newColor = color; this.color = newColor; if(!blocked) { paintColor = newColor; repaint(); } // wywołanie metod propertyChange // obiektów nasłuchujących if(changes != null) { changes.firePropertyChange( "Color", oldColor, newColor ); } } public Boolean getBlocked() { return new Boolean(blocked); } public void setBlocked(Boolean blocked) { boolean newBlocked = blocked.booleanValue(); // when freeing block if(this.blocked && !newBlocked) paintColor = color; this.blocked = newBlocked; repaint(); } public void propertyChange(PropertyChangeEvent evt) { String name = evt.getPropertyName(); Object newValue = evt.getNewValue(); if(name.equals("Color")) { Color newColor = (Color)newValue; setColor(newColor); } else if(name.equals("Blocking")) { Boolean newBlocked = (Boolean)newValue; setBlocked(newBlocked); } } public void addPropertyChangeListener( PropertyChangeListener lst ) { changes.addPropertyChangeListener(lst); } public void removePropertyChangeListener( PropertyChangeListener lst ) { changes.removePropertyChangeListener(lst); } public void paint(Graphics gDC) { int w = Size, h = Size; Color oldColor = gDC.getColor(); for(int i = 0; i < Last+1; i++) { if(i == 0 || i == Last) gDC.setColor(Color.black); else gDC.setColor(paintColor); int t = 1+2*i; gDC.drawOval(i, i, w-t, h-t); } if(blocked) { gDC.setColor(Color.red); gDC.fillOval(w/2-3, h/2-3, 6, 6); } gDC.setColor(oldColor); super.paint(gDC); } public Dimension getPreferredSize() { return new Dimension(Size, Size); } } // ======================================================== package jbBeans.watcher; import java.awt.*; import java.awt.event.*; import java.beans.*; import jbBeans.watcher.Watcher; public class WatcherBeanInfo extends SimpleBeanInfo { Class WATCHER = jbBeans.watcher.Watcher.class; public java.awt.Image getIcon(int kind) { Image img; if(kind == BeanInfo.ICON_COLOR_16x16) img = loadImage("Watcher16.gif"); else if(kind == BeanInfo.ICON_COLOR_32x32) img = loadImage("Watcher32.gif"); else img = loadImage("WatcherMono.gif"); return img; } public BeanDescriptor getBeanDescriptor() { BeanDescriptor beanDS = new BeanDescriptor( WATCHER ); beanDS.setDisplayName("Watcher \u00a9JanB"); return beanDS; } public PropertyDescriptor[] getPropertyDescriptors() { PropertyDescriptor colorPD; try { colorPD = new PropertyDescriptor( "Color", WATCHER ); colorPD.setShortDescription( "Watcher color" ); } catch(IntrospectionException e) { return super.getPropertyDescriptors(); } PropertyDescriptor[] allPD = { colorPD, }; return allPD; } public int getDefaultPropertyIndex() { return 0; } public MethodDescriptor[] getMethodDescriptors() { Class argsVoid[], argsColor[], argsBool[]; argsVoid = new Class [] { }; argsColor = new Class [] { Color.class }; argsBool = new Class [] { Boolean.class }; MethodDescriptor getColor, setColor, getBlocked, setBlocked; try { getColor = new MethodDescriptor( WATCHER. getMethod("getColor", argsVoid) ); setColor = new MethodDescriptor( WATCHER. getMethod("setColor", argsColor) ); getBlocked = new MethodDescriptor( WATCHER. getMethod("getBlocked", argsVoid) ); setBlocked = new MethodDescriptor( WATCHER. getMethod("setBlocked", argsBool) ); } catch(NoSuchMethodException e) { return super.getMethodDescriptors(); } MethodDescriptor[] methods = { getColor, setColor, getBlocked, setBlocked, }; return methods; } public EventSetDescriptor[] getEventSetDescriptors() { EventSetDescriptor change; try { change = new EventSetDescriptor( WATCHER, "propertyChange", PropertyChangeListener.class, "propertyChange" ); } catch (IntrospectionException e) { return super.getEventSetDescriptors(); } EventSetDescriptor[] events = { change, }; return events; } } Kostka Blocker Kostka ma wygląd jak na ekranie Kostki Blocker. Implementuje właściwość Blocking, metody getBlocking i setBlocking oraz zdarzenie propertyChange. Ekran Kostki Blocker ### blocker.gif package jbBeans.blocker; import java.awt.*; import java.awt.event.*; import java.beans.*; public class Blocker extends Button implements ActionListener, VetoableChangeListener { private boolean theBlocking = false; private Font font = new Font("Serif", Font.BOLD, 24); private PropertyChangeSupport changes; public Blocker() { super(""); setFont(font); setBlocking(new Boolean(theBlocking)); // listens to itself addActionListener(this); changes = new PropertyChangeSupport(this); } // required; isBlocking was not enough // for property Blocking description public Boolean getBlocking() { return new Boolean(theBlocking); } public void setBlocking(Boolean blocking) { Boolean oldBlocking = new Boolean(theBlocking), newBlocking = blocking; theBlocking = newBlocking.booleanValue(); if(theBlocking) { setForeground(Color.yellow); setBackground(Color.red); setLabel("!"); } else { setForeground(Color.black); setBackground(Color.white); setLabel("+"); } if(changes != null) { changes.firePropertyChange( "Blocking", oldBlocking, newBlocking ); } } public Boolean isBlocking() { return new Boolean(theBlocking); } public void actionPerformed(ActionEvent evt) { setBlocking(new Boolean(!theBlocking)); } public void vetoableChange(PropertyChangeEvent evt) throws PropertyVetoException { if(theBlocking) { throw new PropertyVetoException( "Change blocking", evt ); } } public Dimension getPreferredSize() { return new Dimension(40, 40); } public void addPropertyChangeListener( PropertyChangeListener lst ) { changes.addPropertyChangeListener(lst); } public void removePropertyChangeListener( PropertyChangeListener lst ) { changes.removePropertyChangeListener(lst); } } // ======================================================== package jbBeans.blocker; import java.awt.*; import java.awt.event.*; import java.beans.*; import jbBeans.blocker.Blocker; public class BlockerBeanInfo extends SimpleBeanInfo { Class BLOCKER = jbBeans.blocker.Blocker.class; public java.awt.Image getIcon(int kind) { Image img; if(kind == BeanInfo.ICON_COLOR_16x16) img = loadImage("Blocker16.gif"); else if(kind == BeanInfo.ICON_COLOR_32x32) img = loadImage("Blocker32.gif"); else img = loadImage("BlockerMono.gif"); return img; } public BeanDescriptor getBeanDescriptor() { BeanDescriptor beanDS = new BeanDescriptor( BLOCKER ); beanDS.setDisplayName("Blocker \u00a9JanB"); return beanDS; } public PropertyDescriptor[] getPropertyDescriptors() { PropertyDescriptor blockingPD; try { blockingPD = new PropertyDescriptor( "Blocking", BLOCKER ); blockingPD.setShortDescription( "Blocked state" ); } catch(IntrospectionException e) { return super.getPropertyDescriptors(); } PropertyDescriptor[] allPD = { blockingPD, }; return allPD; } public int getDefaultPropertyIndex() { return 0; } public MethodDescriptor[] getMethodDescriptors() { Class argsVoid[], argsBool[]; argsVoid = new Class [] { }; argsBool = new Class [] { Boolean.class }; MethodDescriptor isBlocking, setBlocking; try { isBlocking = new MethodDescriptor( BLOCKER. getMethod("isBlocking", argsVoid) ); setBlocking = new MethodDescriptor( BLOCKER. getMethod("setBlocking", argsBool) ); } catch(NoSuchMethodException e) { return super.getMethodDescriptors(); } MethodDescriptor[] methods = { isBlocking, setBlocking, }; return methods; } public EventSetDescriptor[] getEventSetDescriptors() { EventSetDescriptor change; try { change = new EventSetDescriptor( BLOCKER, "propertyChange", PropertyChangeListener.class, "propertyChange" ); } catch (IntrospectionException e) { return super.getEventSetDescriptors(); } EventSetDescriptor[] events = { change, }; return events; } } Jan Bielecki Przechowywanie obiektów Obiekty klas implementujących interfejs Serializable (oraz Externalizable) mogą być przenoszone między pamięcią operacyjną a plikiem. Operacja wyszeregowania do pliku zachowuje wzajemne powiązania obiektów i umożliwia ich wszeregowanie do pamięci operacyjnej. W szczególności, jeśli obiekt one zawiera odnośnik do obiektu two, a obiekt two zawiera odnośnik do obiektu one, to wyszeregowanie każdego z tych obiektów pociąga za sobą wyszeregowanie drugiego, a wszeregowanie uprzednio wyszeregowanego obiektu, wprowadza do pamięci oba i odtwarza ich wzajemne powiązania. Do szeregowania obiektów używa się strumieni klas ObjectOutputStream i ObjectInputStream. W klasach tych zdefiniowano metody umożliwiające szeregowanie obiektów, tablic bajtowych oraz zmiennych typów podstawowych (np. int). public interface Serializable { // puste ciało (sic!) } public class ObjectOutputStream extends OutputStream implements ObjectOutput, ObjectStreamConstants { public ObjectOutputStream(OutputStream out) throws IOException; public final void defaultWriteObject() throws IOException, NotActiveException; public final void writeObject(Object obj) throws IOException; public void write(byte arr[]) throws IOException; public void writeInt(int data) throws IOException; // ... } public class ObjectInputStream extends InputStream implements ObjectInput, ObjectStreamConstants { public ObjectInputStream(InputStream inp) throws StreamCorruptedException, IOException; public final void defaultReadObject() throws IOException, NotActiveException, ClassNotFoundException; public final void readObject() throws OptionalDataException, ClassNotFoundException, IOException; public void readFully(byte arr[]) throws IOException; public void readInt() throws IOException; // ... } Uwaga: Jeśli użyto metody defaultWriteObject, to wyszeregowaniu nie podlegają pola statyczne (static) i nietrwałe (transient), a jeśli użyto metody defaultReadObject, to takie pola otrzymują wartości domyślne. Następujący program, pokazany na ekranie Przechowanie obiektów, wyszeregowuje do pliku informacje o kolorze i położeniu kół wykreślonych na pulpicie ramki. Określenie nazwy pliku odbywa się za pomocą dialogu wejścia-wyjścia. Próba zmiany zawartości ramki, nie poprzedzona wyszeregowaniem obiektu koła, jest sygnalizowana. Ekran Przechowanie obiektów ### saveopen.gif import java.awt.*; import java.awt.event.*; import java.io.*; public class Main extends Frame { protected static Main frame = new Main("Circles"); protected static int Limit = 100; protected static int count = 0; protected static Circle[] circles = new Circle[Limit]; protected static boolean modified = false; protected static Item anew, open, save, exit; public static void main(String[] args) { frame.setSize(300, 300); frame.setVisible(true); frame.addWindowListener( new WindowAdapter() { public void windowClosing(WindowEvent evt) { frame.dispose(); System.exit(0); } } ); frame.addMouseListener( new MouseAdapter() { public void mouseReleased(MouseEvent evt) { int x = evt.getX(), y = evt.getY(); Circle circle = frame.new Circle(x, y); if(count == Limit) { beep(0); return; } circles[count++] = circle; Graphics gDC = frame.getGraphics(); circle.draw(gDC); modified = true; } } ); MenuBar bar = new MenuBar(); Menu file = new Menu("File"); anew = frame.new Item("New"); open = frame.new Item("Open"); save = frame.new Item("Save As"); exit = frame.new Item("Exit"); file.add(anew); file.add(open); file.add(save); file.add(frame.new Item("-")); file.add(exit); bar.add(file); frame.setMenuBar(bar); } public static void beep(int err) { System.out.println("Error " + err); Toolkit.getDefaultToolkit().beep(); if(err != 0) System.exit(err); } public void paint(Graphics gDC) { for(int i = 0; i < count ; i++) circles[i].draw(gDC); } class Item extends MenuItem implements ActionListener { public Item(String cmd) { super(cmd); addActionListener(this); } public void actionPerformed(ActionEvent evt) { Object src = evt.getSource(); if((src == anew || src == open) && modified) { beep(0); MsgDialog dlg = new MsgDialog(frame, "Not saved"); if(!dlg.wasDropped()) return; } if(src == anew) { count = 0; frame.repaint(); } else if(src == open) { FileDialog open = new FileDialog( frame, "Open", FileDialog.LOAD ); open.setVisible(true); String name = open.getFile(); if(name == null) { beep(0); return; } FileInputStream inpF = null; try { inpF = new FileInputStream(name); } catch(FileNotFoundException e) { } ObjectInputStream inp = null; try { inp = new ObjectInputStream(inpF); count = inp.readInt(); for(int i = 0; i < count ; i++) try { circles[i] = (Circle)inp.readObject(); } catch(ClassNotFoundException e) { beep(1); } catch(IOException e) { beep(2); } inp.close(); } catch(IOException e) { beep(3); } frame.repaint(); } else if(src == save) { FileDialog open = new FileDialog( frame, "Save", FileDialog.SAVE ); open.setVisible(true); String name = open.getFile(); if(name == null) { beep(0); return; } FileOutputStream outF = null; try { outF = new FileOutputStream(name); ObjectOutputStream out = new ObjectOutputStream(outF); out.writeInt(count); for(int i = 0; i < count ; i++) out.writeObject(circles[i]); out.close(); } catch(IOException e) { beep(4); } } else if(src == exit) System.exit(0); else super.processEvent(evt); modified = false; } } class Circle implements Serializable { protected int x, y; protected Color c; protected final int r = 30; public Circle(int x, int y) { this.x = x; this.y = y; int r = getRGB(), g = getRGB(), b = getRGB(); c = new Color(r, g, b); } public void draw(Graphics gDC) { gDC.setColor(c); gDC.fillOval(x-r, y-r, 2*r-1, 2*r-1); gDC.setColor(Color.black); gDC.drawOval(x-r, y-r, 2*r-1, 2*r-1); } int getRGB() { return (int)(255 * Math.random()); } } public Main(String caption) { super(caption); } } class MsgDialog extends Dialog implements ActionListener { protected boolean dropped; protected Button y, n; public MsgDialog(Frame parent, String caption) { super(parent, caption, true); setLayout(new FlowLayout()); add(new Label("Drop contents?")); add(y = new Button("Yes")); add(n = new Button("No")); y.addActionListener(this); n.addActionListener(this); pack(); show(); } public void actionPerformed(ActionEvent evt) { dropped = evt.getSource() == y; dispose(); } public boolean wasDropped() { return dropped; } } Uwaga: Klasa szeregowalna może zawierać własne metody writeObject i readObject, wywoływane niejawnie podczas wykonywania metod writeObject i readObject klas ObjectOutputStream i ObjectInputStream. W takim przypadku szeregowanie pól klasy jest określone przez jej własne metody. Tym niemniej, w celu zapewnienia właściwego szeregowania pól obiektowych, stosuje się jawne wywołania metod defaultWriteObject i defaultReadObject. Następujący program ilustruje użycie wyspecjalizowanych metod writeObject i readObject do szeregowania obiektów klasy City, w których zawarto nietrwałe pole code. Wykonanie programu powoduje wyprowadzenie napisu: Warsaw Cracow 137. import java.io.*; public class Main { static String fileName; public static void main(String args[]) throws IOException { if((fileName = args[0]) == null) System.exit(1); City one = new City("Warsaw"), two = new City("Cracow"); one.next = two; two.next = one; // wyszeregowanie FileOutputStream outF; ObjectOutput out; try { outF = new FileOutputStream(fileName); out = new ObjectOutputStream(outF); // wywołuje City.writeObject out.writeObject(one); out.close(); } catch(IOException e) { } // wszeregowanie FileInputStream inpF; ObjectInput inp; try { inpF = new FileInputStream(fileName); inp = new ObjectInputStream(inpF); one = null; try { // wywołuje City.readObject one = (City)inp.readObject(); } catch(ClassNotFoundException e) { System.exit(2); } catch(ClassCastException e) { System.exit(3); } System.out.println( one.name + " " + one.next.next.next.name + " " + one.code ); inp.close(); } catch(IOException e) { } System.in.read(); } } class City implements Serializable { City next = null; String name; transient int code = -13; public City(String name) { this.name = name; } private void writeObject(ObjectOutputStream out) throws IOException { out.defaultWriteObject(); out.writeObject("Secret"); } private void readObject(ObjectInputStream inp) throws IOException, ClassNotFoundException { inp.defaultReadObject(); String string = (String)inp.readObject(); if(string.equals("Secret")) code = 137; } } Jan Bielecki Czcionki i znaki narodowe W celu uzyskania wykazu czcionek zainstalowanych w systemie, należy wykonać następujący program GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment(); Font[] fonts = env.getAllFonts(); for(int i = 0; i < fonts.length ; i++) System.out.println(fonts[i]); Aby umożliwić wykreślanie czcionek z tego zestawu należy do katalogu ...\lib, wstawić plik font.properties.XY, w których XY jest dwuliterowym kodem kraju (np. pl). Reprezentatywny fragment tego pliku podano w tabeli Plik czcionek narodowych. Tabela Plik czcionek narodowych ### # @(#)font.properties.pl # # AWT Font default Properties for Windows - Polish # serif.0=Times New Roman,EASTEUROPE_CHARSET serif.1=WingDings,SYMBOL_CHARSET,NEED_CONVERTED serif.2=Symbol,SYMBOL_CHARSET,NEED_CONVERTED # Exclusion Range info. exclusion.serif.0=0200-ffff # Name aliases alias.timesroman=serif // TimesRoman => Serif # For backword compatibility timesroman.0=Times New Roman,EASTEUROPE_CHARSET helvetica.0=Arial,EASTEUROPE_CHARSET courier.0=Courier New,EASTEUROPE_CHARSET # Static FontCharset info. fontcharset.serif.1=sun.awt.windows.CharToByteWingDings fontcharset.serif.2=sun.awt.CharToByteSymbol # Charset for text input inputtextcharset=EASTEUROPE_CHARSET # Font filenames filename.Times_New_Roman=TIMES.TTF # Default font definition default.char=2751 polish.0=Times New Roman,SYMBOL_CHARSET,NEED_CONVERTED exclusion.polish.0=0200-ffff fontcharset.polish.0=CharToBytePolish ### Sposób interpretowania pliku font.propertiers.pl, w wersji dla systemu Windows, jest następujący 1. Przed wyprowadzeniem znaku czcionki Serif, sprawdza się czy znajduje się on w obszarze wykluczonym (Exclusion Range). 2. Jeśli znak nie jest wykluczony, to używa się czcionki systemowej New Roman. 3. Jeśli znak jest wykluczony, to w związku z użyciem napisu NEED_CONVERTED, sprawdza się, czy zaakceptuje go klasa sun.awt.windows.CharToByteWingDings. Klasa ta zawiera metody canConvert i convert. Pierwsza dostarcza true, gdy istnieje konwersja znaku z Unikodu na kod systemu, a druga definiuje tę konwersję. W wypadku istnienia konwersji, do wyprowadzenia znaku używa się czcionki systemowej WingDings. 4. W analogiczny sposób postępuje się dla pozostałych wpisów na temat czcionki Serif. Jeśli żaden z nich nie doprowadzi do sukcesu, to wyprowadza się pytajnik (?). 5. Jeśli chce się używać własnej czcionki, na przykład o nazwie Bookman, to postępuje się w sposób analogiczny jak dla czcionki Serif. Następujący aplet, pokazany na ekranie Polskie litery, ilustruje wykreślanie polskich liter czcionki Courier New, na którą odwzorowano czcionkę rodzajową Monospaced. Ekran Polskie litery ### polish.gif =============================================== import java.applet.Applet; import java.awt.*; public class Master extends Applet { protected Font font; public void init() { font = new Font("Monospaced", Font.BOLD, 30); } public void paint(Graphics gDC) { gDC.setFont(font); gDC.drawString(" ą ć ę ł ń ó ś ź ż ", 20, 40); gDC.drawString(" Ą Ć Ę Ł Ń Ó Ś Ź Ż ", 20, 80); } } Jan Bielecki Określanie daty i godziny Datę i godzinę reprezentuje się w obiektach klasy GregorianCalendar, pochodnej od Calendar. Po utworzeniu obiektu, do określenia dnia i godziny można użyć metod get. Date date = new Date(); Calendar greg = GregorianCalendar.getInstance(); greg.setTime(date); int hh = greg.get(Calendar.HOUR), mm = greg.get(Calendar.MINUTE), ss = greg.get(Calendar.SECOND); Uwaga: Wywołanie metody getInstance niejawnie ustawia datę bieżącą. Użycie metody setTime jest niezbędne tylko wówczas gdy jest wymagana zmiana daty. Następujący aplet, pokazany na ekranie Zegar analogowy, ilustruje użycie klas do reprezentowania daty i czasu. Ekran Zegar analogowy ### analog.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; import java.util.*; public class Master extends Applet implements Runnable { private double Pi = Math.PI; private int x, y; private Calendar greg = GregorianCalendar.getInstance(); private Graphics gDC; public void init() { new Thread(this).start(); } public void paint(Graphics gDC) { this.gDC = gDC; Dimension s = getSize(); int w = s.width, h = s.height; int r = 7 * Math.min(w, h) / 16, d = 2*r-1; x = w / 2; y = h / 2; // tarcza zegara gDC.setColor(Color.black); gDC.drawOval(x-r, y-r, d, d); gDC.drawOval(x-r-1, y-r-1, d+2, d+2); // oznaczenia godzin for(int i = 0; i < 60 ; i++) { double alpha = i * 2*Pi / 60; int rr = i % 5 == 0 ? r/10 : r/30; gDC.drawLine( (int)(x + (r-rr) * Math.cos(alpha)), (int)(y - (r-rr) * Math.sin(alpha)), (int)(x + r * Math.cos(alpha)), (int)(y - r * Math.sin(alpha)) ); } // pobranie czasu greg.setTime(new Date()); int hh = greg.get(Calendar.HOUR), mm = greg.get(Calendar.MINUTE), ss = greg.get(Calendar.SECOND), secs = ss + (mm + hh * 60) * 60; // wskazówka godzinowa double alpha = 2*Pi/4 - secs / 3600.0 * 2*Pi / 12; drawLine( (int)(x + r * Math.cos(alpha) * 5/8), (int)(y - r * Math.sin(alpha) * 5/8) ); // wskazówka minutowa alpha = 2*Pi/4 - secs % 3600 / 60.0 * 2*Pi / 60; drawLine( (int)(x + (r-3*r/20) * Math.cos(alpha)), (int)(y - (r-3*r/20) * Math.sin(alpha)) ); // wskazówka sekundowa gDC.setColor(Color.red); alpha = 2*Pi/4 - secs % 60 * 2*Pi / 60; drawLine( (int)(x + r * Math.cos(alpha)), (int)(y - r * Math.sin(alpha)) ); } void drawLine(int xTo, int yTo) { gDC.drawLine(x, y, xTo, yTo); } public void run() { while(true) { try { Thread.sleep(1000); } catch(InterruptedException e) { } repaint(); } } } Jan Bielecki Przekształcanie obrazów Typowe przekształcenie obrazu zapamiętanego w pliku w formacie GIF albo JPG polega na załadowaniu go do pamięci operacyjnej w celu wyświetlenia. Rzadziej zachodzi potrzeba wykonania operacji na poszczególnych pikselach obrazu albo potrzeba przekształcenia zestawu pikseli w obraz. Bardzo rzadko należy wyprowadzić obraz do pliku. Pikselowanie obrazu Do przekształcenia obrazu w tablicę pikseli służą metody klasy PixelGrabber. Deklaracja konstruktora tej klasy ma postać public PixelGrabber( Image img, int x, int y, int w, int h, boolean forceRGB ) Argumentami konstruktora są: odnośnik do obrazu, współrzędne narożnika podobrazu, rozmiary podobrazu oraz orzecznik określający, czy pikselowanie podobrazu ma się odbywać w modelu RGB. Pikselowanie może dotyczyć obrazu o nieznanych rozmiarach. Informacje o obrazie uzyskuje się za pomocą metod getWidth, getHeight, getPixels i getColorModel. Następujący aplet, pokazany na ekranie Pikselowanie obrazu, wyświetla obraz w kolorach dopełniających, ale wszystkie szare piksele wyświetla jako białe. Ekran Pikselowanie obrazu ### pixelize.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.image.*; public class Master extends Applet { String theImage = "Duke.gif"; Image img; PixelGrabber grab; byte pixels[]; MediaTracker tracker; public void init() { img = getImage(getDocumentBase(), theImage); grab = new PixelGrabber(img, 0, 0, -1, -1, false); try { grab.grabPixels(); } catch(InterruptedException e) { showStatus("Interrupted"); return; } Object pix = grab.getPixels(); if(pix instanceof byte[]) pixels = (byte [])pix; } public void paint(Graphics gDC) { ColorModel model = grab.getColorModel(); int w = grab.getWidth(), h = grab.getHeight(); for(int x = 0; x < w ; x++) { for(int y = 0; y < h ; y++) { int p = pixels[y * w + x]; p &= 0xff; int r = model.getRed(p), g = model.getGreen(p), b = model.getBlue(p); r = ~r & 0xff; g = ~g & 0xff; b = ~b & 0xff; Color rgb = new Color(r,g,b); if(r == g && r == b) rgb = Color.white; gDC.setColor(rgb); gDC.drawLine(x, y, x, y); } } } } Obrazowanie pikseli Do przekształcenia tablicy pikseli w obraz służy metoda createImage. Jej argumentem jest obiekt klasy MemoryImageSource. Deklaracja konstruktora klasy ma postać public MemoryImageSource( int w, int h, int arr[], int off, int scan, ) Argumentami konstruktora są: szerokość i wysokość obrazu, odnośnik do tablicy pikseli, indeks pierwszego elementu podstawowego tablicy (zazwyczaj 0) oraz odległość między elementami podstawowymi związanymi z parą pikseli sąsiadujących ze sobą w pionie (zazwyczaj w). Uwaga: Dla off=0 i scan=w, piksel o współrzędnych (x, y) jest przechowywany w elemencie y * w + x. Następujący aplet, pokazany na ekranie Obrazowanie pikseli, wyświetla obraz tworzony w pamięci operacyjnej podczas przeciągania kursora. Ekran Obrazowanie pikseli ### imagize.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; import java.awt.image.*; public class Master extends Applet { private MemoryImageSource imgSrc; private Image memImg; private int w, h; private int pixels[]; private Watcher watcher; public void init() { Dimension dim = getSize(); w = dim.width; h = dim.height; pixels = new int [w * h]; imgSrc = new MemoryImageSource( w, h, pixels, 0, w ); imgSrc.setAnimated(true); memImg = createImage(imgSrc); watcher = new Watcher(this); addMouseMotionListener(watcher); } public void paint(Graphics gDC) { gDC.drawImage(memImg, 0, 0, this); } class Watcher extends MouseMotionAdapter { private Applet applet; public Watcher(Applet applet) { this.applet = applet; } public void mouseDragged(MouseEvent evt) { int x = evt.getX(), y = evt.getY(), colorRed = 0xffff0000; pixels[y * w + x] = colorRed; imgSrc.newPixels(x, y, x, y); applet.repaint(); } } } Tworzenie pliku GIF W celu utworzenia pliku w formacie GIF należy użyć biblioteki ACME składającej się z klas GifEncoder ImageEncoder IntHashtable Następujący program, pokazany na ekranie Generowanie pliku GIF, ładuje plik GIF, zmienia jego czarne piksele na żółte, a następnie zapamiętuje jako nowy plik GIF. Uwaga: Argumentami programu są: nazwa katalogu i nazwa pliku GIF (bez rozszerzenia). Ekran Generowanie pliku GIF ### gifize.gif import java.awt.*; import java.awt.event.*; import java.net.URL; import java.io.*; import java.awt.image.*; public class Main extends Frame { private static String folder, name, fileName; private Image img; private Toolkit kit; private URL url; static private Main main; public static void main(String[] args) throws IOException { folder = args[0]; name = args[1]; fileName = folder + '/' + name + ".gif"; main = new Main("Acme GifEncoder"); main.setSize(200, 200); main.setVisible(true); main.addWindowListener( new WindowAdapter() { public void windowClosing(WindowEvent evt) { main.dispose(); System.exit(0); } } ); } public Main(String caption) throws IOException { super(caption); kit = Toolkit.getDefaultToolkit(); url = new URL("file:/" + fileName); MediaTracker tracker = new MediaTracker(this); img = kit.getImage(url); tracker.addImage(img, 0); try { tracker.waitForID(0); } catch(InterruptedException e) { } int w = img.getWidth(this), h = img.getHeight(this); PixelGrabber grabber = new PixelGrabber(img, 0, 0, w, h, true); try { grabber.grabPixels(); } catch(InterruptedException e) { } FileOutputStream out = new FileOutputStream(name + "New.gif"); GifEncoder encoder = new GifEncoder(img, out); int[] pixels = new int [w * h]; pixels = (int [])grabber.getPixels(); int yellow = 0xffffff00; for(int x = 0; x < w ; x++) { for(int y = 0; y < h ; y++) { int p = pixels[y * w + x]; if(new Color(p).equals(Color.black)) pixels[y * w + x] = yellow; } } encoder.encodeStart(w, h); encoder.encodePixels(0, 0, w, h, pixels, 0, w); encoder.encodeDone(); out.close(); fileName = folder + '/' + name + "New.gif"; url = new URL("file:/" + fileName); img = kit.getImage(url); repaint(); } public void paint(Graphics gDC) { Insets insets = main.getInsets(); gDC.drawImage(img, insets.left, insets.top, this); } } Jan Bielecki Przezroczyste obrazy GIF Jeden z kolorów obrazu GIF może być zdefiniowany jako przezroczysty. Wyświetlenie piksela w kolorze przezroczystym nie powoduje wówczas zmiany koloru tła. Do zdefiniowania koloru przezroczystego można użyć edytora graficznego Paint Shop Pro 5.0. W tym celu, po załadowaniu obrazu, należy wydać polecenie Colors / Set Palette Transparency Set the transparency value to palette entry a następnie podać numer jaki w palecie kolorów ma kolor przezroczysty. Następujący aplet, pokazany na ekranie Obrazy przezroczyste, animuje seledynowe kółko „przemykające się” między kolumnami pokazanymi na ekranach Czerwone kolumny i Zielone kolumny. Ekran Obrazy przezroczyste ### transpar.gif Ekran Czerwone kolumny ### redcol.gif Ekran Zielone kolumny ### greencol.gif =============================================== import java.applet.Applet; import java.awt.*; import java.net.*; public class Master extends Applet implements Runnable { private final int Delta = 5; private Thread thread; private MediaTracker tracker; private URL where; private Image buffer, redCol, greenCol; private int dx = Delta, w, h;; private Graphics gDC, mDC; public void init() { Dimension dim = getSize(); w = dim.width; h = dim.height; tracker = new MediaTracker(this); where = getDocumentBase(); redCol = getImage(where, "Red.gif"); greenCol = getImage(where, "Green.gif"); tracker.addImage(redCol, 0); tracker.addImage(greenCol, 0); try { tracker.waitForID(0); } catch(InterruptedException e) { } gDC = getGraphics(); setBackground(Color.yellow); buffer = createImage(w, h); mDC = buffer.getGraphics(); thread = new Thread(this); thread.start(); } public void run() { int x = 0, y = h>>1; while(true) { mDC.clearRect(0, 0, w, h); mDC.drawImage(redCol, 0, 0, this); mDC.setColor(Color.cyan); mDC.fillOval(x, y, 50, 50); mDC.setColor(Color.black); mDC.drawOval(x, y, 50, 50); mDC.drawImage(greenCol, 0, 0, this); gDC.drawImage(buffer, 0, 0, this); try { thread.sleep(20); } catch(InterruptedException e) { } x += dx; if(x > w || x < 0) dx = -dx; } } } Jan Bielecki Animowane obrazy GIF Do tworzenia z sekwencji nie-animowanych obrazów GIF, jednego animowanego obrazu GIF, można użyć programu Gif Construction Set. Uwaga: Do oglądania zwykłych i animowanych obrazów GIF zaleca się używać programu Irfan View. Utworzenie obrazu W celu utworzenia animowanego obrazu GIF należy * Przygotować sekwencję kadrów w formacie GIF. * Wywołać program Gif Construction Set. * Wydać polecenie File / Animation Wizard. * Zastosować się do kolejnych poleceń generatora. * Zapamiętać obraz w pliku. Na ekranie Kadry animacji pokazano 3 obrazy GIF, z których za pomocą animatora można utworzyć jeden animowany obraz GIF. Ekran Kadry animacji ### dukes3.gif Wyświetlenie obrazu Animowane obrazy GIF wyświetla się w taki sam sposób jak obrazy nieanimowane. Trudności pojawiają się dopiero wówczas, gdy animowany obraz GIF jest użyty jako tło w buforowaniu. Uwaga: Jak można się przekonać, zrezygnowanie z buforowania animowanego tła, ma fatalne skutki wizualne. Następujący aplet, pokazany na ekranie Buforowanie animacji, wyświetla animowany obraz GIF, na którego tle poruszają się dwa animowane kółka. Ekran Buforowanie animacji ### backanim.gif Wykonanie apletu przebiega w następujący sposób * Wywołana w init metoda drawImage aktywuje systemowy wątek animatora obrazu GIF. * Dla każdego kadru animacji, wątek ten wywołuje metodę imageUpdate. * Metoda initUpdate poleca odrębnemu wątkowi systemowemu wykonanie metody update. * Synchronizacja wątków systemowych jest niezbędna, gdyż zabezpiecza przed zmianą zawartości bufora obrazu podczas wykonywania metody update. Uwaga: Może się zdarzyć, chociaż jest to mało prawdopodobne, że nastąpi zagłodzenie wątku realizującego metodę update. Dojdzie do tego w jednoprocesorowym systemie ignorującym problem zagłodzenia, w którym wątek animatora ma bardzo wysoki priorytet i po zwolnieniu sekcji krytycznej nie dopuszcza do wykonania drugiego z wątków. Biorąc to pod uwagę, należałoby zastosować synchronizację za pomocą metod wait i notify. =============================================== import java.applet.Applet; import java.awt.*; import java.net.*; public class Master extends Applet { private String fileName = "Diablo.gif"; private Image image, buffer; private int w = 160, h = 160, x1 = 50, y1 = 0, x2 = 0, y2 = 50, d = 30; private Integer lock = new Integer(13); private Graphics mDC; public void init() { URL docBase = getDocumentBase(); image = getImage(docBase, fileName); MediaTracker tracker = new MediaTracker(this); tracker.addImage(image, 0); try { tracker.waitForID(0); } catch(InterruptedException e) { } buffer = createImage(w, h); mDC = buffer.getGraphics(); mDC.drawImage(image, 0, 0, this); } public void update(Graphics gDC) { synchronized(lock) { mDC.clearRect(0, 0, w, h); mDC.drawImage(image, 0, 0, this); mDC.setColor(Color.magenta); mDC.fillOval(x1, y1, d, d); mDC.setColor(Color.cyan); mDC.fillOval(x2, y2, d, d); gDC.drawImage(buffer, 0, 0, this); y1 = (y1+4) % h; x2 = (x2+4) % w; } } public void paint(Graphics gDC) { update(gDC); } public boolean imageUpdate(Image img, int flags, int x, int y, int w, int h) { synchronized(lock) { return super.imageUpdate(img, flags, x, y, w, h); } } } Jan Bielecki Rozbijanie plików GIF Następujący program rozwiązuje problem rozbicia animowanego pliku GIF na jego składowe. Jeśli zostanie wywołany z argumentami source target count na przykład c:\jbJava\DiabloX.gif Devil 12 to z pliku Diablo.gif wyjmie się nie więcej niż 12 klatek i pod nazwami DevilNNN.gif umieści w tym samym katalogu co plik źródłowy. Uwaga: Zaleca się usunięcie z pliku źródłowego polecenia zapętlenia (LOOP), gdyż w przeciwnym razie operacja rozbijania pliku nie zakończy się. Można do tego użyć programu Gif Construction Set. import java.awt.*; import java.awt.event.*; import java.net.URL; import java.io.*; import java.awt.image.*; import java.util.Hashtable; public class Main extends Frame { private static final String Target = "Target"; private static final int Limit = 1000; private static String source, target; private Image img, buffer; private int w, h; private static int limit; private int frameNo = 0, frameCount = 0; public static void main(String[] args) throws IOException { int count = args.length; if(count < 3) { System.out.println("Usage is: source target limit"); Toolkit.getDefaultToolkit().beep(); throw new IllegalArgumentException(); } source = args[0]; int index = source.lastIndexOf("/"); target = source.substring(0, index+1) + args[1] + ".gif"; try { limit = Integer.parseInt(args[2]); if(limit > Limit) limit = Limit; } catch(NumberFormatException e) { limit = Limit; } Main main = new Main("Extraction"); main.setSize(200, 200); main.setVisible(true); main.addWindowListener( new WindowAdapter() { public void windowClosing(WindowEvent evt) { Main main2 = (Main)evt.getSource(); main2.dispose(); System.exit(0); } } ); } public Main(String caption) throws IOException { super(caption); Toolkit kit = Toolkit.getDefaultToolkit(); URL url = new URL("file:/" + source); MediaTracker tracker = new MediaTracker(this); img = kit.getImage(url); tracker.addImage(img, 0); try { tracker.waitForID(0); } catch(InterruptedException e) { } w = img.getWidth(this); h = img.getHeight(this); } public void update(Graphics gDC) { if(frameNo >= limit) return; if(buffer == null) buffer = createImage(w, h); Graphics mDC = buffer.getGraphics(); super.update(mDC); Insets insets = getInsets(); int t = insets.top, l = insets.left; gDC.drawImage(buffer, l, t, this); FileOutputStream out = null; GifEncoder encoder = null; try { String no = "" + frameNo; if(frameNo < 100) no = "0" + no; if(frameNo++ < 10) no = "0" + no; out = new FileOutputStream( Target + no + ".gif" ); encoder = new GifEncoder(buffer, out); int[] pixels = new int [w * h]; PixelGrabber grabber = new PixelGrabber(buffer, 0, 0, w, h, true); try { grabber.grabPixels(); } catch(InterruptedException e) { } pixels = (int [])grabber.getPixels(); encoder.encodeStart(w, h); encoder.encodePixels(0, 0, w, h, pixels, 0, w); encoder.encodeDone(); out.close(); } catch(IOException e) { } } public void paint(Graphics gDC) { Insets insets = getInsets(); int t = insets.top, l = insets.left; gDC.drawImage(img, l, t, this); } public boolean imageUpdate(Image img, int flags, int x, int y, int w, int h) { if((flags & ALLBITS) != 0 || (flags & FRAMEBITS) != 0) frameCount++; else frameCount = 0; // Error or Abort return super.imageUpdate(img, flags, x, y, w, h); } } Jan Bielecki Wyświetlanie komponentów Komponent znajduje się w stanie właściwym albo niewłaściwym. Komponent w stanie niewłaściwym nie jest wyświetlany w ogóle albo jest wyświetlany niewłaściwie. Wykonanie metody invalidate powoduje wprowadzenie komponentu oraz wszystkich jego pojemników w stan niewłaściwy, a wykonanie metody validate powoduje wprowadzenie go w stan właściwy. Uwaga: Jeśli komponent jest pojemnikiem, to w stan właściwy są wprowadzane tylko te jego komponenty, które znajdują się w stanie niewłaściwym. Tuż przed pierwszym wyświetleniem, wszystkie komponenty są wprowadzane w stan właściwy. Dlatego w poprawnie napisanym programie użycie metod invalidate i validate jest konieczne tylko wówczas, gdy modyfikuje się komponenty pojemnika (np. zmienia czcionkę przycisku albo usuwa przycisk z pojemnika). Równie rzadko jak metody invalidate i validate używa się metody addNotify. Jej wywołanie powoduje utworzenie równorzędnika (peer) komponentu, związującego go z daną platformą. Uwaga: Wykreślacz do ramki można utworzyć dopiero po utworzeniu równorzędnika. Ponieważ odbywa się to niejawnie podczas wykonywania metody setVisible, więc jawne wywołanie metody addNotify jest na ogół zbyteczne. Następujący program, pokazany na ekranie Stan komponentów, posługuje się wszystkimi rozpatrzonymi metodami. Ich użycie jest niezbędne, ponieważ bez metody addNotify program wysyła wyjątek klasy NullPointerException, a bez metody invalidate nie wyświetla płótna.. Ekran Stan komponentów ### valid.gif import java.awt.*; import java.awt.event.*; public class Main extends Frame { public static void main(String[] args) { Main main = new Main("Frame"); } public Main(String caption) { super(caption); addWindowListener( new WindowAdapter() { public void windowClosing(WindowEvent evt) { System.exit(0); } } ); Panel panel = new MyPanel(); Canvas canvas = new MyCanvas(); setLayout(new FlowLayout()); add(panel); setSize(200, 200); addNotify(); // do utworzenia gDC Graphics gDC = panel.getGraphics(); setVisible(true); add(canvas); // uaktualnienie pojemnika invalidate(); validate(); gDC.drawString("Hello", 5, 10); } class MyPanel extends Panel { public MyPanel() { setBackground(Color.green); } public Dimension getPreferredSize() { return new Dimension(40, 40); } public Dimension getMinimumSize() { return getPreferredSize(); } } class MyCanvas extends Canvas { public MyCanvas() { setBackground(Color.red); } public Dimension getPreferredSize() { return new Dimension(40, 40); } public Dimension getMinimumSize() { return getPreferredSize(); } } } Przez zastosowanie prostych modyfikacji, z programu można usunąć metody invalidate, validate i addNotify. import java.awt.*; import java.awt.event.*; public class Main extends Frame { public static void main(String[] args) { Main main = new Main("Frame"); } public Main(String caption) { super(caption); addWindowListener( new WindowAdapter() { public void windowClosing(WindowEvent evt) { System.exit(0); } } ); Panel panel = new MyPanel(); Canvas canvas = new MyCanvas(); setLayout(new FlowLayout()); add(panel); add(canvas); setSize(200, 200); setVisible(true); // dopiero po obu add } class MyPanel extends Panel { public MyPanel() { setBackground(Color.green); } public void paint(Graphics gDC) { gDC.drawString("Hello", 5, 10); } public Dimension getPreferredSize() { return new Dimension(40, 40); } public Dimension getMinimumSize() { return getPreferredSize(); } } class MyCanvas extends Canvas { public MyCanvas() { setBackground(Color.red); } public Dimension getPreferredSize() { return new Dimension(40, 40); } public Dimension getMinimumSize() { return getPreferredSize(); } } } Jan Bielecki Dekodowanie obrazów Reprezentacja obrazów w pamięci zewnętrznej odbiega od ich reprezentacji w pamięci operacyjnej. W szczególności dotyczy to obrazów w formacie GIF. Przekształcenie obrazu z formatu zewnętrznego do wewnętrznego odbywa się za pomocą dekodera. Następujący program ilustruje użycie dekodera, który zapisany w pliku kolorowy kod paskowy przekształca w obiekt klasy Image. Definicję paska, składającego się z kolorowych prążków, należy umieścić w pliku z rozszerzeniem .bar. Przykładową definicję paska pokazano w tabeli Kod paskowy. Jak można się przekonać, pierwszy prążek jest czerwony, ma szerokość 5 pikseli i występuje po nim odstęp o szerokości 2 pikseli. Występujące w tabeli komentarze nie wchodzą w skład definicji paska. Tabela Kod paskowy ### 50 72 // rozmiary ramki 4 // liczba kolorów 255 255 0 // żółty 255 0 0 // czerwony 0 255 0 // zielony 0 0 255 // niebieski 9 // liczba prążków 1 5 2 // indeks koloru, szeerokość, odstęp 2 5 6 // j.w. 3 5 1 1 5 2 2 5 6 3 5 1 1 5 2 2 5 6 3 5 1 ### Następujący aplet, pokazany na ekranie Kody paskowe, ilustruje zasadę dekodowania obrazów. Ekran Kody paskowe ### stripes.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; import java.awt.image.*; import java.util.Vector; import java.io.*; public class Master extends Applet { protected String fileName = "BarCode.bar"; protected ColorModel model; protected byte[] pixels; protected BarDecoder barDecoder; class ImgPanel extends Panel { private Image img, memImg; private int w, h; private Graphics mDC; public ImgPanel(Image img, int w, int h) { this.img = img; this.w = w; this.h = h; } public Dimension getPreferredSize() { return new Dimension(w+2, h+2); } public void update(Graphics gDC) { if(memImg == null) { memImg = createImage(w, h); mDC = memImg.getGraphics(); } super.update(mDC); gDC.drawImage(memImg, 0, 0, null); } public void paint(Graphics gDC) { gDC.setClip(0, 0, w+2, h+2); gDC.drawImage(img, 1, 1, this); gDC.drawRect(0, 0, w+1, h+1); } } public void init() { FileInputStream inpF = null; try { inpF = new FileInputStream(fileName); } catch(IOException e) { e.printStackTrace(); } barDecoder = new BarDecoder(inpF); Image img = createImage(barDecoder); int w = img.getWidth(this), h = img.getHeight(this); final ImgPanel imgPanel; add(imgPanel = new ImgPanel(img, w, h)); } } class BarDecoder implements ImageProducer { // Format zestawu prążków // =================== // width height rozmiary obrazu // colors liczba kolorów // r g b składniki kolorów // . . . // . . . // bars liczba prążków // color thick space kolor grubość odstęp // . . . // Błędy formatu // ==================== // 1 Not a number Nie jest liczbą // 2 Number out of range Poza zakresem // 3 Stripe error Błąd wyglądu prążka // 4 Stripe too thick Za gruby prążek // 5 Stripes too thick Za gruby pasek protected StreamTokenizer tokens; protected IndexColorModel model; protected byte[] pixels; protected Vector consumers = new Vector(); protected int colors, bars, width, height, pos; protected byte[] reds, greens, blues; public BarDecoder(FileInputStream inpF) { Reader inp = new BufferedReader(new InputStreamReader(inpF)); tokens = new StreamTokenizer(inp); try { width = getInt(); height = getInt(); } catch(IOException e) { e.printStackTrace(); } pixels = new byte [width * height]; createModel(); try { createPixels(); } catch(IOException e) { e.printStackTrace(); } } public void startProduction(ImageConsumer consumer) { if(consumer == null) return; addConsumer(consumer); int count = consumers.size(); for(int i = 0; i < count ; i++) { consumer = (ImageConsumer)consumers.elementAt(i); sendPixels(consumer); consumers.removeElement(consumer); } } int getInt() throws IOException { if(tokens.nextToken() != StreamTokenizer.TT_NUMBER) throw new IOException("Format error1"); int value = (int)tokens.nval; if(value > 255 | value < 0) throw new IOException("Format error2"); return value; } void createModel() { int colors = 0; try { colors = getInt(); // liczba kolorów reds = new byte[colors]; greens = new byte [colors]; blues = new byte[colors]; for(int i = 0; i < colors ; i++) { reds[i] = (byte)getInt(); greens[i] = (byte)getInt(); blues[i] = (byte)getInt(); } } catch(IOException e) { e.printStackTrace(); } int bits = 0, colors2 = colors; while(colors2 != 0) { bits++; colors2 >>= 1; } model = new IndexColorModel(bits, colors, reds, greens, blues); } void createPixels() throws IOException { pos = 0; try { bars = getInt(); // liczba prążków for(int k = 0; k < bars ; k++) createStripe(); } catch(IOException e) { e.printStackTrace(); } if(pos != width * height) throw new IOException("Format error5"); } void createStripe() throws IOException { int color = getInt(), thick = getInt(), space = getInt(); if(color == 0 | space < 1) throw new IOException("Format error3"); if(pos + (thick+space)*width > width*height) throw new IOException("Format error4"); for(int t = 0; t < thick ; t++) for(int w1 = 0; w1 < width ; w1++) pixels[pos++] = (byte)color; for(int s = 0; s < space ; s++) for(int w2 = 0; w2 < width ; w2++) pixels[pos++] = 0; } void sendPixels(ImageConsumer consumer) { consumer.setDimensions(width, height); consumer.setColorModel(model); consumer.setHints( ImageConsumer.SINGLEPASS | ImageConsumer.SINGLEFRAME ); consumer.setPixels(0, 0, width, height, model, pixels, 0, width); consumer.imageComplete(ImageConsumer.STATICIMAGEDONE); } public synchronized boolean isConsumer(ImageConsumer consumer) { return consumers.contains(consumer); } public void requestTopDownLeftRightResend(ImageConsumer consumer) { consumer.setDimensions(width, height); consumer.setColorModel(model); consumer.setHints(ImageConsumer.SINGLEPASS | ImageConsumer.TOPDOWNLEFTRIGHT | ImageConsumer.SINGLEFRAME); consumer.setPixels(0, 0, width, height, model, pixels, 0, width); } public synchronized void addConsumer(ImageConsumer ic) { if(!consumers.contains(ic)) consumers.addElement(ic); } public synchronized void removeConsumer(ImageConsumer ic) { consumers.removeElement(ic); } } Jan Bielecki Używanie przyborników Osadzenie Maszyny Wirtualnej na nowej platformie wymaga implementowania metod zadeklarowanych w klasach pakietu java.awt.peer. Ich definicji dostarcza się w klasie przybornika. Wśród przyborników platformy jeden jest przybornikiem domyślnym. Odnośnik do niego otrzymuje się za pomocą statycznej metody getDefaultToolkit. Odnośniki do przyborników związanych z komponentami otrzymuje się za pomocą metody getToolkit klasy Component. Następujący program, pokazany na ekranie Przyborniki platformy, ilustruje użycie przybornika do określenia rozmiarów ekranu, wygenerowania dźwięku oraz pobrania obrazu z sieci. Program wymaga określenia parametru, np. Ship.gif. Ekran Przyborniki platformy ### toolkit.gif import java.awt.*; import java.awt.image.ImageObserver; import java.awt.event.*; import java.awt.geom.*; import java.awt.font.*; public class Main extends Frame { private String fileName; private Toolkit kit; private int scrW, scrH; private Image img; public static void main(String[] args) { Main main = new Main("Frame", args); } public Main(String caption, String[] args) { super(caption); addWindowListener( new WindowAdapter() { public void windowClosing(WindowEvent evt) { System.exit(0); } } ); kit = Toolkit.getDefaultToolkit(); Dimension dim = kit.getScreenSize(); scrW = dim.width; scrH = dim.height; setSize(scrW/4, scrH/4); setLocation(scrW/4, scrH/4); if(args.length != 1) error(1); // brak nazwy pliku fileName = args[0]; img = kit.getImage(fileName); if(img == null) error(2); // niepoprawna nazwa pliku kit.prepareImage(img, -1, -1, this); int status; // ładowanie obrazu // np. Bugs.gif (ALLBITS), Gears.gif (FRAMEBITS) while(((status = checkImage(img, this)) & (ImageObserver.ALLBITS | ImageObserver.FRAMEBITS)) == 0) if((status & ImageObserver.ERROR) != 0) error(3); // błąd ładowania obrazu Font font = new Font("Serif", Font.ITALIC, 60); addNotify(); Graphics2D gDC2 = (Graphics2D)getGraphics(); FontRenderContext frc = gDC2.getFontRenderContext(); TextLayout layout = new TextLayout(fileName, font, frc); Rectangle2D bounds = layout.getBounds(); int h = (int)bounds.getHeight(), a = -(int)bounds.getY(); // uniesienie Panel panel = new MyPanel(h); Canvas canvas = new MyCanvas(img); setLayout(new BorderLayout()); add(panel, BorderLayout.NORTH); add(canvas, BorderLayout.CENTER); setVisible(true); Graphics gDC = panel.getGraphics(); gDC.setFont(font); gDC.drawString(fileName, 0, a); } public void error(int errNo) { kit.beep(); System.out.println("Error no. " + errNo); for(long i = 0; i < 100000000 ; i++); System.exit(0); } class MyPanel extends Panel { private int h; public MyPanel(int h) { this.h = h; } public Dimension getPreferredSize() { return new Dimension(0, h); } public Dimension getMinimumSize() { return getPreferredSize(); } } class MyCanvas extends Canvas { private Image img; public MyCanvas(Image img) { this.img = img; } public void paint(Graphics gDC) { gDC.drawImage(img, 0, 0, this); } public Dimension getPreferredSize() { return new Dimension(0, img.getWidth(this)); } public Dimension getMinimumSize() { return getPreferredSize(); } } } Jan Bielecki Wymiarowanie apletów W opisie apletu należy podać parametry width i height, na podstawie których przeglądarka określi rozmiary apletu. Uwaga: Niektóre przeglądarki zezwalają na podanie rozmiarów 0 x 0, a następnie dokonanie ich zmiany już podczas wykonywania apletu. Ze względu na przenośność, nie zaleca się jednak takiego sposobu programowania. Następujące aplety, pokazane na ekranie Zmiana rozmiaru, napisano w taki sposób, że niezależnie od aktualnych rozmiarów okna przeglądarki, na pulpicie apletu jest zawsze wykreślany okrąg. Uwaga: Bezpośrednio po wywołaniu metody setSize jest ponownie wywoływana metoda paint. Ekran Zmiana rozmiaru ### resizer.gif Metoda paint =============================================== import java.applet.Applet; import java.awt.*; public class Master extends Applet { private int ww, hh; public void init() { String wp = getParameter("width"), hp = getParameter("height"); ww = Integer.parseInt(wp); hh = Integer.parseInt(hp); setBackground(Color.red); } public void paint(Graphics gDC) { Dimension dim = getSize(); int w = dim.width, h = dim.height; if(w != ww || h != hh) setSize(ww, hh); else { gDC.setColor(Color.white); gDC.fillOval(0, 0, w, h); } } } Zdarzenie component =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet { public void init() { setBackground(Color.red); class Resizer extends ComponentAdapter { private Applet applet; private Dimension d; public Resizer(Applet applet) { this.applet = applet; d = applet.getSize(); } public void componentResized(ComponentEvent evt) { if(!d.equals(applet.getSize())) applet.setSize(d); } } addComponentListener(new Resizer(this)); } public void paint(Graphics gDC) { Dimension dim = getSize(); int w = dim.width, h = dim.height; gDC.setColor(Color.white); gDC.fillOval(0, 0, w, h); } } Uwaga: Gdyby z programu usunięto wywołanie metody addComponentListener, to po zmianie rozmiaru okna przeglądarki mogłaby zostać wykreślona elipsa. Jan Bielecki Stosowanie modeli kolorów Modele kolorów dzielą się na bezpośrednie i pośrednie (indexed). W modelu bezpośrednim kolor piksela jest opisany przez przezroczystość (transparency) oraz składniki barwy: czerwony, zielony i niebieski. Typowy obraz o 256 kolorach i rozmiarach 100 x 100 pikseli wymaga 40,000 bajtów pamięci. W modelu pośrednim kolor piksela jest opisany przez indeks tabeli, w której znajdują się opisy kolorów. W szczególności, obraz o 256 kolorach i rozmiarach 100 x 100 pikseli wymaga tabeli na 256 kolorów (1 KB) i 10,000 indeksów, to jest 11,024 bajtów pamięci. Wynika stąd, że model indeksowy jest oszczędniejszy, ale ze względu na ograniczenie liczby kolorów (zazwyczaj do 256) uniemożliwia pełne wykorzystanie potencjału karty graficznej TrueColor. Formaty BMP/DIB Poza formatami GIF i JPEG, do najpowszechniej stosowanych formatów obrazu zalicza się BMP/DIB. W takim formacie, plik składa się z 4 elementów: opisu pliku (FileHeader), opisu obrazu (InfoHeader), tabeli kolorów i tabeli pikseli. Uwaga: Jeśli zastosowano model bezpośredni, to jest zbyteczna tabela kolorów. class FileHeader { byte bfTypeB = (byte)'B', // kod litery B bfTypeM = (byte)'M'; // kod litery M int bfSize; // rozmiar pliku short bfReserved1 = 0, // 0 bfReserved2 = 0; // 0 int bfOffBits; // adres pierwszego bajtu // tabeli pikseli (liczony // od początku pliku) } class InfoHeader { int biSize, // rozmiar struktury InfoHeader biWidth, // szerokość obrazu biHeight; // wysokość obrazu short biPlanes, // 1 (zawsze) biBitCount; // liczba bitów na piksel int biCompression, // 0 jeśli bez kompresji biSizeImage, // rozmiar tabeli pikseli biXPelsPerMeter, // liczba pikseli/m (x) biYPelsPerMeter, // liczba pikseli/m (y) biClrUsed, // liczba elem. tabeli kolorów biClrImportant // liczba użytych kolorów } W przypadku obrazu w modelu pośrednim, utworzonego za pomocą programu Paint Shop Pro 5.0 w trybie graficznym 256 kolorów, jest tworzony FileHeader (14 B), InfoHeader (40 B), 256-elementowa tabela kolorów i tabela pikseli, które dla obrazu z ekranu Obraz BMP/DIB pokazano w tabeli Struktura pliku BMP/DIB. Uwaga: Pogrubiono bajty BM, offset do tabeli pikseli oraz obie tabele. Ekran Obraz BMP/DIB ### bmpdib.gif Tabela Struktura pliku BMP/DIB ### 00000 42 4D 46 04 ¦ 00 00 00 00 ¦ 00 00 36 04 ¦ 00 00 28 00 00010 00 00 05 00 ¦ 00 00 02 00 ¦ 00 00 01 00 ¦ 08 00 00 00 00020 00 00 10 00 ¦ 00 00 00 00 ¦ 00 00 00 00 ¦ 00 00 00 01 00030 00 00 05 00 ¦ 00 00 00 00 ¦ FF 00 FF 00 ¦ 00 00 FF FF 00040 FF 00 00 00 ¦ 00 00 00 FF ¦ 00 00 00 00 ¦ 00 00 00 00 00050 00 00 00 00 ¦ 00 00 00 00 ¦ 00 00 00 00 ¦ 00 00 00 00 // ========================================================= 00400 00 00 00 00 ¦ 00 00 00 00 ¦ 00 00 00 00 ¦ 00 00 00 00 00410 00 00 00 00 ¦ 00 00 00 00 ¦ 00 00 00 00 ¦ 00 00 00 00 00420 00 00 00 00 ¦ 00 00 00 00 ¦ 00 00 00 00 ¦ 00 00 00 00 00430 00 00 00 00 ¦ 00 00 01 04 ¦ 04 03 02 00 ¦ 00 00 00 00 00440 01 02 03 00 ¦ 00 00 ### Tabela pikseli jest zbudowana w taki sposób, że najpierw występują w niej indeksy dotyczące najniższej linii obrazu, a dopiero po nich indeksy linii drugiej od dołu, itd. Ponadto, każda linia zajmuje wielokrotność 4 bajtów pamięci. Powoduje to, że pewne bity tabeli pikseli nie są wykorzystane. W szczególności, jeśli obraz o 256 kolorach ma rozmiary 5 x 2, to jego piksele układają się w prostokąt 0 1 2 3 4 5 6 7 8 9 ale jego tabela pikseli zajmuje 16 bajtów 5 6 7 8 9 x x x 0 1 2 3 4 x x x ułożonych w ciąg 5 6 7 8 9 x x x 0 1 2 3 4 x x x Następujący program ilustruje użycie modeli kolorów i sposoby przekształcania obrazów. Z obrazu zawartego w pliku Source.dib, utworzono obraz zapamiętany w pliku Source.db2, a z obrazu zawartego w pliku Source.gif utworzono taki sam obraz i zapamiętano go w pliku Source.bmp. Uwaga: Ponieważ w procesorach firmy Intel, 4-bajtowa zmienna typu int o bajtach abcd jest zachowywana w pamięci w kolejności dcba, do niektórych pól klas FileHeader i InfoHeader użyto funkcji pomocniczej m4 (a do zmiennych typu short funkcji m2). import java.awt.*; import java.awt.image.*; import java.io.*; public class Main extends Frame { protected String path = ".", name = "Source"; protected DibImage dibImg; protected Image img, memImg; protected PixelGrabber grab; protected MemoryImageSource imgSrc; protected byte[] gifVec; protected int[] rgbVec; public static void main(String[] args) { new Main("Colors"); } public Main(String caption) { super(caption); pack(); setVisible(true); dibImg = new DibImage(); // porządkowanie palety try { dibImg.readDib(path + '/' + name + ".dib"); int[] imgVec = dibImg.makeRGB(); int w = dibImg.dibWidth(), h = dibImg.dibHeight(); dibImg.makeDib(imgVec, w, h); dibImg.writeDib(path + name + ".db2"); dibImg.readDib(path + name + ".db2"); new ReportDib(dibImg); } catch(IOException e) { e.printStackTrace(); System.exit(0); } catch(DibImage.UnexpectedException e) { e.printStackTrace(); System.exit(0); } // wczytanie obrazu gif Toolkit kit = Toolkit.getDefaultToolkit(); img = kit.getImage(name + ".gif"); grab = new PixelGrabber( img, 0, 0, -1, -1, false ); try { grab.grabPixels(); } catch(InterruptedException e) { e.printStackTrace(); System.exit(0); } gifVec = (byte [])grab.getPixels(); int w = grab.getWidth(), h = grab.getHeight(); ColorModel model = grab.getColorModel(); imgSrc = new MemoryImageSource( w, h, model, gifVec, 0, w ); imgSrc.setAnimated(true); memImg = createImage(imgSrc); // konwersja obrazu z modelu // pośredniego na bezpośredni int len = gifVec.length, rgb; rgbVec = new int[len]; for(int i = 0; i < len ; i++) { rgb = model.getRGB(gifVec[i] & 0xff); rgbVec[i] = rgb; } // utworzenie pliku z obrazem BMP/DIB dibImg.makeDib(rgbVec, w, h); try { dibImg.writeDib(path + '/' + name + ".bmp"); } catch(IOException e) { e.printStackTrace(); System.exit(0); } catch(DibImage.UnexpectedException e) { e.printStackTrace(); System.exit(0); } } public void paint(Graphics gDC) { gDC.drawImage(memImg, 0, 0, this); } } class DibImage { class UnexpectedException extends Exception { protected String reason; public UnexpectedException(String reason) { this.reason = reason; } } class FileHeader { protected byte bfTypeB = (byte)'B', bfTypeM = (byte)'M'; protected int bfSize = 0; protected short bfReserved1 = 0, bfReserved2 = 0; protected int bfOffBits = 0; public void readHead(DataInputStream inp) throws IOException, UnexpectedException { int fileSize = inp.available(); if(fileSize < 14 + 16) throw new UnexpectedException("Format1"); bfTypeB = inp.readByte(); bfTypeM = inp.readByte(); bfSize = m4(inp.readInt()); if(bfTypeB != 'B' || bfTypeM != 'M' || bfSize != fileSize) throw new UnexpectedException("Format2"); inp.skipBytes(4); // reserved bfOffBits = m4(inp.readInt()); } public void writeHead(DataOutputStream out) throws IOException { out.writeByte('B'); out.writeByte('M'); out.writeInt(m4(bfSize)); out.writeShort(0); // m2 out.writeShort(0); // m2 out.writeInt(m4(bfOffBits)); } public int sizeof() { return 14; } } class InfoHeader { protected int biSize = 40, biWidth = 0, biHeight = 0; protected short biPlanes = 1, biBitCount = 8; protected int biCompression = 0, biSizeImage = 0, biXPelsPerMeter = 0, biYPelsPerMeter = 0, biClrUsed = 0, biClrImportant = 0; public InfoHeader(int w, int h) { biWidth = w; biHeight = h; } void readInfo(DataInputStream inp) throws IOException, UnexpectedException { biSize = m4(inp.readInt()); biWidth = m4(inp.readInt()); biHeight = m4(inp.readInt()); biPlanes = m2(inp.readShort()); biBitCount = m2(inp.readShort()); biCompression = m4(inp.readInt()); if(biCompression != 0) throw new UnexpectedException("Compressed"); inp.skipBytes(12); biClrUsed = m4(inp.readInt()); biClrImportant = m4(inp.readInt()); } public void writeInfo(DataOutputStream out) throws IOException { out.writeInt(m4(biSize)); out.writeInt(m4(biWidth)); out.writeInt(m4(biHeight)); out.writeShort(m2(biPlanes)); out.writeShort(m2(biBitCount)); out.writeInt(0); // Compression out.writeInt(m4(biSizeImage)); out.writeInt(0); // XPelsPerMeter out.writeInt(0); // YPelsPerMeter out.writeInt(m4(biClrUsed)); out.writeInt(m4(biClrImportant)); } public int sizeof() { return 40; } } class Quad { protected int pixel; public void readQuad(DataInputStream inp) throws IOException { // w pliku BMP/DIB kolor rgb jest // przechowywany w kolejności bgr0 (sic!) int b = inp.readByte() & 0xff, g = inp.readByte() & 0xff, r = inp.readByte() & 0xff; inp.skipBytes(1); pixel = 0xff000000 | r << 16 | g << 8 | b; } public void writeQuad(DataOutputStream out) throws IOException { byte b = (byte)pixel, g = (byte)(pixel >> 8), r = (byte)(pixel >> 16); out.writeByte(b); out.writeByte(g); out.writeByte(r); out.writeByte(0); } public int getRed() { return (pixel >> 16) & 0xff; } public int getGreen() { return (pixel >> 8) & 0xff; } public int getBlue() { return pixel & 0xff; } } private FileHeader head; private InfoHeader info; private Quad[] quads; private byte[] bits; private int skip; private boolean dibLoaded = false; public FileHeader dibHead() { return head; } public InfoHeader dibInfo() { return info; } public Quad[] dibQuads() { return quads; } public byte[] dibBits() { return bits; } public int dibWidth() { return info.biWidth; } public int dibHeight() { return info.biHeight; } public int dibNoOfColors() { return info.biClrImportant; } public int dibBitsSize() { return info.biSizeImage; } public int dibSize() { return head.bfSize; } public int dibIndex(int x, int y) { int scan = info.biWidth + skip; return bits[(info.biHeight-1 - y) * scan + x] & 0xff; } public int dibSkip() { return skip; } public void readDib(String fileName) throws IOException, UnexpectedException { FileInputStream inpF = new FileInputStream(fileName); DataInputStream inp = new DataInputStream(inpF); head = new FileHeader(); head.readHead(inp); info = new InfoHeader(0, 0); info.readInfo(inp); int width = info.biWidth, height = info.biHeight; skip = (width + 3) / 4 * 4 - width; info.biSizeImage = (width + skip) * height; int quadsSize = head.bfSize - head.sizeof() - info.biSize - info.biSizeImage; info.biClrUsed = quadsSize / 4; quads = readQuads(inp, info.biClrUsed); bits = readBits(inp, info.biSizeImage); inp.close(); dibLoaded = true; } private Quad[] readQuads(DataInputStream inp, int count) throws IOException { Quad[] quads = new Quad[count]; for(int i = 0; i < count ; i++) { quads[i] = new Quad(); quads[i].readQuad(inp); } return quads; } private void writeQuads(DataOutputStream out, Quad[] quads) throws IOException { int count = quads.length; // 256 for(int i = 0; i < count ; i++) { quads[i].writeQuad(out); } } private byte[] readBits(DataInputStream inp, int count) throws IOException { byte[] bits = new byte[count]; inp.read(bits); return bits; } private void writeBits(DataOutputStream out, byte[] bits) throws IOException { out.write(bits); } public void writeDib(String fileName) throws IOException, UnexpectedException { if(!dibLoaded) throw new UnexpectedException("DibNotLoaded"); FileOutputStream outF = new FileOutputStream(fileName); DataOutputStream out = new DataOutputStream(outF); head.writeHead(out); info.writeInfo(out); writeQuads(out, quads); writeBits (out, bits); out.close(); } public int[] makeRGB() { int w = dibWidth(), h = dibHeight(); int[] imgVec = new int [w * h]; Quad[] quads = dibQuads(); int index, pos = 0; for(int y = 0; y < h ; y++) for(int x = 0; x < w ; x++) { index = dibIndex(x, y); imgVec[pos++] = quads[index].pixel; } return imgVec; } public void makeDib(int[] imgVec, int w, int h) { skip = (w + 3) / 4 * 4 - w; head = new FileHeader(); info = new InfoHeader(w, h); int quadsSize = 256 * 4, // largest possible bitsSize = (w + 3) / 4 * 4 * h; info.biSizeImage = bitsSize; quads = new Quad [256]; for(int i = 0; i < 256 ; i++) quads[i] = new Quad(); bits = new byte [bitsSize]; info.biClrUsed = 256; info.biClrImportant = create(imgVec, w, h, quads, bits); head.bfSize = head.sizeof() + info.sizeof() + quadsSize + bitsSize; head.bfOffBits = head.sizeof() + info.sizeof() + quadsSize; dibLoaded = true; } private int create(int[] imgVec, int w, int h, Quad[] quads, byte[] bits) { int noOfColors = 0; int pixel; int c, pos = (w + skip) * (h-1), p = 0; for(int y = 0; y < h ; y++) { for(int x = 0; x < w; x++) { pixel = imgVec[p++]; for(c = 0; c < noOfColors ; c++) { if(quads[c].pixel == pixel) break; } if(c == noOfColors) { quads[c].pixel = pixel; c = noOfColors++; } bits[pos++] = (byte)c; } pos -= (w<<1) + skip; } return noOfColors; } private int rgbPixel(int r, int g, int b) { r &= 0xff; g &= 0xff; b &= 0xff; return 0xff000000 | r << 16 | g << 8 | b; } private int getRed(int pixel) { return pixel >> 16 & 0xff; } private int getGreen(int pixel) { return pixel >> 8 & 0xff; } private int getBlue(int pixel) { return pixel & 0xff; } static short m2(short v) { return (short)(v << 8 | v >> 8 & 0xff); } static int m4(int v) { return v << 24 | (v & 0xff00) << 8 | (v & 0xff0000) >> 8 | v >> 24; } } class ReportDib { protected DibImage img; public ReportDib(DibImage img) { this.img = img; showHead(img); showInfo(img); showQuads(img); showBits(img); } public String hex(int v) { return Integer.toHexString(v); } public void show(String string) { // Debug.toFrame(string); System.out.println(string); } public void showHead(DibImage img) { show("FileHeader\n=========="); DibImage.FileHeader head = img.dibHead(); show("FileSize = 0x" + hex(head.bfSize)); show("BitsOffset = 0x" + hex(head.bfOffBits)); show(""); } public void showInfo(DibImage img) { show("InfoHeader\n=========="); DibImage.InfoHeader info = img.dibInfo(); show("Width = 0x" + hex(info.biWidth)); show("Height = 0x" + hex(info.biHeight)); show("Colors = 0x" + hex(info.biClrUsed)); show(""); } public void showQuads(DibImage img) { show("Quads\n====="); DibImage.InfoHeader info = img.dibInfo(); int count = img.dibNoOfColors(); DibImage.Quad quads[] = img.dibQuads(), quad; for(int i = 0; i < count ; i++) { quad = quads[i]; int r = quad.getRed(), g = quad.getGreen(), b = quad.getBlue(); show("RGB(" + r + "," + g + "," + b + ")"); } show(""); } public void showBits(DibImage img) { show("AllBits\n======="); int width = img.dibWidth(), height = img.dibHeight(), skip = img.dibSkip(); byte[] bits = img.dibBits(); int count = img.dibBitsSize(), index; String indexes = ""; for(int i = 0; i < count ; ) { index = (int)bits[i] & 0xff; indexes += align(index) + " "; if((++i % (width + skip)) == 0) indexes += "\n"; } show(indexes); show("Indexes\n======="); indexes = ""; for(int y = 0; y < height ; y++) { for(int x = 0; x < width ; x++) { index = img.dibIndex(x, y); indexes += align(index) + " "; } indexes += "\n"; } show(indexes); } public String align(int v) { if(v < 10) return " " + v; if(v < 100) return " " + v; return "" + v; } } Jan Bielecki Interesujące przypadki Większość programistów zgodzi się zapewne z poglądem, że błąd występujący w programie jest interesujący tylko do chwili jego znalezienia. Tuż po tym okazuje się banalny i aż dziw bierze, że można go było popełnić. Jednak od czasu do czasu zdarzają się błędy, które można uznać za ciekawe. Pozostają takimi, ponieważ zwracają uwagę na powiązania, które są mało oczywiste, ale ważne. A jako takie są warte przemyślenia. Każdy z następujących programów zawiera jeden ciekawy błąd. Programy maksymalnie uproszczono, dla każdego podając efekt zamierzony i uzyskany. Poprawienie programu pozostawia się Czytelnikowi. Wykreślanie krzywych Program miał służyć do wykreślania rozłącznych krzywych. Wykreślanie miało się odbywać przez przeciąganie myszki. Wbrew zamierzeniom, program łączy wykreślane krzywe. Przyczyna tkwi w wierszach pogrubionych. Na ekranie Zamiar pokazano jak program miał działać, a na ekranie Skutek pokazano jak działa. Ekran Zamiar ### curves1.gif Ekran Skutek ### curves2.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet { protected Graphics gDC; public void init() { gDC = getGraphics(); addMouseListener(new MouseWatcher()); addMouseMotionListener(new MouseWatcher()); } class MouseWatcher extends MouseAdapter implements MouseMotionListener { protected Point oldPoint; public void mousePressed(MouseEvent evt) { int x = evt.getX(), y = evt.getY(); oldPoint = new Point(x, y); } public void mouseDragged(MouseEvent evt) { int x = evt.getX(), y = evt.getY(); Point newPoint = new Point(x, y); if(oldPoint == null) oldPoint = newPoint; gDC.drawLine( oldPoint.x, oldPoint.y, newPoint.x, newPoint.y ); oldPoint = newPoint; } public void mouseMoved(MouseEvent evt) { } } } Animowanie kół Program miał służyć do animowania zestawu kół, każdego w innym kolorze. Wbrew zamierzeniom, program w istotny sposób zależy od wartości argumentów metod sleep. W pewnych przypadkach (np. dla sleep(0)) System przerywa wykonanie programu. Błąd ujawnia się podczas wykonania jednego z wierszy pogrubionych. Na ekranie Zamiar pokazano jak program miał działać, a na ekranie Skutek pokazano jak działa. Ekran Zamiar ### animate1.gif Ekran Skutek ### animate2.gif =============================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class Master extends Applet implements Runnable { protected Graphics gDC; protected int w, h, x, y; protected Color color = Color.black; protected Color colors[] = { Color.red, Color.green, Color.blue, Color.cyan, Color.magenta, Color.pink }; public void init() { gDC = getGraphics(); w = getSize().width; h = getSize().height; x = w/2; y = h/2; synchronized(color) { for(int i = 0; i < colors.length ; i++) { try { Thread.sleep(10); } catch(InterruptedException e) { } x = 25 * (i+1); y = 25 * (i+1); color = colors[i]; new Thread(this).start(); } } } public void run() { Color color = this.color; int x = this.x, y = this.y; int dx = 1, r = 10; while(true) { gDC.setColor(color); gDC.drawOval(x-r, y-r, 2*r-1, 2*r-1); try { Thread.sleep(5); } catch(InterruptedException e) { } gDC.setColor(Color.white); gDC.drawOval(x-r, y-r, 2*r-1, 2*r-1); x += dx; if(x < r || x > w-r) dx = -dx; } } } Jan Bielecki Narzędzia pakietu JDK Pakiet JDK składa się z programów usługowych, umożliwiających m.in. kompilowanie i wykonywanie programów. Kompilator Kompilatorem programów źródłowych jest javac. Wywołuje się go za pomocą polecenia javac options files w którym mogą wystąpić opcje kompilacji oraz musi wystąpić co najmniej jedna nazwa pliku źródłowego, na przykład javac C:\jbJava\Hello\Master.java Uwaga: W nazwie pliku muszą być użyte ukośniki (\). Nazwa pliku musi zawierać rozszerzenie .java. Ważniejsze opcje -classpath path;path; ... path Określa ścieżki do katalogów i plików .zip, zawierających pliki .class. Jeśli opcji nie użyto, to domniemywa się ją ze ścieżkami wziętymi z parametru środowiska classpath. -d directory Określa nazwę katalogu do umieszczenia hierarchicznie uporządkowanych plików .class. Jeśli jej nie użyto, to pliki *.class są umieszczane w tym samym katalogu, co kompilowane pliki źródłowe, nawet jeśli zdefiniowane w nich klasy nie należą do pakietu domyślnego. -deprecation Do generowania informacji o użyciu przestarzałych API języka. Przeglądarka Przeglądarką pakietu JDK jest appletviewer. Wywołuje się ją za pomocą polecenia appletviewer url w którym url jest lokalizatorem pliku HTML zawierającego opis apletu. Przeglądarkę można wywołać z okna DOS albo za pomocą polecenia Start / Run, na przykład h:\jdk116\bin\appletviewer file:/c:/jbJava/Test/Master.html albo appletviewer -J-cp . -J-Djava.compiler=NONE Master.html Uwaga: Należy zwrócić uwagę na sposób użycia ukośników (\) i skośników (/). Jeśli aplet korzysta z klas niestandardowych, to ścieżkę do ich katalogu należy określić za pomocą polecenia set classpath=path;path; ... path Polecenie to umieszcza się zazwyczaj w pliku autoexec.bat. Interpreter Interpreterem B-kodu jest program java. Wywołuje się go za pomocą polecenia java options classname arguments W poleceniu można podać opcje interpretacji, należy podać nazwę pliku z rozszerzeniem .class oraz można podać argumenty aplikacji, na przykład java -classpath Master Uwaga: Nazwa classname nie może zawierać rozszerzenia .class. Ważna opcja -classpath path;path; ... path Określa ścieżki do katalogów i plików .zip, zawierających pliki .class. Jeśli opcji nie użyto, to domniemywa się ją ze ścieżkami wziętymi z parametru środowiska classpath. Uwaga: Jeśli nie użyto opcji classpath ani parametru classpath, to plików .class poszukuje się w pliku classes.zip, znajdującym się w katalogu instalacyjnym pakietu JDK. Jan Bielecki Studium programowe Następujący program, pokazany na ekranie Współbieżność i animacja, podsumowuje zasady programowania obiektowego, graficznego i współbieżnego. Ekran Współbieżność i animacja ### concur.gif Po naciśnięciu przycisku Start, każde kliknięcie na pulpicie apletu powoduje wykreślenie koła (lewy przycisk) albo kwadratu (prawy przycisk), a następnie utworzenie niezależnego wątku animującego wykreśloną figurę. Po naciśnięciu przycisku Stop, kliknięcie w obrębie figury powoduje usunięcie jej z pulpitu. Uwaga: Położono nacisk na synchronizację wątków. Jest ona dość kosztowna. ========================================== import java.applet.Applet; import java.awt.*; import java.awt.event.*; import java.util.*; import java.net.*; public class Master extends Applet implements Runnable, ActionListener { private int deltaX, // krok poziomy deltaY; // krok pionowy private final int c = 20, s = (int)(20 / 1.2); private Color seeThru = new Color(230, 230, 230); private Color backColor = new Color(244, 244, 0); private Applet applet; private boolean appletStarted = false; private Image image; private Vector vector; private Toolkit toolkit; private Thread drawer; private Client client; private int w, h; private Image buffer; private Button stopper; private boolean afterPaint = false; // paintLock blokuje jednoczesny dostęp // do afterPaint w run i paint private Boolean paintLock = new Boolean(false); private boolean allStopped = true, stoppingThreads = false, runsStopped = true, drawStopped = true; // stoppingLock blokuje jednoczesny dostęp // do stoppingThreads po naciśnięciu na Stop private Boolean stoppingLock = new Boolean(false); private int tally = 0; public void init() { deltaX = Integer.parseInt(getParameter("dx")); deltaY = Integer.parseInt(getParameter("dy")); applet = this; class Resizer extends ComponentAdapter { private Applet applet; private Dimension d; public Resizer(Applet applet) { this.applet = applet; d = applet.getSize(); } public void componentResized(ComponentEvent evt) { if(!d.equals(applet.getSize())) { applet.resize(d); applet.validate(); } } } addComponentListener(new Resizer(applet)); MediaTracker tracker = new MediaTracker(this); URL where = getDocumentBase(); String file = getParameter("background"); image = getImage(where, file); if(image != null) { tracker.addImage(image, 0); try { tracker.waitForID(0); } catch(InterruptedException e) { } } vector = new Vector(); toolkit = Toolkit.getDefaultToolkit(); setLayout(new BorderLayout()); stopper = new Button("Start"); add(stopper, BorderLayout.NORTH); stopper.addActionListener(this); client = new Client(); add(client, BorderLayout.CENTER); Watcher watcher = new Watcher(client); client.addMouseListener(watcher); drawer = new Thread(this); drawer.start(); } public void actionPerformed(ActionEvent evt) { String label = stopper.getLabel(); if(label.equals("Start")) { appletStarted = true; synchronized(stoppingLock) { stoppingThreads = false; allStopped = false; stoppingLock.notifyAll(); stopper.setLabel("Stop"); stopper.setEnabled(true); applet.showStatus("Animation started"); } } else { // naciśnięcie Stop zmienia go w Wait // az do zatrzymania animacji kół appletStarted = false; applet.showStatus("Stopping animation ..."); synchronized(stoppingLock) { stopper.setEnabled(false); stopper.setLabel("Wait"); stoppingThreads = true; drawStopped = false; tally = vector.size(); runsStopped = tally == 0; } while(!(allStopped = drawStopped && runsStopped)) { try { Thread.sleep(100); } catch(InterruptedException e) { return; } } stopper.setLabel("Start"); stopper.setEnabled(true); applet.showStatus("Animation stopped"); } } public void run() // wątek wykreślający koła { // wstrzymanie wątku do chwili // wykonania metody paint apletu synchronized(paintLock) { if(!afterPaint) try { paintLock.wait(); } catch(InterruptedException e) { return; } } while(true) { // wstrzymanie wykreślania // po naciśnięciu na Stop synchronized(stoppingLock) { try { if(stoppingThreads) { drawStopped = true; stoppingLock.wait(); } } catch(InterruptedException e) { return;//? } } // wykreślanie z klonu wektora // (klonowanie wyklucza addElement, // bo obie metody są synchronizowane) Vector vector2 = (Vector)vector.clone(); Enumeration scan = vector2.elements(); Graphics mDC = buffer.getGraphics(); if(image != null) { // dla tła przezroczystego mDC.setColor(seeThru); mDC.fillRect(0, 0, w, h); // dla tła nieprzezroczystego mDC.drawImage(image, 0, 0, w, h, this); } else { // jeśli nie zdefiniowano tła mDC.setColor(backColor); mDC.fillRect(0, 0, w, h); } while(scan.hasMoreElements()) { Object obj = scan.nextElement(); Drawable drw = (Drawable)obj; drw.draw(mDC); } // brak dispose szkodzi wykonaniu mDC.dispose(); Graphics gDC = client.getGraphics(); gDC.drawImage(buffer, 0, 0, this); gDC.dispose(); try { drawer.sleep(50); } catch(InterruptedException e) { break; } } } public void start() { if (!drawer.isAlive()) { drawer = new Thread(this); drawer.start(); } } public void stop() { // zaniechanie wykreślania drawer.interrupt(); try { drawer.join(); } catch(InterruptedException e) { } // zaniechanie animowania int count = vector.size(); for(int i = 0; i < count ; i++) ((Figure)vector.elementAt(i)).stopThread(); vector.removeAllElements(); } class Client extends Panel { public void update(Graphics gDC) { Vector vector2 = (Vector)vector.clone(); Graphics mDC = buffer.getGraphics(); // wykreślanie tła if(image != null) { // dla tła przezroczystego mDC.setColor(seeThru); // dla tła nieprzezroczystego mDC.fillRect(0, 0, w, h); mDC.drawImage(image, 0, 0, w, h, this); } else { // jeśli nie zdefiniowano tła mDC.setColor(backColor); mDC.fillRect(0, 0, w, h); } Enumeration scan = vector2.elements(); while(scan.hasMoreElements()) { Object obj = scan.nextElement(); Drawable drw = (Drawable)obj; drw.draw(mDC); } mDC.dispose(); gDC.drawImage(buffer, 0, 0, this); } public void paint(Graphics gDC) { if(buffer == null) { Dimension d = getSize(); w = d.width; h = d.height; buffer = createImage(w, h); } synchronized(paintLock) { afterPaint = true; paintLock.notifyAll(); } update(gDC); } } class Watcher extends MouseAdapter { private Panel client; private boolean rClick = false; public Watcher(Panel client) { this.client = client; } public void mousePressed(MouseEvent evt) { rClick = evt.isMetaDown(); } public void mouseReleased(MouseEvent evt) { if(!appletStarted && vector.size() == 0) { toolkit.beep(); return; } int x = evt.getX(), y = evt.getY(); synchronized(stoppingLock) { if(!allStopped) { if(stoppingThreads) { // zignorowanie wykreślania toolkit.beep(); return; } Figure figure; if(!rClick) { // wykreślenie okręgu if(x < c || yw-c || y>h-c) { // zignorowanie toolkit.beep(); return; } figure = new Circle(x, y, c, client); } else { // wykreślenie kwadratu if(x < s|| yw-s || y>h-s) { // zignorowanie toolkit.beep(); return; } figure = new Square(x, y, s, client); } vector.addElement(figure); rClick = false; } else { // usunięcie figury Graphics gDC = client.getGraphics(); int count = vector.size(); for(int i = count-1; i >= 0 ; i--) { Object obj = vector.elementAt(i); Drawable drw = (Drawable)obj; if(drw.isInside(x, y)) { Figure figure = (Figure)obj; figure.stopThread(); vector.removeElementAt(i); Shape shape = figure.getBounds(); gDC.setClip(shape); client.update(gDC); break; } } gDC.dispose(); } } } } abstract class Figure implements Runnable { protected int visit = 2; protected long time; protected int x, y, r; protected int dx = deltaX, dy = deltaY; protected Thread thread; protected Color color; public Figure(int x, int y) { this.x = x; this.y = y; thread = new Thread(this); color = new Color(x%256, y%256, (x+y)%256); } public Rectangle getBounds() { return new Rectangle(x-r-1, y-r-1, (r<<1)+2, (r<<1)+2); } public void run() { Dimension d = client.getSize(); int w = d.width, h = d.height; while(true) { synchronized(stoppingLock) { // wstrzymanie animacji // po kliknięciu na Stop if(stoppingThreads) { runsStopped = --tally == 0; try { stoppingLock.wait(); } catch(InterruptedException e) { break; } } } x += dx; y += dy; if(x-r < 0 || x+r > w) dx = -dx; if(y-r < 0 || y+r > h) dy = -dy; try { thread.sleep(150); } catch(InterruptedException e) { break; } } } public void stopThread() { thread.interrupt(); try { thread.join(2000); } catch(InterruptedException e) { } } } interface Drawable { public void draw(Graphics gDC); public boolean isInside(int x, int y); } class Circle extends Figure implements Drawable { private Panel client; public Circle(int x, int y, int r, Panel c) { super(x, y); super.r = r; client = c; thread.start(); } public void draw(Graphics gDC) { gDC.setColor(color); gDC.setClip( x-r-1, y-r-1, (r<<1)+2, (r<<1)+2); gDC.fillOval(x-r, y-r, r<<1, r<<1); gDC.setColor(Color.black); gDC.drawOval(x-r-1, y-r-1, r<<1, r<<1); } public boolean isInside(int x, int y) { int dx = x - this.x, dy = y - this.y; return dx*dx + dy*dy < r*r; } } class Square extends Figure implements Drawable { private Panel client; public Square(int x, int y, int r, Panel c) { super(x, y); super.r = r; client = c; thread.start(); } public void draw(Graphics gDC) { gDC.setColor(color); gDC.setClip( x-r-1, y-r-1, (r<<1)+2, (r<<1)+2); gDC.fillRect(x-r, y-r, r<<1, r<<1); gDC.setColor(Color.black); gDC.drawRect(x-r-1, y-r-1, r<<1, r<<1); } public boolean isInside(int x, int y) { int dx = x - this.x, dy = y - this.y; return Math.abs(dx) < r && Math.abs(dy) < r; } } } Jan Bielecki Dodatki Jan Bielecki Dodatek A Priorytety operatorów Priorytet Wiązanie Operator 1 lewe ++ -- (następnikowe) 2 prawe ++ -- (poprzednikowe) 3 prawe + - ~ ! (Type) 4 lewe * / % 5 lewe + - 6 lewe << >> >>> 7 lewe < <= > >= instanceof 8 lewe == != 9 lewe & 10 lewe ^ 11 lewe | 12 lewe && 13 lewe || 14 prawe ?: 15 prawe = *= /= %= += -= <<= >>= >>>= &= ^= |= Jan Bielecki Dodatek B Definicje stałych Zastosowano następujące skróty psf = public static final *sf = private static final ========== ActionEvent public class ActionEvent extends AWTEvent { psf int SHIFT_MASK = Event.SHIFT_MASK; psf int CTRL_MASK = Event.CTRL_MASK; psf int META_MASK = Event.META_MASK; psf int ALT_MASK = Event.ALT_MASK; psf int ACTION_FIRST = 1001; psf int ACTION_LAST = 1001; psf int ACTION_PERFORMED = ACTION_FIRST; } === (ActionEvent) ========== AdjustmentEvent public class AdjustmentEvent extends AWTEvent { psf int ADJUSTMENT_FIRST = 601; psf int ADJUSTMENT_LAST = 601; psf int ADJUSTMENT_VALUE_CHANGED = ADJUSTMENT_FIRST; psf int UNIT_INCREMENT = 1; psf int UNIT_DECREMENT = 2; psf int BLOCK_DECREMENT = 3; psf int BLOCK_INCREMENT = 4; psf int TRACK = 5; } === (AdjustmentEvent) ========== Color public final class Color { psf Color white = new Color(255, 255, 255); psf Color lightGray = new Color(192, 192, 192); psf Color gray = new Color(128, 128, 128); psf Color darkGray = new Color(64, 64, 64); psf Color black = new Color(0, 0, 0); psf Color red = new Color(255, 0, 0); psf Color pink = new Color(255, 175, 175); psf Color orange = new Color(255, 200, 0); psf Color yellow = new Color(255, 255, 0); psf Color green = new Color(0, 255, 0); psf Color magenta = new Color(255, 0, 255); psf Color cyan = new Color(0, 255, 255); psf Color blue = new Color(0, 0, 255); *sf double FACTOR = 0.7; } === (Color) ========== ComponentEvent public class ComponentEvent extends AWTEvent { psf int COMPONENT_FIRST = 100; psf int COMPONENT_LAST = 103; psf int COMPONENT_MOVED = COMPONENT_FIRST; psf int COMPONENT_RESIZED = 1 + COMPONENT_FIRST; psf int COMPONENT_SHOWN = 2 + COMPONENT_FIRST; psf int COMPONENT_HIDDEN = 3 + COMPONENT_FIRST; } === (ComponentEvent) ========== ContainerEvent public class ContainerEvent extends ComponentEvent { psf int CONTAINER_FIRST = 300; psf int CONTAINER_LAST = 301; psf int COMPONENT_ADDED = CONTAINER_FIRST; psf int COMPONENT_REMOVED = 1 + CONTAINER_FIRST; } === (ContainerEvent) ========== Event public class Event { psf int SHIFT_MASK = 1 << 0; psf int CTRL_MASK = 1 << 1; psf int META_MASK = 1 << 2; psf int ALT_MASK = 1 << 3; psf int HOME = 1000; psf int END = 1001; psf int PGUP = 1002; psf int PGDN = 1003; psf int UP = 1004; psf int DOWN = 1005; psf int LEFT = 1006; psf int RIGHT = 1007; psf int F1 = 1008; psf int F2 = 1009; psf int F3 = 1010; psf int F4 = 1011; psf int F5 = 1012; psf int F6 = 1013; psf int F7 = 1014; psf int F8 = 1015; psf int F9 = 1016; psf int F10 = 1017; psf int F11 = 1018; psf int F12 = 1019; *sf int WINDOW_EVENT = 200; psf int WINDOW_DESTROY = 1 + WINDOW_EVENT; psf int WINDOW_EXPOSE = 2 + WINDOW_EVENT; psf int WINDOW_ICONIFY = 3 + WINDOW_EVENT; psf int WINDOW_DEICONIFY = 4 + WINDOW_EVENT; psf int WINDOW_MOVED = 5 + WINDOW_EVENT; *sf int KEY_EVENT = 400; psf int KEY_PRESS = 1 + KEY_EVENT; psf int KEY_RELEASE = 2 + KEY_EVENT; psf int KEY_ACTION = 3 + KEY_EVENT; psf int KEY_ACTION_RELEASE = 4 + KEY_EVENT; *sf int MOUSE_EVENT = 500; psf int MOUSE_DOWN = 1 + MOUSE_EVENT; psf int MOUSE_UP = 2 + MOUSE_EVENT; psf int MOUSE_MOVE = 3 + MOUSE_EVENT; psf int MOUSE_ENTER = 4 + MOUSE_EVENT; psf int MOUSE_EXIT = 5 + MOUSE_EVENT; psf int MOUSE_DRAG = 6 + MOUSE_EVENT; *sf int SCROLL_EVENT = 600; psf int SCROLL_LINE_UP = 1 + SCROLL_EVENT; psf int SCROLL_LINE_DOWN = 2 + SCROLL_EVENT; psf int SCROLL_PAGE_UP = 3 + SCROLL_EVENT; psf int SCROLL_PAGE_DOWN = 4 + SCROLL_EVENT; psf int SCROLL_ABSOLUTE = 5 + SCROLL_EVENT; *sf int LIST_EVENT = 700; psf int LIST_SELECT = 1 + LIST_EVENT; psf int LIST_DESELECT = 2 + LIST_EVENT; *sf int MISC_EVENT = 1000; psf int ACTION_EVENT = 1 + MISC_EVENT; psf int LOAD_FILE = 2 + MISC_EVENT; psf int SAVE_FILE = 3 + MISC_EVENT; psf int GOT_FOCUS = 4 + MISC_EVENT; psf int LOST_FOCUS = 5 + MISC_EVENT; } === (Event) ========== FlowLayout public class FlowLayout implements LayoutManager { psf int LEFT = 0; psf int CENTER = 1; psf int RIGHT = 2; } === (FlowLayout) ========== FocusEvent public class FocusEvent extends ComponentEvent { psf int FOCUS_FIRST = 1004; psf int FOCUS_LAST = 1005; psf int FOCUS_GAINED = FOCUS_FIRST; psf int FOCUS_LOST = 1 + FOCUS_FIRST; } === (FocusEvent) ========== Frame public class Frame extends Window implements MenuContainer { psf int DEFAULT_CURSOR = 0; psf int CROSSHAIR_CURSOR = 1; psf int TEXT_CURSOR = 2; psf int WAIT_CURSOR = 3; psf int SW_RESIZE_CURSOR = 4; psf int SE_RESIZE_CURSOR = 5; psf int NW_RESIZE_CURSOR = 6; psf int NE_RESIZE_CURSOR = 7; psf int N_RESIZE_CURSOR = 8; psf int S_RESIZE_CURSOR = 9; psf int W_RESIZE_CURSOR = 10; psf int E_RESIZE_CURSOR = 11; psf int HAND_CURSOR = 12; psf int MOVE_CURSOR = 13; } === (Frame) ========== GridBagConstraints public class GridBagConstraints implements Cloneable { psf int RELATIVE = -1; psf int REMAINDER = 0; psf int NONE = 0; psf int BOTH = 1; psf int HORIZONTAL = 2; psf int VERTICAL = 3; psf int CENTER = 10; psf int NORTH = 11; psf int NORTHEAST = 12; psf int EAST = 13; psf int SOUTHEAST = 14; psf int SOUTH = 15; psf int SOUTHWEST = 16; psf int WEST = 17; psf int NORTHWEST = 18; } === (GridBagConstraints) ========== GridBagLayout public class GridBagLayout implements LayoutManager { #sf int MAXGRIDSIZE = 128; #sf int MINSIZE = 1; #sf int PREFERREDSIZE = 2; } === (GridBagLayout) =========== InputEvent public abstract class InputEvent extends ComponentEvent { psf int SHIFT_MASK = Event.SHIFT_MASK; psf int CTRL_MASK = Event.CTRL_MASK; psf int META_MASK = Event.META_MASK; psf int ALT_MASK = Event.ALT_MASK; psf int BUTTON1_MASK = 1 << 4; psf int BUTTON2_MASK = Event.ALT_MASK; psf int BUTTON3_MASK = Event.META_MASK; } === (InputEvent) =========== ItemEvent public class ItemEvent extends AWTEvent { psf int ITEM_FIRST = 701; psf int ITEM_LAST = 701; psf int ITEM_STATE_CHANGED = ITEM_FIRST; psf int SELECTED = 1; psf int DESELECTED = 2; } === (ItemEvent) =========== KeyEvent public class KeyEvent extends InputEvent { psf int KEY_FIRST = 400; psf int KEY_LAST = 402; psf int KEY_TYPED = KEY_FIRST; psf int KEY_PRESSED = 1 + KEY_FIRST; psf int KEY_RELEASED = 2 + KEY_FIRST; psf int VK_ENTER = '\n'; psf int VK_BACK_SPACE = '\b'; psf int VK_TAB = '\t'; psf int VK_CANCEL = 0x03; psf int VK_CLEAR = 0x0C; psf int VK_SHIFT = 0x10; psf int VK_CONTROL = 0x11; psf int VK_ALT = 0x12; psf int VK_PAUSE = 0x13; psf int VK_CAPS_LOCK = 0x14; psf int VK_ESCAPE = 0x1B; psf int VK_SPACE = 0x20; psf int VK_PAGE_UP = 0x21; psf int VK_PAGE_DOWN = 0x22; psf int VK_END = 0x23; psf int VK_HOME = 0x24; psf int VK_LEFT = 0x25; psf int VK_UP = 0x26; psf int VK_RIGHT = 0x27; psf int VK_DOWN = 0x28; psf int VK_COMMA = 0x2C; psf int VK_PERIOD = 0x2E; psf int VK_SLASH = 0x2F; psf int VK_0 = 0x30; psf int VK_1 = 0x31; psf int VK_2 = 0x32; psf int VK_3 = 0x33; psf int VK_4 = 0x34; psf int VK_5 = 0x35; psf int VK_6 = 0x36; psf int VK_7 = 0x37; psf int VK_8 = 0x38; psf int VK_9 = 0x39; psf int VK_SEMICOLON = 0x3B; psf int VK_EQUALS = 0x3D; psf int VK_A = 0x41; psf int VK_B = 0x42; psf int VK_C = 0x43; psf int VK_D = 0x44; psf int VK_E = 0x45; psf int VK_F = 0x46; psf int VK_G = 0x47; psf int VK_H = 0x48; psf int VK_I = 0x49; psf int VK_J = 0x4A; psf int VK_K = 0x4B; psf int VK_L = 0x4C; psf int VK_M = 0x4D; psf int VK_N = 0x4E; psf int VK_O = 0x4F; psf int VK_P = 0x50; psf int VK_Q = 0x51; psf int VK_R = 0x52; psf int VK_S = 0x53; psf int VK_T = 0x54; psf int VK_U = 0x55; psf int VK_V = 0x56; psf int VK_W = 0x57; psf int VK_X = 0x58; psf int VK_Y = 0x59; psf int VK_Z = 0x5A; psf int VK_OPEN_BRACKET = 0x5B; psf int VK_BACK_SLASH = 0x5C; psf int VK_CLOSE_BRACKET = 0x5D; psf int VK_NUMPAD0 = 0x60; psf int VK_NUMPAD1 = 0x61; psf int VK_NUMPAD2 = 0x62; psf int VK_NUMPAD3 = 0x63; psf int VK_NUMPAD4 = 0x64; psf int VK_NUMPAD5 = 0x65; psf int VK_NUMPAD6 = 0x66; psf int VK_NUMPAD7 = 0x67; psf int VK_NUMPAD8 = 0x68; psf int VK_NUMPAD9 = 0x69; psf int VK_MULTIPLY = 0x6A; psf int VK_ADD = 0x6B; psf int VK_SEPARATER = 0x6C; psf int VK_SUBTRACT = 0x6D; psf int VK_DECIMAL = 0x6E; psf int VK_DIVIDE = 0x6F; psf int VK_F1 = 0x70; psf int VK_F2 = 0x71; psf int VK_F3 = 0x72; psf int VK_F4 = 0x73; psf int VK_F5 = 0x74; psf int VK_F6 = 0x75; psf int VK_F7 = 0x76; psf int VK_F8 = 0x77; psf int VK_F9 = 0x78; psf int VK_F10 = 0x79; psf int VK_F11 = 0x7A; psf int VK_F12 = 0x7B; psf int VK_DELETE = 0x7F; psf int VK_NUM_LOCK = 0x90; psf int VK_SCROLL_LOCK = 0x91; psf int VK_PRINTSCREEN = 0x9A; psf int VK_INSERT = 0x9B; psf int VK_HELP = 0x9C; psf int VK_META = 0x9D; psf int VK_BACK_QUOTE = 0xC0; psf int VK_QUOTE = 0xDE; psf int VK_FINAL = 0x18; psf int VK_CONVERT = 0x1C; psf int VK_NONCONVERT = 0x1D; psf int VK_ACCEPT = 0x1E; psf int VK_MODECHANGE = 0x1F; psf int VK_KANA = 0x15; psf int VK_KANJI = 0x19; psf int VK_UNDEFINED = 0x0; psf char CHAR_UNDEFINED = 0x0; } === (KeyEvent) ========== Label public class Label extends Component { psf int LEFT = 0; psf int CENTER = 1; psf int RIGHT = 2; } === (Label) ========== MediaTracker public class MediaTracker { psf int LOADING = 1; psf int ABORTED = 2; psf int ERRORED = 4; psf int COMPLETE = 8; sf int DONE = (ABORTED | ERRORED | COMPLETE); } === (MediaTracker) ========== MouseEvent public class MouseEvent extends InputEvent { psf int MOUSE_FIRST = 500; psf int MOUSE_LAST = 506; psf int MOUSE_CLICKED = MOUSE_FIRST; psf int MOUSE_PRESSED = 1 + MOUSE_FIRST; psf int MOUSE_RELEASED = 2 + MOUSE_FIRST; psf int MOUSE_MOVED = 3 + MOUSE_FIRST; psf int MOUSE_ENTERED = 4 + MOUSE_FIRST; psf int MOUSE_EXITED = 5 + MOUSE_FIRST; } === (MouseEvent) ========== PaintEvent public class PaintEvent extends ComponentEvent { psf int PAINT_FIRST = 800; psf int PAINT_LAST = 801; psf int PAINT = PAINT_FIRST; psf int UPDATE = PAINT_FIRST + 1; } === (PaintEvent) ========== Scrollbar public class Scrollbar extends Component { psf int HORIZONTAL = 0; psf int VERTICAL = 1; } === (Scrollbar) ========== StreamTokenizer public class StreamTokenizer { *sf byte CT_WHITESPACE = 1; *sf byte CT_DIGIT = 2; *sf byte CT_ALPHA = 4; *sf byte CT_QUOTE = 8; *sf byte CT_COMMENT = 16; psf int TT_EOF = -1; psf int TT_EOL = '\n'; psf int TT_NUMBER = -2; psf int TT_WORD = -3; } === (StreamTokenizer) ========== TextEvent public class TextEvent extends AWTEvent { psf int TEXT_FIRST = 900; psf int TEXT_LAST = 900; psf int TEXT_VALUE_CHANGED = TEXT_FIRST; } === (TextEvent) ========== WindowEvent public class WindowEvent extends ComponentEvent { psf int WINDOW_FIRST = 200; psf int WINDOW_LAST = 206; psf int WINDOW_OPENED = WINDOW_FIRST; psf int WINDOW_CLOSING = 1 + WINDOW_FIRST; psf int WINDOW_CLOSED = 2 + WINDOW_FIRST; psf int WINDOW_ICONIFIED = 3 + WINDOW_FIRST; psf int WINDOW_DEICONIFIED = 4 + WINDOW_FIRST; psf int WINDOW_ACTIVATED = 5 + WINDOW_FIRST; psf int WINDOW_DEACTIVATED = 6 + WINDOW_FIRST; } === (WindowEvent) Jan Bielecki Dodatek C Klasa Debug Zdefiniowana tu klasa uruchomieniowa (wersja z lutego1999), ułatwia wyszukiwanie błędów występujących podczas wykonywania aplikacji i apletów. Użycie klasy Debug Wykonanie instrukcji new Debug(); albo new Debug(w, h); w której w i h sa wyrażeniami całkowitymi, powoduje utworzenie okna o rozmiarach w x h pikseli, w którym są wyświetlane wartości wyrażeń określonych przez argumenty przeciążonej funkcji toFrame oraz funkcji setLoc, setLimit, setSize i assert. Opracowanie fabrykatora new Debug(...) może być tylko jednokrotne. W przypadku naruszenia tego wymagania jest czyszczone okno uruchamiacza. Jeśli nie utworzono obiektu klasy Debug, to wywołanie dowolnej jej funkcji nie wywołuje żadnych skutków. Debug.toFrame("Hello"); Debug.toFrame(fix+1); Debug.toFrame("count = " + count); Debug.toFrame("x = " + x + " y = " + y); Debug.toFrame(count == 100); Debug.toFrame(Thread.currentThread()); Debug.setLoc(200, 200); // ustawienie pozycji narożnika (x,y) Debug.setLimit(50); // określenie maksymalnej liczby wierszy Debug.setSize(20); // określenie rozmiaru czcionki Debug.assert("In paint: boxWidth > 5", boxWidth > 5); Uwaga: Bezpośrednio po wywołaniu funkcji off, a przed najbliższym wywołaniem funkcji on, wstrzymuje się wyświetlanie argumentów metod toFrame i assert. new Debug(100, 100); // wywołanie konstruktora // ... Debug.off(); // zaniechanie wyświetlania // ... Debug.on(); // przywrócenie wyświetlania // ... Definicja klasy Debug Przytoczona tu definicja klasy Debug może służyć za jej kompletny i jednoznaczny opis. Zapoznanie się z nim ułatwi posługiwanie się klasą i umożliwi dostosowanie jej do indywidualnych potrzeb. package janb.debug; import java.awt.*; import java.awt.event.*; public class Debug extends Frame { /** Klasa uruchomieniowa Copyright © Jan Bielecki 1999.02.10 */ private static Debug frame; private static Font font = new Font("Monospaced", Font.BOLD, 10); private static int limit = -1000000; private static String NULL = "NULL String"; private static int tally = 0; public static synchronized boolean check() { if(limit == 0) { textArea.append("\nLimit reached!\n"); limit = 1; } if(limit < 0) { limit++; return true; } else return false; } public Font getFont() { return frame.font; } class MyTextArea extends TextArea { public MyTextArea(String header) { super(header); } public synchronized void paint(Graphics gDC) { gDC.setFont(frame.getFont()); } } String header = "Debug\n=====\n"; static MyTextArea textArea; static boolean opened = false, active = false; int xLoc = 320, yLoc = 0; public Debug() { this(200, 400); } public Debug(int width, int height) { super("Debug"); frame = this; if(opened) { clear(); toFrame(); toFrame("Only one window allowed"); return; } setLocation(xLoc, yLoc); opened = true; active = true; setSize(width, height); textArea = new MyTextArea(header); add(textArea, BorderLayout.CENTER); addWindowListener( new WindowAdapter() { public void windowClosing(WindowEvent evt) { setVisible(false); dispose(); } } ); setVisible(true); } public static synchronized void toFrame(String string) { if(active && check()) { if(string != null) tally += string.length() + 1; else tally += NULL.length() + 1; if(tally > 50000) { String msg = "Window FORCEFULLY cleared!\n"; textArea.setText(msg); tally = msg.length() + string.length() + 1; } if(string == null) string = NULL; textArea.append(string + '\n'); } } public static synchronized void toFrame() { toFrame(""); } public static synchronized void toFrame(int num) { toFrame("" + num); } public static synchronized void toFrame(boolean bool) { toFrame("" + bool); } public static synchronized void toFrame(double num) { toFrame("" + num); } public static synchronized void toFrame(Object object) { if(active && check()) { if(object != null) toFrame(object.toString()); else toFrame("NULL Object"); } } public static synchronized void assert(String what, boolean isTrue) { if(what == null) what = ""; if(active && !isTrue) toFrame("Assertion \"" + what + "\" failed\n"); } public static synchronized void on() { if(opened) active = true; } public static synchronized void off() { active = false; } public static synchronized void setLoc(int x, int y) { if(active) frame.setLocation(x, y); } public static synchronized void setSize(int size) { if(active) font = new Font( "Monospaced", Font.BOLD, size ); } public static synchronized void clear() { if(active) textArea.setText("Window cleared!\n"); } public static synchronized void setLimit(int limit) { if(active) { if(limit <= 0) return; Debug.limit = -limit; } } } Jan Bielecki Dodatek D Symantec Visual Cafe Wykonanie programu w środowisku Visual Cafe 2.5 należy poprzedzić utworzeniem, skonfigurowaniem i zbudowaniem projektu. Utworzenie projektu W celu utworzenia projektu należy wydać polecenie File/New Project. Spowoduje to wyświetlenie dialogu, z którego można wybrać rodzaj projektu, m.in. Projekt pusty (Empty Project), Prosty Aplet (Basic Applet) albo Prostą Aplikację (Basic Application). Bezpośrednio po utworzeniu projektu należy zapamiętać go w pliku. Odbywa się to za pomocą polecenia File/Save As albo przez kliknięcie ikony Save. Podczas wykonywania tej operacji należy podać nazwę projektu i upewnić się co do nazwy katalogu. Skonfigurowanie projektu W celu skonfigurowania projektu należy wydać polecenie Project/Options. Spowoduje to wyświetlenie dialogu z zakładkami Project i Directories. Zakładka Project Zakładka określa czy projekt dotyczy wersji uruchomieniowej (Debug) czy dystrybucyjnej (Final), rozstrzyga o typie projektu oraz umożliwia podanie argumentów programu (jeśli jest aplikacją). W takim przypadku w klatce Main class można podać pełną nazwę funkcji main (np. jbMains.jbApp.main) Zakładka Directories Zakładka umożliwia wyspecyfikowanie nazw katalogów używanych do poszukiwania plików klas źródłowych (Source files), plików klas importowanych (Class files) oraz plików klas wyjściowych (Output files). W przypadku aplikacji rodzimej umożliwia wyspecyfikowanie nazw katalogów zawierających biblioteki (Library files). Zaleca się następujące postępowanie 1) Utworzenie odrębnego katalogu dla projektu (np. C:\jbFolder). 2) Umieszczenie klas projektu (np. Project) w odrębnym pakiecie (np. jbPackage). 3) Umieszczenie plików wyjściowych w odrębnym katalogu (np. C:\jbOutputs). 4) Nazwanie głównej klasy apletu Master, a głównej klasy aplikacji Main. 5) Nazwanie pliku zawierającego opis apletu Index.html. Uwaga: Jeśli jako nazwę katalogu wyjściowego podano C:\jbOutputs, to pliki wynikowe będą umieszczane w katalogu C:\jbOutputs\jbPackage. Jeśli nazwy katalogu wyjściowego nie podano, to będą umieszczane w tych samych katalogach, co ich pliki źródłowe. Następujący program posługuje się klasą Debug. Jeśli kod klasy, należącej do pakietu janb.debug znajduje się w katalogu C:\jbPacks\janb\debug, to w liście Source root directories należy podać nazwę katalogu C:\jbPacks. =================================== package jbPackage; import janb.debug.Debug; public class Master { public init() { new Debug(); Debug.toFrame("Hello, I am JanB!"); } } Wstawianie plików do projektu W celu wstawienia plików do projektu należy w oknie Project wybrać zakładkę Files, p-kliknąć w obszarze okna, a następnie wydać polecenie Insert/Remove Files. Spowoduje to wyświetlenie okna, w którym można określić zestaw plików wchodzących w skład projektu. Budowanie programu W celu zbudowania programu należy wydać polecenie Project/Build albo Project/Execute. W drugim przypadku, bezpośrednio po zbudowaniu programu, nastąpi jego wykonanie. Jeśli program zawiera błędy składniowe, to ich opisy pojawią się w oknie Messages. Aby je wyświetlić, należy wydać polecenie View/Messages. Wykonanie programu W celu wykonania programu należy wydać polecenie Project/Execute. Jeśli program jest aplikacją, to spowoduje to wywołanie funkcji main, a jeśli jest apletem, to nastąpi zinterpretowanie pliku HTML wchodzącego w skład projektu i wyświetlenie opisanych w nim apletów. Jan Bielecki Dodatek E Borland JBuilder Wykonanie programu w środowisku Borland JBuilder 2.0 należy poprzedzić utworzeniem, skonfigurowaniem i zbudowaniem projektu. Utworzenie projektu W celu utworzenia projektu należy wydać polecenie File/New. Spowoduje to wyświetlenie dialogu, w którym należy podać pełną nazwę pliku projektowego, np. C:\jbFolder\Project.jpr albo C:\jbFolder\jbPackage\Project.jpr Jej ostatni człon (tu: Project.jpr) określa nazwę projektu, a przedostatni (tu: jbPackage) jest nazwą pakietu. Skonfigurowanie projektu W celu skonfigurowania projektu należy wydać polecenie File/Project Properties. Spowoduje to wyświetlenie dialogu, w którym można podać ścieżki do katalogów źródłowych (Source root directories) i katalogu wynikowego (Output root directory). Zaleca się następujące postępowanie 1) Utworzenie odrębnego katalogu dla projektu (np. C:\jbFolder). 2) Umieszczenie klas projektu (np. Project) w odrębnym pakiecie (np. jbPackage). 3) Umieszczenie plików wyjściowych w odrębnym katalogu (np. C:\jbOutputs). 4) Nazwanie głównej klasy apletu Master, a głównej klasy aplikacji Main. 5) Nazwanie pliku zawierającego opis apletu Index.html. Uwaga: Jeśli jako nazwę katalogu wyjściowego podano C:\jbOutputs, to pliki wynikowe będą umieszczane w katalogu C:\jbOutputs\jbPackage. Jeśli nazwy katalogu wyjściowego nie podano, to będą umieszczane w tych samych katalogach, co ich pliki źródłowe. Następujący program posługuje się klasą Debug. Jeśli kod klasy, należącej do pakietu janb.debug znajduje się w katalogu C:\jbPacks\janb\debug, to w liście Source root directories należy podać nazwę katalogu C:\jbPacks. =================================== package jbPackage; import janb.debug.Debug; public class Master { public init() { new Debug(); Debug.toFrame("Hello, I am JanB!"); } } Wstawianie plików do projektu W celu wstawienia plików do projektu, należy w panelu projektowym kliknąć ikonę za znakiem + (plus), a w wyświetlonym wówczas dialogu podać nazwę pliku (np. Master.java) i odhaczyć nastawę Add to project. Budowanie programu W celu zbudowania programu należy wydać polecenie Build/Make Project albo Run/Run Applet. W drugim przypadku, bezpośrednio po zbudowaniu programu, nastąpi jego wykonanie. Jeśli program zawiera błędy składniowe, to ich opisy pojawią się w panelu komunikatów. Wykonanie programu W celu wykonania programu należy wydać polecenie Run/Run. Jeśli program jest aplikacją, to spowoduje to wywołanie funkcji main, a jeśli jest apletem, to nastąpi zinterpretowanie pliku HTML wchodzącego w skład projektu i wyświetlenie opisanych w nim apletów. Jan Bielecki Dodatek F Tek-Tools Kawa Wykonanie programu w środowisku Tek-Tools Kawa 3.13 należy poprzedzić utworzeniem, skonfigurowaniem i zbudowaniem projektu. Przed przystąpieniem do tworzenia projektów należy skonfigurować środowisko uruchomieniowe. Uwaga: Konieczność skonfigurowania środowiska wynika stąd, że Kawa jest przystosowana do współpracy z dowolną wersją pakietu JDK. Skonfigurowanie środowiska W celu skonfigurowania środowiska należy wydać polecenie Customize/Options, a następnie określić ścieżki do katalogów Java Bin, Java Lib i Java Documents, np. Java Bin Directory: D:\JDK12Run\bin Java Lib Directory: D:\JDK12Run\lib Java Documents: D:\JDK12RunDocs\jdk1.2\docs W celu dodania Indeksu Użytkownika należy wydać polecenie Customize/User Index, a następnie podać ścieżkę do indeksu, np. D:\JDK12RunDocs\JDK1.2\docs\api\index.html Uwaga: Indeks użytkownika można wywołać za pomocą polecenia Help/Search/JDK, po którym należy wybrać nazwę indeksu. Utworzenie projektu W celu utworzenia projektu należy wydać polecenie Project/New. W wyświetlonym wówczas dialogu należy podać nazwę pliku projektowego, np. Project Spowoduje to powstanie projektu umieszczonego w pliku z rozszerzeniem .kawa. Skonfigurowanie projektu W celu skonfigurowania projektu należy wydać polecenie Project/Compiler Options. Spowoduje to wyświetlenie dialogu, w którego zakładce Compiler można ustawić odhaczenia w nastawach Debugging tables i Deprecated API, a w zakładce Interpreter odhaczenie w nastawie Debug. Zaleca się następujące postępowanie 1) Utworzenie odrębnego katalogu dla projektu (np. C:\jbProject). 2) Umieszczenie klas projektu (np. Project) w odrębnym pakiecie (np. jbPackage). 3) Umieszczenie plików wyjściowych w odrębnym katalogu (np. C:\jbOutputs). 4) Nazwanie głównej klasy apletu Master, a głównej klasy aplikacji Main. 5) Nazwanie pliku zawierającego opis apletu Index.html i umieszczenie w tym opisie frazy codebase odwołującej się do katalogu wyjściowego (file:/C:/jbOutputs). Uwaga: Jeśli jako nazwę katalogu wyjściowego podano jako C:\jbOutputs, to pliki wynikowe będą umieszczane w katalogu C:\jbOutputs\jbPackage. Jeśli nazwy katalogu wyjściowego nie podano, to będą umieszczane w tych samych katalogach, co ich pliki źródłowe. Następujący program posługuje się klasą Debug. Jeśli kod klasy, należącej do pakietu janb.debug znajduje się w katalogu C:\jbPacks\janb\debug, to w klatce wyświetlonej po wydaniu polecenia Packages/Classpath należy umieścić nazwę katalogu C:\jbPacks. ====================================== package jbPackage; import janb.debug.Debug; public class Master { public init() { new Debug(); Debug.toFrame("Hello, I am JanB!"); } } Wstawienie plików do projektu W celu wstawienia pliku do projektu, należy p-kliknąć nazwę projektu, po czym wydać polecenie Add File, a w celu określenia głównego pliku HTML (np. Index.html), należy p-kliknąć nazwę pliku i wydać polecenie Main HTML. Budowanie programu W celu zbudowania programu należy wydać polecenie Build/Rebuild Dirty albo Build/Rebuild All. Jeśli program zawiera błędy składniowe, to ich opisy pojawią się w oknie wyjściowym Wykonanie programu W celu wykonania programu należy wydać polecenie Build/Run. Jeśli program jest aplikacją, to spowoduje to wywołanie funkcji main, a jeśli jest apletem, to nastąpi zinterpretowanie pliku HTML wchodzącego w skład projektu i wyświetlenie opisanych w nim apletów. Uwaga: Środowisko Kawa znajduje się w Internecie pod adresem www.tek-tools.com/kawa. Dla studentów jest dostępna bezpłatna wersja 3-miesięczna. [PPL1]cznym [PPL2] [PPL3] [PPL4]zanego 446