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).
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: (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.



1


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.


r

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).

r 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:

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.

r
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);
  }
}
1 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:
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:


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ę).

1 Chcemy, by:

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
r
r
r
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").

1


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.).