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.
Uwagi:
- Jeśli nie podano inaczej - klasy pochodzą z pakietu java.awt.event
- Większość zdarzeń Swingowych (z pakietu javax.swing.event) jest ściśle
związana z architekturą "Model-View-Controller". Będziemy je zatem omawiać
przy tej okazji.
- Zdarzenie PropertyChangeEvent związane jest z koncepcją JavaBeans.
Oznacza ono zmianę właściwości "ziarna", dotyczy wszystkich komponentów wizualnych
AWT i Swing (bo są ziarnami), ale ma również szersze znaczenie dla "niewizualnych"
ziaren. O tym będzie traktował wykład o JavaBeans w przyszlym semestrze.
- Klasy HierarchyEvent i AncestorEvent określają zdarzenia polegające
na dodaniu, usunięciu, uwidocznieniu itp. komponentu na wyższym (od żródła
zdarzenia) poziomie hierarchii zawierania się komponentów. Generalnie zdarzenia
te są obsługiwane przez JVM, a ich obsługę dla programistów udostępniono
wyłącznie w celach informacyjnych.
- Klasa InvocationEvent służy do budowania zdarzeń, polegających na wykonaniu
kodu (podanego w metodzie run() obiektu klasy implementującej Runnable i
przekazanego przy tworzeniu zdarzenia jako argument konstruktora ). Zdarzenia
te - po użyciu metody dispatch() z klasy InvocationEvent - są umieszczane
w kolejce zdarzeń do późniejszego "wykonania". W ten sposób można "zastąpić"
działanie metod invokeLater i invokeAndWait, które robią to samo i są obowiązkowym
sposobem na działanie na komponentach Swingu po ich realizacji (uwidocznieniu
lub spakowaniu okna). Konieczność taka wynika z tego, że komponenty Swingu
nie są wielowątkowo bezpieczne.
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:
- ActionEvent -
"akcja",
- ItemEvent
- "zmiana zaznaczeń w obiekcie mogącym mieć 0 lub więcej
zaznaczeń - stanów",
- AdjustmentEvent -
"dostosowanie obiektu dostosowywalnego",
- TextEvent - "zmiana
tekstu obiektu tekstowego" (dotyczy tylko AWT i obecnie praktycznie nie jest stosowane)
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.
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 |
KeyEvent | Wszystkim komponentom AWT i Swingu, które mogą otrzymywać fokus |
WindowEvent |
Wszystkim komponentom pochodnym od klasy Window |
InternalFrameEvent | Wewnę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:
- kiedy zaszło - metoda getWhen() zwracająca czas zdarzenia w milisekundach
- czy towarzyszyło mu wciśnięcie specjalnych klawiszy: alt, altgraph (prawy alt), shift, ctrl, meta - metody isAltDown(), isAltGraphDown(), isShiftDown(), isControlDown(), isMetaDown(), zwracające true jeśli odpowiedni klawisz został wciśnięty i false w przeciwnym razie.
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:
- Point getPoint(), int getX(), int getY() - lokalizacja kursora
myszki (w momencie zajścia zdarzenia) względem lewego górnego rogu komponentu
(lewy górny róg komponentu ma współrzędne (0,0); dostajemy odpowiednio punkt
(x,y), współrzędną x , współrzędną y,
- Point getLocationOnScreen(), int getXOnScreen(), int getYOnScreen() - j.w. ale we współrzędnych całego ekranu,
- int getButton() - który klawisz myszki uzyty; możliwe wartości określają stałe statyczne z klasy MouseEvent: BUTTON1, BUTTON2, BUTTON3.
- int getClickCount() - zwraca liczbę kliknięć,
- boolean isPopupTrigger() - czy to zdarzenie myszki może dla danej platformy systemowej spowodować otwarcie menu kontekstowego,
- void translatePoint(int x, int y) - powoduje przekształcenie współrzędnych zdarzenia poprzez dodanie do nich odpowiednio liczb x i y.
Przykładowy program pokazuje zastosowanie tych metod.
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-komponentpm - 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).
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ę:
- przyłączenie MouseListenera do komponentu,
- w obsłudze zdarzeń myszki sprawdzanie isPopupTrigger() i jeśli tak - otwieranie menu kontekstowego poprzez show.
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.
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:
-
gdy jest ono zadeklarowane na poziomie klasy, to oczywiście w metodzie obsługi
mamy dostęp do zmiennej oznaczającej menu i możemy łatwo uzyskać dostęp do
komponentu na którym zostało ono otwarte:
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
// ...
}
};
// ...
-
jeśli nie mamy dostępu do zmiennej oznaczającej menu kontekstowe, możemy
je uzyskać odpytując wybrany element menu o jego rodzica. Będzie nim właśnie
menu kontekstowe. A dalej w analogiczny jak wyżej sposób dowiadujemy się
na jaki komponencie menu zostało otwarte:
ActionListener al = new ActionListener() {
public void actionPerformed(ActionEvent e) {
Component c = (JComponent) e.getSource();
JPopupMenu popup = (JPopupMenu) c.getParent();
Component invoker = popup.getInvoker();
// ...
}
};
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.
- popupMenuWillBecomeVisible(PopupMenuEvent e) - wywolywana tuż przed otwarciem menu kontekstowego,
- popupMenuWillBecomeInvisible(PopupMenuEvent e) - wywolywana tuż przed zamknięciem menu kontekstowego,
- popupMenuCanceled(PopupMenuEvent e) - wywoływana przy anulowaniu menu kontekstowego.
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).
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.
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:
- możemy się teraz dowiedzieć w metodzie focusGained od jakiego komponentu dany komponent dostał fokus, a w metodzie focusLost - na rzecz jakiego komponentu dany komponent traci fokus. Informacji tej dostarcza metoda Component getOppositeComponent() z klasy FocusEvent,
- możemy przechwytywać zdarzenia z klawiatury przed ich dojściem do KeyboardFocusManagera za pomocą interfejsu KeyEventDispatcher, a także otrzymywać wszystkie zdarzenia z klawiatury do ew. obróbki za .pomcoą interfejsu KeyEventPostProcessor już po ich przetworzeniu przez właścicieli fokusu,
- możemy zmieniać/blokować/ustalać zestaw kluczy zmian fokusu dla każdego komponentu oraz politykę zmian fokusu w konenerach (interfejs FocusTravelsalPolicy
- w jakiej kolejności komponenty otzrymują fokus), m.in możemy w ogóle zablokować
użycie kluczy zmian fokusu za pomocą metody, Component.setFocusTraversalKeysEnabled(false)
, umożliwiając w ten sposób otrzymywanie tych kluczy do obsługi w metodach
obsługi zdarzeń z klawiatury na dowolnych komponentach, co we wcześniejszych
wersjach Javy nie było mozliwe.
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();
}
});
}
}
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:
- zdefiniować Słuchacza zdarzeń okiennych (WindowListener) poprzez implementację
interfejsu WindowListener lub wykorzystanie adaptora WindowAdapter,
- stworzyć Słuchacza (nowy obiekt).
- przyłączyć Słuchacza do głównego okna aplikacji.
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:
- Checkbox, CheckboxMenuItem, Choice, List (z AWT)
- JCheckBox, JRadioButton, JComboBox (ze Swingu)
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:
- ItemSelectable getItemSelectable() - zwraca komponent wyboru jako ItemSelectable
- int
getStateChange() - zwraca rodzaj zmiany jako
stałą całkowitoliczbową ItemEvent.SELECTED lub ItemEvent.DESLECTED
- Object getItem() - zwraca element podlegający zmianie
Przykładowy program ilustruje użycie ItemListenera.
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".