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