Obsługa zdarzeń - konkrety



"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. Hierarchia klas zdarzeniowych

Każde zdarzenie jest obiektem jakiejś klasy. Hierarchię klas zdarzeniowych przedstawia poniższy schemat.

1


Uwagi:

Zdarzenia są zawsze obiektami odpowiednich klas zdarzeniowych. Klasy określają typ (rodzaj) zdarzenia. W ramach jednego rodzaju zdarzenia może być więcej niż jedno konkretne zdarzenie (np. typ zdarzenia mouse - zdarzenia związane z myszką i konkretne zdarzenia: wciśnięcie klawisza, zwolnienie klawisza, przesunięcie kursora myszki itd).



Klasy wyprowadzane z klasy ComponentEvent konstytuują zdarzenia fizyczne (np. kliknięcie myszką, czy zmiana rozmiaru komponentu). Wszystkie te zdarzenia dotyczą komponentów (stąd uszczegółowiony sposób identyfikacji źródła - metoda getComponent() z klasy ComponentEvent zwracająca referencję do obiektu klasy Component).
Wszystkie typy zdarzeń pochodne od ComponentEvent zawierają więcej niż jedno konkretne zdarzenie.
Specyficznym zdarzeniem jest tu PaintEvent, generowane przez JVM, gdy komponent wymaga odrysowania. W przeciwieństwie do innych zdarzeń komponentowych nie dostarczono bowiem interfejsu nasłuchu pozwalającego na tworzenie i przyłączanie słuchaczy tego zdarzenia. Jedynym sposobem jego "obsługi" jest przedefiniwanie metody paint(Graphics) lub -dla J-komponentów - wywoływanych przez nią: paintComponent(), paintBorder(), paintChildren() lub ew. bezposrednie dzialanie na kolejce zdarzeń.

Cztery inne klasy, nie będące podklasami ComponentEvent dotyczą zdarzeń semantycznych, mających znaczenie zależne od kontekstu, a nie fizycznych właściwości zdarzenia: Znaczenie tych zdarzeń (i sposób generowania na podstawie zdarzeń fizycznych) zostało określone dla wybranych komponentów wizualnych AWT i Swing, nic jednak nie stoi na przeszkodzie, by zdarzenia te traktować bardziej abstrakcyjnie i definiować je (oraz ich obsługę) w kontekście innych obiektów, w tym niewizualnych.
Warto zauważyć, że zdarzenie TextEvent (i odpowiadające mu interfejsy nasłuchu) są w tej chwili określone tylko dla tekstowych komponentów AWT. Tekstowe J-komponenty zamiast nasłuchu tekstu wykorzystują nasłuch zmian dokumentu, o czym przy okazji MVC.
W przypadku zdarzeń semantycznych typ zdarzenia określa konkretne zdarzenie (mamy po jednym konkretnym zdarzeniu dla każdego typu).


2. Obsługa zdarzeń - przegląd

Ogólne reguły obsługi zdarzeń podane poprzednio (i których zastosowanie  poznaliśmy szczegółowo na przykładzie zdarzeń typu action) stosują się do wszystkich innych zdarzeń (z nielicznymi wyjątkami, jak np. PaintEvent).

W tabeli przedstawiono odpowiadające poszczególnym zdarzeniom pochodnym od klasy AWTEvent interfejsy i metody obsługi.

Tabela. Obsługa zdarzeń

Klasa zdarzeń
Zdarzenia
Metody obsługi
Nasłuch 

ActionEvent

ACTION_PERFORMED

actionPerformed

ActionListener

AdjustmentEvent

ADJUSTMENT_VALUE_CHANGED

adjustmentValueChanged

AdjustmentListener

ItemEvent

ITEM_STATE_CHANGED

itemStateChanged

ItemListener

TextEvent

TEXT_VALUE_CHANGED

textValueChanged

TextListener

MouseEvent 

MOUSE_ENTERED 
MOUSE_EXITED 
MOUSE_PRESSED 
MOUSE_RELEASED 
MOUSE_CLICKED

mouseEntered 
mouseExited 
mousePressed 
mouseReleased 
mouseClicked

MouseListener

MOUSE_MOVED 
MOUSE_DRAGGED

mouseMoved 
mouseDragged

MouseMotionListener

MouseWheelEvent

MOUSE_WHEEL_MOVED

mouseWheelMoved

MouseWheelListener

KeyEvent

KEY_PRESSED 
KEY_RELEASED 
KEY_TYPED

keyPressed 
keyReleased 
keyTyped

KeyListener

FocusEvent

FOCUS_GAINED 
FOCUS_LOST

focusGained 
focusLost

FocusListener

ContainerEvent

COMPONENT_ADDED 
COMPONENT_REMOVED

componentAdded 
componentRemoved

ContainerListener

ComponentEvent

COMPONENT_HIDDEN 
COMPONENT_SHOWN 
COMPONENT_MOVED 
COMPONENT_RESIZED

componentHidden 
componentShown 
componentMoved 
componentResized

ComponentListener

WindowEvent

WINDOW_ACTIVATED 
WINDOW_DEACTIVATED 
WINDOW_ICONIFIED 
WINDOW_DEICONIFIED 
WINDOW_OPENED 
WINDOW_CLOSING 
WINDOW_CLOSED

windowActivated 
windowDeactivated 
windowIconified 
windowDeiconified 
windowOpened 
windowClosing 
windowClosed

WindowListener

WINDOW_STATE_CHANGED

windowStateChanged

WindowStateListener

WINDOW_GAINED_FOCUS
WINDOW_LOST_FOCUS

windowGainedFocus
windowLostFocus

WindowFocusListener
Uwagi: w kolumnie "Zdarzenie" podano nazwę finalnego statycznego pola (typu int) danej klasy, oznaczającego stałą identyfikującą zdarzenie (identyfikator zdarzenia). 
W kolumnie "Metoda" podano nazwę metody obsługi danego zdarzenia. 
Metody te są publiczne (public), nie zwracają wyniku (void) i zawsze mają jeden argument - referencję do obiektu danej klasy zdarzenia.

Wszystkie zdarzenia można odpytać o ich źródło za pomocą metody getSource() z klasy EventObject.
Zdarzenia klas  pochodnych od AWTEvent można zapytać o identyfikator zdarzenia za pomocą metody getID().
Zdarzenia pochodne od klasy ComponentEvent dostarczają referencji do źródła zdarzenia jako do obiektu typu Component po wywołaniu getComponent().

Warto zauważyć, że dla obsługi wszystkich zdarzeń stosowane są spójne, identyczne reguły nazewnicze.
Np. jeśli mamy konkretne zdarzenie wciśnięcia klawisza, to możemy wyróżnić jego ogólny typ: "key", a w ramach typu nazwać konkretne zdarzenie: "pressed", "released" itd. Klasa zdarzeniowa będzie nazywać się KeyEvent  (duża litera!), interfejs nasłuchu KeyListener (duża litera!), identyfikator zdarzenia KEY_PRESSED (wszystkie duże litery, podkreślenie), metoda obsługi - keyPressed (zaczynamy od małej, notacja węgierska).
Dotyczy to obsługi wszystkich zdarzeń i polega na odpowiednim zmienianiu wielkości liter.
Ogólny schemat przedstawiono poniżej.

1

