Rozbudowane możliwości Swingu
Zajmiemy się teraz dodatkowymi, rozbudowanymi, ciekawymi możliwościami komponentów Swingu.
Nie są one bardzo skomplikowane i na pewno warto je stosować w codziennym
programowaniu
1. Architektura okien
Prawie wszystkie komponenty Swingu są komponentami lekkimi, co oznacza m.in., że:
mają taki sam wygląd niezależny od platformy systemowej (zależny wyłacznie
od wybranego przez programistę "stylu" - "look and feel")
Jednak komponenty lekkie nie mogą istnieć bez jakiegoś komponentu ciężkiego
(czyli takiego, który jest realizowany przez odwołanie do natywnego komponentu
graficznego API danej platformy systemowej). Taki komponent (choć jeden) musi znaleźć się
u początku hierarchii zawierania się komponentów, tak aby komponenty lekkie
mogły być na czymś rysowane.
Dlatego w Swingu kontenery najwyższego poziomu (okna i dialogi) zrealizowane
są jako komponenty ciężkie (są to jedyne ciężkie komponenty Swingu).
Hierarchię klas realizujących okna w Swingu przypomina rysunek.
Jak widać okno ramowe, dialog i aplet są komponentami ciężkimi, bowiem pochodzą od odpowiednich klas AWT.
Klasa JInternalFrame jest natomiast komponentem lekkim. Umożliwia ona tworzenie "okien wewnętrznych" (zawartych w obszarze określonego pulpitu).
Praca z kontenerami najwyższego poziomu (oknami) w Swingu zdecydowanie
różni się od pracy z oknami w AWT ze względu na inną architekturę budowy
tych komponentów.
Wszystkie w/w typy okien zbudowane są z części.
Każde okno zawiera przede wszystkim kontener rootPane (typu JRootPane).
Ten z kolei zawiera kontenery glassPane (szyba - obiekt typu JComponent)
i layeredPane (kontener warstwowy, obiekt typu JLayeredPane).
Kontener layeredPane zawiera contentPane (kontener do którego zwykle dodajemy komponenty) oraz ew. pasek menu (JMenuBar).
Zatem struktura okna jest złożona, co pokazują rysunki
Złożoność ta wynika po części ze zmieszania komponentów ciężkich (którymi
są same okna) z lekkimi (komponenty Swingu), po części zaś - z chęci dostarczenia
programiście metod wyszukanej konstrukcji GUI.
Warstwy
Bezpośrednim potomkiem (w hierarchii zawierania się komponentów) każdego okna jest rootPane, który z kolei ma swoje "dzieci": glassPane i layeredPane. Ten ostatni dopiero zawiera contentPane (i ew. pasek menu).
Kontener layeredPane umożliwia operowanie warstwami komponentów.
W ten sposób możliwe jest zarządzanie nakładaniem się na siebie komponentów (Z-order).
Mamy tu dwa porządki:
- warstw, głebokość których określa obiekt typu Integer (numer warstwy).
Czym większy numer, tym warstwa bliższa przedniego planu (Z-order), Np. komponent
dodany do warstwy 2 będzie zasłaniał komponent dodany do warstwy 1.
- pozycji komponentów (po osi Z) umieszczonych w jednej warstwie; tutaj
porządek jest odwrotny: komponent o niższej pozycji przykrywa komponent o
wyższej pozycji.
Klasa JLayeredPane definiuje wygodne stałe, oznaczające często używane warstwy (tabela)
Nazwa | Wartość | Opis |
FRAME_CONTENT_LAYER | new Integer(-30000) | Na tym poziomie dodawane są contentPane i pasek menu. |
DEFAULT_LAYER | new Integer(0) | Warstwa domyślna: na
tym poziomie dodawane są do layeredPane komponenty, jeśli nie podano inaczej. |
PALETTE_LAYER | new Integer(100) | Warstwa pasków narzędzi i palet. |
MODAL_LAYER | new Integer(200) | Warstwa dialogów modalnych |
POPUP_LAYER | new Integer(300) | Warstwa menu kontekstowego |
DRAG_LAYER | new Integer(400) | Dla przeciągania komponentów (operacja drag). |
Taka kolejność zapewnia np. że paski narzędzi będą ponad zwykłymi komponentami,
dialog modalny - jeszcze wyżej, menu kontekstowe może pojawić się ponad dialogiem
modalnym, a operacja przeciągania komponentów będzie zawsze wizualnie obrazowana.
Można też używać własnych warstw i dodawać do nich komponenty.
Służą temu przeciążone metody add, wywoływane wobec kontenera typu JLayeredPane.
Odniesienie do kontenera layeredPane okna frame możemy uzyskać poprzez:
JLayeredPane lp = frame.getLayeredPane();
Dodanie komponentu comp do warstwy n (liczba całkowita):
lp.add(comp, new Integer(n));
Dodanie komponentu comp do warstwy n na pozycji m (liczba całkowita):
lp.add(comp, new Integer(n), m);
Oprócz tego dostępne są metody klasy JLayeredPane, które dynamicznie zmieniają
położenie komponentów w ramach warstwy i pomiędzy
warstwami:
lp.moveToFront(comp)
lp.moveToBack(comp);
lp.setLayer(comp, n);
lp.setLayer(comp, n, m);
Program na wydruku pokazuje jak można korzystać z klasy JLayeredPane.
package layers1;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class Layer extends JFrame implements ActionListener {
private JLayeredPane l = null;
public Layer() {
l = getLayeredPane();
getContentPane().setBackground(Color.white); // widzimy contentPane
int x = 10, y = 10;
for (int i = 1; i <= 5; i++) {
JButton b = new JButton("Przycisk " + i);
b.setHorizontalAlignment(JButton.CENTER);
b.setVerticalAlignment(JButton.TOP);
b.setBounds(x, y, 150, 100);
b.addActionListener(this);
l.add(b, new Integer(i));
x += 30;
y += 30;
}
JButton b = new JButton("P(5,1)");
b.addActionListener(this);
b.setHorizontalAlignment(JButton.RIGHT);
b.setVerticalAlignment(JButton.BOTTOM);
b.setBounds(x + 50, y, 100, 100);
b.setBackground(Color.yellow);
l.add(b, new Integer(5), 1);
setSize(400, 300);
setVisible(true);
}
public void actionPerformed(ActionEvent e) {
JComponent c = (JComponent) e.getSource();
l.moveToFront(c);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new Layer();
}
});
}
}
Dodajemy tu pięc przycisków - każdy w innej warstwie (od 1 do 5).
W piątej warstwie dodajemy jeszcze jeden - żółty przycisk z etykietą P(5,1)
- na pozycji wyższej od już znajdującego się w tej warstwie przycisku (który
ma pozycję 0).
Ustalamy lokalizację przycisków tak, by wzajemnie nakładały się na siebie
(zob. rys.); wyższe numery warstw oznaczają "bliżej nas", wyższy numer pozycji
w warstwie powoduje schowanie przycisku P(5,1) pod przyciskiem Przycisk 5.
Obsługa kliknięcia w przycisk powoduje wywołanie metody moveToFront().
Metoda ta działa tylko wobec przycisków znajdujących się w tej samej
warstwie
zmieniając ich położenie (przesuwając dany przycisk na pierwszy plan).
Po kliknięciu w przycisk P(5,1) znajdzie się on na wierzchu.
Warstwy można wykorzystać np. przy tworzeniu przezroczystych przycisków, nakładających się na inne komponenty.
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.border.*;
public class Opaq {
public static void main(String[] args) {
JFrame f = new JFrame();
f.setDefaultCloseOperation(3);
JLabel img = new JLabel(
new ImageIcon("pool2.jpg"));
f.getContentPane().add(img);
JButton b = new JButton("Zrób coś");
b.setFont(new Font("Dialog", Font.BOLD, 16));
b.setForeground(Color.red);
b.setOpaque(false);
b.setBounds(10,10, 100, 50);
Border rbevel = BorderFactory.createRaisedBevelBorder(),
lbevel = BorderFactory.createLoweredBevelBorder();
b.setBorder(BorderFactory.createCompoundBorder(rbevel,lbevel));
f.getLayeredPane().add(b);
f.pack();
f.setVisible(true);
}
}
Szyba
Innym kontenerem zawartym w rootPane jest "szyba", przykrywająca cały rootPane.
Inicjalnie szyba jest niewidoczna.
Możemy ją uaktywnić poprzez uwidocznienie (setVisible(true)).
Wtedy oddziela ona nas od okna.
Na szybie możemy:
- przechwytywać zdarzenia wejściowe (mysz i klawiatura),
- rysować.
Dostęp do szyby uzyskujemy za pomocą metody getGlassPane();
Component g = frame.getGlassPane();
g.setVisible(true); // w tej chwili szyba oddziela nas od okna.
Możemy też stworzyć własną szybę.
Jest to szczególnie użyteczne wtedy gdy na szybie chcemy coś rysować (zatem
powinniśmy odziedziczyć klasę JComponent i dostarczyć w naszej klasie metody
paintComponent()).
"Własną" szybę podstawiamy na miejsce standardowej za pomocą metody setGlassPane().
Prosty przykład zastosowania szyby poznamy przy okazji przykładu okien wewnętrznych.
2. Okna wewnętrzne
Okna wewnętrzne (JInternalFrame) są lekkimi komponetami o funkcjonalności okien ramowych.
Podstawowe różnice wobec zwykłych okien ramowych (JFrame):
- niezależny od platformy wygląd,
- muszą być dodawane do innych kontenerów,
- dodatkowe możliwości programistyczne (np. dynamicznych zmian właściwości
takich jak możliwość zmiany rozmiaru, możliwość maksymalizacji itp.).
Ponieważ okna wewnętrzne zawarte są zawsze w jakimś kontenerze, to stanowią one okna na wirtualnym pulpicie (nie mogą "wyjść" poza pulpit).
Pulpitem (kontenerem zawierającym okna wewnętrzne) jest zwykle obiekt typu JDesktopPane, choć może to być i jakiś inny kontener.
JDesktopPane zapewnia jednak pewne dodatkowe metody (np. uzyskanie listy okien na pulpicie).
Przy tworzeniu obiektów typu JInternalFrame możemy podać (w postaci wartości boolowskich) czy okno ma właściwości:
- maximizable (czy może być maksymalizowane)
- iconifiable (czy może być minimalizowane)
- closable (czy może być zamykane)
- resizable (czy można zmieniać jego rozmiary).
Właściwości te można także pobierać za pomocą metod is.. i ustalać za pomocą metod set..
Przy minimalizacji okna na pulpicie (kontenerze w którym jest zawarte okno)
pojawia się ikonka przedztawiająca zminimalizowane okno. Ikonę tę możemy
ustalić za pomocą odpowiedniej metody set..., jak również możemy ustalić
ikonę pojawiającą się w górnym lewym rogu okna.
Przedstawiony dalej przykład użycia okien wewnętrznych powraca do tematyki wykorzystania JLayeredPane.
Zauważmy: JDesktopPane jest klasą pochodną od JLayeredPane.
Zatem okna wewnętrzne używające JDesktopPane zawsze korzystają z "warstwowości".
Przy okazji zobaczymy jak od JDesktopPane uzyskać listę okien i jak można poslugiwać się szybą.
Funkcje prezentowanej dalej drobnej aplikacji ilustują rysunki.
Przyciski "To front" i "To back" zmieniaja uporządkowanie okien po osi Z,
przycisk z napisem HTML "Active glass..." uwidacznia szybę. Ponadto kliknięcie
prawym klawiszem myszki na pulpicie otworzy listę okien wewnętrznych (również
tych niewidocznych). Z listy możemy wybrać okno do schowania lub ponownego
otwarcia, jak również dodać nowe okno do pulpitu.
Kliknięcie w przycisk "Active GLASS..." uwidoczni szybę. Od tej chwili interakcja
z aplikacją za pomoca kliknięć w pulpit będzie przechwytywana na szybie
i klinięcia będą rysowac na niej czerwone kropki.
W tej chwili mamy już wystarczającą wiedzę, by zrozumieć tekst programu. Dlatego
jego analizę pozostawiam jako samodzielne ćwiczenia.
package internframes;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.util.*;
public class InternalWin extends JFrame implements ActionListener {
private static ImageIcon toFront =
new ImageIcon(InternalWin.class.getResource("images/arrnorth.gif"));
private static ImageIcon toBack =
new ImageIcon(InternalWin.class.getResource("images/arrsouth.gif"));
private JDesktopPane desk = new JDesktopPane();
private Component glass;
private final int MAXC = 4;
InternalWin() {
super("Desktop");
// Menu kontekstowe będziemy dynamicznie tworzyć tuz przed pokazaniem
desk.addMouseListener(new MouseAdapter() {
public void mousePressed(MouseEvent e) {
createAndShowPopup(e);
}
public void mouseReleased(MouseEvent e) {
createAndShowPopup(e);
}
void createAndShowPopup(MouseEvent e) {
if (e.isPopupTrigger()) {
int x = e.getX(), y = e.getY();
JPopupMenu pm = makePopup(x, y);
pm.show(e.getComponent(), x, y);
}
}
});
// Tworzymy okna wewnętrzne
int x = 0, y = 0;
for (int i = 0; i < MAXC; i++) {
x += 50;
y += 50;
makeInternalWindow(i, x, y);
}
// Szyba
glass = getGlassPane();
glass.addMouseListener(new MouseAdapter() {
public void mousePressed(MouseEvent e) {
if (e.isMetaDown()) {
glass.setVisible(false);
desk.revalidate();
} else {
Graphics glassGraphics = glass.getGraphics();
glassGraphics.setColor(Color.red);
glassGraphics.fillOval(e.getX() - 25, e.getY() - 25, 50, 50);
desk.revalidate();
}
}
});
add(desk);
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
setSize(600, 600);
setLocationRelativeTo(null);
setVisible(true);
}
public void actionPerformed(ActionEvent e) {
JButton c = (JButton) e.getSource();
JRootPane rp = c.getRootPane();
JInternalFrame w = (JInternalFrame) rp.getParent();
final String cmd = e.getActionCommand();
if (cmd.equals("To front")) {
desk.setLayer(w, desk.highestLayer());
w.toFront();
} else if (cmd.equals("To back")) {
desk.setLayer(w, desk.lowestLayer());
w.toBack();
} else {
glass.setVisible(true);
JOptionPane
.showMessageDialog(desk,
"Next mouse press will draw red oval, use right button to exit this mode");
}
}
private JPopupMenu makePopup(final int x, final int y) {
JPopupMenu pm = new JPopupMenu();
JPanel p = new JPanel(new BorderLayout());
JLabel lab = new JLabel(
"<html><center><b>Window list</b><br>Select one to close/open<br>"
+ "or add new window option</center></html>");
lab.setBackground(Color.yellow);
lab.setOpaque(true);
p.add(lab);
p.setBorder(BorderFactory.createLineBorder(Color.blue, 5));
pm.add(p);
final JInternalFrame[] jif = desk.getAllFrames();
for (int i = 0; i < jif.length; i++) {
JMenuItem mi = new JMenuItem(jif[i].getTitle());
pm.add(mi);
mi.putClientProperty("WinToClose", jif[i]);
mi.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
JComponent c = (JComponent) e.getSource();
JInternalFrame wclo = (JInternalFrame) c
.getClientProperty("WinToClose");
if (!wclo.isVisible())
wclo.setVisible(true);
else
wclo.doDefaultCloseAction();
}
});
}
pm.addSeparator();
JMenuItem mi = new JMenuItem("Add new window");
mi.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
makeInternalWindow(jif.length, x, y);
}
});
pm.add(mi);
return pm;
}
void makeInternalWindow(int i, int x, int y) {
JInternalFrame w = new JInternalFrame("Okienko " + i, true, true, true,
true);
w.setLayout(new BorderLayout(5, 5));
JPanel controls = new JPanel();
controls.setBorder(BorderFactory.createRaisedBevelBorder());
JButton b = new JButton("To front", toFront);
b.addActionListener(this);
controls.add(b);
b = new JButton("To back", toBack);
b.addActionListener(this);
controls.add(b);
w.add(controls, "North");
b = new JButton("<html><center><b><font color=red>Active</font><br>"
+ "<font color=blue>GLASS</font><br>"
+ "<b>will prevent interaction</b></center></html>");
b.addActionListener(this);
w.add(b, "Center");
w.setDefaultCloseOperation(JInternalFrame.HIDE_ON_CLOSE);
w.pack();
desk.add(w, i);
w.setLocation(x, y);
w.setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new InternalWin();
}
});
}
}
Zobacz działanie programu
Okna klasy JInternalFrame nie generują zdarzeń typu WindowEvent.
Zamiast tego dostępne są zdarzenia typu InternalFrameEvent, a do ich obsługi służą metody z interfejsu InternalFrameListener
3. Wyspecjalizowane kontenery Swingu
Swing wprowadził bardzo ciekawe i użyteczne rozwiązanie: wszystkie jego komponenty (J-komponenty) są kontenerami.
Wynika to bezpośrednio z hierarchii dziedziczenia klas:
Object
|
|
Component
|
|
Container
|
|
JComponent
Nic np. nie stoi na przeszkodzie, aby przycisk potraktować jako kontener,
do którego dodajemy panel z innymi przyciskami. Przycisk-kontener będzie
działał jak zwykły przycisk (wywołując przy kliknięciu skojarzoną z nim akcję),
a mniejsze przyciski dodane do przycisku-kontenera mogą mieć swoje akcje.
Na wydruku przedstawiono program testowy, sprawdzający taką możliwość:
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class Test1 extends JFrame implements ActionListener {
public Test1() {
setLayout(new FlowLayout(0, 0, FlowLayout.LEFT));
((JComponent) getContentPane()).setBorder(BorderFactory
.createEmptyBorder(10, 10, 10, 10));
JButton mb = new JButton("Główny przycisk");
mb.setHorizontalAlignment(AbstractButton.RIGHT);
mb.setPreferredSize(new Dimension(200, 150));
mb.addActionListener(this);
JPanel p = new JPanel(new GridLayout(0, 1));
p.setBorder(BorderFactory.createLineBorder(Color.blue));
for (int i = 1; i <= 5; i++) {
JButton bb = new JButton("" + i);
bb.addActionListener(this);
p.add(bb);
}
mb.setLayout(new FlowLayout(0, 0, FlowLayout.LEFT));
mb.add(p);
add(mb);
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
pack();
setLocationRelativeTo(null);
setVisible(true);
}
public void actionPerformed(ActionEvent e) {
System.out.println(e.getActionCommand());
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new Test1();
}
});
}
}
Tabela pokazuje działanie programu.
|
|
|
Start programu
|
Przyciśnięto główny przycisk
Na konsolę wyprowadzony
zostanie napis
"Główny przycisk"
|
Przyciśnięto jeden z dodanych
przycisków. Na konsolę
wyprowadzony będzie jego numer
|
Warto zauważyć, że domyślnym rozkładem dla J-komponentu (który nie jest
Swingowym kontenerem) jest rozkład OverlayLayout (ogólnie niezbyt użyteczny).
W programie (zwykłymi środkami, stosowanymi wobec kontenerów) zmieniliśmy
go na FlowLayout, dzięki czemu panel z przyciskami nie przykrył napisu na
głównym przycisku.
Ogólnie, nie należy nadużywać możliwości traktowania wszystkich komponentów Swingu jako kontenerów.
Swing dostarcza bowiem łatwiejszych i bardziej użytecznych sposobów grupowania
komponentów w wyspecjalizowanych panelach (panelach Swingu).
Mamy do dyspozycji:
- JPanel - najprostszy panel, różniący się od panelu AWT tym, że jest
J-komponentem (ze wszystkimi, omawianymi wcześniej konsekwencjami),
- JSplitPane - panel składających się z dwóch rozdzielonych paskiem podziału
części, w których mogą być umieszczone dowolne komponenty (w tym oczywiście
- kontenery)
- JTabbedPane - panel z zakładkami. Każda z zakładek daje dostęp do innego
komponentu (zwykle kontenera) po kliknięciu w zakładkę wypełniającego cały
panel,
- JScrollPane - panel przewijany, z suwakami; odpowiednik ScrollPane z AWT, ale o rozbudowanych możliwościach,
- JToolBar - pasek narzędzi, zawierający dowolne komponenty (zwykle przyciski obrazkowe).
Warto pamiętać, że wszystkie te panele są jednocześnie J-komponentami, a
więc można wobec nich używać metod klasy JComponent (np. ramki, przeźroczystość
itp.).
3.1. Panel dzielony - JSplitPane
JSplitPane jest panelem podzielonym na dwie części, w których znajdują się różne komponenty.
Części rozdzielone są paskiem podziału, który możemy przesuwać myszką, zmieniając
wielkość obszarów, w których widoczne są komponenty. Jeśli ustalona jest
właściwość boolowska continousLayout, to wraz ze zmianą wielkości obszarów
zmieniają się rozmiary komponentów.
Ustalenie boolowskiej właściwości oneTouchExpandable dodaje do paska
podziału dwie ikonki-strzalki. Kliknięcie w odpowiednią ikonkę maksymalnie
rozszerza (lub zwęża) jeden z obszarów.
W zależności od ustalonej orientacji podział na obszary jest poziomy lub
pionowy. Orientację określają stałe: JSplitPane.HORIZONTAL_SPLIT (uwaga:
pasek podziału jest pionowy!) i JSplitPane.VERTICAL_SPLIT (pasek podziału
jest poziomy).
Dla właściwego działania JSplitPane należy ustalić (za pomocą metod setMinimumSize
i setPreferredSize) minimalne i preferowane rozmiary komponentów zawartych
w obu jego obszarach.
W klasie JSplitPane znajdziemy kilka konstruktorów oraz wiele metod zmieniających
zawartość i działanie tego panelu (m.in. lokalizację paska podziału i inne
właściwości).
Wygodnym konstruktorem jest:
JSplitPane(orientacja, continousLayout, lewy | górny komponent, prawy | górny komponent);
Split-panele można wkładać jeden-w-drugi, tworząc panele dzielone na wiele obszarów.
Przykład:
Mamy trzy panele p1, p2, p3 z ramkami o tytułach "1 panel", "2 panel", "3 panel" i z odpowiednio ustalonymi rozmiarami.
Panele p1 i p2 dodajemy do split-panelu sp1 podzielonego pionowym paskiem.
Następnie do drugiego split-panelu sp2, podzielonego poziomym paskiem, dodajemy u góry split-panel sp1, a u dołu panel p3.
Do paska podziału split-panelu sp1 dodajemy ikonki natychmiastowego rozszerzenia (własność oneTouchExpandable).
boolean continousLayout = true;
JSplitPane sp1 = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT,
continousLayout, p1, p2);
JSplitPane sp2 = new JSplitPane(JSplitPane.VERTICAL_SPLIT,
continousLayout, sp1, p3);
sp1.setOneTouchExpandable(true);
Wygląd panelu sp2 przedstawia rysunek
3.2. Panel zakładkowy - JTabbedPane
Panel zakładkowy udostępnia zakładki (tekst, tekst i ikona lub sama
ikona, możliwe jest również ustalenie jako zakładki dowolnego
komponentu).
Z każdą zakładką związany jest inny komponent, który po kliknięciu w
zakładkę
wypełnia panel.
Zakładki mogą być ulokowane u góry, u dołu, po lewej lub po prawej stronie panelu.
Mogą być kolorowane, można z nimi związać podpowiedzi (fly-over-help).
Klasa JTabbedPane dostarcza wielu metod operowania na zakładkach i związanych z nimi komponentach.
M.in. można uzyskiwać wszelkie informacje o zakładkach i komponentach i dynamicznie
zmieniać ich własności: wybrana zakładka i komponent, właściwości zakładek
(kolory, teksty, ikony, lokalizacja).
Kilka takich metod poznamy przy okazji przykładowego programu.
Tworzymy panel zakładkowy z pięcioma różnie pokolorowanymi zakładkami.
Z każdą zakładką związany jest JPanel, w każdym z JPaneli umieszczono pięć przycisków.
Przyciski opisują położenie zakładek. Kliknięcie w przycisk ma spowodować:
- Odpowiednie umiejscowienie zakładek (np. po lewej stronie)
- Uczynienie wszystkich przycisków w danym JPanelu nieaktywnymi
- Przejście do następnej zakładki (wybór panelu, związanego z następną zakładką)
Przyjrzyjmy się programowi.
Utworzenie JTabbedPane jest proste: wystarczy użyć konstruktora bezparametrowego.
Alternatywnie można wywołać konstruktor z argumentem określającym położenie zakładek.
Argument specyfikuje się jako odpowiednią stałą określoną w interfejsie SwingConstants.
Klasa JTabbedPane implementuje ten interfejs, można więc używać nazw JTabbedPane.TOP,
JTabbedPane.LEFT, JTabbedPane,RIGHT, JTabbedPane.BOTTOM.
Lepszym jednak rozwiązaniem jest implementacja interfejsu we własnej klasie
(nie trzeba wtedy podawać nazwy klasy przy odwołaniu do stałych statycznych).
A skoro tak - można też rozszerzyć interfejs o własne stałe.
W omawianym przykładzie stworzono interfejs Constants rozszerzający SwingConstants
i uzupełniający go o stałe oznaczające kolory (RED, BLUE itp.).
W konstruktorze przykładowej klasy są tworzone J-panele, dodawane do nich
przyciski, po czym dla każdego J-panelu tworzone są zakładki w panelu zakładkowym.
Ponieważ są to czynności powtarzające się umieszczono je w pętlach, korzystając
z tablic kolorów, tekstów i lokalizacji.
Dla sprawnej obsługi kliknięcia w przycisk skorzystamy z możliwości przypisywania
J-komponentom dodatkowej informacji w postaci par: klucz - wartość (metoda
putClientProperty(Object key, Object value) z klasy JComponent). Tą informacją
będzie wartość stałej (przekształconej do Integer, bo musi być obiektem),
określająca położenie zakładek.
Po stworzeniu zakładek (metoda addTab(tekst, komponent)) ustalamy kolory
ich tła i napisów (metody setBackgroundAt(...) i setForegroundAt(...)).
Wszystko to pokazuje pierwszy fragment programu.( fragment 1)
fragment 1
public class Tabbed1 extends JFrame
implements ActionListener, Constants {
JTabbedPane tp = new JTabbedPane();
Tabbed1() {
Color[] back = { BLUE, YELLOW, RED, WHITE, BLACK };
Color[] fore = { WHITE, BLACK, YELLOW, BLACK, WHITE };
String[] txt = { "Top", "Left", "Right", "Bottom", "Default" };
String[] loc = { "North", "West", "East", "South", "Center" };
int[] place = { TOP, LEFT, RIGHT, BOTTOM, TOP };
JButton b = null;
JPanel p = null;
for (int i=0; i<back.length; i++) {
p = new JPanel(new BorderLayout());
for (int j=0; j<txt.length; j++) {
b = new JButton(txt[j]);
b.addActionListener(this);
b.putClientProperty("Place", new Integer(place[j]));
p.add(b, loc[j]);
}
tp.addTab("Tab"+(i+1), p);
tp.setBackgroundAt(i, back[i]);
tp.setForegroundAt(i, fore[i]);
}
getContentPane().add(tp);
setSize(300, 200);
setVisible(true);
}
...
}
Mając jako pole klasy zmienną tp, oznaczającą utworzony JTabbedPane, przy obsłudze kliknięcia w przycisk:
-
pobieramy informację o żądanym położeniu zakładek (metoda getClientProperty(...) z klasy JComponent)
-
ustalamy nowe położenie zakładek (tp.setTabPlacement(...))
-
dowiadujemy się, który komponent-panel jest akurat wybrany przez zakładkę (tp.getSelectedComponent())
-
w tym panelu dezaktywujemy wszystkie przyciski
-
dowiadujemy się ile jest w ogóle zakładek (tp.getTabCount())
-
dowiadujemy się jaki jest indeks wybranej zakładki (tp.getSelectedIndex())
-
jeżeli indeks nie wskazuje na ostatnią zakładkę, przechodzimy do zakładki
z indeksem o 1 większym; w przeciwnym razie - do pierwszej zakładki (tp.setSelectedIndex(...).
Te czynności wykonywane są w metodzie actionPerformed zaimplementowanej w
omawianej przykładowej klasie ( zob. wydruk fragment 2)
Fragment 2
public void actionPerformed(ActionEvent e) {
JComponent c = (JComponent) e.getSource();
Integer prop = (Integer) c.getClientProperty("Place");
tp.setTabPlacement(prop.intValue());
JComponent p = (JComponent) tp.getSelectedComponent();
Component[] b = p.getComponents();
for (int i=0; i<b.length; i++) b[i].setEnabled(false);
int tabs = tp.getTabCount();
int index = tp.getSelectedIndex();
if (index == tabs-1) index = 0;
else index++;
tp.setSelectedIndex(index);
}
Mamy też dodatkową możliwość ustalania "polityki rozkładu zakładek"
w konstruktorze lub za pomocą metody setTabLayoutPolicy, używając przy tym
argumentu – stałej całkowitoliczbowej:
JTabbedPane.SCROLL_TAB_LAYOUT – przewijany wiersz/kolumna zakładek
JTabbedPane.SCROLL_TAB_LAYOUT - zakładki (jeśli się nie mieszczą w widocznym
obszarze układane są w kilku wierszach lub kokumnach).
Rysunki pokazują ułożenie 10 zakładek w stylu SCROLL_TAB_LAYOUT u góry (rys. A) i WRAP_TAB_LAYOUT z prawej (rys B).
Kody dla konstrukcji i dynamicznej zmiany układu zakladek:
JTabbedPane jtp = new JTabbedPane(JTabbedPane.TOP,
JTabbedPane.SCROLL_TAB_LAYOUT);
jtp.setLayout(JTabbedPane.WRAP_TAB_LAYOUT);
Poczynając od wersji JDK 1.6 zakładki mogą być dowolnymi
komponentami, co np. pozwala na różnego rodzaju zaawansowane efekty
graficzne.
3.3. Panel przewijany - JScrollPane
Służy do przedstawiania komponentów, których rozmiar jest większy niż widoczny w panelu obszar.
Do przewijania widoku komponentu przeznaczone są suwaki.
W Swingu pełni bardzo ważną rolę, gdyż wiele komponentów, które w AWT miały
wbudowane suwaki (takich jak np wielowierszowe pole edycyjne czy lista) w
Swingu musi być umieszczona w panelu przewijania i dopiero wtedy pojawią się
suwaki.
Stąd najprostsze zastosowanie JScrollPane: "obudowanie" nim J-komponentu, który powinien mieć suwaki np.
public static void main(String[] args) {
JScrollPane scroll = new JScrollPane(new JTextArea(25,80));
scroll.setPreferredSize(new Dimension(200,200));
scroll.setBorder(BorderFactory.createTitledBorder("Edytor"));
JFrame f = new JFrame();
f.getContentPane().add(scroll);
f.pack();
f.setVisible(true);
}
Uwaga: dla uproszczenia pominięto invokeLater
Ale użyteczność JScrollPane na tym się nie kończy. Ma on dużo bardziej skomplikowaną
budowę niż ScrollPane z AWT i dostarcza znacznie większych możliwości.
JScrollPane zarządza dziewięcioma komponentami:
- widokiem komponentu (np. dużej grafiki, która tylko częściowo jest
widoczna). W angielskiej terminologii komponent taki nazywa się "scrollable
client" (przewijalny klient) lub "data model" dla JScrollPane (o koncepcji
modeli danych będzie mowa w następnym wykładzie). Aktualnie widoczna część
komponentu obsługiwana jest przez klasę JViewPort i nazywa się "viewPortView".
- nagłówkami kolumn i wierszy, które mogą być dowolnymi komponentami,
widocznymi również częściowo, w sposób zsynchronizowany z widokiem komponentu
(przeiwjanie komponentu powduje odpowiednie przewijanie nagłówków) . Te elementy
również obsługuje klasa JViewPort. Nagłówki nazywają się odpowiedni: columnHeader
i rowHeader.
- rogami (corners) panelu. W każdym z czterech rogów (o ile występują
w danej sytuacji) można umieścić dowolny komponent ilustracyjny lub funkcjonalny.
- paskami przewijania poziomego i pionowego, które są komponentami typu JScrollBar.
Rysunek pokazuje schemat budowy JScrollPane.
Obecność pasków przewijania regulowana jest przez tzw. politykę przewijania.
Możemy zadekretować np., by pojawiały się zawsze, albo tylko wtedy gdy jest
taka potrzeba (przewijalny klient jest większy od rozmiarów widoku w danym
kierunku).
Do ustalania widoku klienta służy metoda setViewportView(Component).
Nagłówki ustalamy za pomocą metod setRowHeaderView(Component) i setColumnHeaderView(Component).
Zawartość rogów możemy definiować za pomocą metody setCorner(int jaki_róg,
Component), gdzie jaki róg - stała satyczna typu String określająca do którego
rogu dodawany jest komponent.
Przykładowy program na wydruku wykorzystuje niektóre możliwości JScrollPane.
Tworzymy duży panel "komórek" - etykiet, rozmieszczonych w układzie GridLayout.
Jak w arkuszach kalkulacyjnych, wiersze rozkładu są oznaczane liczbami, a kolumny - literami.
Każda komórka-etykieta zawiera napis obrazujący jej "adres" (np. A1 lub V11).
Panel jest tak duży, że nie mieści się w oknie o zadekretowanych rozmiarach.
Zatem będzi on umieszczony w JScrollPane jako widok przewijalnego klienta.
Ustalimy dla naszego przykładowego JScrollPane naturalne nagłówki kolumn (litery) i wierszy (liczby).
Dodatkowo w górnych rogach (lewym i prawym) umieścimy przycisk obrazkowy
(z ikonką "UP"), którego kliknięcie spowoduje przejście do komórki A1. W
tym celu w obsłudze kliknięcia w przycisk (actionPerformed) użyto metody
z klasy JComponent scrollRectToVisible(Rectangle), która przewija JScrollpane
w ten sposoób, by uwidocznić podany prostokąt komponentu (tego przewijalnego).
Działanie programu ilustruje rysunek.
public class Scroll2 extends JFrame implements ActionListener {
int cellW = 50, cellH = 20, rows = 30, cols = 26;
JPanel cont = new JPanel(new GridLayout(rows, cols, 0, 0));
JPanel colHead = new JPanel(new GridLayout(1, cols, 0, 0));
JPanel rowHead = new JPanel(new GridLayout(rows, 1, 0, 0));
Scroll2() {
Color lYellow = new Color(255,255,240),
lBlue = new Color(219,232,255);
String[] lit = new String[cols+1];
JLabel l = null;
for (int j = 1; j <=cols; j++) {
lit[j] = "" + (char) ('A'+(j-1));
l = createLabel(lit[j], Color.black, cellW, cellH);
colHead.add(createLabel(lit[j], Color.black, cellW, cellH));
}
for (int i = 1; i<=rows; i++) {
rowHead.add(createLabel(""+i, Color.black, cellH, cellH));
for (int j = 1; j<=cols; j++)
cont.add(createLabel(lit[j]+i, Color.blue, cellW, cellH));
}
JScrollPane sp = new JScrollPane();
cont.setBackground(lYellow);
sp.setViewportView(cont);
rowHead.setBackground(lBlue);
sp.setRowHeaderView(rowHead);
colHead.setBackground(lBlue);
sp.setColumnHeaderView(colHead);
ImageIcon up = new ImageIcon("Up16.gif");
JButton leftUp = new JButton(up);
leftUp.addActionListener(this);
JButton rightUp = new JButton(up);
rightUp.addActionListener(this);
sp.setCorner(JScrollPane.UPPER_LEFT_CORNER, leftUp);
sp.setCorner(JScrollPane.UPPER_RIGHT_CORNER, rightUp);
JComponent cp = (JComponent) getContentPane();
cp.add(sp);
cp.setPreferredSize(new Dimension(300,300));
pack();
setVisible(true);
}
JLabel createLabel(String s, Color c, int w, int h) {
JLabel l = new JLabel(s, JLabel.CENTER);
l.setBorder(BorderFactory.createLineBorder(c));
l.setPreferredSize(new Dimension(w, h));
l.setOpaque(false);
return l;
}
public void actionPerformed(ActionEvent e) {
cont.scrollRectToVisible(new Rectangle(0,0,cellW,cellH));
}
}
Zawartość panelu przewijalnego (przewijalny klient, nagłówki kolumn i wierszy
, rogi) można zmieniać dynamicznie (w trakcie działania programu).
3.4. Pasek narzędzi - JToolBar
Klasa JToolBar definiuje tzw. pasek narzędzi.
Przykładowy pasek narzędzi pokazuje rysunek.
JToolBar jest kontenerem, do którego możemy dodawać dowolne komponenty.
Np. aby uzyskać pasek jak na rysunku stworzono obiekt JToolBar:
JToolBar tb = new JToolBar();
i za pomocą metody add dodana do niego cztery przyciski obrazkowe, etykietę
("Szukaj"), pole tekstowe oraz panel z przyciskami ponumerowanymi 1, 2, 3,
Następnie dodano JToolBar do okna o rozkładzie BorderLayout na północy: frame.getContentPane().add("North");
Od zwykłych kontenerów JToolBar różni się tym, że:
- może być przesywany myszką i doczepiany do dowolnego brzegu okna lub całkiem od okna odczepiony (właściwość "floatable")
- można do niego dodać separator za pomocą metody addSeparator,
- można do niego dodawać obiekty typu Action (metoda add(Action a))
- ma domyślny rozkład BoxLayout.
4. Akcje
Jeśli mamy kilka komponentów (np. przycisk na pasku narzędzi, element menu
rozwijalnego lub element menu kontekstowego), które powinny mieć tę samą
funkcjonalność, to możemy w łatwy sposób, jednokrotnie, funkcjonalność tę
zdefiniować i łatwo przypisać ją tym komponentom.
Służą temu obiekty typu Action.
Obiekty typu Action:
- definiują teksty i/lub ikony, które mogą pojawiać się na przyciskach,
w tym w kontenerze JToollBar, i elementach menu (rozwijalnego lub kontekstowego)
- pozwalają ustalać inne atrybuty (w szczególności "actionCommand", mnemoniki,
teksty podpowiedzi, a dla elementów menu - akceleratory)
- definiują obsługę akcji (na przycisku lub wyboru opcji menu)
- mogą być równolegle dodawane do paska narzędzi lub do menu i zapewniają
scentralizowane zmiany stanu aktywności (właściwość enabled).
Atrakcyjność obiektu-akcji polega na tym, iż po jego utworzeniu może on być równolegle dodany:
- do paska narzędzi (klasa JToolBar ma metodę add(Action)),
- do menu rozwijalnego (klasa JMenu ma metodę add(Action)),
- do menu kontekstowego
Ponadto dla każdego abstrakcyjnego przycisku (wszystkich przycisków, elementów
menu) możemy dynamicznie zmieniać właściwości związane z akcją za pomocą
metody setAction(Action) z klasy AbstractButton.
Wywołanie setAction(...) dynamicznie zmienia właściwości przycisku
(napis, ikonę, podpowiedź itd.) oraz słuchacza akcji (definiowanego przez
obiekt typu Action) nie ruszając innych słuchaczy, przyłączonych za pomocą
addActionListener.
Action jest interfejsem. Abstrakcyjna klasa AbstractAction implementuje część
metod tego interfejsu i dostarcza konstruktora z argumentami: napis, ikona.
Aby utworzyć własny obiekt-akcję wystarczy:
- stworzyć klasę dziedziczącą AbstractAction,
- dostarczyć w niej implementacji metody actionPerformed(...) - co ma się dziać, gdy pojawi się zdarzenie akcji,
- stworzyć obiekt nowej klasy.
Przykładowy program na wydruku tworzy dwa obiekty-akcje, które będą związane
z elementami menu rozwijalnego, paska narzędzi oraz menu kontekstowego. Użytkownik
może dokonać wyboru każdej z dwóch akcji albo klikając w przycisk na pasku
narzędzi, albo wybierając opcję z menu rozwijalnego albo z kontekstowego,
uruchamianego prawym kliknięciem myszki na wielopolu edycyjnym.
Tworząc akcję podajemy napis i ikonę, które pojawią się na przyciskach (paska narzędzi, menus).
Oprócz tego specyfikujemy obsługę akcji - implementację metody actionPerformed(..)
Samo dodanie obiektów-akcji (metoda add) do paska narzędzi lub do menu automatycznie
tworzy ich wizualny opis (napis i ikonka na przycisku paska narzędzi lub
w menu) oraz zapewnia, że po kliknięciu w przycisk lub opcję menu powstanie
odpowiednie zdarzenie akcji i zostanie obsłużone przez metodę actionPerformed
implementowaną w klasie obiektu-akcji.
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class Akcja extends JFrame {
JTextArea ta = new JTextArea(10, 20);
JScrollPane sp = new JScrollPane(ta);
Action newAct = new AbstractAction("New", new ImageIcon("New24.gif")) {
public void actionPerformed(ActionEvent e) {
newFile();
}
};
Action openAct = new AbstractAction("Open", new ImageIcon("Open24.gif")) {
public void actionPerformed(ActionEvent e) {
openFile();
}
};
JPopupMenu popup = new JPopupMenu();
public Akcja() {
JToolBar tb = new JToolBar();
JMenu menu = new JMenu("File");
tb.add(newAct);
tb.add(openAct);
menu.add(newAct);
menu.add(openAct);
popup.add(newAct);
popup.add(openAct);
ta.setComponentPopupMenu(popup);
JMenuBar mb = new JMenuBar();
mb.add(menu);
setJMenuBar(mb);
add(tb, "North");
add(sp);
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
pack();
setLocationRelativeTo(null);
setVisible(true);
}
private void newFile() {
// utworzenie pliku
}
private void openFile() {
// otwarcie pliku
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new Akcja();
}
});
}
}
Atrybuty akcji (określające właściwości przycisku) są przechowywane w obiekcie
typu akcja i możemy je uzyskać lub ustalić (przed ustaleniem akcji dla przycisku)
za pomocą metod getValue i putValue interfejsu Action.
Standardowo zdefiniowano m.in. następujące rodzaje atrybutów, jako stałe interfejsu Action:
-
ACCELERATOR_KEY - akcelerator - obiekt typu KeyStroke
-
ACTION_COMMAND_KEY - actionCommand (String)
-
LONG_DESCRIPTION - opis dla systemów kontekstowej pomocy (String)
-
MNEMONIC_KEY - mnemonika (int, ale przy ustalaniu metodą put i pobieraniu metodą get - używamy obiektu klasy Integer)
-
NAME - nazwa akcji = napis na przycisku
-
SHORT_DESCRIPTION - tool tip (String)
-
SMALL_ICON -ikona na przycisku (Icon)
Przykładowo, jeśli obiekt a jest klasy implementującej interfejs Action, to po:
a.putValue(Action.SMALL_ICON, new ImageIcon("jakis.gif"));
przyciski (np. na pasku narzędzi lub w menu), dla których ustalimy akcję a zyskają ikonę z pliku jakis.gif.
Można definiować (w klasach własnych komponentów) nowe/inne atrybuty akcji
i zapewnić mechanizm dynamicznych zmian akcji za pomocą użycia metody configurePropertiesFromAction()
oraz nasłuchu zmian właściwości "action".
5. Mapy akcji i mapy klawiaturowe
W Javie istnieje możliwość skojarzenia z każdym J-komponentem - akcji klawiaturowych (czyli akcji wykonywanych na skutek naciśnięcia klawiszy na klawiaturze).
Powiązanie skrótów klawiaturowych z akcją odbywa się za pomocą dwóch map: mapy akcji (obiekt klasy ActionMap) i mapy klawiatury (obiekt klasy InputMap).
Mapa akcji zawiera pary: nazwa akcji - akcja (obiekt typu Action), mapa klawiatury
zawiera pary: klucz (obiekt typu KeyString) - nazwa akcji (mówiąc ściślej,
"nazwa akcji" może być dowolnym obiektem, zwykle jednak jest to napis).
Każdy komponent "posiada" swoją mapę akcji oraz swoją mapę klawiaturową (a nawet trzy takie mapy, ale o tym za chwilę).
Akcje wiązane są z klawiszami w następujący sposób: gdy wciśnięto klawisz,
w mapie klawiaturowej komponentu wyszukiwane jest odzwzorowanie: klawisz
- nazwa akcji. Jeśli istnieje takie odwzorowanie, to nazwa akcji staje się
kluczem wyszukiwania samej akcji w mapie akcji komponentu. Odnaleziona akcja
jest wykonywana, to znaczy wywoływana jest metoda actionPerformed z klasy
akcji, przy czym źródłem zdarzenia ActionEvent jest dany komponent.
Przykładowe powiązanie obu map pokazuje rysunek.
Mechanizm akcji klawiaturowych jest ogólniejszy i silniejszy niż obsługa
klawiatury za pomocą KeyListenera czy też użycie mnemonik lub akceleratorów,
gdyż źródłem akcji klawiaturowych mogą być dowolne J-komponenty i to niezależnie
od tego czy mają akurat fokus czy nie.
Z każdym J-komponentem związane są bowiem trzy mapy klawiaturowe, oznaczane stałymi całkowitoliczbowymi:
- JComponent.WHEN_FOCUSED - jest używana, gdy komponent ma fokus
- JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT - jest używana,
gdy komponent ma fokus lub (na dowolnym poziomie heirarchii zawierania się
komponentów) zawiera komponent, który ma fokus,
- JComponent.WHEN_IN_FOCUSED_WINDOW - jest używana, gdy okno, w którym zawarty jest komponent ma fokus lub zawiera dowolny komponent, który ma fokus.
Mapę klawiaturową J-komponentu comp uzyskujemy za pomocą odwolania:
InputMap imap = comp.getInputMap(int rodzaj);
gdzie:
imap - mapa klawiaturowa,
rodzaj - rodzaj mapy:
WHEN_FOCUSED,
WHEN_ANCESTOR_OF_FOCUSED_COMPONENT,
WHEN _IN_FOCUSED_WINDOW.
Mapę akcji komponentu comp uzyskujemy poprzez:
ActionMap amap = comp.getActionMap();
Możemy też użyć odpowiednich metod setInputMap(...) i
setActionMap(...). do ustalania map dla komponentów.
Dodawanie powiązań: klawisze - nazwa akcji do mapy klawiaturowej odbywa się za pomocą metody put(KeyStroke, Object).
Pierwszy jej argument określa "skrót klawiaturowy", drugi - nazwę (identyfikację) akcji.
Aby uzyskać obiekt klasy KeyStroke, reprezentujący skrót klawiaturowy stosujemy statyczmną metodę tej klasy:
KeyStroke getKeyStroke(String)
której argumentem jest napis, oznaczający skrót klawiaturowy, np. "control D".
Dodawanie powiązań: nazwa akcji - akcja do mapy akcji odbywa się za pomocą
analogicznej metody put(Object, Action). z argumentami: nazwa akcji, obiekt
klasy implementującej interefejs Action.
Oczywiście, możemy używać dowolnych innych metod interfejsu Map, np. dla
uzyskania informacji o powiązaniach w mapach akcji i mapach klawiaturowych
jakichś komponentów.
Uwaga: usunięcie powiązania klawisz - akcja odbywa się poprzez związanie klawisza ze specjalną nazwą akcji - "none":
InputMap imap;
KeyStroke key;
//...
imap.put(key, "none");
Przykładowy program ilustruje użycie map akcji i map klawiaturowych.
W programie tworzymy mapę akcji i pod nazwą "write" zapisujemy do niej akcję,
która zastępuje zaznaczony tekst w komponencie tekstowym tekstem pobranym
z własciwości clientProperty żródła zdarzenia.
Ustalamy tę mapę jako mapę akcji dla trzech etykiet, a do pobranych od nich
map klawiaturowych (typu WHEN_IN_FOCUSED_WINDOW) dopisujemy odpowiednie skróty
klawiaturowe powodujące wywołanie akcji "write".
import javax.swing.*;
import javax.swing.text.*;
import java.awt.event.*;
import java.awt.*;
class Writer extends AbstractAction {
JTextComponent tc;
public Writer(JTextComponent t) {
super("write");
tc = t;
}
public void actionPerformed(ActionEvent e) {
Object src = e.getSource();
JComponent c = (JComponent) e.getSource();
String txt = (String) c.getClientProperty("text");
tc.replaceSelection(txt);
}
}
public class KMap extends JFrame {
String[] txt = { "Pies", "Kot", "Tygrys" };
String[] keys = { "control P", "control K", "control T" };
ActionMap amap = new ActionMap();
public KMap() {
JTextArea ta = new JTextArea(20, 20);
add(new JScrollPane(ta));
amap.put("write", new Writer(ta));
add(new JScrollPane(ta));
JPanel p = new JPanel();
p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS));
p.setBorder(BorderFactory.createLineBorder(Color.blue));
for (int i = 0; i < txt.length; i++) {
JLabel l = createLabel(txt[i], keys[i]);
l.putClientProperty("text", txt[i]);
l.setAlignmentX(JLabel.RIGHT);
p.add(l);
JSeparator js = new JSeparator();
js.setMaximumSize(new Dimension(1200, 7));
p.add(js);
}
add(p, "West");
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
pack();
setLocationRelativeTo(null);
setVisible(true);
}
JLabel createLabel(String txt, String key) {
JLabel l = new JLabel(txt + " ");
l.setPreferredSize(new Dimension(100, 50));
l.setToolTipText("Wciśnij : ");
InputMap imap = l.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
imap.put(KeyStroke.getKeyStroke(key), "write");
l.setActionMap(amap);
return l;
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new KMap();
}
});
}
}
6. Obieralny wygląd (pluggable look & feel)
Do chwili pojawienia się Swingu komponenty Javy wyglądały zawsze tak, jak dekretowała platforma systemowa.
Przyciski na platformach Win, Motif czy OS/2 - wyglądają inaczej. A Java
ma być językiem wieloplatformowym! Zatem, powinna mieć propozycję unifikacyjną:
jednakowy wygląd na róznych platformach, I to w Swingu zrealizowano.
I więcej: wygląd i zachowanie (look and feel) elementów GUI może być ustalane dynamicznie.
Można także definiować nowe rodzaje "wyglądów".
Zarządzaniem wyglądem komponentów zajmuje się klasa UIManager.
Aby ustalić "look and feel" (L&F) należy wywołać metodę setLookAndFeel
(statyczna metoda klasy UIManager) z argumentem specyfikującym pełną kwalifikowaną
nazwę klasy określającej wygląd i zachowanie komponentów.
Standardowo dostępne są następujące klasy, określające "look and feel" . - javax.swing.plaf.metal.MetalLookAndFeel - "Java Look and Feel" - standardowy wygląd na wszystkich platformach
- com.sun.java.swing.plaf.motif.MotifLookAndFeel - wygląd Motif
- javax.swing.plaf.mac.MacLookAndFeel - wygląd Mac (L&F dostępny dla platformy Mac)
- com.sun.java.swing.plaf.windows.WindowsLookAndFeel
- wygląd Windows (L&F dostępny nie na wszystkich platformach),
- com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel (od wersji 6 update 10).
- a także dodatkowe np. GTK.
Oprócz tego w klasie UIManager dostępne są statyczne metody:
- getCrossPlatformLookAndFeelClassName() - zwracająca nazwę klasy, która
gwarantuje jednolity wygląd komponentów na wszystkich platformach (obecnie
jest Java L&F)
- getSystemLookAndFeelClassName() - zwraca nazwę klasy, która daje specyficzny dla aktualnej platformy L&F
Ustalenie L&F powinno odbyć się przed stworzeniem jakichkolwiek komponentów.
Jeśli chcemy dynamicznie zmienić L&F w trakcie działania programu, to należy:
- wywołać UIManager.setLookAndFeel(...)
- następnie powiadomić wszystkie komponenty o zmianie za pomocą statycznej metody updateComponentTreeUI
z klasy SwingUtilities; argumentem tej metody powinna być referencja do kontenera
najwyższego poziomu (zwykle okna ramowego aplikacji).
Na wydruku pokazano przykład ustalania "Look and Feel" zarówno na początku
programu, jak i dynamicznie w trakcie jego działania.
import java.awt.event.*;
import javax.swing.*;
import net.miginfocom.swing.*;
public class Test extends JFrame implements ActionListener {
final static String MotifLF = "com.sun.java.swing.plaf.motif.MotifLookAndFeel",
JavaLF = UIManager.getCrossPlatformLookAndFeelClassName(),
WindowsLF = "com.sun.java.swing.plaf.windows.WindowsLookAndFeel",
NimbusLF = "com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel";
Test(String tit) {
super(tit);
setLayout(new MigLayout());
add(new JCheckBox("CheckBox"));
add(new JRadioButton("Radio"));
JTextField tf = new JTextField(10);
add(new JLabel("textField"), "align right");
add(new JTextField(10), "span, wrap");
add(new JLabel("Slider"), "align right");
add(new JSlider(0, 100), "span, wrap");
add(createButton("Motif", MotifLF), "align right");
add(createButton("Java", JavaLF));
add(createButton("Nimbus", NimbusLF));
add(createButton("Windows", WindowsLF));
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
pack();
setLocationRelativeTo(null);
setVisible(true);
}
JButton createButton(String txt, String lafClass) {
JButton b = new JButton(txt);
b.setActionCommand(lafClass);
b.addActionListener(this);
return b;
}
public void actionPerformed(ActionEvent e) {
String laf = e.getActionCommand();
try {
UIManager.setLookAndFeel(laf);
setTitle(laf.substring(laf.lastIndexOf('.') +1));
} catch (Exception exc) {
System.out.println("Nie umiem ustalić L&F = " + laf);
}
SwingUtilities.updateComponentTreeUI(this);
pack();
}
public static void main(String[] args) {
try {
UIManager.setLookAndFeel(JavaLF);
} catch (Exception excp) {
System.out.println("Nie umiem ustalić L&F");
}
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new Test("MetalLookAndFeel");
}
});
}
}
Zobacz działanie programu
Architektura "Pluggable Look & Feel" jest bardzo rozbudowana i elastyczna. Umożliwia
- łatwe ustalanie niektórych wybranych właściwości komponentów Swingu,
- tworzenie od podstaw całkiem nowego wyglądu wybranego komponentu Swingu,
- tworzenie własnych spójnych wyglądów całego zestawu komponentów Swingu,
Niewątpliwie najłatwiej jest ingerować w pewne wybrane charakterystyki, określające
wygląd i (po części) zachowanie komponentów. Każdy look&feel ma tzw.
ustawienia. Właśnie one określają wspomniane charakterystyki komponentów.
Domyślne ustawienia możemy uzyskać od aktualnego wyglądu (obiektu klasy LookAndFeel)
w postaci obiektu klasy UIDefaults (który tak naprawdę jest mapą, z kluczami
będącymi nazwami atrybutów).
Pokazuje to poniższy programik.
public class PlafDefaults {
public static void main(String[] args) {
UIDefaults defaults = UIManager.getLookAndFeel().getDefaults();;
for (Object key : defaults.keySet()) {
System.out.println(key + " = " + defaults.get(key));
}
}
}
Program pozwala zorientować się jakie atrybuty, określające wygląd (a po
częsci i zachowanie) komponentów są dla nas dostępne i zarazem jakie mają
domyślne wartości.
Otrzymamy domyślne właściwości dla wszystkich komponentów. Cały wydruk programu
liczy ponad 500 wierszy, możemy zatem przedstawić tylko jego częśc (np. właściwości
przycisków klasy JButton).
Button.background = javax.swing.plaf.ColorUIResource[r=204,g=204,b=204]
Button.border = javax.swing.plaf.BorderUIResource$CompoundBorderUIResource@cd2c3c
Button.darkShadow = javax.swing.plaf.ColorUIResource[r=102,g=102,b=102]
Button.disabledText = javax.swing.plaf.ColorUIResource[r=153,g=153,b=153]
Button.focus = javax.swing.plaf.ColorUIResource[r=153,g=153,b=204]
Button.focusInputMap = javax.swing.plaf.InputMapUIResource@1d99a4d
Button.font = javax.swing.plaf.FontUIResource[family=Dialog,name=Dialog,style=bold,size=12]
Button.foreground = javax.swing.plaf.ColorUIResource[r=0,g=0,b=0]
Button.highlight = javax.swing.plaf.ColorUIResource[r=255,g=255,b=255]
Button.light = javax.swing.plaf.ColorUIResource[r=255,g=255,b=255]
Button.margin = javax.swing.plaf.InsetsUIResource[top=2,left=14,bottom=2,right=14]
Button.select = javax.swing.plaf.ColorUIResource[r=153,g=153,b=153]
Button.shadow = javax.swing.plaf.ColorUIResource[r=153,g=153,b=153]
Button.textIconGap = 4
Button.textShiftOffset = 0
ButtonUI = javax.swing.plaf.metal.MetalButtonUI
Widzimy tutaj ustawienia dotyczące m.in. kolorów, pisma, ramek itp. dla
przycisków JButton (w nazwach klas, określających wygląd komponentów Swingu
standardowo pomija się literę J). Wartości ustawień są reprezentowane jako
obiekty klas xxxxUIResource (np. ColorUIResource albo FontUIResource). Dlaczego
nie po prostu Color albo Font?
Otóż, przy dynamicznych (w trakcie wykonania programu) zmianach look&feel
Java musi odróżniać ustawienia związane z look&feel od tych właściwości
które zostały nadane przez programistę bez zmiany samego look & feel.
Np. jeżeli kolor tła przycisku określony został przez programistę za pomoca
metody setBackground(...) i jest obiektem klasy Color, to ew. zmiana look&feel
nie powinna zmienić koloru tła tego przycisku. Może to natomiast zrobić,
gdy kolor jest określony przez obiekt klasy ColorUIResource.
Aktualne ustawienia możemy zmienić za pomocą metodu put(...) z klasy UIManager.
Poniższy przykładowy program zmienia ustawienia kolorów selekcji w menu rozwijalnym
(JMenu) i jego elementach (JMenuItem) oraz pismo JMenu.
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.border.*;
import javax.swing.plaf.*;
import java.awt.*;
import javax.swing.*;
import javax.swing.plaf.*;
public class MenuPlaf extends JFrame {
private static int x = 300, y = 300;
public MenuPlaf() {
JMenu menu = new JMenu("File");
String[] labels = { "New", "Open", "Save", "Save as..", "Properties" };
for (int i=0; i<labels.length; i++) {
JMenuItem mi = new JMenuItem(labels[i]);
menu.add(mi);
}
JMenuBar mbar = new JMenuBar();
mbar.add(menu);
String[] addMenus = { "Edit", "View", "Tools" };
for (int i=0; i<addMenus.length; i++) {
menu = new JMenu(addMenus[i]);
mbar.add(menu);
}
setJMenuBar(mbar);
setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
setSize(200, 100);
setLocation( x+=100, y += 100 );
}
public static void main(String[] args) {
ColorUIResource selBack = new ColorUIResource(Color.black);
ColorUIResource selFore = new ColorUIResource(Color.white);
UIManager.put("Menu.selectionBackground", selBack);
UIManager.put("Menu.selectionForeground", selFore);
UIManager.put("Menu.font",
new FontUIResource(new Font("Dialog", Font.BOLD, 20)));
UIManager.put("MenuItem.selectionBackground", selBack);
UIManager.put("MenuItem.selectionForeground", selFore);
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new MenuPlaf().setVisible(true);
new MenuPlaf().setVisible(true);
}
});
}
}
Od momentu ustalenia tych właściwości w metodzie main(), każde menu będzie
już je miało. Program ilustracyjnie tworzy dwa okna z przykładowymi menu,
które wyglądaja jak na rysunku.
Zobacz działanie programu
Nasza ingerencja w wygląd i działanie komponentów może być posunięta i
znacznie dalej.
Można
np. zmieniać nie tylko poszczególne własności komponentów. ale całe
zachowanie i wygląd konkretnych komponentów. W tym celu należy
przedefiniować klasę delagata UI dla danego komponentu i ustalić ją
jako aktualną przy danym Look And Feel.
Tak naprawdę najczęściej będziemy korzystac z
gotowych pakietów PLAF. Biblioteki obieralnych wyglądów
(pluggable look and feels) są rozwijane coraz bardziej, a w
Internecie można znaleźć ich całe mnóstwo. Do najbardziej eleganckich
nalezą wyglądy JGoodies (autor, Karsten Lentzsch, jest wybitnym
znawcą zasad tworzenia GUI).
Ilustruje to rysunek:
Aby uzyskać pokazany efekt wystarczyło napisać:
UIManager.setLookAndFeel(
"com.jgoodies.looks.plastic.PlasticXPLookAndFeel");
Natomaist
do najbardzeij elastycznych i konfigurowalnych co do efektów
graficznych nalezy Substance Kirila Grouchnikow'a, wspierany przez
pakiet laf-widgets tego samego autora.
Nie sposób wymienić
wszystkich możliwości Substance i laf-widgets (zresztą laf-widgets
mogą być stosowane i wobec innych wyglądów). Przedstawię tylko dwa
przykłady: podgląd zakładek panelu JTabbedPane i animowane
przyciski z efektami typu ghost/spring.
Program na wydruku
tworzy panel zakładkowy z automatyczniie dodawanym przyciskiem.
otwierającym podgląd zakładek.
Tworzenie panelu zakładkowego z podglądem zakładek.
import java.awt.*;
import javax.swing.*;
import org.jvnet.lafwidget.*;
import org.jvnet.lafwidget.tabbed.*;
import org.jvnet.substance.*;
import org.jvnet.substance.theme.*;
public class TPview extends JFrame {
public TPview() {
super("Przegląd zakładek");
Color[] c = { Color.BLUE, Color.RED, Color.YELLOW };
JTabbedPane tp = new JTabbedPane();
for (int i = 0; i < c.length; i++) {
JPanel p = new JPanel();
p.setBackground(c[i]);
int n = i + 1;
p.add(new JButton("Przycisk " + n));
tp.add("Tab " + n, p);
}
add(tp, BorderLayout.CENTER);
// Ustalenie podglądu zakładek
tp.putClientProperty(LafWidget.TABBED_PANE_PREVIEW_PAINTER,
new DefaultTabPreviewPainter());
setSize(500, 300);
setLocationRelativeTo(null);
setDefaultCloseOperation(EXIT_ON_CLOSE);
setVisible(true);
}
public static void main(String[] args) throws Exception {
UIManager.setLookAndFeel(new SubstanceLookAndFeel());
SubstanceLookAndFeel.setCurrentTheme(new SubstanceSteelBlueTheme());
SwingUtilities.invokeLater( new Runnable() {
public void run() {
new TPview();
}
});
}
}
Poniższy rysunek przedstawia panel zakładkowy z przyciskiem przeglądu zakładek
a podgląd zakładek widoczny jest poniżej.
Doskonalenie
wyglądów nie
omija i standardu Javy. Już w wersji 5 wprowadzono dający niezwykle
duże możliwości konfiguracyjne mechanizm SynthLookAndFeel. Można
odnieść wrażenie, że pozostał on niedoceniony, a naprawdę wart jest
uwagi. Szósta wersja Javy znacząco poprawiła różne aspekty
grafiki, szczególnie dla Windows a w update 10 pojawił się nowy
elegancki wygląd Nimbus. W wersji siódmej ma on zastąpić dotychczasowy
Ocean jako wygląd typu "cross-platform" .
Przykładowa aplikacja pokazująca wygląd Nimbus.
public class NimbusTest {
public static void main(String[] args) throws Exception {
UIManager.setLookAndFeel("com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel");
SwingUtilities.invokeLater( new Runnable() {
public void run() {
JFrame frame = new JFrame("Nimbus look and feel");
frame.add(new JScrollPane(new JTable(
new Object[][] { { "Reksio", "Pies", 10, 20 },
{ "Saba", "Tygrys", 5, 70 },
{ "Mruk", "Kot", 1, 5 },
{ "Krówka", "Żubr", 6, 100 },
{ "Puchacz", "Niedźwiedź", 7, 120 },
{ "Śpioch", "Borsuk", 2, 10 },
},
new String[] { "Imię", "Rodzaj", "Wiek", "Waga" }
)
));
JPanel p = new JPanel();
p.add(new JLabel("Opis"));
p.add(new JTextField(10));
p.add(new JSlider(0, 10));
p.add(new JButton("Ok"));
p.add(new JButton("Cancel"));
frame.add(p, "South");
frame.pack();
frame.setLocationRelativeTo(null);
frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
frame.setVisible(true);
}
});
}
}
Wygląd Nimbus (pole tekstowe ma zaznaczony ramką fokus, przycisk Ok jest w stanie "rollover")
Zobacz działanie programu
7. Integracja z pulpitem
Jakiś
czas temu w ramach SwingLab powstał projekt Java Desktop Integration
Components (JDIC). Jego celem było (i jest) zintegrowanie Javy z
elementami powłoki graficznej systemu operacyjnego, w tym z pulpitem.
Chodzi o umożliwienie wykonywania z poziomu aplikacji Javy
podobnych operacji jak na pulpicie m.in. uruchamianie aplikacji przez
wybór dokumentów (plików) skojarzonych z tymi aplikacjami, otwieranie
przeglądarki i klienta pocztowego, umieszczanie ikon w obszarze
powiadamiania (tray).
JDIC rozwija się w dalszym ciągu, jednocześnie
koncepcja trafiła do standardu Javy w wersji 6 w postaci udostępnienia
podobnych możliwości. Niestety, wygląda na to, że nie było ścisłej
współpracy pomiędzy projektem JDIC a implementacją rozwiązań
"pulpitowych" w Javie 6. Ogólnie można powiedzieć, że rozwiązania JDIC
są bardziej zaawansowane (dają więcej możliwości). Ale ponieważ
standard jest standardem przyjrzymy się mu w pierwszej kolejności, o
niektórych rozszerzeniach JDIC wspominając przy okazji.
7.1. Dokumentocentryczne uruchamianie rodzimych aplikacji. Przeglądarka i poczta.
Bardzo przyjemna (bo niewielka) klasa Desktop z pakietu java.awt pozwala na:
- uruchamianie zarejestrowanych w systemie aplikacji w celu otwarcia, edycji lub drukowania pliku (metody open(File), edit(File) i print(File)),
- uruchamianie domyślnej przeglądarki z podanym adresem internetowym (metoda browse(URI)),
- uruchamianie domyślnego klienta poczty (metody mail(...)
- z możliwością podania URI typu mailto:, co wypełnia pola nagłówka i
ew. treść listu elementami z podanego URI np. to:, subject:, cc:, body:)
Aby
korzystać z tych metod należy najpierw upewnić się czy na danej
platformie dostępna jest integracja Javy z pulpitem (statyczna metoda
Desktop.isDektopSupported()) i uzyskać obiekt klasy Desktop za pomocą
statycznej metody Desktop.getDesktop().
Zastosowanie klasy Desktop ilustruje przykładowa aplikacja )zob. tysunek i listing).
Jest to swoista atrapa jakiejś części "przeglądarki" hoteli.
Oprócz
obejrzenia informacji o hotelu, możemy otworzyć broszurę PDF, obejrzeć
film o hotelu, zobaczyć jego stronę czy wreszcie otworzyć klienta
pocztowego z adresem hotelu (by np. zarezerwować miejsce).
Wszystkie te operacje w łatwy sposób wykonuje się za pomocą klasy Desktop (zob. poniższy listinh).
public class HotelView extends JFrame implements ActionListener {
// Obiekt klasy Dekstop
private static Desktop desk;
public HotelView() {
// Teksty na przyciskach
String[] txt = { "<html><center>Informacja<br>o hotelu</center></html>",
"Broszura", "Film", "Web", "Mail" };
// ActionCommand przycisków
String[] cmd = { "Details", "roband.pdf", "45sec.mpg",
"http://oberoibali.com/",
"mailto:oberoi@bali.com" };
// Ikony na przyciskach
Icon[] icon = {
new ImageIcon("oberoi.jpg"), new ImageIcon("pdf_icon.gif"),
new ImageIcon("player.jpg"), new ImageIcon("firefox.png"),
new ImageIcon("Email3.gif")
};
setLayout(new FlowLayout(FlowLayout.LEFT));
for (int i = 0; i < icon.length; i++) {
JButton b = new JButton(txt[i], icon[i]);
b.setActionCommand(cmd[i]);
b.setVerticalTextPosition(SwingConstants.BOTTOM);
b.setHorizontalTextPosition(SwingConstants.CENTER);
b.addActionListener(this);
add(b);
}
setDefaultCloseOperation(EXIT_ON_CLOSE);
pack();
setVisible(true);
}
// Obsługa przycisków
public void actionPerformed(ActionEvent e) {
String cmd = e.getActionCommand();
if (cmd.equals("Details")) {
showDetails();
} else if (!cmd.startsWith("http:") && ! cmd.startsWith("mailto:")) {
try {
desk.open(new File(cmd)); // otwarcie aplikacji skojarzonej z plikiem
} catch (IOException exc) {
exc.printStackTrace();
}
} else try { // przeglądarka lub poczta
URI uri = new URI(cmd);
if (cmd.startsWith("http:"))
desk.browse(new URI(cmd)); // przeglądarka z podanym adresem
else desk.mail(uri); // klient pocztowego z podanym mailto:
} catch (IOException exc) {
exc.printStackTrace();
} catch (URISyntaxException exc) {
exc.printStackTrace();
}
}
private void showDetails() {
// ...
}
public static void main(String[] args) {
if (!Desktop.isDesktopSupported()) {
System.out.println("Aplikacja nie działa na tej platformie");
System.exit(1);
}
desk = Desktop.getDesktop();
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new HotelView();
}
});
}
}
Klasa Desktop w Javie 6 jest bardzo podobna do klasy Desktop z JDIC.
Może
najistotniejszą różnicą jest to, że w Javie 6 adresy internetowe (dla
przeglądarki i dla klienta pocztowego) traktuje się jednolicie jako
URI. W JDIC natomiast mamy URL dla przeglądarki i specjalny obiekt
klasy Message dla poczty. Na obiekcie klasy Message możemy wykonywać
operacje ustalania adresata, przedmiotu, treści itp. Jest to łatwiejsze
niż działanie z URI typu "mailto:", choć - być może - mniej ogólne.
Warto
podkreślić, że projekt JDIC ma dużo większe możliwości niż elementy
integracji z pulpitem zawarte w Javie 6. Należy do nich m.in.
operowanie na asocjacjach (typy danych - aplikacje). Szczególnie
istotna jest jednak zdolność do wbudowywania działania rodzimej
(domyślnej) przeglądarki w aplikacje Javy. Obiekt klasy WebBrowser,
który jako komponent może być umieszczony np. w panelu w
oknie aplikacji Javy, pokazuje wyniki działania przeglądarki.
Stwarza to możliwość budowania bardzo elastycznych wolnostojących
aplikacji, w prosty sposób korzystających z bogactwa Internetu.
7.2. Ikonki obszaru powiadomień (tray)
Poczynając od wersji 6 aplikacja Javy może dodawać ikonki do obszaru powiadamiania (SystemTray).
Ikonki reprezentowane są przez klasę TrayIcon, przy czym możemy:
- ustalić podpowiedź, która ma się pojawić po wskazaniu myszką ikonki,
- ustalić menu kontekstowe (otwierane prawym klawiszem myszki) dla ikonki,
- wyświetlać komunikaty (tzw. "dymki" - baloon messages) na ikonce,
- obsługiwać zdarzenie ActionPerformed (zwykle kliknięcie) na "dymku" komunikatu,
- obsługiwać zdarzenia myszki na ikonce.
Przykładowa
aplikacja wykorzystująca w/w możliwości symuluje przeszukiwanie dysku w
poszukiwaniu plików. Naturalnym jest uruchomienie takiej aplikacji w
postaci ikony w obszarze powiadomień (lewa ikonka na rys. a).
Rys. a. Aplikacja jako ikona w obszarze powiadomień Windows
Podpowiedź (tooltip) mówi o tym co to za aplikacja (rys. b)
Rys. b. Podpowiedź pokazuje postępu przeszukiwania
Tooltip się zmienia wraz z postępami przeszukiwania (rys. c).
Rys. c. Postępy przeszukiwania
Po zakończeniu szukania zostanie wyświetlony "balonowy" komunikat (rys. d).
Rys. d. Komunikat o zakończeniu przeszukiwania
Z menu kontekstowego ikony możemy wybrać opcje działania aplikacji (rys. e).
Rys. e. Menu kontekstowe ikonki
Program (wraz z komentarzami wyjaśniającymi jego elementy) przedstawia poniższy wydruk
public class TrayDemo1 implements ActionListener {
// Ikonka aplikacji
private TrayIcon ticon;
// Menu kontekstowe
private PopupMenu pm = new PopupMenu();
// Obszar powiadomień (tray)
private SystemTray tray = SystemTray.getSystemTray();
// Wątek przeszukiwania
private volatile Thread finderThread;
// Wynik przezukiwania
private String result;
// Zadanie wykonywane w wątku przeszukiwania
// (symulacja przeszukiwania dysku)
Runnable finder = new Runnable() {
String[] msg = { "Search finished.\nClick to view results.",
"Search canceled.\nClick to view partial results."
};
public void run() {
int mnr = 0, k;
for (k=0; k<1000; k+=100) { // tu winno być pzreszukiwanie
try { // symulujemy pętlą ze sleepem
Thread.sleep(1000);
} catch (InterruptedException exc) {
mnr = 1;
break;
}
ticon.setToolTip(k + " files searched."); // aktualne info
}
// Po zakończeniu przeszukiwania
result = (k-1) + " files found"; // wynik
ticon.setToolTip("Search files"); // przywracamy oryginalny tooltip
// Pokazujemy dymek z info, że szukanie zakończone
ticon.displayMessage("Finder", msg[mnr], TrayIcon.MessageType.INFO);
finderThread = null;
}
};
public TrayDemo1() {
// Ustalenie menu kontekstowego
String[] cmds = { "Suspend", "Cancel", "Start new search", "Exit" };
for (int i = 0; i < cmds.length; i++) {
MenuItem mi = new MenuItem(cmds[i]);
mi.addActionListener(this);
pm.add(mi);
}
// Stworzenie ikonki aplikacji
ticon = new TrayIcon(new ImageIcon("jsearch.gif").getImage(),
"Search files",
pm);
ticon.setImageAutoSize(true);
// Reakcja na kliknięcie w "balon"
ticon.addActionListener( new ActionListener() {
public void actionPerformed(ActionEvent e) {
JOptionPane.showMessageDialog(null, result);
}
});
// Dodanie ikonki do obszaru powiadomień
try {
tray.add(ticon);
} catch (AWTException exc) {
exc.printStackTrace();
}
// Start wątku przeszukiwania
finderThread = new Thread(finder);
finderThread.start();
}
// Obsługa wyborów z menu kontekstowego
public void actionPerformed(ActionEvent e) {
String cmd = e.getActionCommand();
if (cmd.equals("Exit")) System.exit(0);
else if (cmd.equals("Cancel")) {
if (finderThread != null) {
finderThread.interrupt(); // zakończenie wątku
finderThread = null;
}
}
else if (cmd.equals("Start new search")) {
if (finderThread == null) {
finderThread = new Thread(finder);
finderThread.start();
}
}
else {
System.out.println("Command not implemented.");
}
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new TrayDemo1(); }
});
}
}
Należy zwrócić uwagę na to, że menu kontekstowe w TrayIcon jest typu PopupMenu z pakietu java.awt.
Jest
to duża niedogodność, ponieważ PopupMenu ma całkowicie przestarzałą
konstrukcję. Nie umieścimy w nim ikon i nie dodamy wygodnych akcji
(obiektów klas implementujących interfejs Action). Swingowe JPopupMenu
już dawno zmieniło tę sytuację: opcje menu są pochodne od klasy
AbstractButton i mają wszystkie właściwości przycisków Swingu, do menu
można też bezpośrednio dodawać akcje.
Mniej więcej rok temu
Artem Ananiev przedstawił w swoim blogu proste rozwiązanie problemu:
nie należy ustalać PopupMenu dla TrayIcon, ale w obsłudze zdarzeń
myszki otworzyć swingowe JPopupMenu. Rozwiązanie okazało się
niewystarczające pod wieloma względami, w szczególności menu nie
znikało przy kliknięciu poza jego obszarem, co więcej menu i
jego elementy nie miały fokusu (brak możliwości działania z
klawiatury, np. przemieszczania zaznaczeń klawiszami kursora).
Tymczasem
projekt JDIC dostarcza nam właśnie JPopupMenu dla TrayIcon. Dlaczego
tego nie uwzględniono w Javie 6? Być może ograniczeniem było to, że
JPopupMenu w żaden sposób nie chce nakładać się na TaskBar z
ustawieniem "OnTop", a wobec tego zachowuje się nieco inaczej niż
rodzime "popupy" ikon.
Jeśli jednak ta różnica nam nie przeszkadza,
możemy używać JDIC i mieć prawdziwy JPopupMenu na ikonach obszaru
powiadamiania.
A czy można coś zrobić, pozostając przy standardzie
Javy 6 (który zresztą pod pewnymi względami jest lepszy od JDIC - np.
możliwość obsługi zdarzeń myszki na tray-ikonach)?
Kłopoty
"wczesnych" rozwiązań "JPopup na TrayIcon" wynikały z niewłaściwego
ustalenia komponentu na którym menu jest otwierane (właściwość
"invoker").
Otóż musi to być komponent widoczny (a zatem zawarty w
widocznym oknie najwyższego poziomu). Właściwym wyborem będzie JDialog.
Okno jest nam potrzebne tylko do uwidocznienia menu, dlatego nie
chcemy, aby jego ikonka był a widoczna na pasku zadań (stąd JDialog).
Ustalimy mu również rozmiary na (0, 0).
Nie można jednak ustalić
dialogu jako komponentu na którym pojawia się menu, ponieważ jest on
komponentem ciężkim i stracimy fokus. Naturalnym wyborem będzie
contenPane tego dialogu. Ponadto, przy uwidacznianiu dialogu należy go
przesunąć na pierwszy plan (toFront).
Wreszcie TrayIcon nie pochodzi
od klasy Component. Wobec tego - jeśli uwidacznianie menu kontekstowego
zwiążemy wyłącznie z obsługą dość wysokopoziomowego zdarzenia
mouseReleased czy mousePressed, to zdarzenia niższego poziomu (rodzime,
fizyczne zdarzenia myszki) będą docierać do BasicPopupUI
(delegata odpowiedzialnego za zachowanie menu kontekstowego) i
powodować powstanie wyjątku ClassCastException (z TrayIcon do
Component). Powierzchowne i szybkie rozwiązanie tego problemu polega na
przechwyceniu tego wyjątku. Jednak powstaje on w wątku obsługi zdarzeń
(EDT) i nasz kod nie może go normalnie obsłużyć. Wyjściem jest
ustanowienie handlera nieprzechwyconych wyjątków EDT, co w tej chwili
można zrobić ustalając właściwość systemową
"sun.awt.exception.handler".
Podsumowaniem tych rozważań jest poniższy listing, przedstawiający narzędziową klasę "TrayIcon z JPopupMenu".
package traypopup;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
public class TrayIconPopup extends TrayIcon {
// okno potrzebna dla fokusu (dialog, aby nie bylo widoczne na pasku)
private static JDialog frame;
private JPopupMenu menu = new JPopupMenu();
// Trochę wygodniejszy konstruktor
public TrayIconPopup(ImageIcon icon, String tooltip) {
this(icon.getImage(), tooltip);
}
// Standardowy konstruktor
public TrayIconPopup(Image image, String tooltip) {
super(image, tooltip);
// aby uniknąc od czasu do czasu pojawiającego się błędu z EDT
// spowodowanego zbyt wysokim poziomem (u nas) obsługi zdarzeń myszki
System.setProperty("sun.awt.exception.handler",
"traypopup.EdtUncaughtExcHandler");
configureMenu();
// Pokazuje menu kontekstowe z TrayIcom
addMouseListener(new MouseAdapter() {
@Override
public void mouseReleased(MouseEvent e) {
if (e.isPopupTrigger()) {
if (frame == null) {
frame = new JDialog((Frame)null);
frame.setAlwaysOnTop(true); // nie zwalczymy tym WinTaskBaru on top!
frame.setSize(0, 0);
}
frame.setVisible(true);
int shift = menu.getPreferredSize().width;
menu.show(frame.getContentPane(), e.getX() - shift, e.getY());
frame.toFront();
}
}
});
}
private void configureMenu() {
menu.setLightWeightPopupEnabled(false);
menu.addPopupMenuListener( new PopupMenuListener() {
public void popupMenuCanceled(PopupMenuEvent e) {}
public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
frame.setVisible(false);
frame.toBack();
}
public void popupMenuWillBecomeVisible(PopupMenuEvent e) { }
});
}
// Dodanie do menu kontekstowegp
public void addToPopup(JMenuItem mi) {
menu.add(mi);
}
// Dodanie do menu kontesktowegp
public void addToPopup(Action a) {
menu.add(a);
}
// Ustalenie innego menu kontekstowego
public void setPopup(JPopupMenu popup) {
menu = popup;
configureMenu();
}
// Zwraca JPopupMenu, możemy na nim działać dynamicznie
public JPopupMenu getPopup() {
return menu;
}
public void addToPopup(JSeparator separator) {
menu.add(separator);
}
}
Klasę handlera nieprzechwyconych wyjątków EDT pokazano niżej.
Handler nieprzechwyconych wyjątków EDT.
package traypopup;
public class EdtUncaughtExcHandler {
public void handle(Throwable t) {
if (t.toString()
.indexOf("TrayIconPopup cannot be cast to java.awt.Component") == -1) {
t.printStackTrace();
}
}
}
Możemy sprawdzić działanie tej klasy dla przypadku prostego przykładowego JPopupMenu (ryunek i listing poniżej(
Rys. TrayIcon z JPopupMenu
Test TrayIcon z JPopupMenu
package traypopup;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import com.jgoodies.looks.windows.*;
public class Test {
public static void main(String[] args) {
// Aby menu kontekstowe wyglądało ładnie uzyjemy JGoodies
try {
UIManager.setLookAndFeel(new WindowsLookAndFeel());
} catch (Exception e) {
e.printStackTrace();
}
ImageIcon[] icons = {
new ImageIcon("jsbook.gif"), new ImageIcon("jsearch.gif"),
new ImageIcon("middle.gif"), new ImageIcon("error.gif"),
};
TrayIconPopup ti =
new TrayIconPopup(icons[0], "Test JPopupMenu with TrayIcon");
for (int i=1; i< icons.length; i++) {
ti.addToPopup(new JMenuItem("Element " + i, icons[i]));
}
ti.addToPopup(new JSeparator());
ti.addToPopup(new JMenuItem("Wykonanie operacji x"));
ti.addToPopup(new AbstractAction("Quit") {
public void actionPerformed(ActionEvent e) {
System.exit(0);
}
});
SystemTray tray = SystemTray.getSystemTray();
try {
tray.add(ti);
} catch (AWTException exc) {
exc.printStackTrace();
}
JFrame f = new JFrame("Okno aplikacji");
f.add(new JButton("Może coś robi, jest jakieś GUI, itd. itp."));
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.setLocationRelativeTo(null);
f.pack();
f.setVisible(true);
}
}
8. Dlugotrwałe zadania i SwingWorker
Co się stanie, jeśli z wątku obsługi zdarzeń (ang. event dispatching thread, w skrócie EDT) uruchomimy jakieś długo wykonujące się zadanie?
Cóż, nasze GUI będzie zamrożone, niereaktywne, dopóki zadanie się nie skończy.
Przekonuje
o tym następujący programik, w którym metoda longTask() spędza ok.
5 sekund na jakichś obliczeniach (co symulujemy przez sleep(..)).
import java.awt.event.*;
import javax.swing.*;
public class LongTask extends JFrame {
LongTask() {
JButton b = new JButton(
"Uruchamiam długie zadanie. Kliknij i spróbuj coś wpisać w pole tekstowe");
final JTextField tf = new JTextField(40);
b.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
int wynik = longTask();
tf.setText("Wynik: " + wynik);
}
});
add(tf, "Center");
add(b, "South");
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
pack();
setLocationRelativeTo(null);
setVisible(true);
}
public int longTask() {
try {
Thread.sleep(5000);
} catch (InterruptedException exc) {
exc.printStackTrace();
}
return 100;
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new LongTask();
}
});
}
}
Zobacz działanie programu
Ważny wniosek.
Długotrwałe czynności nie mogą być wykonywane w wątku obsługi zdarzeń (EDT).
Należy takie czynności uruchamiać jako odrębne wątki.
Ale
robiąc to całkiem "ręcznie" natkniemy się na kilka problemów. Po
pierwsze, uzyskania wyniku działań (on przecież będzie dostępny
asynchronicznie, w przyszłości, nie wiemy kiedy). Po drugie,
komunikacji z GUI (zmiany w GUI - jak wiemy - zawsze muszą być
dokonywane w EDT).
Rozwiązaniem tych problemów jest zastosowanie klasy SwingWorker.
Klasę tę wykorzystujemy przez dziedziczenie.
Działania (te właśnie długotrwałe czynnośic) wykonywane w odrębnym od EDT wątku określamy w przedefiniowanej metodzie doInBackground(...).
Aby uruchomić zadanie stosujemy metodę execute() klasy SwingWorker.
Działania, które mają być wykonane po zakończeniu zadania określamy w przedefiniowanej metodzie done().
Jest ona wywoływana po zakończeniu "długotrwałych czynności" i
wykonywana w EDT, a więc z jej poziomu możemy swobodnie operować na GUI
(np. w celu prezentacji wyników).
Wynik zaś uzyskujemy za pomocą metody get()
z klasy SwingWorker (w metodzie done() zadanie jest zakończone, więc
get() nie blokuje wątku; inne jego użycie jest blokujące albo do
uzyskania wyniku albo przez określony jako argument czas).
Zobaczmy na zmodyfikowanym przykładzie "dlugiego zadania".
import java.awt.event.*;
import javax.swing.*;
public class SwingWorker1 extends JFrame {
private JTextField tf = new JTextField(40);
class Worker extends SwingWorker<Integer, Integer> {
@Override
protected Integer doInBackground() throws Exception { // działa poza EDT
return longTask();
}
@Override
protected void done() { // działa w EDT, wołane po zakończeniu zadania
try {
int wynik = get(); // uzyskanie wyniku
tf.setText( tf.getText() + " Wynik: " + wynik );
} catch (Exception exc) {
exc.printStackTrace();
}
}
}
SwingWorker1() {
JButton b = new JButton(
"Uruchamiam długie zadanie. Teraz GUI będzie responsywne");
b.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
new Worker().execute();
}
});
add(tf, "Center");
add(b, "South");
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
pack();
setLocationRelativeTo(null);
setVisible(true);
}
public int longTask() {
try {
Thread.sleep(5000);
} catch (InterruptedException exc) {
exc.printStackTrace();
}
return 100;
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new SwingWorker1();
}
});
}
}
Uwagi:
- SwingWorker
ma dwa parametry typu: pierwszy określa typ zwracany przez
doInBackground i get, o drugim powiemy parę słów za chwilę,
- doInBackground zwraca wynik długiego zadania, ale uzyskujemy go poprzez get(...)
Zobacz działanie programu
No włanie, drugi parametr typu klasy
SwingWorker określa typ wyników posrednich. Często bowiem istnieje
potrzeba uzyskiwania wyników pośrednich (jeszcze przed zakończeniem
długo wykonujących się czynności), choćby po to, aby pokazywac w GUI
postępy zadania.
Wyniki pośrednie udastępniamy w metodzie doInBackground za pomocą wywołania publish(...). To się wykonuje poza EDT, ale wyniki pośrednie stają się dostępne dla metody process(List), która jest wykonywana w EDT.
W ten sposób na bieżąco możemy w GUI odzwierciedlać postępy wykonania zadania.
Obrazuje
to poniższy kod, w którym obliczenia składają się z kolejnych kroków
(ich liczbę podajemy przy tworzeniu instancji SwingWorkera) i
używamy w metodzie doInBackground, która podaną liczbę razy
wywołuje longTask(...) (tym razem co 2 sek. losująca kolejną liczbę z
przedziału 100-199).
import java.awt.event.*;
import java.util.*;
import javax.swing.*;
public class SwingWorker2 extends JFrame {
private JTextArea ta = new JTextArea(20, 20);
class Worker extends SwingWorker<List<Integer>, Integer> {
private int steps;
public Worker(int n) {
steps = n;
}
@Override
protected List<Integer> doInBackground() {
List<Integer> nums = new ArrayList<Integer>();
int wyn = 0;
for (int i=1; i <= steps; i++) {
wyn = longTask();
nums.add(wyn);
publish(wyn); // publikacja pośredniego wyniku - do wykorzystania przez process w edt
}
return nums;
}
// Metoda process() działa w EDT, a otrzymuje jako argument pośrednie wyniki od publish
// Może je więc pokazywać w GUI
@Override
protected void process(List<Integer> chunks) {
for (Integer n : chunks) {
ta.append("\n"+ n);
}
}
@Override
protected void done() {
List<Integer> nums = null;
try {
nums = get();
} catch (Exception exc) {
exc.printStackTrace();
}
String liczby = "";
for (Integer n : nums) {
liczby += n + " ";
}
ta.append("\nGotowe:\n" + liczby );
}
}
SwingWorker2() {
JButton b = new JButton(
"Uruchamiam długie zadanie. Teraz GUI będzie pokazywało wyniki pośrednie");
b.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
new Worker(10).execute();
}
});
add(new JScrollPane(ta), "Center");
add(b, "South");
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
pack();
setLocationRelativeTo(null);
setVisible(true);
}
private Random rand = new Random();
public int longTask() {
try {
Thread.sleep(2000);
} catch (InterruptedException exc) {
exc.printStackTrace();
}
return rand.nextInt(100) + 100;
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new SwingWorker2();
}
});
}
}
Zobacz działanie programu
Klasa SwingWorker ma
jeszcze sporo innych możliwości: np. przerywania zadań czy
asynchronicznego powiadamiania o postępach na zasadzie nasłuchu
zdarzeń. Dobre ich opanowanie wymaga jednak wiedzy o narzędziach
pakietu java.utilc.oncurrent oraz o nasłuchu zmian właściwości w modelu
JavaBeans. O tym będziemy mówić w niedalekiej przyszłości, w ramach
nieco bardziej zaawansowanego kursu.