Delegacyjny model obsługi zdarzeń - zasady
"Sercem" działania aplikacji z graficznymi interfejsami użytkownika jest obsluga zdarzeń.
Ale jest to temat ważny nie tylko ze względu na GUI. Koncepcje, które będziemy
poznawać mają bowiem dużo szersze zastosowanie, a zdarzenia wcale nie muszą
oznaczać jakichś wizualnych interakcji.
1. Reguły ogólne i mechanizm obsługi zdarzeń
Interakcja
użytkownika z GUI naszej aplikacji polega na wywoływaniu
zdarzeń (np. kliknięcie w przycisk, wciśnięcie klawisza na
klawiaturze etc).
- Zatem
programowanie GUI jest programowaniem zdarzeniowym. Jest ono
oparte na koncepcji tzw. funkcji callback. Funkcja typu
callback zawarta w naszym
programie jest wywoływana (zazwyczaj) nie przez nasz program, ale
przez sam system na przykład w reakcji na zdarzenia takie jak
wciśnięcie klawisza czy kliknięcie myszką. Funkcja ta obsługuje
zdarzenie.
- W
Javie dodatkowo zastosowano koncepcję delegacyjnego
modelu obsługi zdarzeń,
która umożliwia przekazanie obsługi zdarzenia, które
przytrafia się jakiemuś obiektowi (Źródło zdarzenia)
do innego obiektu (Słuchacza
zdarzenia).
- Zdarzenia
są obiektami odpowiednich klas, określających rodzaj zdarzeń.
- Słuchacze
są obiektami
klas implementujących
interfejsy nasłuchu. Interfejsy
nasłuchu określają zestaw metod obsługi danego rodzaju zdarzeń.
- W klasach
słuchaczy definiuje się metody odpowiedniego
interfejsu nasłuchu
zdarzeń, które określają co ma się dziać w wyniku
zajścia określonego zdarzenia (metody obsługi odpowiednich
zdarzeń)
- Zdarzenie
(obiekt odpowiedniej klasy zdarzeniowej) jest przekazywane
do obsługi obiektowi-słuchaczowi tylko wtedy gdy
Słuchacz ten
jest przyłączony do Źródła
zdarzenia. (przyłączenie za pomocą odwołania
z.addNNNListener(h),
gdzie: z – Źródło zdarzenia, NNN - rodzaj zdarzenia, h
– Słuchacz danego rodzaju zdarzenia)
- Przekazanie
zdarzenia do obsługi polega na wywołaniu
odpowiedniej dla danego zdarzenia metody
obsługi zdarzenia (zdefiniowanej w klasieSłuchacza) z
argumentem obiekt-zdarzenie.
- Argument (obiekt
klasy zdarzeniowej) zawiera informacje
o okolicznościach zajścia zdarzenia (np. komu się przytrafiło?
kiedy? jakie ma inne właściwości?). Jako parametr w metodzie obsługi
może być odpytany o te informacje.
W
Javie standardowo zdefiniowano bardzo dużo różnych rodzajów
zdarzeń i interfejsów ich nasłuchu. Rozpatrzmy ogólne
zasady na przykładzie zdarzenia "akcja" (obiekt klasy
ActionEvent), które powstaje:
- po
kliknięciu w przycisk lub naciśnięciu spacji, gdy przycisk ma fokus
(zdolność do przyjmowania zdarzeń z klawiatury),
- po
naciśnięciu ENTER w polu edycyjnym,
- po wyborze opcji
menu,
- po
podwójnym kliknięciu / ENTER na liście AWT (ale tylko
AWT, nie dotyczy to listy Swingu - JList) lub
na liście rozwijalnej Swingu - JComboBox
- w
innych okolicznościach, ew. zdefiniowanych przez program
(powstaje -
o ile do Źródła przyłączono odpowiedniego Słuchacza akcji –
czyli obiekt klasy implementującej interfejs nasłuchu akcji -
ActionListener)
Uwaga: zdarzenia
"akcja" jest zdarzeniem
semantycznym - niezależnym od fizycznego kontekstu (zauważmy jak
w różnych fizycznych okolicznościach ono powstaje –
kliknięcie, naciśnięcie spacji lub ENTER, w różnych
"fizycznych" kontekstach – na przycisku, w polu
edycyjnym, w menu). Nie należy go mylić ze zdarzeniem kliknięcia
myszką (czysto fizyczne zdarzenie).
Wyobraźmy sobie teraz, że w naszym GUI mamy jeden przycisk:
import javax.swing.*;
class GUI extends JFrame {
public static void main(String[] a) { new GUI(); }
GUI() {
JButton b = new JButton("Przycisk");
getContentPane().add(b);
pack();
show();
}
}
Uwaga: świadomie nie stosujemy tu
kreacji GUI w wątku obsługi zdarzeń, by nie zaciemniać kodu. Generalnie
należałoby tworzyć GUI za pomocą metody invokeLater(..). Zob. podpunkt
"Komponenty Swing a wielowątkowość. Schemat aplikacji Swing" w
wykładzie "Programowanie GUI: komponenty wizualne"
Klikając w ten przycisk (w tym programie) nie uzyskamy żadnych efektów (oprócz
widocznej – zagwarantowanej przez domyślny look&feel – zmiany stanu przycisku:
wyciśnięty – wciśnięty – wyciśnięty).
Co zrobić, by po kliknięciu w przycisk (lub naciśnięciu SPACJI, gdy przycisk
ma fokus) uzyskać jakiś użyteczny efekt? Choćby wyprowadzenie na konsolę
napisu "Wystąpiło zdarzenie!".
Wiemy już, że kliknięcie lub naciśnięcie spacji na przycisku może wywołać zdarzenie "akcja". Wywoła je, jeśli do przycisku przyłączymy słuchacza akcji.
Zgodnie z podanymi wcześniej regułami musimy zatem stworzyć obiekt – słuchacza akcji i przyłączyć go do przycisku. Klasa słuchacza akcji musi implementować interfejs nasłuchu akcji (ActionListener), i w konsekwencji - zdefiniować jego jedyną metodę – actionPerformed
(zobaczcie w dokumentacji - jest!) W tej metodzie zawrzemy kod, który zostanie
uruchomiony po kliknięciu w przycisk, np. wyprowadzenie na konsolę napisu
"Wystąpiło zdarzenie!".
Ideowy schemat rozwiązania naszego problemu przedstawia poniższy rysunek.
Zgodnie z tym schematem zmodyfikujemy nasz program:
import java.awt.event.*; // konieczny dla obłsugi zdarzeń
import javax.swing.*;
class Handler implements ActionListener {
public void actionPerformed(ActionEvent e) {
System.out.println("Wystąpiło zdarzenie!");
}
}
public class GUI extends JFrame {
public GUI() {
JButton b = new JButton(
"<html><center>Przycisk<br>reagujący na wciśnięcie</center></html>");
Handler h = new Handler();
b.addActionListener(h);
add(b);
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
pack();
setLocationRelativeTo(null);
setVisible(true);
}
public static void main(String[] a) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new GUI();
}
});
}
}
Dopiero teraz nasz przycisk będzie reagował na kliknięcia (lub wciśnięcie
spacji) – w efekcie na konsoli uzyskamy komunikat "Wystąpiło zdarzenie!".
Zobacz działanie programu
Pytanie: czy jest to jedyny sposób kodowania? Zdecydowanie nie. Przecież każda klasa może być klasą słuchacza zdarzeń (jeśli tylko implementuje odpowiedni interfejs).
Możemy zatem implementować interfejs ActionListener w klasie, definiującej okno naszej aplikacji:
class GUI extends JFrame implements ActionListener {
GUI() {
JButton b = new JButton("Przycisk");
b.addActionListener(this); // TEN obiekt będzie słuchaczem akcji
add(b);
pack();
show();
}
public void actionPerformed(ActionEvent e) {
System.out.println("Wystąpiło zdarzenie!");
}
public static void main(String[] a) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new GUI();
}
});
}
}
Możemy także użyć anonimowych klas wewnętrznych.
2. Anonimowe klasy wewnętrzne dla obsługi zdarzeń
Nic nie stoi na przeszkodzie, by implementację interfejsu nasłuchu umieścić w anonimowej klasie wewnętrznej.
Jest to rozwiązanie podobne do implementacji interfejsu w klasie GUI (bo mamy jeden obiekt
, obsługujący zdarzenia, który możemy przyłączyć do różnych źródeł). W przypadku
interfejsów z jedną metodą obsługi zdarzeń jest w zasadzie kwestią gustu
wybór jednego z tych dwóch rozwiązań (dodajmy jednak, że anonimowa klasa
wewnętrzna tworzy dodatkowy plik klasowy; implementując interfejs nasłuchu
w nazwanej klasie możemy używać jej obiektów do obsługi zdarzeń w innych
klasach, co nie jest możliwe przy anonimowej klasie wewnętrznej; za to kod
anonimowej klasy wewnętrznej może być lepiej zlokalizowany pod względem czytelności
dużego programu).
Natomiast w przypadku interfejsów z wieloma metodami obsługi,
spośród których interesuje nas tylko jedna lub dwie, implementacja interfejsu
w klasie GUI obciąża nas koniecznością zdefiniowania pustych metod obsługi
(metod obsługi tych zdarzeń, którymi nie jesteśmy zainteresowani). Wtedy
lepszym rozwiązaniem może okazać się odziedziczenie odpowiedniego adaptera
w anonimowej klasie wewnętrznej.
Prawie wszystkie interfejsy nasłuchu zdarzeń (za wyjątkiem niektórych z pakietu
javax.swing.event) mają odpowiadające im adaptery.
Np. interfejs nasłuchu zdarzeń związanych z myszką deklaruje pięć metod:
public interface MouseListener extends EventListener {
public void mouseClicked(MouseEvent e); // kliknięcie
public void mousePressed(MouseEvent e); // wciśnięcie klawisza myszki
public void mouseReleased(MouseEvent e);// zwolnienie klawisza myszki
public void mouseEntered(MouseEvent e); // wejście kursora w obszar komponentu
public void mouseExited(MouseEvent e); // wyjście kursora z obszaru komponentu
}
W klasie implementującej musimy zdefiniować wszystkie 5 metod, choć być może
interesuje nas tylko obsługa wciśnięcia klawisza myszki (mousePressed).
Dla ułatwienia sobie życia możemy skorzystać z gotowego adaptera (klasa MouseAdapter),
definiującego puste metod interfejsu, i przedefiniować tylko te które nas
interesują:
class GUI extends JFrame {
MouseListener handler = new MouseAdapter() {
public void mousePressed(MouseEvent e) {
System.out.println("Myszka wciśnięta!");
}
};
GUI() {
JButton b = new JButton("Przycisk");
b.addMouseListener(handler) {
add(b);
pack();
show();
}
public static void main(String[] a) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new GUI();
}
});
}
}
Jeżeli chcemy mieć tylko jeden obiekt klasy słuchacza do obsługi jednego źródła zdarzenia to dobrym rozwiązaniem będzie lokalna anonimowa klasa wewnętrzna:
// konieczne importy: javax.swing.*; i java.awt.event.*;
class GUI extends JFrame {
GUI() {
JButton b = new JButton("Przycisk");
b.addActionListener( new ActionListener() {
public void actionPerformed(ActionEvent e) {
System.out.println("Wystąpiło zdarzenie!");
}
});
add(b);
pack();
show();
}
public static void main(String[] a) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new GUI();
}
});
}
}
Zaletą jest tu umieszczenie kodu blisko jego wykorzystania (zwiększenie czytelności
programu). Ale zaleta może przekształcić się w wadę, jeśli tylko kod będzie
zbyt rozbudowany (zmniejszenie czytelności programu).
Oczywiście, nic nie stoi na przeszkodzie, by ta sama lokalna anonimowa klasa
wewnętrzna służyła do tworzenia wielu obiektów-słuchaczy, które można przyłączyć
do wielu komponentów. Warunkiem jest wykorzystanie pętli.
import java.awt.*; // dla FlowLayout
import java.awt.event.*; // dla zdarzenie akcji
import javax.swing.*; // dla Swingu (JFrame. JButton)
class GUI extends JFrame {
final int BNUM = 3; // liczba przycisków
GUI() {
super("GUI");
setLayout(new FlowLayout());
for (int i = 1; i <= BNUM; i++) {
JButton b = new JButton("Przycisk " + i);
b.addActionListener( new ActionListener() {
public void actionPerformed(ActionEvent e) {
System.out.println("Wystąpiło zdarzenie!");
}
});
add(b);
}
pack();
show();
}
public static void main(String[] a) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new GUI();
}
});
}
}
Program ten da w efekcie okienko z trzema przyciskami.
Kliknięcie w każdy z przycisków wyprowadzi na konsolę komunikat: "Wystąpiło zdarzenie!"
Jest to dobra ilustracja, ale praktycznie niezbyt użyteczna.
Przecież po to tworzymy różne przyciski, by związać z nimi różne akcje.
Przykładowo – dwa przyciski służące do wczytywania i zapisywania pliku.
class GUIfile extends JFrame {
//...
GUIfile() {
setLayout(new FlowLayout());
JButton open = new JButton("Open file");
open.addActionListener( new ActionListener() {
public void actionPerformed(ActionEvent e) {
readFile();
}
});
JButton save = new JButton("Save file");
save.addActionListener( new ActionListener() {
public void actionPerformed(ActionEvent e) {
saveFile();
}
});
...
add(open);
add(close);
pack();
show();
}
.....
void readFile() { //... }
void saveFile() { // ... }
....
public static void main(String[] a) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new GUIFile();
}
});
}
Tutaj będą stworzone dwie anonimowe klasy wewnętrzne (do obsługi przycisków
open i close) oraz po jednym obiekcie-słuchaczu tych klas.
Jest to całkiem usprawiedliwiony sposób kodowania, bowiem z przyciskami związane
są dwie (prawie) zupełnie różne akcje (na marginesie: można przygotować
prostszy, bardziej czytelny kod).
Ale wyobraźmy sobie, że trzy przyciski służą do ustalania różnych kolorów tła contentPane.
Zobacz jak to działa
Zbyt proste zastosowanie lokalnych klas wewnętrznych doprowadzi nas do zbyt rozbudowanego programu.
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class GUI1 extends JFrame {
public GUI1() {
super("GUI - 2");
final Container cp = getContentPane();
cp.setLayout(new FlowLayout());
JButton b = new JButton("Red");
b.addActionListener( new ActionListener() {
public void actionPerformed(ActionEvent e) {
cp.setBackground(Color.red);
}
});
cp.add(b);
b = new JButton("Yellow");
b.addActionListener( new ActionListener() {
public void actionPerformed(ActionEvent e) {
cp.setBackground(Color.yellow);
}
});
cp.add(b);
b = new JButton("Blue");
b.addActionListener( new ActionListener() {
public void actionPerformed(ActionEvent e) {
cp.setBackground(Color.blue);
}
});
cp.add(b);
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
pack();
setLocationRelativeTo(null);
setVisible(true);
}
public static void main(String[] a) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new GUI1();
}
});
}
}
Uwaga - Pamiętajmy! Zmienna lokalna musi być zadeklarowana ze specyfikatorem final, jeśli jej używamy w anonimowej klasie wewnętrznej. Tu dotyczy to zmiennej cp.
Faktycznie, kod jest horrendalny. Co gorsza, tworzy on trzy różne klasy anonimowe (pliki klasowe są zapisywane na dysku).
Aby tego uniknąć możemy skorzystać z informacji, które niosą ze sobą obiekty-zdarzenia.
3. Uzyskiwanie informacji o zdarzeniach
Nieprzypadkowo metodom obsługi przekazywany jest argument – referencja do
obiektu-zdarzenia. Dzięki temu możemy uzsyskać rozliczne informacje o zdarzeniu,
które mamy obsłużyć.
Klasy zdarzeniowe AWT (w tym ActionEvent) pochodzą od klasy AWTEvent, a ta od EventObject.
java.lang.Object
|
+----java.util.EventObject
|
+----java.awt.AWTEvent
|
+----java.awt.event.ActionEvent
Wszystkie klasy zdarzeniowe dziedziczą metodę:
Object getSource()
z klasy EventObject, która zwraca referencję do obiektu –źródła zdarzenia.
Dodatkowo, w każdej klasie zdarzeniowej znajdują się metody do uzyskiwania specyficznej dla danego zdarzenia informacji.
Np. w klasie ActionEvent mamy metodę:
String getActionCommand()
która zwraca napis, skojarzony ze zdarzeniem akcji (swoiste „polecenie”).
Domyślnie jest to:
- dla przycisku i elementu menu - etykieta (napis na przycisku)
- dla pola edycyjnego – tekst zawarty w tym polu
- dla listy rozwijalnej (JComboBox) – wybrany element listy
Domyślne ustawienie możemy zmienić, za pomocą użycia metody:
void setActionCommand(String)
z klas definiujących wszystkie komponenty mogące generować akcje (Button,
AbstractButton, MenuItem, TextField, JTextField, List, JComboBox).
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class GUI2 extends JFrame implements ActionListener {
public GUI2() {
super("GUI");
setLayout(new FlowLayout());
String[] ctab = { "Red", "Yellow", "Blue" };
for (String txt : ctab) {
JButton b = new JButton(txt);
b.addActionListener(this);
add(b);
}
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
pack();
setLocationRelativeTo(null);
setVisible(true);
}
public void actionPerformed(ActionEvent e) {
String cmd = e.getActionCommand();
Color c = Color.white;
if (cmd.equals("Red"))
c = Color.red;
else if (cmd.equals("Yellow"))
c = Color.yellow;
else if (cmd.equals("Blue")) c = Color.blue;
getContentPane().setBackground(c);
}
public static void main(String[] a) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new GUI2();
}
});
}
}
Niestety, nie jest to najlepsze rozwiązanie. Zauważmy:
Jedną z zalet delegacyjnego modelu obsługi zdarzeń jest możliwość unikania
pisania rozbudowanych instrukcji warunkowych przy obsłudze zdarzeń (co za
zdarzenie? skąd przychodzi? etc).
W powyższym przykładowym kodzie jakby zaprzeczamy tej idei, zbyt wiele miejsca
poświęcając na instrukcje warunkowe. Ponadto sama obsługa zbyt ściśle, na
sztywno wiąże się z konkretami: napisami, oznaczającymi kolory itp.
Warto zatem wiedzieć, że w niektórych przypadkach możemy uniknąć zarówno
instrukcji warunkowych jak i lepiej odseparować kod obsługi od konkretów.
Po pierwsze – dzięki wykorzystaniu tablic. Po drugie - dzięki nadaniu pewnych
właściwości źródłu zdarzenia, które to właściwości mogą być przy obsłudze
zdarzenia odczytane i ukierunkować działanie programu.
Zastosujmy tablice:
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import static java.awt.Color.*;
public class GUI3 extends JFrame implements ActionListener {
String[] ctab = { "Red", "Yellow", "Blue" };
Color[] color = { RED, YELLOW, BLUE };
public GUI3() {
super("GUI");
setLayout(new FlowLayout());
for (int i = 0; i < ctab.length; i++) {
JButton b = new JButton(ctab[i]);
b.setActionCommand("" + i);
b.addActionListener(this);
add(b);
}
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
pack();
setLocationRelativeTo(null);
setVisible(true);
}
public void actionPerformed(ActionEvent e) {
int index = Integer.parseInt(e.getActionCommand());
getContentPane().setBackground(color[index]);
}
public static void main(String[] a) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new GUI3();
}
});
}
}
Pewną wadą tego rozwiązania jest konieczność spójnego prowadzenia dwóch tablic:
napisów oznaczających kolory oraz samych kolorów.
Można tego uniknąć poprzez zawarcie kolorów "w samych przyciskach".
Np. ustalając ich właściwość "Foreground" na odpowiedni kolor, a przy obsłudze akcji pobierając ten kolor.
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import static java.awt.Color.*;
public class GUI4 extends JFrame implements ActionListener {
public GUI4() {
super("GUI");
setLayout(new FlowLayout());
add(createButton("Red", RED, this));
add(createButton("Yellow", YELLOW, this));
add(createButton("Blue", BLUE, this));
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
pack();
setLocationRelativeTo(null);
setVisible(true);
}
// Metoda tworzy przycisk z podanym tekstem i jego kolorem
// Przyłącza też przekazany ActionListenr
JButton createButton(String s, Color c, ActionListener al) {
JButton b = new JButton(s);
b.setForeground(c);
b.addActionListener(al);
return b;
}
public void actionPerformed(ActionEvent e) {
Component b = (Component) e.getSource(); // Uwaga: getSource zwraca Object
getContentPane().setBackground(b.getForeground()); // rzutujemy do Component, by wywołać getForeground()
}
public static void main(String[] a) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new GUI4();
}
});
}
}
A co jeśli nie chcemy by kolory pojawiały się w jakikolwiek sposób na przyciskach?
Możemy skorzystać z właściwości clientProperty J-komponentów.
Ale o tym po omówieniu specjalizowanych słuchaczy zdarzeń.
4. Specjalizowani uniwersalni słuchacze zdarzeń
Tworzenie odrębnych nazwanych klas słuchaczy ma sens wtedy, gdy chcemy
dostarczyć wyspecjalizowanej obsługi zdarzeń o uniwersalnym charakterze lub
odseparować obsługę zdarzeń od GUI.
O zmianie kolorów możemy przecież pomyśleć w sposób bardziej generalny. Przygotujmy
więc wyspecjalizowanego słuchacza akcji – zmieniacza kolorów. Jego uniwersalność
będzie polegać na tym, że będzie on mógł zmieniać dowolne kolory dowolnych
komponentów na skutek akcji generowanych przez dowolne źródła.
Taka generalna klasa zmieniacza kolorów mogłaby wyglądać tak:
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class ColorChanger implements ActionListener {
private Component comp; // jakiego komponentu dotyczy zmiana koloru
private Color fore, back; // ustawiane kolory: pierwszy plan, tło
private String which; // jeśli nie podano dwóch kolorów - which powie, ktory
// chcemy zmieniać
// Będziemy chcieli zmieniać oba kolory na podanym komponencie
public ColorChanger(Component c, Color f, Color b) {
comp = c;
fore = f;
back = b;
}
// Będziemy chcieli zmieniać tylko jeden kolor.
// Który – powie parametr which
public ColorChanger(Component c, String which, Color color) {
comp = c;
if (which.equals("Foreground"))
fore = color;
else
back = color;
}
// Nie podaliśmy koloru do ustalenia.
// Wykorzystamy dialog JColorChooser
public ColorChanger(Component c, String which) {
comp = c;
this.which = which;
}
// Obsługa akcji
public void actionPerformed(ActionEvent e) {
if (fore == null && back == null) { // oznacza, że mamy skorzystać z
// JColorChoosera
Color color = JColorChooser.showDialog(null, which, null);
if (color == null) return;
if (which.equals("Foreground"))
fore = color;
else
back = color;
}
if (fore != null) comp.setForeground(fore);
if (back != null) comp.setBackground(back);
}
}
Przykładowy program, który wykorzystuje uniwersalnego zmieniacza kolorów oraz jego graficzny interfejs mogą wyglądac tak:
import java.awt.*;
import javax.swing.*;
import static java.awt.Color.*;
public class ColorChangerGUI extends JFrame {
ColorChangerGUI() {
JLabel lab = new JLabel("Test kolorów");
lab.setOpaque(true);
lab.setBorder(BorderFactory.createLineBorder(Color.red));
lab.setFont(new Font("Dialog", Font.BOLD, 24));
add(lab);
JPanel p = new JPanel(new GridLayout(0, 1));
JButton b = new JButton("Red on yellow");
b.addActionListener(new ColorChanger(lab, RED, YELLOW));
p.add(b);
b = new JButton("Blue foreground");
b.addActionListener(new ColorChanger(lab, "Foreground", BLUE));
p.add(b);
b = new JButton("Choose background");
b.addActionListener(new ColorChanger(lab, "Backround"));
p.add(b);
add(p, "West");
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
pack();
setLocationRelativeTo(null);
setVisible(true);
}
public static void main(String[] a) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new ColorChangerGUI();
}
});
}
}
Zobacz działanie programu
5. Właściwość clientProperty i jej wykorzystanie przy obsłudze zdarzeń
Każdy J-komponent ma właściwość clientProperty.
Jest to faktycznie tablica asocjacyjna (mapa), do której można dodawać dowolne pary: klucze – wartości.
Zatem każdy J-komponent może zawierać w sobie dowolną informację, a w dowolnej
metodzie obsługi zdarzeń możemy te informacje odczytać (gdyż zawsze mamy
tam dostęp do źródła zdarzenia).
Pary: klucze-wartości umieszczamy w mapie za pomocą odwołania:
comp.putClientProperty(klucz, wart);
gdzie:
comp – dowolny J-komponent
klucz – referencja do dowolnego obiektu
wart – referencja do dowolnego obiektu
a odczytujemy wartości spod kluczy za pomocą odwołania:
Object wart = comp. getClientProperty(klucz);
Zwykle kluczami będą łańcuchy znakowe, ale mogą to być (tak samo jak i wartości) dowolne obiekty.
Warto też podkreślić, że par: kluczy-wartości może być wiele.
Wróćmy do przykładu uniwersalnego zmieniacza kolorów.
Zamiast tworzyć wielu słuchaczy (obiekty klasy ColorChanger) możemy użyć
tylko jednego obiektu-słuchacza. W jego metodzie obsługi akcji będziemy odczytywać
ze źródła zdarzenia niezbędną do wykonania zmiany koloru informację, zapisaną
pod kluczami:
- "ChangeComponent" – jaki komponent podlega zmianie?
- "Foreground" – jaki kolor pierwszego planu chcemy ustalić?
- "Background" – jaki kolor tła chcemy ustalić?
- "ChooseWhichColor" – jeśli nie podano kolorów, to mamy wybrać kolor w dialogu (który – tła czy pierwszego planu?)
Klasę uniwersalnego zmieniacza kolorów możemy teraz zdefiniować jako anonimową
klasę wewnętrzną (w jakimś naszym GUI) i dostarczyć jednego obiektu tej klasy
– niech nazywa się colorChanger.
Aby to wszystko działało musimy w przyciskach, akcja na których ma wywoływać
zmiany koloru jakichś innych komponentów, zapisać niezbędną informację. Nie
musimy zapisywać wszystkich kluczy, tylko niezbędne dla danej sytuacji np.
komponent, który ma podlegać zmianom oraz kolor tła.
Do takich przycisków musimy też przyłączyć naszego słuchacza colorChanger.
Wygodnie będzie wyróżnić te działania w odrębnej metodzie, która jako argumenty
otrzyma niezbędne informacje (gdy jakichś nie podajemy, jako argument specyfikujemy
null). Metodę nazwiemy cB.
Mając te dwa uniwersalne kawałki kodu możemy w naszym programie tworzyć
dowolne zestawy przycisków zmieniające dowolne kolory na dowolnych innych
komponentach GUI.
To wszystko pokazuje poniższy kod.
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import static java.awt.Color.*;
public class ColorChangerGUI2 extends JFrame {
// Obsluga akcji
private ActionListener colorChanger = new ActionListener() {
public void actionPerformed(ActionEvent e) {
JComponent src = (JComponent) e.getSource();
Component comp = (Component) src.getClientProperty("ChangeComponent");
String which = (String) src.getClientProperty("ChooseWhichColor");
Color fore = (Color) src.getClientProperty("Foreground");
Color back = (Color) src.getClientProperty("Background");
if (comp == null) return;
if (which != null) {
Color color = JColorChooser.showDialog(null, which, null);
if (color == null) return;
if (which.equals("Foreground"))
fore = color;
else
back = color;
}
if (fore != null) comp.setForeground(fore);
if (back != null) comp.setBackground(back);
}
};
public ColorChangerGUI2() {
JLabel lab = new JLabel("Test kolorów");
lab.setOpaque(true);
lab.setBorder(BorderFactory.createLineBorder(Color.red));
lab.setFont(new Font("Dialog", Font.BOLD, 24));
add(lab);
JPanel p = new JPanel(new GridLayout(0, 1));
p.add(cB(lab, "Red on yellow", null, RED, YELLOW);
p.add(cB(lab, "Blue foreground", null, BLUE, null));
p.add(cB(lab, "Choose background", "Background", null, null));
add(p, "West");
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
pack();
setLocationRelativeTo(null);
setVisible(true);
}
// Tworzenie przycisków i nadawanie właściwości clientProperty
JButton cB(Component target, String txt, String which, Color f, Color back) {
JButton b = new JButton(txt);
b.putClientProperty("ChangeComponent", target);
if (which != null) b.putClientProperty("ChooseWhichColor", which);
if (f != null) b.putClientProperty("Foreground", f);
if (back != null) b.putClientProperty("Background", back);
b.addActionListener(colorChanger);
return b;
}
public static void main(String[] a) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new ColorChangerGUI2();
}
});
}
}
6. Selekcja obsługiwanych zdarzeń i komponentów
Ważną cechą delegacyjnego modelu obsługi zdarzeń jest możliwość selekcji
zdarzeń do obsługi i komponentów, którym te zdarzenia mogą się przytrafiać.
Przyłączanie Słuchaczy zdarzeń do Źródeł oznacza:
- wybór zdarzeń (dla danego źródła powstają tylko te zdarzenia,
których metody obsługi deklaruje implementowany w klasie Słuchacza interfejs
nasłuchu i tylko te zdarzenia muszą być obsługiwane),
- wybór źródeł (inne komponenty, do których nie przyłączono danego
słuchacza nie są źródłami zdarzeń, obsługiwanych przez słuchacza; dla tych
komponentów zdarzenia takie w ogóle nie powstają).
Ma to ważne konsekwencje pod względem efektywności (generowane są tylko te
zdarzenia i tylko dla tych komponentów, którymi programista wyraził zainteresowanie
poprzez przyłączenie odpowiednich słuchaczy).
Poza tym zwiększa łatwość i niezawodność kodowania (nie trzeba sprawdzać
identyfikatorów zdarzeń i tworzyć rozbudowanych instrukcji warunkowych).
Wyobraźmy sobie np., że tworzymy proste GUI do operowania na jakiejś bazie danych osób.
Mamy dwa pola tekstowe: imię i nazwisko oraz przyciski, służące do wykonywania
operacji na bazie: dodawanie podanej osoby, wyszukiwania po nazwisku i imieniu,
usuwanie rekordu z danymi osoby (hipotetycznie zakładamy, że imię i nazwisko
jednoznacznie identyfikuje osobę).
Chcemy, by:
- kliknięcie w przycisk powodowało wykonanie odpowiedniej operacji,
- wciśnięcie ENTER w polu nazwiska powodowało wykonanie operacji "Szukaj"
- przycisk "Usuń" był podświetlany ostrzegawczo, przy najechaniu na niego kursorem myszki
Efekt ten możemy osiągnąć przez odpowiednie przyłączenie odpowiednich sluchaczy.
Zauważmy: tylko pole nazwiska chcemy mieć aktywne "na Enter". Dlatego do
niego przyłączymy słuchacza akcji, a do pola "imię" – nie. Tylko przycisk
"Usuń" ma być podświetlany ostrzegawczo – zatem tylko do niego przyłączymy
słuchacza myszki, a do innych przycisków – nie.
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class EvSelection extends JFrame implements ActionListener {
JTextField tfFirstName = new JTextField(20); // pole imienia
JTextField tfLastName = new JTextField(20); // pole nazwiska
JComponent cp = (JComponent) getContentPane();
public EvSelection() {
cp.setLayout(new BoxLayout(cp, BoxLayout.Y_AXIS));
cp.setBorder(BorderFactory.createEmptyBorder(20,20,20,20));
tfLastName.setActionCommand("Szukaj"); // nazwa akcji na polu nazwisko
tfLastName.addActionListener(this);
addNamePanel("Imię", tfFirstName); // dla uproszczenia kodu stworzono
addNamePanel("Nazwisko", tfLastName); // metodę addNamePanel - zob. dalej
JPanel p = new JPanel(); // do tego panelu dodajemy przyciski
String[] cmd = { "Dodaj", "Szukaj", "Usuń" };
final boolean[] warn = { false, false, true }; // które z nich mają być
// podświetlane?
for (int i=0; i < cmd.length; i++) {
JButton b = new JButton(cmd[i]);
b.addActionListener(this);
//------------------------------------------------ obsługa zdarzeń myszki
if (warn[i]) b.addMouseListener( new MouseAdapter() {
public void mouseEntered(MouseEvent e) { // wejście w obszar komponentu
JComponent c = (JComponent) e.getSource();
c.putClientProperty("OldColor", c.getBackground());// zapisujemy kolor
c.setBackground(Color.orange); // ustalamy kolor ostrzegawczy
}
public void mouseExited(MouseEvent e) { // wyjście z obszaru komponentu
JComponent c = (JComponent) e.getSource();
c.setBackground((Color) c.getClientProperty("OldColor")); // odtwarzamy
// kolor
}
});
//------------------------------------------------------------------------
p.add(b);
}
cp.add(p);
pack();
show();
}
void addNamePanel(String lab, JTextField tf) {
JPanel p = new JPanel(new FlowLayout(FlowLayout.LEFT));
JLabel l = new JLabel(lab);
l.setPreferredSize(new Dimension(70,20));
l.setHorizontalAlignment(JLabel.RIGHT);
p.add(l);
p.add(tf);
cp.add(p);
}
public void actionPerformed(ActionEvent e) {
System.out.println("Akcja: " +
e.getActionCommand());
}
public static void main(String[] a) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new EvSelection();
}
});
}
}
Zobacz działanie programu
7. Dynamiczne zmiany funkcjonalności: przyłączanie i odłączanie słuchaczy
Słuchacza ls typu XXX przyłączonego do źródła zr można odłączyć za pomocą odwołania:
zr.removeXXXListener(ls);
gdzie: XXX - Action, Mouse etc.
Odłączenie słuchacza ls (czyli konkretnego obiektu implementującego interfejs
nasłuchu) od źródła zr oznacza, iż ten słuchacz (ls) nie będzie obsługiwał
zdarzeń dla tego źródła (zs).
Dynamiczne (w trakcie wykonania
programu) odłączanie i przyłączanie słuchaczy może być wygodnym sposobem
organizowania różnych działań i uzależnionych od aktualnego kontekstu zmian
funkcjonalności GUI.
Działanie mechanizmu dynamicznego przyłączania i odłączania słuchaczy ilsutruje przykładowy program.
Mamy 10 przycisków oznaczających cyfry. Kliknięcie w przycisk wyprowadza
cyfrę na konsolę. To zapewnia słuchacz akcji, który w programie nazywa się
buttAct.
Przycisk przełącznikowy "Recording" włącza lub wyłącza rejestrację kliknięć.
Po włączeniu rejestracji, do każdego przycisku przyłączany jest dodatkowy
słuchacz akcji (w programie nazywa się recordAction), który dodaje do listy
kliknięte przyciski. Po wyłączeniu rejestracji słuchacz ten jest odłączany
od przycisków. Przycisk "Play" odtwarza z listy zarejestrowane kliknięcia
w przyciski.
Początek programu
|
Po wciśnięciu "Recording"
i kliknięciu w przyciski 168
|
Po wyłączeniu rejestracji
i kliknięciu w przycisk Play
|
|
|
|
NA KONSOLI >>>
|
168
|
168
168
|
Zobacz działanie programu
Kod programu, wraz z komentarzami wyjaśniającymi, zawiera poniższy listing
import java.awt.FlowLayout;
import java.awt.GridLayout;
import static java.awt.Color.*;
import java.awt.event.*;
import javax.swing.*;
import java.util.*;
public class Recorder extends JFrame {
private JButton bnum[] = new JButton[10]; // przyciski numeryczne
private JToggleButton record = new JToggleButton("Recording"); // zapisywanie
private JButton play = new JButton("Play"); // odtwarzanie
// lista zarejestrowanych przycisków
List<JButton> playList = new ArrayList<JButton>();
public Recorder() {
// ikonki pobieramy z obrazków z katalogu images w kontekście aplikacji
// getClass() - zwraca klasę tego (this) obiektu, getResource() - zasób o podanej nazwie
record.setIcon(new ImageIcon(getClass().getResource("images/green.gif")));
record.setSelectedIcon(new ImageIcon(getClass().getResource("images/red.gif")));
// Dodatkowy słuchacz akcji - rekorder - dynamicznie przyłączany i odłączany
final ActionListener recorder = new ActionListener() {
public void actionPerformed(ActionEvent e) {
playList.add((JButton) e.getSource()); // dodaje przycisk-źródło do play-listy
}
};
// Obsluga przełącznika "Record"
record.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (((JToggleButton) e.getSource()).isSelected()) { // włączenie nagrywania
play.setEnabled(false); // no to przycisk Play nie może być aktywny
playList.clear(); // będziemy zapisywać od nowa na listę
for (JButton b : bnum)
b.addActionListener(recorder); // przyłączamy słuchacza rejestrującego
}
else { // nastąpiło wyłączenie nagrywania
for (JButton b : bnum)
b.removeActionListener(recorder); // no to odłączamy słuchacza rejestrującego
if (playList.size() > 0) play.setEnabled(true); // jesli coś jest na liście - można odegrać
}
}
});
// Obsługa odtwarzania
play.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
System.out.print('\n');
for(JButton b : playList)
b.doClick();
}
});
// Panel sterujący
JPanel pcon = new JPanel(new FlowLayout(FlowLayout.RIGHT));
pcon.setBorder(BorderFactory.createLineBorder(BLUE));
pcon.add(record);
pcon.add(play);
add(pcon,"South");
// ten słuchacz obsługuje zwykłe
// "kliknięcia" w przyciski numeryczne
ActionListener btnListener = new ActionListener() {
public void actionPerformed(ActionEvent e) {
System.out.print(e.getActionCommand());
}
};
// panel przycisków numerycznych
JPanel p = new JPanel(new GridLayout(3,0));
for (int i = 0; i<bnum.length; i++) {
bnum[i] = new JButton(""+i);
bnum[i].addActionListener(btnListener);
p.add(bnum[i]);
}
add(p, "Center");
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
pack();
setLocationRelativeTo(null);
setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new Recorder();
}
});
}
}
8. Separacja
Delegacyjny model obsługi zdarzeń oznacza oddelegowanie obsługi do innego
obiektu, niż ten któremu przytrafia się zdarzenie. Dzięki tej koncepcji w
łatwy sposób możemy izolować na poziomie merytorycznym różne fragmenty aplikacji.
Na przykład, możemy oddzielić kod graficznego interfejsu użytkownika od kodu
odpowiedzialnego za prawdziwą "pracę". Zmiany w jednej części nie dotkną
drugiej, nie będzie też potrzebna rekompilacja wszystkiego.
Dla ilustracji "roboczą" część aplikacji umieścimy w klasie o nazwie MainWork. Zawiera ona
różne metody wykonujące "prawdziwe" czynności (np. dodawanie elementów do
bazy danych).
W innej klasie (o nazwie Gui) zawrzemy konstrukcję interfejsu graficznego, który ma pozwalać użytkownikowi na wybór czynności.
Klasa MainWork nie musi wiedzieć jaki jest ten interfejs, klasa Gui nie musi
wiedzieć jakie konkretnie czynności daje użytkownikowi do wyboru, ani jak
je obsłużyć.
Obsługą zajmie się klasa MainWork. Jej obiekt będzie Słuchaczem zdarzeń przychodzących
z interfejsu (tu założymy, że chodzi nam wyłącznie o zdarzenia "akcja").
Przykładowa klasa Gui
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
class Gui extends JFrame {
Gui(String labels[], String commands[], ActionListener a) {
super("Test");
JButton b[] = new JButton[labels.length];
setLayout(new GridLayout());
for (int i = 0; i < labels.length; i++) {
b[i] = new JButton(labels[i]);
b[i].setActionCommand(commands[i]);
b[i].addActionListener(a);
add(b[i]);
}
pack();
show();
}
}
Przyjęto, że czynności mają się pojawiać jako przyciski (z odpowiednimi etykietami
i ustalonym napisem przywiązanym do zdarzenia-akcji poprzez setActionCommand).
Osiągnęliśmy istotną separację.
To GUI może być zastosowane do zupełnie innych czynności (wystarczy utworzyć je z innej niż MainWork klasy).
Jednocześnie możemy - nie ruszając nic w klasie MainWork - przeprojektować
GUI (np. umieszczając wybór akcji na liście lub w menu, dodając jakieś elementy
graficzne itp. itd.).