W standardowych pakietach Javy określono związek pomiędzy zdarzeniami a ich  możliwymi źródłami.

Typ zdarzenia
Komu może się przytrafić?
ComponentEvent Wszystkim komponentom AWT i Swingu
ContainerEvent Kontenerom AWT, kontenerowi Box i wszystkim J-komponentom
MouseEvent Wszystkim komponentom AWT i Swingu
FocusEvent
Wszystkim komponentom AWT i Swingu, dla których określono zdolność otrzymywania fokusu
KeyEventWszystkim komponentom AWT i Swingu, które mogą otrzymywać fokus
WindowEvent Wszystkim komponentom pochodnym od klasy Window
InternalFrameEventWewnętrznym oknom Swingu
PropertyChangeEvent Wszystkim komponentom AWT i Swingu
ActionEvent Komponentom klas: Button, JButton, JToggleButton, JCheckBox, JRadioButton, MenuItem, JMenuItem, CheckBoxMenuItem, JCheckBoxMenuItem, JRadioButtonMenuItem, TextField, JTextField, List, JComboBox
TextEvent TextField, TextArea
ItemEvent CheckBox, CheckBoxMEnuItem JToggleButton, JCheckBox, JRadioButton, JCheckBoxMenuItem, JRadioButtonMenuItem, List, JComboBox
AdjustingEvent ScrollBar, JScrollBar

 



3. Obsługa zdarzeń myszki


Do obsługi zdarzeń myszki służą trzy interfejsy: MouseListener, MouseMotionListener i MouseInputListener oraz odpowiadające im adaptery.

MouseInputListener (z pakietu javax.swing.event) rozszerza oba interfejsy MouseListener i MouseMotionListener (łączy ich wszystkie metody).

Podział na dwa interfejsy MouseListener i MouseMotionListener wynika zaś ze względów efektywnościowych: jeżeli nie jesteśmy zainteresowani nasłuchem poruszeń myszki, a jedynie zdarzeniami o mniejszej częstotliwości to stosujemy słuchaczy klas implementujących MouseListener (co zmniejsza wydatnie generację zdarzeń w naszej aplikacji).

Warto podkreślić, że do konkretnego komponentu możemy przyłączać sluchaczy myszki (addMouseListener) i/lub  słuchaczy poruszeń myszki (addMouseMotionListener), nie ma zaś metody przyłączenia obu słuchaczy naraz (hipotetyczny addMouseInputListener).

Zdarzenia myszki

Zdarzenie

Metoda obsługi

Interfejs nasłuchu

wejście kursora myszki w obszar komponentu

mouseEntered

 

MouseListener lub MouseInputListener

wyjście k ursora myszki z obszaru komponentu

mouseExited

wciśnięcie klawisza myszki

mousePressed

zwolnienie klawisza myszki

mouseReleased

kliknięcie

mouseClicked

przes unięcie kursora myszki

mouseMoved

MouseMotionListener
lub MouseInputListener

wleczenie kursora myszki (przesunięcie przy 

wciśnietym klawiszu)

mouseDragged


Niektóre zdarzenia myszki na niektórych komponentach (m.in. przyciski i elementy menu) mogą wywołać zdarzenie akcji (jeśli przyłączono słuchacza akcji).

Zdarzenia myszki (klasa MouseEvent) razem ze zdarzeniami przychodzącymi z klawiatury (klasa KeyEvent) należą do ogólnego typu zdarzeń wejściowych (klasa InputEvent bazowa dla obu klas MouseEvent i KeyEvent).

Od każdego zdarzenia klasy InputEvent (oprócz ogólnych informacji o źródle i identyfikatorze zdarzenia) możemy się dowiedzieć o następujących okolicznościach zdarzenia:

W przypadku zdarzeń myszki metoda isMetaDown()  określa czy użyto prawego klawisza myszki (zwraca wtedy true).


Informacje o dodatkowych klawiszach możemy uzyskać zbiorczo za pomocą metody getModifiers() , zwracającej flagę typu long, w której ustawiono odpowiednie bity, określające które z dodatkowych klawiszy wciśnięto przy zajściu zdarzenia. Z flagi modyfikatorów informacje wyłuskujemy za pomocą operacji bitowych, których drugim argumentem są odpowiednie stałe z klasy InputEvent.

Oprócz ogólnych dla zdarzeń wejściowych metod uzyskiwania informacji, klasa MouseEvent dostarcza specyficznych dla zdarzeń myszki metod:

Przykładowy program pokazuje zastosowanie tych metod.

r Działanie programu jest następujące.
Zwolnienie klawisza myszki na pulpicie (contentPane) wstawia w miejscu kursora etykietę z kolejnym znakiem Unicodu (poczynając od 'A'). Normalnie etykieta jest w czarnej ramce. Wskazanie etykiety myszką sygnalizowane jest czerwoną ramką. Etykietę można usunąć przez ctrl-kliknięcie lub przesuwać wciskając dowolny klawisz myszki i wlokąc etykietę po pulpicie (wtedy pojawi się niebieska grubsza ramka).
Kliknięcie prawym klawiszem myszki w pulpit zmienia widoczność etykiet (jeśli są widoczne - stają się niewidoczne i odwrotnie). Jeśli przy tym wciśnięto klawisz ctrl, to wszystkie etykiety są usuwane.

Zobacz działanie programu








Poniżej przedstawiono kod programu.


import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.border.*;

class MousePlay extends JFrame implements MouseInputListener {

  private Container cp = getContentPane();
  private int currIndex = 0;              // do tworzenia kolejnych znaków
  private int diffX =0, diffY =0;         // używane przy wleczeniu
  private boolean isDragged;              // czy wleczenie

  // Ramki dla etykiet
  Border normal = BorderFactory.createLineBorder(Color.black),
        pointed = BorderFactory.createLineBorder(Color.red, 2),
        dragged = BorderFactory.createLineBorder(Color.blue, 4);

  public MousePlay() {

   cp.setLayout(null); // bez rozkładu!

   cp.addMouseListener(new MouseAdapter() {
     public void mouseReleased(MouseEvent e) {
       if (e.isMetaDown()) {                           // prawy klawisz  
         if (e.isControlDown()) removeAllComponents(); // z Ctrl - usunięcie wszystkich
         else  setVisibility();                        // bez - ukrycie
       }
       else addLabel(e.getX(), e.getY());              // lewe klawisz - dodanie etykiety
     }
   });
   setDefaultCloseOperation(DISPOSE_ON_CLOSE);
   setSize(300, 300);
   setLocationRelativeTo(null);
   setVisible(true);
  }

  // Utworzenie i dodanie etykiety w miejscu kursora myszki (x, y)
  private void addLabel(int x, int y) {
     JLabel l = new JLabel("" + (char) ('A' + currIndex++));
     l.setBounds(x, y, 50, 50);
     l.setBorder(normal);
     l.setFont(new Font("Dialog", Font.BOLD, 24));
     l.setHorizontalAlignment(JLabel.CENTER);
     l.setVerticalAlignment(JLabel.CENTER);
     l.addMouseListener(this);
     l.addMouseMotionListener(this);
     cp.add(l);
  }

  // zmiana widoczności komponentów w panelu
  private void setVisibility() {
    for (Component comp : cp.getComponents() ) comp.setVisible(!comp.isVisible());
  }

  // usunięcie wszystkich komponentów
  private void removeAllComponents() {
    cp.removeAll();
    cp.repaint();
  }

  // Metody obsługujące zdarzenia myszki dla etykiet

  // Przy wejściu kursora w obszar etykiety - zmiana ramki, ale tylko wtedy
  // gdy nie wleczemy jakiejś innej etykiety

  public void mouseEntered(MouseEvent e) {
    if (isDragged) return;
    ((JComponent) e.getSource()).setBorder(pointed);
  }

  // Przywrócenie ramki z uwagą j.w.
  public void mouseExited(MouseEvent e) {
    if (isDragged) return;
    ((JComponent) e.getSource()).setBorder(normal);
  }


  public void mousePressed(MouseEvent e) {
   isDragged = true; // być może zaczynamy wleczenie
   ((JComponent) e.getSource()).setBorder(dragged); // ramka dla wleczenia
   diffX = e.getX(); // w jakiej odległości kursor od górnego rogu komponentu
   diffY = e.getY(); // - potrzebne do korygowania zmian lokalizacji
  }                  //   przy wleczeniu

  public void mouseReleased(MouseEvent e) {
    isDragged = false;           // ew. koniec wleczenia
    if (e.isControlDown()) {     // usunięcie etykiety, jeśli wciśnięto Ctrl
      cp.remove(e.getComponent());
      cp.repaint();
      return;
    }
    ((JComponent) e.getSource()).setBorder(pointed);
  }

  // wleczenie polega na zmianie położenia
  public void mouseDragged(MouseEvent e) {
    Component c = e.getComponent();
    // nowe położenie musimy skorygować w zależności od tego
    // w jakim miejscu schwyciliśmy etykietę (diffX, diffY)
    c.setLocation(c.getX() + e.getX() - diffX, c.getY() + e.getY() - diffY);
  }

  public void mouseClicked(MouseEvent e) {}
  public void mouseMoved(MouseEvent e) {}

  public static void main(String[] a) {
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        new MousePlay();
      }
    });
  }

}

Drugi przykład dotyczy nieudekorowanego (pozbawionego ramek i paska tytułu) okna JFrame. Tego typu okienka sa często stosowane do "drobnej informacji" (zajmują mało miejsca i moga sobie gdzieś pływac po ekranie, pozostając "zawsze na wierzchu" i przypominając o różnych rzeczach). Brak dekoracji uzyskamy metodą setUndecorated(..), opcję "na wierzchu" - metodą setAlwaysOnTop(). Ale jak zapewnić przesuwanie takiego okienka (przecież nie ma go za co uchwycić). Tu na pomoc przyjdą wspomniane wcześniej metoyd getXOnScreen() i getYOnScreen() zwracające położenie okna w prawdziwych współrzędnych ekranowych. Znając aktualne położenie, przy wleczeniu myszki możemy je odpowiednio zmieniac i w ten sposób przesuwac okno po ekranie.

Zobacz działanie programu







Poniżej przedstawiono kod programu.
import java.awt.*;
import java.awt.event.*;

import javax.swing.*;
import javax.swing.border.*;
import javax.swing.event.*;

public class StickyWin extends JFrame {
  
  private static Icon icon = new ImageIcon(StickyWin.class.getResource("images/close.gif"));
  private static boolean again = true;  // czy po zamknięciu okna mozna je pokazać z nowym tekstem?

  private int diffX, diffY;     // dostosowanie (nowe pozycje okna zależą od miejca jego uchwycenia)

  // Obsługa 
  MouseInputAdapter mover = new MouseInputAdapter() {

    @Override
    public void mousePressed(MouseEvent e) {
      diffX = e.getXOnScreen() - StickyWin.this.getX(); // wyraźne odwołanie do this klasy otaczającej!
      diffY = e.getYOnScreen() - StickyWin.this.getY();
    }

    @Override
    public void mouseDragged(MouseEvent e) {  // wleczenie myszki zmienia położenie okna na ekranie
      int x = e.getXOnScreen() - diffX;
      int y = e.getYOnScreen() - diffY;
      setLocation(x, y);
    }

  };

  public StickyWin(String txt) {
    // Zdejmiemy dekoracje z okna JFrame (ramki i tytuły)
    this.setUndecorated(true);
    // Niech będzie zawsze na wierzchu
    this.setAlwaysOnTop(true);
    // Ramki
    Border empty = new EmptyBorder(10, 20, 10, 5);
    Border line = new LineBorder(Color.BLUE, 2);
    ((JComponent) getContentPane()).setBorder(BorderFactory
        .createCompoundBorder(line, empty));
    // Box dla komponentów
    Box box = Box.createHorizontalBox();
    box.add(new JLabel(txt));
    box.add(Box.createHorizontalStrut(10));
    JLabel x = new JLabel(icon);
    x.setForeground(Color.RED);
    x.setFont(new Font("Dialog", Font.BOLD, 14));
    x.setToolTipText("Close");
    x.addMouseListener(new MouseAdapter() {  // 'x' zamyka okno

      @Override
      public void mouseReleased(MouseEvent e) {
        dispose();
        if (again) {
          String in = JOptionPane
              .showInputDialog("Teraz możesz podać swój tekst");
          again = false;
          new StickyWin(in);
        }

      }
    });
    box.add(x);
    add(box);
    addMouseListener(mover);
    addMouseMotionListener(mover);
    pack();
    setVisible(true);
  }

  public static void main(String[] args) {
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        new StickyWin("<html>Telefon:<br>111-111-222<html>");
      }
    });
    
  }
}


4. Obsługa menu

4.1. Menu kontekstowe


Menu kontekstowe niewiele różni się od menu rozwijalnego. Jego podstawową cechą jest to, iż może się pojawić w kontekście dowolnego komponentu na skutek zajścia określonego zdarzenia (zazwyczaj, tradycyjnie użycia prawego klawisza myszki).

Aby stworzyć menu kontekstowe, musimy użyć konstruktora klasy JPopupMenu:
 
    JPopupMenu pm = new JPopupMenu();
 
Alternatywnie, kontekstowemu menu możemy nadać etykietę:
 
    JPopupMenu pm = new JPopupMenu("Menu");


 
Po stworzeniu menu kontekstowego możemy dodawać do niego elementy. Elementy menu są obiektami klasy JMenuItem (lub JCheckBoxMenuItem lub JRadioButtonMenuItem), musimy więc najpierw stworzyć takie obiekty, a następnie dodać je do menu.
Wygląda to mniej więcej tak:

JMenuItem mi = new JMenuItem("Nazwa elementu menu");
pm.add(mi); // pm oznacza menu kontekstowe .


 
Aby zapewnić możiwość uwidocznienie menu kontekstowego standardowymi dla danej platformy sposobami (np. użycie prawego klawisza myszki "na komponencie" lub wciśnięcie specjalnego klawisza) wystarczy zastosować następujące odwołanie:

comp.setComponentPopupMenu(pm),

gdzie:
comp - dowolny J-komponent
pm - menu kontekstowe (referencja do obiektu klasy JPopupMenu)


Przykład:
import java.awt.*;
import javax.swing.*;

public class PopupTest extends JFrame {
  
  private Icon icon = new ImageIcon(getClass().getResource("images/green.gif"));

  public PopupTest() {
    setLayout(new FlowLayout());
    JPopupMenu pm = createPopup("Jeden", "Dwa", "Trzy");
    JButton[] btns =  { new JButton("Przycisk 1"), 
                        new JButton("Przycisk 2"),
                        };
    for (JButton b : btns) {
      b.setComponentPopupMenu(pm);
      add(b);
    }
    setDefaultCloseOperation(DISPOSE_ON_CLOSE);
    pack();
    setLocationRelativeTo(null);
    setVisible(true);
  }
  
  JPopupMenu createPopup(String ... items ) {
    JPopupMenu pm = new JPopupMenu();
    for (String s : items) {
      pm.add(new JMenuItem(s, icon));  
    }
    return pm;
  }

  public static void main(String[] args) {
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        new PopupTest();
      }
    });
  }

}
Użycie prawego klawisza myszki na którymś z przycisków "Przycisk 1" lub "Przycisk 2" spowoduje otwarcie menu kontekstowego (zob. rysunek).

r

Komponenty, którym nie przypisano menu kontekstowego za pomocą wywołania setComponentPopupMenu, mogą przejmować menu kontekstowe  "rodziców" (komponentów znajdujących się wyżej w hierarchii zawierania się komponentów) o ile ustalimy własciwość inheritPopupMenu na true poprzez wywołanie comp.inheritPopupMenu(true).

Np. poniższy fragment (zmodyfikowany kawałek poprzedniego programu) ustala menu kontekstowe dla contentPane, a każdy z przycisków odziedziczy je (tzn. będzie się ono ukazywać również w kontekście tych przycisków po użyciu standardowych środków platformy - np. prawego klawisza myszki).

    JPopupMenu pm = createPopup("Jeden", "Dwa", "Trzy");
    
    ((JComponent) getContentPane()).setComponentPopupMenu(pm);
    
    JButton[] btns =  { new JButton("Przycisk 1"), 
                        new JButton("Przycisk 2"),
                      };
    
    for (JButton b : btns) {
      b.setInheritsPopupMenu(true);
      add(b);
    }

Tak naprawdę, metoda setComponentPopupMenu jest tylko wygodnym skrótem dla tradycyjnych, klasycznych sytuacji.
 
Ogólnie, aby pokazać menu kontekstowe należy użyć metody show z klasy JPopupMenu.
Metoda show ma jako argumenty: komponent "na którym" otwiera się menu oraz współrzędne x i y (liczone względem tego komponentu), określające miejsce pojawienia się menu.


Kiedy na ekranie ma pojawić się menu kontekstowe?
Metoda show może być zastosowana w różnych (praktycznie dowolnych) sytuacjach.
A zwykle?
Wydaje się, że menu kontekstowe powinno być otwierane prawym kliknięciem myszki. Ale to nieprawda. Niektóre systemy preferuje otwarcie menu kontekstowego na skutek zwolnienia prawego przycisku myszki, inne  pokazują je gdy prawy przycisk został wciśnięty.
 
W klasie MouseEvent zdefiniowano metodę isPopupTrigger(). Jest ona tak zaprojektowana, iż - wołana z metody obsługi zdarzenia typu MousEvent - dla każdej konkretnej implementacji zwraca wartość "prawda" tylko wtedy, gdy dane zdarzenie może otworzyć menu kontekstowe. Np. "wewnątrz" metody mouseClicked "sprawdzenie" isPopupTrigger() zwraca zawsze wartość "fałsz".

Zatem w wielu starszych kodach mozna znaleźć typową sekwencję:
Poczynając od wersji JDK 1.5 takie typowe sytuacje załatwia dla nas metoda setCompoenntPopupMenu.
Ale podany schemat jest ogólniejszy i może się czasem przydać



 

4.2. Obsługa wyboru opcji menu kontekstowego i rozwijalnego

Wybór opcji z menu może spowodowac wystąpienie zadrzenia ActioinEvent
Zazwyczaj więc będziemy mieli tu słuchacza akcji, ale czasem (gdy element menu jest typu JCheckBoxMenuItem lub JRadioButtonMenuItem, co oznacza, iż kliknięciem przeprowadzany raczej zaznaczenie niż powodujemy akcję) może pojawić się "słuchacz zaznaczeń elementów" (ItemListener).

Słuchacz wyborów z menu musi być przyłączony do elementów (opcji) menu.

r



Obrazuje to poniższy programik, w którym stworzono menu rozwijalne File oraz menu kontekstowe, otwierane na polu tekstowym JTextArea. Do elementów menu jest przyłaczony słuchacz zdarzeń akcji, który po prostu wypisuje na pasku stanu (zrealizowanym jako etykieta) nazwę akcji (actionCommand).







import java.awt.event.*;
import java.util.*;

import javax.swing.*;

public class TestMenu extends JFrame implements ActionListener {
  
  private JLabel statusBar = new JLabel(" ");
  
  public TestMenu() {
    List<JMenuItem> fileItems = createMenuItems("Open", "Save", "Save as", "Exit");
    List<JMenuItem> editItems = createMenuItems("Copy", "Cut", "Paste", "Select");
    JMenu pulldown = new JMenu("File");
    for(JMenuItem mi : fileItems) pulldown.add(mi);
    JPopupMenu popup = new JPopupMenu();
    for(JMenuItem mi : editItems) popup.add(mi);  
    
    JMenuBar mb = new JMenuBar();
    mb.add(pulldown);
    setJMenuBar(mb);
    
    JTextArea ta = new JTextArea(10,20);
    ta.setComponentPopupMenu(popup);
    add(new JScrollPane(ta));
    
    add(statusBar, "South");
    
    setDefaultCloseOperation(DISPOSE_ON_CLOSE);
    pack();
    setLocationRelativeTo(null);
    setVisible(true);
  }
  
  private List<JMenuItem> createMenuItems(String ... items) {
    List<JMenuItem> list = new ArrayList<JMenuItem>();
    for(String s : items) {
      JMenuItem mi = new JMenuItem(s);
      mi.addActionListener(this);
      list.add(mi);
    }
    return list;
  }

  @Override
  public void actionPerformed(ActionEvent e) {
    statusBar.setText("Ostatnio wykonano: " + e.getActionCommand());
  }

  public static void main(String[] args) {
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        new TestMenu();
      }
    });
  }


}

Przy okazji tego programu powstaje pytanie: co zrobić, aby mieć to samo menu jako rozwijalne i jako kontekstowe? Na przykład, menu z opcjami edycyjnymi.
Niestety, dodanie tych samych elementów (opcji) do obu menus nie da spodziewanego efektu (znajdą się tylko w jednym). Tworzenie odrębnych opcji (JMenuItem), tak naprawdę mających taką samą treść, i oddzielne dodawanie ich do różnych menu oczywiście zadziała, ale jest raczej zniechęcające.
Eleganckim rozwiązaniem jest użycie obiektów-akcji (o czym w następnym wykładzie).
Ale można też zastosować inne sposoby.
Zobaczmy.

4.3. Invoker menu kontekstowego. Menu kontekstowe i rozwijalne w jednym


Często istotne jest, by przy obsłudze zdarzenia wyboru elementu menu kontekstowego wiedzieć na jakim komponencie menu zostało otwarte (bo np. wykonywana akcja ma dotyczyć właśnie tego komponentu).
W obsłudze zdarzenia mamy dostęp do jego źródła, którym - oczywiście - jest element menu (a nie komponent na którym menu otwarto). Istnieje jednak łatwy sposób, aby uzyskać informacje o tym komponencie. Możemy o to zapytać samego menu kontekstowego za pomocą metody getInvoker() z klasy JPopupMenu, która zwraca referencję do komponentu (typ Component), na którym menu zostało otwarte.

W obsłudze zdarzenia wyboru elementu menu możemy mieć dostęp do samego menu kontekstowego na dwa sposoby:
JPopupMenu popup = new JPopupMenu();
// ...
ActionListener al = new ActionListener() {
    public void actionPerformed(ActionEvent e) {
      JMenuItem c = (JMenuItem) e.getSource(); // źródło zdarzenia wyboru elementu
      Component invoker = popup.getInvoker(); // komponent na którym otwarto menu
      // ...
    }
};  
// ...
 ActionListener al = new ActionListener() {
    public void actionPerformed(ActionEvent e) {
      Component c = (JComponent) e.getSource();
      JPopupMenu popup = (JPopupMenu) c.getParent();
      Component invoker = popup.getInvoker();
      // ...
    }
}; 

r



Przykładowy program pokazuje użycie metody getInvoker() w celu ustalenia jakiego komponentu ma dotyczyć zmiana koloru. Menu z opcjami kolorów tła i pierwszego planu możemy otworzyć na contentPane i na każdym z zawartych w nim przycisków. Po to by zmienić kolory właściwego komponentu (tego na którym otwarto menu) stosujemy opisany wyżej sposób. Rysunek obok przedstaiwa wygląd programu testowego, a kod pokazano poniżej.


package popupinvoker;

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

import static java.awt.Color.*;

public class PopupInvoker extends JFrame {

  private JPopupMenu popup = new JPopupMenu();

  private ActionListener colorChanger = new ActionListener() {

    public void actionPerformed(ActionEvent e) {
      Component c = (Component) e.getSource();
      Color back = c.getBackground();
      Color fore = c.getForeground();
      JPopupMenu popup = (JPopupMenu) c.getParent();
      Component invoker = popup.getInvoker();
      invoker.setBackground(back);
      invoker.setForeground(fore);
    }
  };

  public PopupInvoker() {
    super("Invoker test");
    createPopupMenu();
    setLayout(new FlowLayout());
    JComponent cp = (JComponent) getContentPane();
    cp.setComponentPopupMenu(popup);
    for (int i = 1; i <= 3; i++) {
      JButton b = new JButton("Przycisk " + i);
      b.setInheritsPopupMenu(true);
      add(b);
    }
    setDefaultCloseOperation(DISPOSE_ON_CLOSE);
    setSize(400,200);
    setLocationRelativeTo(null);
    setVisible(true);
  }

  private void createPopupMenu() {
    Color[] back = { YELLOW, BLUE, BLACK, WHITE };
    Color[] fore = { BLUE, YELLOW, WHITE, RED };
    for (int i = 0; i < back.length; i++) {
      JMenuItem mi = new JMenuItem("Color change");
      mi.setBackground(back[i]);
      mi.setForeground(fore[i]);
      mi.addActionListener(colorChanger);
      popup.add(mi);
    }
  }

  public static void main(String[] args) {
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        new PopupInvoker();
      }
    });
  }
}
Możemy też dynamicznie zmieniać właściwość "invoker" za pomocą  metody setInvoker(...).
Wykorzystamy to przy budowie menu, ktore będzie dzialać jako rozwijalne i jako kontekstowe zarazem.

Przede wszystkim należy zauważyć, że menu rozwijalne JMenu pokazywane jest za pomocą menu "wyskakującego"  - popup (tego właśnie, które nazywamu kontekstowym), otwieranego w odpowiednim miejscu, w relacji do paska menu. W klasie JMenu znajdziemy nawet metodę getPopupMenu(), która zwraca właśnie menu typu "popup", służące pokazywaniu tego  menu rozwijalnego.
A więc problem zastosowania jednego i tego samego menu jako rozwijalnego i kontekstowego zarazem został rozwiązany? Czy wystarczy napisać tak:
    JMenu editMenu = createMenu( ... "Copy", "Cut", "Paste", "Select");
    
    JPopupMenu popup = editMenu.getPopupMenu();

    JMenuBar mb = new JMenuBar();
    mb.add(editMenu);

    JTextArea ta = new JTextArea(10,20);
    ta.setComponentPopupMenu(popup);
Niestety spotka nas niemiła niespodzianka. Po otwarciu menu editMenu jako kontekstowego w obszarze edytora, ponowna próba otwarcia go z  paska menu (np. opcja "Edit") nie przyniesie rezultatu.
Okazuje się bowiem, że otwarcie menu w kontekście jakiegoś komponentu ustala właściwość "invoker" dla tego menu na ten właśnie komponent. I to później nie ulega już zmianie. Tymczasem dla "popup" menu rozwijalnego właściwy "invoker" to obiekt klasy JMenu (to właśnie menu rozwijalne). Należy więc go przywrócić w momencie zamykania menu kontekstowego.

Można to zrobić wykorzystując nasłuch zdarzeń PopupMenuEvent, do czego służą następujące metody interfejsu PopupMenuListener.


Oto kod, który tuż przed zamknięciem menu kontekstowego, przywraca menu rozwijalne (zmienna editMenu) jako  "invoker" tego "popupu".
      public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
        popup.setInvoker(editMenu);
      }
Poniższy przykładowy program realizuje menu o nazwie editMenu jako rozwijalne (otwierane z paska menu) i zarazem kontekstow (otwierane prawym klawiszem myszki na polu edycyjnym JTetxtArea)..

package menu2;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;
import javax.swing.event.*;

public class TestMenu extends JFrame implements ActionListener {
  
  private JLabel statusBar = new JLabel(" ");
  
  public TestMenu() {
    JMenu fileMenu = createMenu("File", "Open", "Save", "Save as", "Exit");
    final JMenu editMenu = createMenu("Edit", "Copy", "Cut", "Paste", "Select");
    
    // uzyskanie "popupu" menu rozwijalnego editMenu
    final JPopupMenu popup = editMenu.getPopupMenu();
    
    // menus ida na pasek
    JMenuBar mb = new JMenuBar();
    mb.add(fileMenu);
    mb.add(editMenu);
    setJMenuBar(mb);
    
    // a za chwilę editMenu będzie też jako kontektstowe
    JTextArea ta = new JTextArea(10,20);
    ta.setText("Menu Edit dostępne jest jako rozwijalne i jako kontekstowe.\nSpróbuj!");
    ta.setComponentPopupMenu(popup);
    add(new JScrollPane(ta));
    
    // zapewniamy przywrócenie JMenu jako invokera
    popup.addPopupMenuListener( new PopupMenuListener() {

      public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
        popup.setInvoker(editMenu);
      }

      public void popupMenuCanceled(PopupMenuEvent e) { }
      public void popupMenuWillBecomeVisible(PopupMenuEvent e) { }
    });

    
    add(statusBar, "South");
    
    setDefaultCloseOperation(DISPOSE_ON_CLOSE);
    pack();
    setLocationRelativeTo(null);
    setVisible(true);
  }
  
  private JMenu createMenu(String ... txt) {
    JMenu menu = new JMenu(txt[0]);
    for (int i = 1; i < txt.length; i++) {
      JMenuItem mi = new JMenuItem(txt[i]);
      mi.addActionListener(this);
      menu.add(mi);
    }
    return menu;
  }

  @Override
  public void actionPerformed(ActionEvent e) {
    statusBar.setText("Ostatnio wykonano: " + e.getActionCommand());
  }

  public static void main(String[] args) {
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        new TestMenu();
      }
    });
  }


}

Zobacz działanie programu








5. Fokus


Fokus - to zdolność do przyjmowania zdarzeń z klawiatury.


Ustalamy fokus na komponecie klikając w niego myszką (jeśli dla danego komponentu taka operacja może ustalić fokus) lub używając klawiszy zmian fokusu (domyślnie: Tab, Shift-Tab, Ctrl-Tab i Ctrl-Shift-Tab). Sposób działania tych klawiszy (kolejność w jakiej komponenty otrzymują fokus) określa  KeyboardFocusManager, który pozwala również na przedefiniowanie zestawu klawiszy zmian fokusu, ogólnie lub dla każdego komponentu oddzielnie.

Zmianę fokusu możemy wymusić także programistycznie, m.in za pomocą odwołania:

    comp.requestFocusInWindow()  // ustala fokus na komponencie comp (jesli to możliwe)

lub:


transferFocus()   // przesuwa fokus zgodnie z kolejnością określaną przez politykę zmian
                           // fokusu (klasę implementującej interfejs FocusTraversalPolicy) 



Domyślnie wszystkie komponenty mają zdolność przyjmowania fokusu .
Tę domyślną zdolność możemy zmienić za pomocą odwołania:

   comp.setFocusable(false);    // komponent comp nie będzie otrzymywał fokusu


Zdarzenia zmian fokusu można obsługiwać za pomocą słuchacza fokusu (FocusListener), który dostarcza dwóch metod:

public void focusGained(FocusEvent)  // komponent otrzymuje fokus

public void focusLost(FocusEvent)     // komponent traci fokus


Uwaga: dla okien zdefiniowano interfejs WindowFocusListener z odpowiednimi metodami windowGainedFocus(WindowEvent) i windowLostFocus(WindowEvent).

r
W przykładowym programie za pomocą obsługi zmian fokusu wyróżniamy niebieską grubszą ramką to pole tekstowe, które ma fokus.









package fokus1;

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.border.*;

public class Fokus1 extends JFrame implements FocusListener {

  final Border NORMAL = BorderFactory.createLineBorder(Color.black),
               FOCUS = BorderFactory.createLineBorder(Color.blue, 3),
               EMPTY = BorderFactory.createEmptyBorder(10,10,10,10);

  public Fokus1() {
    super("Fokus");
    setLayout(new GridLayout(0, 1, 5, 5));
    ((JComponent) getContentPane()).setBorder(EMPTY);
    for (int i = 0; i < 5; i++) {
      JTextField tf = new JTextField(20);
      tf.setFont(new Font("Dialog", Font.BOLD, 18));
      tf.setBorder(NORMAL);
      tf.addFocusListener(this);
      add(tf);
    }
    setDefaultCloseOperation(DISPOSE_ON_CLOSE);
    pack();
    setLocationRelativeTo(null);
    setVisible(true);
  }

  public void focusGained(FocusEvent e) {
    ((JComponent) e.getSource()).setBorder(FOCUS);
  }

  public void focusLost(FocusEvent e) {
    ((JComponent) e.getSource()).setBorder(NORMAL);
  }

  public static void main(String args[]) {
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        new Fokus1();
      }
    });
  }

}


Przy zmianie fokusu na polach tekstowych możemy weryfikować poprawność wprowadzonej informacji i wizualnie sygnalizować błędy.

r


Zmieniając w poprzednim programie definicję metod obsługi fokusu, zapewnimy, że jeśli w polu wprowadzono wadliwe dane (tu: coś co nie jest liczbą całkowitą) a użytkownik próbuje zmienić fokus, to wywoływany jest sygnał czerwony (dosłownie - poprzez czerwony kolor ramki i w przenośni - poprzez uniemożliwienie zmiany fokusu.




// ...
final Border ERROR = BorderFactory.createLineBorder(Color.red, 4);

JComponent inError = null; // pole tekstowe, które zawiera błąd

public void focusGained(FocusEvent e) {
  JComponent c = (JComponent) e.getSource();
  if (inError != null) {                               // jeżeli jest błędne pole
     inError.setBorder(ERROR);                         // dla niego: czerwona ramka
     if (c != inError) inError.requestFocusInWindow(); // i przywrocenie fokusu
  } else c.setBorder(FOCUS);
}

public void focusLost(FocusEvent e) {
  JTextField tf = (JTextField) e.getSource();
  if (inError != null && inError != tf) return;
  String txt = tf.getText();
  if (!txt.equals("")) {
    try {                   // Sprawdzenie poprawności tekstu
      Integer.parseInt(txt);
    } catch (NumberFormatException exc) {  // tekst wadliwy
        inError = tf;                      // zapamiętujemy błędne pole
        return;
    }
  }

  // Tu na pewno wiemy, że tekst jest poprawny
  inError = null;
  tf.setBorder(NORMAL);
}


Prostszym sposobem na weryfikację tekstów jest użycie klasy InputVerifier.

Jest to klasa abstrakcyjna - musimy ją zatem odziedziczyć i zdefiniować metodę:

         boolean verify(JComponent).

Obiekt tej klasy przyłączamy do weryfikowanego komponentu (np. pola tekstowego) za pomocą metody setInputVerifier(InputVerifier). Od tego momentu przed oddaniem fokus wywoływana jest metoda shouldYieldFocus z klasy InputVerifier, która woła z kolei verify i zwraca jej wynik. Jeżeli wynik jest false - fokus nie może opuścić komponentu. Komponent, do którego przyłączono weryfikatora przekazywany jest metodzie verify jako argument, dzięki czemu możemy go sprawdzić.



Poniższa modyfikacja poprzedniego programu pokazuje to w praktyce.

package inputver;

import java.awt.*;
import java.awt.event.*;

import javax.swing.*;
import javax.swing.border.*;

public class InpVer extends JFrame implements FocusListener {

  final Border NORMAL = BorderFactory.createLineBorder(Color.black),
               FOCUS = BorderFactory.createLineBorder(Color.blue, 4),
               ERROR = BorderFactory.createLineBorder(Color.red, 4);

  // Tworzymy obiekt typu InputVerifier
  InputVerifier inputVerifier = new InputVerifier() {

    public boolean verify(JComponent c) {         // zdefiniowanie metody verify
      String txt = ((JTextField) c).getText();    // jaki tekst w polu tekstowym
      if (txt.equals("")) return true;            // pusty jest OK
      try {                                       // sprawdzamy czy liczba
        Integer.parseInt(txt);
      } catch (NumberFormatException exc) {       // jeśli nie -
        c.setBorder(ERROR);                       // ramka czerwona!
        return false;                             // i nie wolno zmienić fokusu
      }
      return true;
    }
  };

  public InpVer() {
    super ("Input Verifier");
    setLayout(new GridLayout(0, 1, 5, 5));
    for (int i = 0; i < 5; i++) {
      JTextField tf = new JTextField(20);
      tf.setBorder(NORMAL);
      tf.addFocusListener(this);
      tf.setInputVerifier(inputVerifier);
      add(tf);
    }
    setDefaultCloseOperation(DISPOSE_ON_CLOSE);
    pack();
    setLocationRelativeTo(null);
    setVisible(true);
  }

  // Poprzednie najprostze metody pokazywania gdzie fokus
  public void focusGained(FocusEvent e) {
    ((JComponent) e.getSource()).setBorder(FOCUS);
  }

  public void focusLost(FocusEvent e) {
    ((JComponent) e.getSource()).setBorder(NORMAL);
  }

  public static void main(String args[]) {
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        new InpVer();
      }
    });
  }
}




Uwaga. dostępna jest też klasa JFormattedTextField, która zapewnia nie tylko weryfikację, ale i odpowiednie formatowanie informacji wejściowej


Jak wspomniano na wstępie zarządzaniem fokusem zajmuje się klasa KeyboardFocusManager. Rozbudowane API zarządzania fokusem obejmuje zresztą wiele klas i właściwości.

W szczególności:

   

6. Obsługa klawiatury

Zdarzenia typu KeyEvent  powstają przy naciśnięciu (keyPressed), zwolnieniu klawisza (keyReleased); wpisaniu znaku (keyTyped - dotyczy tylko znaków, a nie na przykład klawiszy, którym nie odpowiadają znaki).

Zdarzenie KeyEvent można zapytać o kod klawisza, który je spowodował.

        int getKeyCode()

Kody klawiszy - statyczne stałe typu int zdefiniowane w klasie KeyEvent, np. KeyEvent.VK_A, KeyEvent.VK_ENTER albo KeyEvent.VK_F1.
Dla tych klawiszy, ktorym odpowiadają znaki możemy je uzyskać za pomocą metody

        char getKeyChar()

Metody getModifiers(), isAltDown, isControlDown() itd. z klay Input Event (omówione już przy okazji zdarzeń myszki) pozwalają określić, czy dany klawisz został wciśnięty wraz z modyfikatorami.

Oprócz tego możemy zapytać o "ludzką" nazwę kodu stosując statyczną metodę klasy KeyEvent:

        String getKeyText(int kod_klawisza)

która zwraca nazwę klawisza, np. "Enter" lub "Home".oraz o "ludzką" nazwę modyfikatorów, stosując również statyczną metodę z klasy KeyEvent:

        String getKeyModifiersText(int modyfikatory)



Jako przykład obsługi klawiatury rozpatrzymy program, w którym zdefiniowano słuchacza klawiatury ustalającego tekst etykiet, przycisków i komponentow tekstowych na skutek naciśnięcia odpowiedniego klawisza. Klawisze są skojarzone (w mapie - klasa TreeMap) z tekstami do wpisania.

Przy konstrukcji obiektu-słuchacza podajemy opisy klawiszy i skojarzone z nimi teksty:

import java.awt.event.*;
import java.util.*;

import javax.swing.*;
import javax.swing.text.*;

public class KbShort extends KeyAdapter {

  private TreeMap<String, String> hm = new TreeMap<String, String>();

  public KbShort(String[] keys, String[] txt) {
    for (int i = 0; i < keys.length; i++)
      hm.put(keys[i], txt[i]);
  }

  public void keyReleased(KeyEvent key) {
    int k = key.getKeyCode();
    int m = key.getModifiers();
    String t = hm.get(KeyEvent.getKeyModifiersText(m) +
                      KeyEvent.getKeyText(k));
    if (t == null) return;
    Object o = key.getSource();
    if (o instanceof JLabel)
      ((JLabel) o).setText(t);
    else if (o instanceof AbstractButton)
      ((AbstractButton) o).setText(t);
    else if (o instanceof JTextComponent) ((JTextComponent) o).setText(t);
  }
}
Po to, by zastosować takiego sluchacza do zmian tekstów etykiet, musimy zapewnić, by etykiety dostawały fokus przy kliknięciu myszką.
Zwięzła klasa dostarczająca takich etykiet może wyglądać tak:
import java.awt.*;
import java.awt.event.*;

import javax.swing.*;
import javax.swing.border.*;

public class FocusLabel extends MouseAdapter implements FocusListener {

  private static final Border lbord = BorderFactory.createLineBorder(Color.red, 2);

  private JLabel lab;

  public FocusLabel(String txt) {
    lab = new JLabel(txt);
    lab.addMouseListener(this);
    lab.addFocusListener(this);
  }

  public JLabel getLabel() {
    return lab;
  }

  public void mousePressed(MouseEvent e) {
    lab.requestFocusInWindow();
  }

  public void focusGained(FocusEvent e) {
    lab.setBorder(lbord);
  }

  public void focusLost(FocusEvent e) {
    lab.setBorder(null);
  }
}
Uwaga. Obiekty tej klasy nie są etykietami. By uzyskać tworzone przez nią etykiety potrzebne jest użycie metody getLabel().

Obie klasy - specjalizowanego słuchacza klawiatury (KbShort) i etykiet z fokusem (FocusLabel) wykorzystamy w programie testowym.

import java.awt.*;

import javax.swing.*;

public class Keys extends JFrame {

  String[] keys = { "AltW", "AltK", "AltP" };         // klucze i
  String[] txt = { "Warszawa", "Kraków", "Poznań" };  // związane z nimi teksty
  
  KbShort ks = new KbShort(keys, txt);                // słuchacz klawiatury

  public Keys() {
    setLayout(new FlowLayout());
    
    // dla zwięzłości kodu konfigurowanie i dodawanie komponentów
    // powierzamy metodzie addComponent(...)
    addComponent(new FocusLabel("Miasto1").getLabel());
    addComponent(new FocusLabel("Miasto2").getLabel());
    addComponent(new JTextField(10));
    addComponent(new JButton("Przycisk"));

    setDefaultCloseOperation(DISPOSE_ON_CLOSE);
    pack();
    setLocationRelativeTo(null);
    setVisible(true);
  }

  void addComponent(JComponent c) {
    c.addKeyListener(ks);
    add(c);
  }

  public static void main(String[] args) {
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        new Keys();
      }
    });
  }

}

1 Efekt działania programu po użyciu zdefiniowanych klawiszy dla etykiet, pola tekstowego i przycisku pokazuje rysunek. Zwróćmy uwagę na ramkę wokół etykiety z napisem "Poznań" (ta etykieta ma aktualnie fokus, wciśnięcie Alt-W zamieniłoby tekst na etykiecie na "Warszawa").

Zobacz działanie programu









7. Obsługa okien

Szczególną rolę obsluga zdarzeń "okiennych" spełniała w aplikacjach AWT. Przypomną, trochę ze względów historycznych.
Zakończenie działania aplikacji AWT poprzez zamknięcie jej głównego okna uzyskujemy jedynie obsługując zdarzenie WINDOW_CLOSING.

Aby obsłużyć to zdarzenie należy:
Możliwe są następujące warianty realizacyjne

1. Klasa dziedziczy Frame i stanowi główne okno aplikacji:

    W konstruktorze piszemy:

  addWindowListener(new WindowAdapter() {
     public void windowClosing(WindowEvent e) {
        dispose();           // usuwa okno
        System.exit(0);  // kończy działanie aplikacji
        }
     });


2. Głównym oknem aplikacji jest obiekt typu Frame oznaczany zmienną mframe:
   
Piszemy:

mframe.addWindowListener(new WindowAdapter() {
     public void windowClosing(WindowEvent e) {
        mframe.dispose();
        System.exit(0);
        }
     });


3. Możemy stworzyć klasę AppEnd, która usuwa okno i zamyka aplikację  np.

public class Aplikacja {
   public static void main(String[] args) {
      Frame f = new Frame("Główne okno aplikacji");
      f.addWindowListener(new AppEnd());
      ....
      f.setSize(300,300);
      f.setVisible(true);
   }
}

class AppEnd extends WindowAdapter {
    public void windowClosing(WindowEvent e) {
       e.getWindow().dispose();   // metoda getWindow() dostarcza referencji do okna
       System.exit(0);
       }
}



Dla okien Swingu takie postępowanie ma mniejsze znaczenie (gdyż mamy tu metodę setDefaultCloseOperation() , może być jednak użyte np. dla przypomnienia użytkownikowi o zachowaniu jakiś danych.

Np.

public void windowClosing(WindowEvent e) {
    Window w = e.getWindow();
    String[] opt = { "Tak", "Nie" };
    int rc = JOptionPane.showOptionDialog(null,
              "Czy zachowałeś dane i można zamknąć aplikację?",
              "Uwaga!",
              JOptionPane.DEFAULT_OPTION,
              JOptionPane.WARNING_MESSAGE,
              null, opt, opt[0]);
    if (rc != 0) { w.show(); return; }
    w.dispose();
    System.exit(0);
  }


Jest także wiele innych zdarzeń związanych z oknami (zob. tabela zdarzeń), które możemy obsługiwać (np. minimalizacja, maksymalizacja, aktywacja lub deaktywacja - co jest szczególnie użyteczne w wielu przypadkach, kiedy potrzebne są jakieś działania inicjalacyjne).

Do ciekawych  należy możliwość nasłuchu zmian stanu okna, a także metoda setExtendedState() pozwalająca (ala nie na każdej platformie) ustalać dodatkowe stany okna.

Możliwości te ilustruje poniższy program.
mport java.awt.event.*;
import java.util.*;
import javax.swing.*;

public class Okna extends JFrame {

  // mapa: stan okna (stała int z klasy JFrame) -> opis stanu
  HashMap<Integer, String> wstat = new HashMap<Integer, String>();  

  // Sluchacz zmiany stanu okna
  WindowStateListener wsl = new  WindowStateListener() {

    { // blok inicjacyjny
       wstat.put(NORMAL, "Normal");
       wstat.put(ICONIFIED, "Minimized");
       wstat.put(MAXIMIZED_VERT, "Maximized vertically");
       wstat.put(MAXIMIZED_HORIZ,"Maximized horizontally");
       wstat.put(MAXIMIZED_BOTH,"Maximized");
    }

    public void windowStateChanged(WindowEvent e) {  // obsługa zdarzenia zmiany stanu
      String old = wstat.get(e.getOldState());
      String now =  wstat.get(e.getNewState());
      System.out.println("Zmiana stanu z " + old + " na " + now);
    }

  };

  public Okna() {
    addWindowStateListener(wsl);
    
    add(new JLabel("Przyciski poniżej zmieniają stan okna"));
    
    JPanel p = new JPanel();

    for (int k : wstat.keySet()) {
      String opis = wstat.get(k);              // opis stanu
      JButton b = new JButton(opis);
      b.setActionCommand(""+k);                // command = identyfikator stanu
      b.addActionListener( new ActionListener() {
       public void actionPerformed(ActionEvent e) { // ustalenie stanu okna
          setExtendedState(Integer.parseInt(e.getActionCommand()));
        }
      });
      p.add(b);
    }
    add(p, "South");
    setDefaultCloseOperation(DISPOSE_ON_CLOSE);
    pack();
    setLocationRelativeTo(null);
    setVisible(true);
  }

  public static void main(String args[]) {
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        new Okna();
      }
    });
  }
}
Uwaga: nie wszystkie platformy dostarczają możliwości maksymalizacji okien w pionie czy w poziomie.


8. Zdarzenia na komponentach wyboru

Komponenty wyboru to: 

Wszystkie komponenty wyboru implementują interfejs ItemSelectable.
Ten z kolei zawiera metodę addItemListener rejestrującą semantycznego słuchacza dla obsługi semantycznego zdarzenia ITEM_STATE_CHANGED ( zmina stanu zaznaczenia - element zaznaczony lub nie).

Obsługę zapewnia metoda itemStateChanged z argumentem typu ItemEvent.
Wobec zdarzenia ItemEvent możemy stosować różne zapytania:

Przykładowy program ilustruje użycie ItemListenera.
r Mamy tu dwie listy rozwijalne. Pierwsza z nich zawiera państwa, a druga ma zawierać miasta dla każdego państwa.
Inicjalne dane specyfikujemy w postaci tablic napisów, które póżniej przekształcamy w tablicę asocjacyjną (mapę), której kluczami są państwa, a wartościami kolekcje (typu TreeSet - posortowany zbiór) miast w państwie.

Wybór państwa na pierwszej liście powoduje wpisanie zestawu miast danego kraju do drugiej listy.




package itemsel;
import java.awt.*;
import java.awt.event.*;

import javax.swing.*;
import javax.swing.event.*;
import java.util.*;

public class ItemSel extends JFrame implements ItemListener {


  private String[] countries = { "Polska", "Rosja", "Hiszpania", };
  private String[][] towns = { { "Warszawa", "Poznań", "Kraków", },
                                   { "Moskwa", "Władywostok", },
                                   { "Madryt", "Barcelona", },   };

  private JComboBox cbCountry = new JComboBox(countries);
  private JComboBox cbTown = new JComboBox(towns[0]);

  private HashMap<String, Set<String>> countryTowns = new HashMap<String, Set<String>>();

  public ItemSel() {
    for (int i=0; i < countries.length; i++) {
      String key = countries[i];
      TreeSet<String> tSet = new TreeSet<String>();
      for (int j =0; j < towns[i].length; j++) {
        tSet.add(towns[i][j]);
      }
      countryTowns.put(key, tSet);
    }
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        setLayout(new FlowLayout());
        cbCountry.addItemListener(ItemSel.this);
        add(cbCountry);
        add(cbTown);
        setDefaultCloseOperation(DISPOSE_ON_CLOSE);
        pack();
        setLocationRelativeTo(null);
        setVisible(true);        
      }
    }); 
  }

  public void itemStateChanged(ItemEvent e) {
    if (e.getStateChange() == ItemEvent.SELECTED) {
       String country = (String) e.getItem();
       if (country == null) return;
       Set<String> towns =  countryTowns.get(country);
       if (towns == null) return;
       cbTown.removeAllItems();
       for(String t : towns) cbTown.addItem(t);
    }
  }

  public static void main(String args[]) {
    new ItemSel();
  }

}

Warto zauważyć, że ten sposób oprogramowania JComboBox nie jest najlepszy.
Tak naprawdę powinniśmy się posługiwać modelem danych listy rozwijalnej (jak również modelem selekcji). Jest to zarówno bardziej efektywne przy większych rozmiarach umieszczonej na liście informacji, jak i niezbędne, gdy chcemy przeprowadzać bardziej zaawansowane dzialania z listami rozwijalnymi.
Ale ten temat omówimy w wykładzie o architekturze "Model-View-Controller".