Programowanie GUI: komponenty wizualne i kontenery
Ten wykład rozpoczyna cykl poświęcony programowaniu graficznych interfejsów
użytkownika. Jest to nie tylko ważna i ogólna dziedzina (niewiele jest współcześnie
aplikacji, które nie mają graficznych interfejsów użytkownika), ale również
jest to jedna z najważniejszych składowych środowiska Javy, a przy tym doskonały
przykład zastosowania koncepcji programowania obiektowego (od polimorfizmu
poczynając, poprzez delegowanie uprawnień, po architekturę "Model-View-Controller").
1. Ogólne reguły działania z komponentami GUI.
Standardowe
pakiety java.awt (AWT) oraz javax.swing (Swing) zawierają klasy
definiujące wiele różnorodnych komponentów wizualnej
interakcji programu z użytkownikiem (okna, przyciski, listy, menus,
tablice itp.). Są gotowe do wykorzystania w naszych programach.
Można sformułowac następujące reguły dzialania z komponentami GUI.
- Komponenty
tworzymy za pomocą wyrażenia new
wywołującego odpowiedni konstruktor klasy komponentu, któremu
podajemy argumenty, określające niektóre właściwości
komponentu (np. tekst/ikonę na przycisku).
- Komponenty
mają właściwości (np. kolory, pismo tekstu na przycisku),
które możemy ustalać (lub pobierać) za pomocą metod z
odpowiednich klas komponentów (metody setNNN(...),
getNNN(...), isNNN(...), gdzie NNN nazwa właściwości).
- Większość
właściwości komponentów jest reprezentowane przez obiekty
odpowiednich klas (np. pismo – klasa Font, kolor – klasa
Color)
- Komponenty,
które mogą zawierać inne komponenty nazywają się kontenerami.
- Do kontenerów
dodajemy inne komponenty (w tym inne kontenery)
- Z
każdym kontenerem związany jest określony zarządca rozkładu,
który określa układ komponentów w kontenerze i ich
zachowanie (zmiany rozmiarów i położenia) przy zmianie
rozmiarów kontenera. Inaczej: rozkład jest jedną z
właściwości kontenera.
- Zarządcy
rozkładu są
obiektami odpowiednich klas
- Dla
każdego kontenera możemy ustalić wybranego zarządcę
rozkładu
- Aplikacja
komunikuje się z użytkownikiem za pomocą okna lub
wielu okien
- Okna AWT
są kontenerami, do których dodajemy komponenty wizualnej
interakcji z użytkownikiem (w tym inne kontenery)
- Okna Swingu
zawierają tzw. contentPane, który jest
kontenerem do którego domyślnie dodajemy komponenty wizualnej
interakcji (w tym inne kontenery).
- Okna (w tym
"okno" apletu) są zawsze kontenerami najwyższego
poziomu w hierarchii zawierania się komponentów
- Współdziałanie
użytkownika z aplikacją odbywa się na zasadzie obsługi zdarzeń
(np. zdarzenia kliknięcia w przycisk). Obsługą zdarzeń zarządza
specjalny, równolegle z naszym programem wykonujący się kod w
JVM – wątek obsługi zdarzeń. O obsłudze zdarzeń – w
następnym wykładzie.
Przykładowa aplikacja Swing (pokazuje trzy przyciski z obrazkami i tekstem) ilustruje większość z podanych wyżej zasad..
package intro;
import java.awt.*;
import javax.swing.*;
class Intro {
public static void main(String[] args) {
// ta klasa (potrzebne do niezależnego pobierania zasobów, takich jak obrazki)
Class<Intro> klas = Intro.class;
// Ikony z plików graficznych zawartych w katalogu images aplikacji
Icon[] icon = { new ImageIcon(klas.getResource("images/ocean.jpg")),
new ImageIcon(klas.getResource("images/pool.jpg")),
new ImageIcon(klas.getResource("images/town.jpg")),
};
String[] opis = { "Ocean", "Pool", "Town" }; // tekst na przyciskach
JFrame frame = new JFrame("First Swing"); // utworzenie okna ramowego z podanym tytułem
frame.setLayout(new FlowLayout()); // ustalenie rozkładu jego contentPane
for (int i=0; i<icon.length; i++) {
// tworzenie kolejnych przycisków
JButton b = new JButton(opis[i], icon[i]);
// Ustalenie pisma i koloru napisu na przyciskach
b.setFont( new Font("Dialog", Font.BOLD | Font.ITALIC, 18));
b.setForeground(Color.blue);
// Ustalenie pozycji tekstu na przycisku względem ikony
b.setVerticalTextPosition(SwingConstants.BOTTOM);
b.setHorizontalTextPosition(SwingConstants.CENTER);
frame.add(b); // dodanie przycisku do contentPane okna
}
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
frame.pack(); // spakowanie okna
// (wymiary okna takie by dokładnie zmieścić komponenty)
frame.setLocationRelativeTo(null); // centrowanie okna w obszarze pulpitu
frame.setVisible(true); // pokazanie okna
}
}
Zobacz działanie programu
2. Komponenty AWT a komponenty Swingu
AWT (Abstract Windowing
Toolkit) – obecny w Javie od samego początku - jest zestawem klas, definiujących
proste komponenty wizualnej interakcji.
Problemy AWT:
- ubogie możliwości graficzne i interakcyjne komponentów,
- brak komponentów istotnych dla oprogramowania nowoczesnych GUI (np. tabel)
- zależny od platformy systemowej wygląd komponentów
Odpowiedzią na te problemy oraz ich rozwiązaniem był projekt Swing (ogólniej:
Java Foundation Classes - JFC), początkowo występujący jako dodatek do JDK
1.1.8, a później włączony w sklad Java 2 Platform.
Pakiet Swing (javax.swing i podpakiety) zawiera dużo więcej niż AWT komponentów
- nowych wobec AWT oraz mających rozbudowane właściwości odpowiedników AWT.
Wszystkie komponenty Swingu oprócz kontenerów znajdujących się najwyżej w
hierarchii zawierania się komponentów (kontenerów najwyższego poziomu) są
komponentami lekkimi. W przeciwieństwie – gotowe komponenty AWT są komponentami ciężkimi.
Komponenty ciężkie są realizowane poprzez użycie graficznych bibliotek GUI systemu operacyjnego.
Komponenty lekkie są natomiast rysowane za pomocą kodu Javy w obszarze
jakiegoś komponentu ciężkiego znajdującego się wyżej w hierarchii zawierania
się komponentów (zwykle jest to kontener najwyższego poziomu).
Z tego wynika, że – w przeciwieństwie do komponentów ciężkich:
- komponenty lekkie mogą być przezroczyste, a zatem mogą przybierać wizualnie dowolne kształty
- komponenty lekkie mają wygląd niezależny od platformy.
Lekkie komponenty Swingu spełniają oba te warunki, a architektura klas Swingu pozwala wybierać wygląd jego lekkich komponentów (pluggable look and feel).
Uwaga: możliwe jest umieszczanie w jednym kontenerze komponentów lekkich
(np. Swingu) i ciężkich (AWT), jednak jest to nie polecane i obarczone pewnymi
restrykcjami, dotyczącymi m.in. porządku nakładania się komponentów po osi
Z (Z-order). M.in. z tego ostatniego względu architektura okien Swingowych
jest złożona (o czym dalej) i standardowo powinniśmy dodawać komponenty Swingu
do contentPane okna ramowego Swingu (JFrame).
Podstawową hierarchię klas komponentów GUI przedstawia rysunek.
Wnioski, które wynikają z tej hierarchii :
podstawowe
właściwości wszystkich komponentów (i AWT i Swingu, i
komponentów terminalnych i kontenerów) określa klasa
Component z pakietu java.awt; w klasie tej znajdziemy mnóstwo
użytecznych metod do ustalania i pobierania właściwości dla każdego
z możliwych komponentów,
podstawowe
właściwości i użyteczne metody dla wszystkich kontenerów
określa klasa Container,
szczególne
dla lekkich komponentów Swingu, ale wspólne dla nich
wszystkich, właściwości określa klasa JComponent
specyficzne
właściwości i funkcjonalność poszczególnych rodzajów
komponentów zdefiniowane są w klasach tych komponentów
3. Krótki przegląd komponentów Swingu
Dla efektywmnego posługiwania się komponentami wizualnymi użyteczna jest
znajomość ich hierarchii dziedziczenia. Poniższe rysunki przedstawiając
hierarchie klas komponentów Swingu..
Komponenty AWT są proste w użyciu, ale ubogie. Swing dostarcza wielu nowych komponentów, których nie było w AWT. Natomiast
te komponenty, które były w AWT obecne (np. przyciski, czy listy) zyskały
nowe, rozbudowane możliwości, czasem bardzo różniące je od "starych" komponentów.
W
tej chwili mało kto posluguje się komponentami AWT, ale być może
informacja o tym jakie prostsze odpowiedniki komponentów Swingu są w
AWT dostępne komuś się przyda. Stąd dwa rysunki.
Rys. Komponenty Swingu o rozbudowanych możliwosciach w stosunku do AWT (AWT ma swoje odpowiedniki)
Żródło: Magellan Institute Swing Short Course.
Rysunek . Hierarchia klas komponentów Swingu, nie mających odpowiedników w AWT
Żródło: Magellan Institute Swing Short Course.
W poniższej tabeli przedstawiono przegląd komponentow Swingu z krótkimi
komentarzami co od ich właściwości. Konkretne kompoenenty będziemy
poznawać
dokladnie w toku dalszego wykladu. Warto też zaznaczyć, że ilustrcaje
mają charakter schematyczny (tak naprawdę komponenty wyglądają dużo
lepiej, szczególnie w dobrze dobranym obieralnym wyglądzie - np.
Nimbus, lub wyglądy JGoodies))
Komponenty Swingu |
|
Przyciski: klasy JButton, JToggleButton, JCheckBox, JRadioButton
Możliwości:
- tekst i/lub ikona na przycisku z dowolnym pozycjonowaniem
- różne ikony dla różnych stanów (wciśnięty, kursor myszki nad przyciskiem etc)
- ustalanie tekstu w HTML
- programistyczne symulowanie kliknięć (metoda doClick())
- ustalanie mnemoniki (metoda setMnemonic())
|
|
Etykieta: klasa JLabel
Możliwości:
- tekst i/lub ikona z dowolnym pozycjonowaniem
- tekst HTML
- ustalenie mnemoniki i związanie jej z innym komponentem np. polem edycyjnym
(wciśnięcie alt-mnemonika powoduje przejście fokusu do danego komponentu
np. pola edycyjnego)
|
|
Menu rozwijalne: klasy JMenu, JMenuItem, JCheckBoxMenuItem, JRadioMenuItem.
Ponieważ pochodzą od AbstractButton - wszystkie właściwości przycisków!
Menu kontekstowe: klasa JPopupMenu |
|
Suwak: klasa JSlider
Ustalanie wartości za pomocą suwaka. W pełni konfigurowalny, jako etykiety może zawierać ikony. |
|
Dialog wyboru koloru: JColorChooser
łatwy w użyciu w wersji standardowej. W pełni konfigurowalny - możliwość
tworzenia wersji niestandardowych i wbudowywania ich w inne kompoennetu GUI.
Inny dialog wyboru - JFilaChooser - wybór plików. |
|
Pole edycyjne: JTextField
do wprowadzania hasła: JPasswordField
weryfikacja tekstu: za pomocą dziedziczenia klasy abstrakcyjnej InputVerifier i zdefiniowania metody verify(Component).
W JDK 1.4 - nowa klasa JFormattedTextField - z wbudowaną weryfikacją tekstu: |
|
Wielowierszowe pole edycyjne: JTextArea
Uwaga: wszystkie komponenty tekstowe pochodzą od klasy JTextComponent,
która zapewnia bardzo elastyczne możliwości tworzenia różnych edytorów. Komponenty
tekstowe są bardzo rozbudowane, jesli chodzi o architekturę. Procesory dokumentów:. JEditorPane i JTextPane |
|
Lista: klasa JList
- oparta na współpracy modelu danych listy z widokiem tych danych
- elementy: teksty i/lub obrazki, a nawet inne komponenty GUI (wygląd)
- rózne elementy listy mogą mieć różny wygląd (kolor, pismo, obrazek lub nie etc).
|
|
Lista rozwijalna: JComboBox
oszczędność miejsca w GUI
te same właściwości co lista + możliwość przechodzenia do elementu tekstowego
po wciśnięciu pierwszej litery napisu, który on reprezentuje |
|
Tabela: klasa JTable
Ogromne możliwości konfiguracyjne, przestawianie kolumn (myszką i programistycznie),
różny wygląd kolumn (teksty, obrazki, komponenty interakcyjne), sortowanie
wierszy, wielozaznaczanie (po wierszach i po kolumnach) |
|
Drzewo: klasa JTree
Reprezentowanie hierarchii. Węzły drzewa mają te same właściwości co
elementy tabeli (tzn. mogą być reprezentowane w postaci napisów i/lub ikon
oraz innych komponentów) |
Lekkie i wyspecjalizowane kontenery Swingu
|
|
Panel: klasa JPanel
służy do grupowania komponentów |
|
Panel dzielony: klasa JSplitPane
podział kontenera na dwie części (poziomo lub pionowo) z możliwością
przesuwania belki podziału dla ustalenia rozmiarów widoków części |
|
Panel zakładkowy: JTabbedPane
zakładki służą do wybierania komponentów, które mają być uwidocznione w panelu |
|
Panel przewijany: JScrollPane
służy do pokazywania komponentów, które nie mieszczą się w polu widoku;
suwaki umożliwiają "przewijanie" widoku komponentu, tak, by odsłaniać kolejne
jego fragmenty.
JTextArea i JList powinny być umieszczane w JScrollPane, jeśli ich zawartość
(wiersze tekstu, elementy listy) może się powiększać. |
|
Pasek narzędzi: JToolBar |
Źródło rysunków: Swing Connection, Sun.
Uwaga: okna (w tym wewnętrzne) zostaną omówione oddzielnie.
Poniższy rysunek przedstawia zrzut ekranu programu demonstracyjnego SwinSet3.
Źródło: SwingSet3, Sun Microsystem 2008
Zobacz działanie programu na żywo.
4. Wspólne właściwości komponentów (AWT i Swing)
Wszystkie
komponenty wywodzą się z abstrakcyjnej klasy Component, która
definiuje metody, m.in. ustalające właściwości komponentów.
Mamy dwa rodzaje komponentów: komponenty-kontenery (takie,
które mogą zawierać inne komponenty) oraz komponenty
terminalne (nie mogą zawierać innych komponentów).
Właściwości
mogą być pobierane za pomocą metod getNNN()
lub (dla właściwości zero-jedynkowych, typu boolean) isNNN()
i ustalane (jeśli to możliwe) za pomocą metod setNNN(...).,
gdzie NNN – nazwa
własciwości.
Do
najogólniejszych właściwosci wszystkich komponentów
należą.
Właściwość | Komentarz |
rozmiar (Size)
|
ustalanie i
pobieranie tych właściwości ma ograniczone zastosowanie (zob.
dalej)
|
szerokość
(Width) |
wysokość
(Height) |
położenie
(Location) |
rozmiar i
położenie (Bounds) |
minimalny rozmiar (MinimumSize) |
Właściwości
ważne dla niektórych zarządców rozkładu (zob.
dalej). Mogą być ustalane tylko dla komponentów Swingu. W
AWT – ustalanie poprzez zdefiniowanie metod get... w klasie
dziedziczącej klasę komponentu standardowego.
|
preferowany rozmiar (PreferredSize) |
mksymalny rozmiar (MaximumSize) |
wyrównanie po osi X (AlignmentX) | określa
położenie komponentów wobec innych. Uwagi j.w.
|
wyrównanie po osi Y (AlignmentY) |
pismo (Font) | pismo jest obiektem klasy Font |
kolor tła
(Background) | kolor jest obiektem
klasy Color kolor tła
ma znaczenie tylko dla nieprzezroczystych komponentow
|
kolor pierwszego planu (Foreground) |
rodzic (Parent) | kontener, który
zawiera dany komponent. Właściwość
tylko do odczytu. |
nazwa (Name) | Komponenty
otrzymują domyślne nazwy. Można je zmieniać. |
Właściwości
typu 0-1 |
widzialność
(Visible) | czy
widoczny? można zmieniać |
lekkość
(LigthWeight) | czy komponent lekki? |
przezrosczystość
(Opaque) | zmiany tylko dla komponentów Swingu |
dostępność
(Enabled). | czy możliwa
interakcja z komponentem? |
Rozmiary
i położenie komponentów nie są znane dopóki
komponenty nie zostaną zrealizowane (uwidocznione lub okno w którym
się znajdują nie zostanie spakowane).
Położenie
komponentu określane jest przez współrzędne (x,y), a punkt
(0,0) oznacza lewy górny róg obszaru w którym
znajduje się komponent (jest to ten obszaru kontenera, do którego
mogą być dodane komponenty, a więc np. za wyłączeniem paska menu w
oknie).
Zmiana położenia i rozmiarów za pomocą metod set
ma w zasadzie sens tylko dla komponentów znajdujących się w
kontenerach bez zarządcy rozkładu (o zarządcach rozkładu powiemy za
chwilę) lub dla komponentów-okien.
Aby
ustalić rozmiar okna frame piszemy np.:
frame.setSize(200,
200);
Szczególna
metoda w klasie Window (dziedziczonej przez Frame i JFrame) –
pack() pozwala ustalić rozmiary okna, tak by były dokladnie takie (i
nie większe) żeby zmieścić wszystkie znajdujące się w nim komponenty:
frame.pack();
Pismo
jest obiektem klasy Font, tworzonym za pomocą konstruktora
Font(nazwa_pisma, styl, rozmiar)
gdzie:
nazwa_pisma - jest łańcuchem znakowym, określającym rodzaj pisma
(np. "Dialog")
styl
- jest jedną ze stałych statycznych typu int z klasy Font:
Font.BOLD
Font.ITALIC
Font.PLAIN
(kombinacje uzyskujemy poprzez sumę logiczną np. Font.BOLD |
Font.ITALIC)
rozmiar
- liczba całkowita określająca rozmiar pisma w punktach.
Podstawowe, logiczne, nazwy pisma to: Serif, SansSerif, Dialog i MonoSpaced.
Zatem,
aby np. dla przycisku b ustalić pismo, piszemy
JButton b = new
JButton("Tekst na przycisku");
b.setFont(new
Font("Dialog", Font.PLAIN, 14);
Kolor
jest obiektem klasy Color, która ma kilka konstruktorów
oraz udostępnia stałe statyczne typu Color z predefiniowanymi
kolorami, np. Color.red, Color.blue, Color.white...
Kolory
przycisku możemy więc ustalić za pomocą takich konstrukcji:
b.setBackground(Color.blue);
b.setForeground(Color.white);
albo:
int r, g,
b;
r = 200; g = 200; b = 255;
b.setBackground(new
Color(r,g,b));
Zablokowanie/odblokowanie
komponentu
Komponenty
gotowe do interakcji są odblokowane (enabled). Zablokowanie
komponentu uniemożliwia interakcję z nim.
Np. jeśli b jest
przyciskiem
b.setEnabled(false);
// zablokowanie; kliknięcia w przycisk nie będą "przyjmowane"
//
odblokowanie:
if
(!b.isEnabled()) b.setEnabled(true);
Uwidacznianie
komponentów
Wszystkie
komponenty, oprócz tych wywodzących się z klasy Window, są
domyślnie widoczne. Pojawiają się one na ekranie, gdy zostały dodane
do jakiegoś kontenera i kontener jest/został uwidoczniony.
W
trakcie działanie programu można czynić komponenty niewidocznymi i
przywracać ich widzialność, np:
JButton
b = new JButton(...);
....
b.setVisible(false); // stanie się
niewidoczny
...
if (!b.isVisible()) b.setVisible(true);
// gdy niewidoczny, uwidaczniamy
Przezroczystość
komponentów
Wszystkie
komponenty AWT (jako ciężkie) są nieprzezroczyste (isOpaque() zwróci
true).
Komponenty
lekkie mogą być przezroczyste lub nie.
Domyślnie
większość lekkich komponentów Swingu jest
nieprzezroczysta.
Wyjątkiem
jest etykieta JLabel.
Zatem
aby ustalić tło etykiety Swingu musimy napisać:
JLabel
l = new JLabel("Jakaś etykieta");
l.setOpaque(true);
l.setBackground(Color.yellow);
5. Własne komponenty i rysowanie
Warto zastanowić się nad tym, w jaki sposób komponenty pojawiają się na ekranie?
Otóż odpowiada za to metoda paint. To ona "maluje"
komponenty na ekranie (mówiąc ściślej - rysowanie jest odelegowane
przez tę metodę do odpowiednich klas odpowiedzialnych za pluggable look
and feel),
Klasa każdego komponentu zawiera definicję metody public void paint(Graphics).
Metoda ta jest zdefiniowana w klasie Component (od której pochodzą wszystkie
komponenty). W klasach konkretnych komponentów AWT jest ona przedefiniowana.
W przypadku komponentów Swingu - metoda jest przedefiniowana w klasie JComponent,
a z jej wnętrza wywoływane są polimorficznie inne metody, odpowiedzialne
za rysowanie
Metoda paint(..) jest wywoływana przez JVM (na zasadzie callback, czyli "jestem i czekam, aż ktoś mnie wywoła" ) zawsze wtedy, gdy graficzny kontekst komponentu wymaga odświeżenia tj:
- komponent staje sie widoczny na ekranie,
- zmieniają się rozmiary komponentu,
- coś innego zasłoniło komponent, a potem został odsłonięty.
W metodzie paint(...) dostarczany jest kod, który powoduje wyrysowanie komponentu.
Ten kod będzie wywołany przez system - gdy trzeba odświeżyć komponent.
Wykreślanie ciężkich i lekkich komponentów różni się nie tylko pod względem
odwołań do natywnego systemu graficznego (ciężkie się odwołują, lekkie –
rysują bezpośrednio w obszarze "pożyczonym" od ciężkiego kontenera z wyższego
poziomu hierarchii ), ale również gdy chodzi o wewnętrzne mechanizmy wykreślania
(o czym więcej w przyszłym semestrze w wykładzie o zaawansowanej grafice).
Lekkie komponenty Swingu definiują, oprócz metody paint(...), trzy inne metody:
protected void paintComponent(Graphics g) // wykreśla sam komponent
protected void paintBorder(Graphics
g) // wykreśla
ramkę komponentu (jeśli jest)
protected void paintChildren(Graphics g) // wykreśla hierarchię zawartych komponentów
Są one wywoływane "z wnętrza" metody paint().
Ostatnia z trzech metod wymaga komentarz: jak można zauważyć ze schematu
dziedziczenia klas, wszystkie lekkie komponenty Swingu są kontenerami. Dlatego
dla każdego definiowana jest metoda paintChildren(...), wykreślająca komponenty
zawarte w odrysowywanym komponencie.
Metody paint(...) nie wolno wołać z poziomu aplikacji.
Jeśli istnieje konieczność odrysowania komponentu przez aplikację, to należy użyć metody repaint(...).
Opisany skrótowo mechanizm pozwala na wykonywanie własnych rysunków na gotowych komponentach.
Aby przedefiniować sposób wykreślania komponentów dziedziczymy ich klasy i przedefiniowujemy:
- w AWT metodę public void paint(Graphics),
- a w Swingu – metodę public void paintComponent(Graphics).
Ten sposób jest użyteczny szczególnie w odniesieniu do "upiększania" gotowych
komponentów AWT. Chociaż możemy go zastosować wobec komponentów Swingu, to
istnieją lepsze podejścia związane z wykorzystaniem mechanizmów pluggable
lookk & feel (o czym parę słów powiemy dalszym toku wykładów).
W istocie, dziedziczac klasy komponentów i przedefiniowując metody paint... tworzymy własne komponenty.
Te własne komponenty możemy budować całkiem od podstaw (wykorzystując "minimalne"
komponenty, np. JComponent lub JPanel), albo też możemy skorzystać z gotowej
funkcjonalności bardziej rozbudowanych komponentów (np. JButton), dziedzicząc
ich klasy.
Niewątpliwie najczęściej będziemy wykorzystywać opisany sposób do prezentowania
jakichś rysunków. Dlatego musimy wiedzieć w jaki sposób rysować podstawowe
kształty, napisy oraz wykreślać obrazy z plików graficznych.
Kluczem jest tu klasa Graphics. Referencja do obiektu tej klasy stanowi parametr metod paint...
Obiekt ten określa kontekst graficzny komponentu. Cóż to takiego?
Kontekst graficzny jest swoistym logicznym "urządzeniem
wyjściowym". Zwykle jest to ekran komputera, ale może to być np. wydruk
lub bufor w pamięci. Logiczny kontekst graficzny może więc być związany z
różnymi "urządzeniami wyjściowymi" na których "rysowany" jest komponent.
W klasie Graphics zdefiniowano wiele metod umożliwiających m.in.:
- uzyskiwanie i ustalanie właściwości kontekstu graficznego,
- rysowanie linii i figur,
- wypisywanie tekstów,
- rysowanie obrazów (obiektów klasy Image).
I właśnie dzięki temu mamy pełną kontrolę nad tym co ma być rysowane w obszarze naszego komponentu.
M.in. dostępne są następujące metody (proszę przejrzeć zestaw metod klasy Graphics w dokumentacji):
void | drawLine(int x1,
int y1,
int x2,
int y2)
Rysuje linię prostą pomiędzy punktami
(x1, y1) i (x2, y2) |
void | drawOval(int x,
int y,
int width,
int height)
Rysuje okrąg (elipsę). |
void | drawRect(int x,
int y,
int width,
int height)
Rysuje prostokąt |
Analogiczne metody fill... pozwalają na rysowanie wypełnionych kształtów (figur).
Rysowanie i wypełnianie odbywa się bieżącym kolorem kontekstu graficznego,
który jest domyślnie kolorem pierwszego planu (Foreground) komponentu, ale
może być dla potrzeb każdego rysunku zmieniany za pomocą metody setColor(Color)
z klasy Graphics. Metoda getColor() zwraca bieżący (ustalony) kolor.
W metodach rysowania, wypisywania, malowania obrazów posługujemy się współrzędnymi.
Przy rysowaniu figur należy pamiętać o tym, że górny lewy róg komponentu
ma współrzędne (0, 0). Zwiększanie wspólrzędnych następuje w prawo i w dół.
Współrzędne określają punkty POMIĘDZY odpowiednimi pikselami "urządzenia wyjścia".
Przy rysowaniu kształtów trzeba rozumować tak:
- rysowanie jest realizowane przez pióro, które zostawia ślad o szerokości i wysokości jednego piksela,
- pióro zostawia ślad PONIŻEJ i Z PRAWEJ strony ścieżki współrzędnych określonych w danej operacji rysowania.
Zatem zrobienie ramki wokół komponentu wygląda tak:
public void paintComponent(Graphics g) {
g.drawRect(0, 0, getWidth()-1, getHeight()-1);
}
Nieco inaczej wygląda sytuacja przy wypełnianiu figur:
public void paint(Graphics g) {
g.fillRect(0,0,getWidth(),getHeight());
}
Tutaj wypełniane jest WNĘTRZE ścieżki po której idzie pióro wyimaginowanego plotera.
Zobaczmy pierwszy przykład - proste "upiększenie" przycisku małymi czerwonymi
kwadracikami umieszczonymi w jego rogach (zob. rysunek)
Aby to osiągnąć, odziedziczymy klasę JButton i przedfiniujemy w niej metodę
paintComponent. W tej metodzie wyrysujemy narożne kwadraciki. Uzyskamy w
ten sposób w pełni funkcjonalny przycisk (mający wszystkie cechy JButton)
z dodatkiem (raczej ilustracyjnym, bo jego celowość jest mało sensowna) w
postaci czerwonych "narożników".
import javax.swing.*;
import java.awt.*;
class CustButton extends JButton {
public CustButton(String txt) {
super(txt);
setFont(new Font("Dialog", Font.PLAIN, 24));
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
int w = getWidth(); // aktualna szerokość ...
int h = getHeight(); // i wysokość komponentu
g.setColor(Color.red); // ustalenie koloru rysunku
// rysowanie kwadracików
g.fillRect(0, 0, 10, 10);
g.fillRect(w-10, 0, 10, 10);
g.fillRect(0, h-10, 10, 10);
g.fillRect(w-10, h-10, 10, 10);
}
}
public class ButtonTest extends JFrame {
public ButtonTest() {
CustButton mb = new CustButton("To jest przycisk");
add(mb);
setDefaultCloseOperation(EXIT_ON_CLOSE); // domyślna operacja zamknięcia okna: koniec pracy
pack();
setLocationRelativeTo(null);
setVisible(true);
}
public static void main(String args[]) {
new ButtonTest();
}
}
Zwróćmy uwagę na ważną kwestię.
Aby zagwarantować odpowiedni wygląd i funkcjonalność gotowych komponentów,
których klasy dziedziczymy, w przedefiniowywanej metodzie paintComponent należy
na początku wywołać metodę paintComponent z nadklasy
Budując własne komponenty calkowicie od podstaw (np. obszary do
rysowania), wykorzystujemy zwykle możliwie "minimalne" klasy.
Budowanie komponentów wizualnych od podstaw
|
Komponenty terminalne
|
Komponenty-kontenery
|
Ciężkie
komponenty
AWT
|
class NewComp extends Canvas {
...
}
|
class NewComp extends Panel {
...
}
|
Lekkie
komponenty
AWT
|
class NewComp extends Component {
...
}
|
class NewComp extends Container {
...
}
|
Lekkie
komponenty Swingu
|
class NewComp extends JComponent {
...
}
|
class NewComp extends JPanel {
...
}
|
Przy budowaniu "od podstaw" ważne jest nie tylko przedefiniowanie metody
paint, ale również metod określających minimalne, maksymalne i preferowane
rozmiary komponentów. W przeciwnym razie nasze komponenty mogą być niewidoczne
(klasy "minimalne", takie jak Canvas czy JComponent, dają komponenty o zerowych
rozmiarach).
Przykład: stworzyć rysunek siatki niebieskich linii (zob. rysunek). Obszarem
rysowania będzie własny, zbudowany od podstaw, komponent dziedziczący klasę
JComponent.
Musieliśmy zatem ustalić (różne od zera) jego minimalne, maksymalne i prefreowane rozmiary. :
setMinimumSize(Dimension d);
setPrefferedSize(Dimension d);
setMaximumSize(Dimension d);
Uwaga: obiekt klasy Dimension, opisuje
wymiary - szerokość i wysokość.
Obiekt taki możemy stworzyć za pomocą konstruktora Dimension(szerokośc,
wysokośc).
import java.awt.*;
import javax.swing.*;
class ObszarRysunku extends JComponent {
public ObszarRysunku(int w, int h) {
Dimension d = new Dimension(w, h);
setMinimumSize(d);
setPreferredSize(new Dimension(d));
setMaximumSize(d);
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
int w = getWidth();
int h = getHeight();
g.setColor(Color.blue);
g.drawRect(0,0,w-1,h-1);
int hor = 10, vert = 10;
while (hor < h) {
g.drawLine(1, hor, w-1, hor);
hor += 10;
}
while (vert < w) {
g.drawLine(vert, 1 , vert, h-1);
vert += 10;
}
}
}
public class Siatka extends JFrame {
public Siatka() {
super("Siatka");
add(new ObszarRysunku(200, 100));
setDefaultCloseOperation(EXIT_ON_CLOSE);
pack();
setLocationRelativeTo(null);
setVisible(true);
}
public static void main(String[] args) {
new Siatka();
}
}
Czasami na rysunkach będziemy potrzebowali napisów.
Zwykle uzyskujemy je za pomocą metody void drawString(String s, int x, int y)
Lokalizacja tekstu (x, y) - INACZEJ NIŻ PRZY RYSOWANIU KSZTAŁTÓW i FIGUR
- oznacza POŁOŻENIE LINII BAZOWEJ TEKSTU (base line). Różnicę obrazuje rysunek.
Aby dobrze rozplanować położenie tekstu należy uzyskać charakterystyki pisma
i wykorzystać je przy układaniu tekstu. Służy temu klasa FontMetrics np.
FontMetrics fm;
fm = g.getFontMetrics() // zwraca metrykę dla bieżącego pisma kontekstu g
W klasie FontMetrics mamy np. metodę
stringWidth(String s)
która zwraca szerokość zajmowaną przez napis s wyrażony w konkretnym piśmie (tej metryce).
Można ją wykorzystać np. do wycentrowania napisu w poziomie:
public void paintComponent(Graphics g) {
String s = "jakis tekst";
int y = ... ; // położenie w pionie
int w = getWidth();
int h = getHeight();
g.drawString(s, (w - g.getFontMetrics().stringWidth(s))/2, y);
...
}
Inne metody klasy FontMetrics pozwalają na rozmieszczanie napisów w pionie.
Mamy tu takie metody jak:
- getHeight(),
- getAscent(),
- getDescent(),
- getLeading()
Ich znaczenie pokazuje rysunek (źródło: Java Tutorial).
Za pomocą metod klasy Graphics możemy także wykreślać obrazy z plików graficznych (typów JPEG, GIF i PNG).
Mówiąc ściślej, dostępne są metody wykreślania obrazów, które są obiektami klasy Image.
Aby uzyskać obiekt Image, reprezentujący obraz z pliku, możemy zastosować
dwa podejścia: użyć klasy ImageIcon (o czym w następnym wykładzie, przy okazji
omawiania interfejsu Icon) albo użyć metody getImage() z klasy Toolkit.
Głównym zadaniem klasy Toolkit jest zapewnienie współpracy pomiędzy komponentami
AWT, a środkami platformy systemowej, szczególnie komponantami realizowanymi
przez graficzne API systemu. Niewiele z metod tej klasy można i należy wykorzystywać
w programach użytkowych. Do tych niewielu należy np. metoda pobierania infrmacji
o rozdzielczości ekranu oraz - właśnie - metoda getImage(), stosowana do
uzsyakania obrazu z pliku graficznego.
Aby użyć metody getImage() musimy najpierw uzyskać obiekt klasy Toolkit.
Można to zrobić m.in. za pomocą statycznej metody getDefaultToolkit() z klasy
Toolkit.
Dostęp do obrazu z pliku możemy uzyskać za pomocą odwołania:
Image img = Toolkit.getDefaultToolkit().getImage(nazwa_pliku);
Wykreślaniem obrazu w obszarze komponentu wizualnego zajmuje się metoda drawImage z klasy Graphics
Uzyskanie odpowiedniej referencji do obrazu z pliku za pomocą metody getImage() nie powoduje załadowania obrazka z pliku
Ładowanie następuje w trakcie wyświetlania przez metodę drawImage. Zwraca
ona sterowanie, gdy tylko część obrazu zostanie zaladowana i może być wyświetlona.
Metoda paint wołana jest przez system ponownie i ponownie, dopóki caly obraz
nie zostanie załadowany i wyświetlony. Takie ładowanie (i wyświetlanie)
etapami - przy dużych obrazach - może trwać długo (za każdym razem odrysowywany
jest od nowa cały komponent wizualny).
Dlatego w Javie zapewniono dwa sposoby, umożliwiające załadowanie obrazu przed wyświetleniem.
Pierwszy polega na dostarczeniu obiektu typu ImageObsrever. Referencja
do tego obiektu jest ostatnim argumentem przekazywanym metodzie drawImage,
a sam obiekt może zajmować się szczegółowym śledzeniem postępów ladowania
obrazu, co pozwala na jednokrotne wykreślenie komponentu zawierającego obraz
wtedy, gdy cały obraz jest załadowany. Nie będziemy (na razie) korzystać
z tego sposobu, wspomnimy tylko, że ImageObserver jest interfejsem i że klasa
Component implememntuje ten interfejs, zatem jako ostatni argument wywołania
metody drawImage możemy podać referencję do dowolnego komponentu (w szczególności
tego, w obszarze którego obraz jest wyświetlany).
Drugim sposobem na "zaczekanie na załadowanie obrazka" jest użycie klasy MediaTracker.
Nie daje on tak precyzyjnych informacji jak ImageObserver, ale za to jest
łatwiejszy w użyciu i umożliwia śledzenie ładowania wielu obrazków (co jest
użyteczne np. przy animacji).
Konstruktor klasy MediaTracker ma jeden argument - komponent na którym ewentualnie (ale niekoniecznie) będzie malowany obrazek
W zasadzie możemy tu użyć dowolnego komponentu np. aplikacji dziedziczącej okno
Po stworzeniu obiektu MediaTracker dodaajemy do nego obrazy (do śledzenia).
MediaTracker mt = new MediaTracker(this);
Toolkit tk = Toolkit.getDefaultToolkit();
Image img = tk.getImage(nazwaPliku);
mt.addImage(img, 1); // drugi argument - identyfikaor, pozwalający dzielić
// obrazy na grupy z różnymi priorytetami ładowania
Następnie żądamy od MediaTrackera, by rozpoczął ładowanie i zaczekał na załadowanie wszystkich dodanych obrazków:
try {
mt.waitForAll();
} catch(InterruptedException e) { System.exit(1); }
Albo – by ładował obrazy konkretnej grupy (waitForID(...))
Wywołanie metod waitForAll lub waitFor... wstrzymuje dzialanie aplikacji,
dopóki wszystkie obrazy dodane do MediaTrackera (lub obrazy podanej grupy)
nie zostaną załadowane.
To oczekiwanie na załadowanie może przerwane z zewnątrz - stąd potrzeba obsługi wyjątku InterruptedException.
Gdy obrazek jest już zaladowany możemy go wykreślić za pomocą metody drawImage
np. w metodzie paintComponent klasy komponentu, który ma stanowić obszar
prezentacji.
Jedna z przeciążonych wersji tej metody wygląda następująco.
boolean drawImage(Image img,
// obraz do wykreślenia
int x,
int y,
// górny lewy róg w obszarze komponentu
int width,
int height, // szerokość i wysokość
(skalowanie)
ImageObserver observer)
Jako przyklad zbudujmy klasę ImagePanel, której obiekty będą stanowić kontenery
z obrazkiem z podanego pliku jako tłem. Do takiego kontenera możemy dodawać
inne komponenty i będą prezentowane na tle obrazu. Inicjalne rozmiary kontenera
będą równe rozmiarom obrazka, a przy zmianach rozmiarów kontenera obrazek
stanowiący jego tło będzie reskalowany. Rysunek obok pokazuje taki kontener
z dodanym przyciskiem "Jakiś przycisk".
Kod programu przedstawiono poniżej.
import javax.swing.*;
import java.awt.*;
class ImagePanel extends JPanel {
Image img;
boolean loaded = false; // czy obrazek został załadowany?
public ImagePanel(String imgFileName) {
loadImage(imgFileName);
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
if (img != null && loaded)
g.drawImage(img, 0, 0, getWidth(), getHeight(), this);
else
g.drawString("Bez obrazka", 10, getHeight() - 10);
}
private void loadImage(String imgFileName) {
img = Toolkit.getDefaultToolkit().getImage(imgFileName);
MediaTracker mt = new MediaTracker(this);
mt.addImage(img, 1);
try {
mt.waitForID(1);
} catch (InterruptedException exc) {
}
int w = img.getWidth(this); // szerokość obrazka
int h = img.getHeight(this); // wysokość obrazka
if (w != -1 && w != 0 && h != -1 && h != 0) {
loaded = true;
setPreferredSize(new Dimension(w, h));
} else
setPreferredSize(new Dimension(200, 200));
}
}
public class ImagePanelTest extends JFrame {
public ImagePanelTest(String fname) {
ImagePanel p = new ImagePanel(fname);
p.add(new JButton("Jakiś przycisk"));
add(p);
pack();
setLocationRelativeTo(null);
setVisible(true);
}
public static void main(String args[]) {
new ImagePanelTest("pool2.jpg"); // argument - nazwa pliku graficznego (z
// roboczego katalogu aplikacji)
}
}
Komentarze i uwagi.
- W metodzie loadImage ładujemy obrazek i uzyskujemy jego wymiary. Służą
temu metody getWidth(...) i getHeight(...) z klasy Image. Ich argumentem
jest ImageObserver, możemy podać this, bo klasy wszystkich komponentów implementują
interfejs ImageObserver. Zgodnie z tymi wymiarami ustalamy preferowane rozmiary
kontenera, Jeśli obrazek (z jakichś przyczyn) nie jest gotowy do wyświetlenia
(-1 jako wynik metod getWidth lub getHeight) - ustalamy domyślne rozmiary
kontenera, a w metodzie paintComponent - wykreślimy u jego dołu napis "Bez
obrazka".
- Zauważmy też, że wykreślając obrazek metodą drawImage jako jego szerokośc
i wysokość podajemy aktualne rozmiary kontenera. Rozmiary te mogą się zmieniać
i zgodnie z tymi zmianami obrazek będzie reskalowany.
- Nieco prostszy program uzyskamy używając klasy ImageIcon, która przy
konstrukcji obiektów automatycznie używa MediaTrackera i synchronicznie
ładuje obraz. Nie będziemy więc musieli zapisywać kodu związanego z MediaTrackerem,
ale za to będziemy musieli od obiektu klasy ImageIcon uzyskać obraz klasy
Image (więcej o tym w za chwilę).
Kończąc to syntetyczne "wprowadzenie do rysowania" trzeba powiedzieć, że
w Javie istnieją dużo bardziej rozbudowane możliwości graficzne. W zakresie
grafiki dwuwymiarowej dostarcza ich klasa Graphics2D oraz szereg innych związanych
z nią klas.
6. Kontenery
Kontenery - to komponenty, które mogą zawierać inne komponenty (w tym inne kontenery).
Komponenty są dodawane do kontenerów za pomocą metody add.
kontener.add(komponent); // z ew. dodatkowymi argumentami
Usunięcie komponentu z kontenera realizuje metoda remove:
kontener.remove(komponent)
Dodawanie
do okien
Okna także są kontenerami. W
AWT dodajemy komponenty bezpośrednio do okien. W
Swingu zwykle dodajemy komponenty (w tym
kontenery) do contentPane (specjalnego kontenera, stanowiącego
standardową zawartość okna).
Jeżeli
win oznacza okno (np. klasy JFrame), to jego contentPane uzyskamy
przez odwołanie:
Container
cp = win.getContentPane();
i
dodawanie komponentu comp może wyglądać tak:
cp.add(comp); // z
ew. dodatkowym argumentem
a usuwanie tak:
cp.remove(comp);
Dla ułatwienia można pisać bezpośrednio:
win.add(comp);
czy
win.remove(comp);
czy też:
win.setLayout(...);
i zostanie to przetłumaczone na odwołania do contentPane.
Ale np. ustalenie tła na którym mają pojawić się komponenty w oknie już wymaga bezpośredniego odwołania do contentPane:
win.getContentPane().setBackground(...);
|
Najprostsze kontenery -
panele (klasy Panel i JPanel)
- służą do grupowania elementów.
W
Swingu mamy też wyspecjalizowane kontenery (panele dzielone,
zakładkowe, przewijane), które mają specjalną konstrukcję i
wobec nich używamy innych metod ustalania zawartości.
Panele
są umieszczane w innych kontenerach - np. oknach. Domyślnie
panele są widoczne, a okna - nie.
Dlatego,
uwidocznienie okna (czy dialogu) wymaga użycia metody
setVisible(true) lub show() np.
frame.setVisible(true); lub frame.show();
7. Okna
Okna są (oprócz okien wewnętrznych, o których później) kontenerami najwyższego
poziomu. Za pomocą okien aplikacja komunikuje się z użytkownikiem.
Hierarchię klas realizujacych okna (a także kontener apletu) przedstawia rysunek.
Podstawowe
pojęcia, dotyczące okien
|
okno
wtórne
(secondary window, child window) = okno które ma
właściciela - inne okno okno pierwotne,
właściciel innych okien (owner) = okno, które jest
właścicielem innych okien
Skutki
prawa własności:
- zamknięcie
okna pierwotnego powoduje zamknięcie okien wtórnych, które
są jego własnością,
- minimalizacja
okna pierwotnego powoduje minimalizację okien wtórnych,
- przesunięcie
okna pierwotnego powoduje przesunięcie okien wtórnych (nie
na wszystkich platformach).
Okno
wtórne może być oknem modalnym lub nie.
Modalność
oznacza, iż interakcja z oknem pierwotnym jest zablokowana do
chwili zamknięcia okna wtórnego. Przy niemodalnym oknie
wtórnym - możemy dalej działać na oknie pierwotnym.
Z -
order = uporządkowanie
okien "po osi Z" (czyli jak się okna na siebie
nakładają).
|
Wszystkie
okna w Javie, za wyjątkiem "okna" apletu (które tak
naprawdę jest panelem) oraz okien wewnętrznych Swingu, pochodzą od
klasy Window. Ale klasa ta (jak również swingowa JWindow) może
być używana samodzielnie do tworzenia okien o następujących cechach:
- okno
bez ramki, menu i tytułu.
- ma
właściciela - inne okno.
- nie
podlega przesunięciom wraz z właścicielem.
- nie jest modalne.
Okna
typu Window możemy tworzyć za pomocą konstruktorów, w których
jako argument podajemy właściciela okna (dla JWindow nie musimy tego
robić - zostanie stworzone niewidoczne okno JFrame, które
będzie jego właścicielem).
Niektóre metody klasy
Window (dziedziczone przez inne typy okien)
|
dispose() | usunięcie
zasobów graficznych związanych z oknem |
Component getFocusOwner() | zwraca
komponent znajdujący się w oknie, który ma fokus. Okno musi
mieć fokus. |
Component getMostRecentFocusOwner()
| zwraca
komponent znajdujący się w oknie, który otrzyma fokus, gdy
okno otrzyma fokus. |
Toolkit getToolkit() | zwraca
Toolkit (klasa Toolkit zawiera metody m.in. opisujące graficzne
środowisko działania) |
boolean isShowing() | czy jest na ekranie |
void pack() | upakowanie okna zgodnie z
preferowanymi rozmiarami komponentów w nim zawartych
(rozmiar okna będzie
dokładnie taki, by pomieścić zawarte komponenty i nie większy) |
void setCursor(Cursor) | ustal typ kursora nad oknem |
void toBack() | w tło
(zmiana Z-order) |
void toFront() | na pierwszy plan (zmiana Z-order) |
void setLocationRelativeTo(Component c)
| jeśli c
jest null, lub komponent c nie jest widoczny na ekranie, centruje
okno w obszarze ekranu. |
Window getOwner() | właściciel
okna |
Window[] getOwnedWindows() | tablica
okien, których dane okno jest właścicielem |
Ważnym
rodzajem okna jest okno ramowe,
mające ramki, pasek tytułowy, ikonki sterujące, oraz ew. pasek menu.
Realizowane jest przez klasy Frame (w AWT) i JFrame (w Swingu). Nie
ma właściciela i nie może być oknem modalnym.
Główne
okno naszej aplikacji będzie zwykle obiektem klasy pochodnej od Frame
Okno ramowe
tworzymy za pomocą konstruktora bezparametrowego lub z argumentem
String, stanowiącym tytuł okna.
Niektóre metody klas
Frame/JFrame
|
Image getIconImage() | jaka ikonka przy minimalizacji? |
MenuBar getMenuBar() | pasek menu (dla Frame) |
JMenuBar getJMenuBar() | pasek menu (dla JFrame) |
String getTitle() | tytul |
boolean isResizable() | czy
możliwe zmiany rozmiarów? |
remove(MenuComponent) | usunięcie
paska menu |
setIconImage(Image) | jaka ikonka przy minimalizacji? |
setMenuBar(MenuBar) | ustala pasek menu (dla Frame) |
setJMenuBar(JMenuBar) | ustala pasek menu (dla JFrame) |
setResizable(boolean) | ustalenie
możliwosci zmiany rozmiarów |
setTitle(String)
| zmiana
tytułu |
setUndecorated(boolean) | ustala,
czy okno ma mieć "dekoracje" (tj. pasek tytułu, ramkę
itp.) |
setExtendedState(int stan) | Ustala
stan okna, reprezentowany przez jedną ze stałuch statycznych z klasy Frame (podawane
jako argument - stan): * NORMAL * ICONIFIED * MAXIMIZED_HORIZ * MAXIMIZED_VERT * MAXIMIZED_BOTH Uwaga nie
na wszystkich platformach
wszystkie w/w stany są możliwe do osiągnięcia. Aby stwierdzić,
które tak, a które nie, używamy metody: Toolkit.isFrameStateSupported(int
stan)
|
Kolejnym
typem okien są dialogi, obiekty klas Dialog i JDialog .
Dialog -
to okno z ramką i tytułem, które ma właściciela i nie może
mieć paska menu.
Może natomiast być oknem modalnym lub nie, co
zależy od naszych ustaleń.
Podobnie
jak Frame/JFrame klasa Dialog/JDialog zawiera metody pozwalające na
pobieranie - ustalanie tytułu oraz możliwości
zmiany rozmiarów. Dodatkowo metody boolean isModal() i
setModal(boolean) pozwalają sprawdzać i ustalać właściwość
modalności.
W
Swingu mamy jeszcze do dyspozycji 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.)
Więcej
o oknach wewnętrznych - w wykładzie o architekturze okien,
Operacja
zamknięcia okna
Wszystkie okna Swingu (JFrame, JDialog,, JInternalFrame...) pozwalają na
ustalenia co ma się stać w przypadku zamknięcia okna.
Służy temu
metoda setDefaultCloseOperation(int), której argument może
przyjmować następujące wartości (są to nazwy
odpowiednich statycznych stałych):
- DO_NOTHING_ON_CLOSE
- HIDE_ON_CLOSE - operacja domyślna (chowa okno, ale nie likwiduje go)
- DISPOSE_ON_CLOSE
- usunięcie okna
- EXIT_ON_CLOSE - powoduje zamknięcie aplikacji
W
AWT - dla wywołania określonych efektów przy zamknięciu
okna (np. zakończenia aplikacji) musimy obsługiwać zdarzenie
zamykania okna (o czym dalej w wykładzie o obsłudze zdarzeń).
8. Rozkłady
8.1. Zarządcy rozkładów - zasady ogólne
Z
każdym kontenerem jest skojarzony tzw. zarządca rozkładu, który
określa rozmiary i położenie komponentów przy wykreślaniu
kontenera (a więc gdy jest on uwidaczniany lub zmieniają się jego
rozmiary).
W
klasie każdego z komponentów znajdują się metody:
getPreferredSize(), getMinimuSize() i getMaximuSize(),
zwracające - różne w zależności od typu komponentu - rozmiary
preferowane, minimalne i maksymalne (obiekty typu Dimension z
publicznymi polami width i height)
Rozmiary te - w różny
sposób przez różnych zarządców rozkładów
- są brane pod uwagę przy układaniu komponentów.
Dla
lekkich komponentów Swingu rozmiary te możemy ustalać za
pomocą odpowiednich metod set...
Zdarza
się czasem, że zmiany rozmiarów jakiegoś komponentu,
umieszczonego w kontenerze (np. na skutek jakichś pośrednich odwołań
z programu) nie skutkują w rozkładzie komponentów w kontenerze. Należy wtedy wywołać
metodę revalidate() na rzecz
komponentu, którego rozmiary uległy zmianie, co spowoduje
ponowne ułożenie komponentów w kontenerze przez zarządcę
rozkładu. Jeśli zmiany ułożenie nie będą uwidocznione na ekranie -
należy dodatkowo wywołać metodę repaint(),
również na rzecz zmienionego komponentu.
Użycie
pack() wobec okna zawsze powoduje wywołanie metod układania
komponentów przez zarządców rozkładu dla wszystkich
kontenerów w hierarchii zawierania się komponentów w
oknie.
Jak prawie wszystko w Javie - zarządca rozkładu są obiektami odpowiednich klas.
Klasy te implementują interfejs LayoutManager lub rozszerzający go LayoutManager2.
Np. rozkłady Flow, Border i Greed są zarządzane przez obekty klas FlowLayout, BorderLayout i GreedLayout.
Aby ustalić rozkład komponentów musimy:
- stworzyć obiekt-zarządce rozkładu,
- ustalić ten rozkład dla konetnera.
Ustalenie zarządcy rozkładu dla kontenera odbywa się za pomocą metody setLayout np.
FlowLayout flow = new FlowLayout();
Frame f = new Frame();
f.setLayout(flow);
Uwaga
Okna Swingu mają złożoną architekturę i nie możemy ingerować w rozkład komponentów
okna. Zamiast tego ustalamy zwykle rozkład komponentów contentPane okna np.:
JFrame frame = new JFrame();
frame.getContentPane().setLayout(new FlowLayout());
Samo odwołanie: frame.setLayout(...) ustala rozkład w contentPane (a nie w oknie).
|
Bardzo proste, obecne w Javie od zawsze, ale za to nie mające zaawansowanych możliwości układania komponentów są rozkłady: FlowLayout, BorderLayout i GridLayout.
Do bardziej zaawansowanych rozkładów należą: GridBagLayout, CardLayout oraz GroupLayout.
Pierwszy z nich jest niezwykle elastyczne, ale jednocześnie trudny w
użyciu, drugi - mało praktyczny (trudno znaleźć sensowne przykłady
jego zastosowania), ostatni - przeznaczony raczej do zastosowania w
środowiskach wizualnych edytorów GUI.
Prostym w użyciu, a jednocześnie elastycznym jest rozkład BoxLayout.
Wszystkie te klasy implementują interfejs LayoutManager2,
który rozszerza interfejs LayoutManager
. Implementując te interfejsy we własnych klasach możemy również
tworzyć własnych zarządców rozkładu.
Dostępne są także gotowe, przygotowane przez różnych programistów,
ciekawe
rozkłady, które nie są w "standardzie" Javy, ale mogą być łatwo
doinstalowane. Np. bardzo elastyczne i łatwe w użyciu są rozkłady TableLayout i MigLayout.
Możliwe jest także, by kontener nie miał żadnego rozkładu. Ponieważ wszystkie kontenery (oprócz ScrollPane
w AWT i wyspecjalizowanych kontenerów Swingu) mają jakiś domyślny rozkład,
to brak rozkładu musimy zaordynować sami za pomocą odwołania:
kontener.setLayout(null);
W takim kontenerze pobsługujemy się "absolutnym" pozycjonowaniem i
wymiarowaniem komponentów np. za pomocą metody setBounds(x,y,w,h), gdzie x, y - współrzędne położenia komponentu, w,h - szerokość i wysokość.
Po co są rozkłady i jak je stosować?
Użycie rozkładów pozwala programiście unikać oprogramowania zmian rozmiarów
i położenia komponentów przy zmianie rozmiarów kontenera.
Zwykle wyszukane układy komponentów GUI uzyskamy łatwiej za pomocą stosowania rozkładów (poprzez umiejętne
kombinowanie zawierania się paneli z różnymi rozkładami
) niż za pomocą śledzenia i oprogramowania reakcji na zmiany rozmiarow okna,
ktore muszą się przecież przekładać na zmiany położenia i rozmiarów komponentów
w kontenerach bez rozkładu.
8.2. Proste rozkłady - FlowLayout, BorderLayot i GreedLayout
Charakterystyki
rozkładów FlowLayout, BorderLayout i GridLayout przedstawiono
w tabeli.
Tabela. Charakterystyki rozkładów Flow, Border, Greed
Rozkład
|
Właściwości
|
FlowLayout
(rozkład
domyślny dla Panel, JPanel)
| - Komponenty
ułożone są w wierszu.
- Przy
zmianie rozmiarów kontenera rozmiary komponentów
nie zmieniają się
- Jeśli
szerokość kontenera jest za mała, pojawiają się dodatkowe
wiersze.
- Można
ustalić, jak mają być wyrównane komponenty (do lewej, w
centrum, do prawej): służą temu stałe FlowLayout.LEFT,
FlowLayout.CENTER, FlowLayout.RIGHT podawane jako argument
konstruktora
- Można
ustalić odstępy (w pionie i poziomie) pomiędzy komponentami.
Wersje konstruktorów
FlowLayout() // (center, odstępy 5)
FlowLayout(int) //
podane wyrówanie
FlowLayot(int, int, int) // podane wyrównanie oraz
odstępy poziom, pion
|
BorderLayout
(układ
domyślny dla Frame, Window i Dialog oraz contentPane okien Swingu)
| - Komponenty ułożone
są "geograficznie": "North", "East",
"South", "West", "Center"
- Używa
się metody cont.add(comp, loc), gdzie loc - napis
oznaczający miejsce lub stała całkowitoliczbowa
BorderLayout.NORTH, BorderLayout.CENTER, etc.
- Komponent dodany w
miejscu "Center"
wypełnia całe pozostawiane przez inne komponenty miejsce.
- Komponenty
zmieniają rozmiary wraz ze zmianami rozmiaru kontenera:
- "North" i "South" - w poziomie, ale nie w
pionie
- "West" i "East" - w
pionie, ale nie w poziomie
- "Center"
- w obu kierunkach
- Można
podać odstępy między komponentami (pion, poziom)
new BorderLayout(odst_poz, odst_pion)
|
GridLayout
| - Siatka (tablica)
komponentów
- Rozmiary
wszystkich komponentów będą takie same
- Zmieniają
się wraz ze zmianami rozmiaru kontenera
Konstruktory:
GridLayout(n.m) // tablica n x m komponentów,
(jeśli
n=0 lub m=0, to dany wymiar tablicy zostanie ustalony dynamicznie
na podstawie drugiego wymiary i liczby komponentów w
kontenerze)
GridLayout(n,
m, hgap, vgap) // z podanymi odstępami w poziomie i pionie
|
Poniższy rysunek pokazuje różne rozkłady.
Program, który prezentuje te rozkłady przedstawiono poniżej:
import java.awt.*;
import javax.swing.*;
public class LayShow {
public static void main(String[] args) {
final int CNUM = 5; // liczba komponentów w panelach
String lmNames[] = {
"Flow Layout", // opisy rozkładów
"Flow (left aligned)", "Border Layout", "Grid Layout(1,num)",
"Grid Layout(num, 1)", "Grid Layout(n,m)", };
LayoutManager lm[] = {
new FlowLayout(), // rozkłady
new FlowLayout(FlowLayout.LEFT), new BorderLayout(),
new GridLayout(1, 0), new GridLayout(0, 1), new GridLayout(2, 0), };
// argumenty dla rozkladu BorderLayout
String gborders[] = { "West", "North", "East", "South", "Center" };
// Kolory paneli
Color colors[] = { new Color(191, 225, 255), new Color(255, 255, 200),
new Color(201, 245, 245), new Color(255, 255, 140),
new Color(161, 224, 224), new Color(255, 255, 200), };
Icon redDot = new ImageIcon("red.gif"); // ikonka na przycisku
JFrame frame = new JFrame("Layouts show"); // okno i contentPane
frame.setLayout(new GridLayout(0, 2));
for (int i = 0; i < lmNames.length; i++) {
JPanel p = new JPanel();
p.setBackground(colors[i]); // kolor tła panelu
p.setBorder(BorderFactory.createTitledBorder(lmNames[i])); // ramka
p.setLayout(lm[i]); // ustalenie rozkładu
Icon icon = null;
// Możemy sprawdzić z jakim rozkładem mamy do czynienia
// i odpowiednio do tego coś zrobić (tu: ikonka na przyciskach)
if (lm[i] instanceof BorderLayout) icon = redDot;
for (int j = 0; j < CNUM; j++) { // dodajemy przyciski do paneli
JButton b = new JButton("P " + (j + 1), icon);
p.add(b, gborders[j]);
}
frame.add(p);
}
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack();
frame.setVisible(true);
}
}
Zobacz działanie programu
Proszę uruchomić ten program, a następnie
zmieniając rozmiary okna zaobserwować jak zmieniają się rozmiary i położenie
komponentów.
8.3. Rozkład BoxLayout
Rozkład
BoxLayout układa kompenenty w jednym wierszu lub w jednej kolumnie
(poziomo lub pionowo).
W odróżnieniu od rozkładu
GridLayout brane są przy tym pod uwagę preferowane i maksymalne
rozmiary komponentów oraz ich wyrównanie w kontenerze
(do lewej, w centrum, do prawej).
Aby
istniejącemu kontenerowi cont nadać rozkład BoxLayout:
- pionowy:
używamy cont.setLayout(new BoxLayout(cont, BoxLayout.Y_AXIS);
- poziomy:
używamy cont.setLayout(new BoxLayout(cont, BoxLayout.X_AXIS);
W
przypadku tworzenia nowego kontenera można użyć klasy Box,
która definiuje
kontener lekki o rozkładzie BoxLayout (uwaga: Box nie jest
J-komponentem!)
Tworzenie obiektu Box:
- za
pomocą konstruktora: Box(orient),
- gdzie orient - BoxLayout.X_AXIS lub
Y_AXIS
- za
pomocą statycznych metod zwracających referencję do utworzonego
obiektu Box:
- Box.createHorizontalBox()
W
klasie Box zdefiniowano też wygodne metody statyczne do wprowadzania
"wypełniaczy" przestrzeni w rozkładzie BoxLayout. Te
wypełniacze to:
- klej (glue)
- sztywny obszar (rigid
area) oraz
- wypełniacz
(box-filler).
Sztywny obszar służy
do wprowadzenia przerw między komponentami.
Np. w układzie
poziomym aby wprowadzić 10-pikselowy odstęp pomiędzy dwoma dodanymi
komponentami c1 i c2 można napisac:
Box b =
Box.createHorizontalBox();
b.add(c1);
b.add(Box.createRigidArea(new Dimension(10,10));
b.add(c2);
Klej
służy do kontrolowania dystrybucji nadmiarowego miejsca.
BoxLayout
stara się nadać komponentom ich preferowane rozmiary. Jeżeli miejsca
w kontenerze jest więcej lub mniej niż wynika to z preferowanych
rozmiarów komponentów, BoxLayout dopasowuje rozmiary
komponentów tak, by jak najlepiej wypełnić
miejsce. Bierze jednak przy tym pod uwagę minimalne i maksymalne
rozmiary komponentów (są dla niego ograniczeniem). W przypadku
jeśli pozostaje jakieś wolne miejsce, dodawane jest ono u dołu (w
rozkładzie pionowym) lub z boku (w rozkładzie poziomym)
kontenera.
Klej jest niewidzialnym, dynamicznie
zmieniającym swoje rozmiary komponentem-wypełniaczem.
Po dodaniu
komponentów-klei do kontenera - ew. nadmiarowe miejsce będzie
proporcjonalnie podzielone między te komponenty.
Np.
jeśli do kontenera o poziomym rozkładzie BoxLayout dodamy dwa
przyciski o ustalonych maksymalnych rozmiarach (100,100), a kontener
będzie miał rozmiary (300, 300), to nadmiarowe miejsce pojawi u dołu
kontenera. Jeśli jednak za pomocą statycznej metody Box.createGlue()
pomiędzy komponentami dodamy klej:
cont.add(butt1);
cont.add(Box.createGlue());
cont.add(butt2);
to
nadmiarowe miejsce będzie ulokowane pomiędzy przyciskami.
Wypełniacz
(obiekt klasy Box.Filler) jest swoistym połączeniem sztywnego obszaru
i kleju.
Tworzymy go
za pomocą wyrażenia new Box.Filler(min, pref, max),
gdzie: min, pref, max - minimalne, preferowane i maksymalne
rozmiary (obiekty klasy Dimension) niewidocznego komponentu, który
następnie możemy dodać do kontenera. Rozmiary tego komponentu mogą
się zmieniać w ramach zadekretowanych minimalnych, preferowanych i
maksymalnych wartości i w ten sposób uzyskujemy efekt
kontrolowanego wypełniania odstępów pomiędzy komponentami
(zmniejszenie i zwiększenie, ale tylko do pewnych granic, określanych
przez minimalne i
maksymalne rozmiary komponentu - wypełniacza).
Sposób wyrównania elementów względem siebie w rozkładzie BoxLayout
określany jest przez ustalenie wyrównania każdego z nich (metody
setAlignmentX(..) i setAlignmentY(...)
Podsumujmy.
Rozkład
BoxLayout jest bardzo elastyczny i pozwala w prosty sposób
tworzyć wyszukane układy komponentów (np. przy kombinowaniu
kilku Box-ów).
Do kontroli rozkładu komponentów
służą następujące środki:
- ustalanie minimalnych i maksymalnych rozmiarów komponentów (dla komponentów
Swingu setMinimuSize() i setMaximumSize())
- ustalanie wyrównania
komponentów (w Swingu:
metody setAlignmentX(...) i setAlignmentY(...) z argumentami -
stałymi statycznymi z klasy Component: LEFT_ALIGNMENT,
CENTER_ALIGNMENT, RIGHT_ALIGNMENT, BOTTOM_ALIGNMENT, TOP_ALIGNMENT
- sztywne obszary
(Box.createRigidArea(Dimension)),
klej (Box.createGlue()) oraz wypełniacz (new Box.Filler(min, pref,
max).
Przykładowe rozkłady typu BoxLayout pokazano w tabeli.
Tabela. Przykładowe rozkłady typu BoxLayout
|
Trzy komponenty dodane do pionowego
BoxLayout. Ich maksymalne rozmiary są ograniczone, wolne miejsce pojawia
się u dołu kontenera. Domyślne wyrównanie - do lewej
|
|
Zmiana wyrównania komponentów po osi X: setAlignmentX(Component.CENTER_ALIGNMENT)
|
|
Pomiędzy komponentami dodano sztywne obszary stworzone metodą Box.createRigidArea(new Dimension(0,10);
Wolne miejsce nadal jest lokowane u dołu kontenera, a odstępy między komponentami są stałe
|
|
Tym razem pomiędzy komponentami
dodano dwa komponenty-kleje (Box.createGlue()). Przy zmianie rozmiarów kontenera
ew. wolne miejsce jest lokowane pomiędzy komponentami. Jeśli nie ma nadmiarowego
miejsca komponenty stykają się ze sobą.
|
|
Tu komponenty nie mają ograniczonych
maksymalnych rozmiarów lub rozmiary te są odpowiednio duże. Wolne miejsce
jest wypełniane przez BoxLayout poprzez zmianę rozmiaru komponentów. Maksymalne
rozmiary komponentów możemy zmieniać dynamicznie za pomocą metody setMaximumSize. |
8.4. Rozkłady TableLayout i MigLayout
Rozkłady TableLayout i MigLayout łączą prostotę użycia z zaawansowanymi możliwościami układania komponentów.
Aby użyć rozkładu TableLayout należy:
- ściągnąć odpowiedni JAR ze strony https://tablelayout.dev.java.net/
- dodać ten JAR do BuildPath naszego projektu (w Eclipse opcja Configure BuildPath, Libraries, Add External JARs).
TableLatout dziel kontener na komórki, znajdujące się w wierszach i kolumnach.
Komponentu
dodawane do kontenera trafiają do odpowiednich komórek. Mogą też
zajmować kilka komórek lub może być kilka komponentów w jednej komórce.
Rozmiary komórek reagują na zmiany rozmiarów kontenera, przy czym
przy tworzeniu rozkładu TableLayout określamy sposób tej reakcji, przez
podanie rozmiarów kolumn i wierszy.
Mianowicie, jeden z konstruktorów klasy TableLayout ma dwa argumenty tablicę rozmiarów kolumn i tablicę rozmiarów wierszy:
TableLayout(double[] cols, double[] rows)
W tablicach tych podajemy rozmiary w następujący sposób:
- jako rozmiary absolutne - w pikselach (wartości od 1.0 w górę),
- jako rozmiary relatywne - w procentach wolnej przestrzeni (wartości z przedziału 0-0.99, np. 0.5 oznacza 50% wolnego miejsca),
- jako stałą TableLayout.PREFERRED (oznacza rozmiar wiersza lub kolumny równy maksymalnemu z preferowanych rozmiarów umieszczonych w komórkach komponentów),
- jako stałą TableLayout.FILL,
co oznacza, że rozmiar kolumny lub wiersza będzie wypełniał całą
dostępną po alokacji innych szerokości (absolutnych i relatywnych)
przestrzeń,
Miejsce w kontenerze alokowane jest w następujący
sposób. Najpierw alokowane są rozmiary absolutne i te określane przez
stałą TableLayout.PREFERRED. Pozostałe miejsce przydzielane jest
komórkom skalowalnym (wg ich relatywnych rozmiarów - uwaga procenty
oznaczają tu procent pozostałego wolnego miejsca). Pozostałe po tym
miejsce przydzielane jest proporcjonalnie komórkom z parametrem
TableLayout.FILL.
Komponenty dodajemy do rozkładu TableLayout za pomocą metody:
add(komponent, ograniczenia)
Ograniczenia podawane są w postaci napisu (String) i mają następującą formę:
zakres_komórek [ [,] wyrównanie]
Zakres komórek
określa miejsce w tablicy rozkładu, w którym ma się znaleźć komponent.
Podajemy go w postaci numer_kolumny numer_wiersza (komponent zajmuje
jedną komórkę) lub - gdy komponent ma zajmować kilka komórek -
specyfikując prostokątny obszar w postaci "adres lewej górnej komórki
adres dolnej prawej komórki". Przykłady:
add (comp, "0 0") - dodanie komponentu do komórki w pierwszej kolumnie i pierwszym wierszu
add(comp, "3 1" ) - w 4 kolumnie i drugim wierszu
add(comp,
"0 0 2 1") - komponent zajmuje obszar od komórki w pierwszej kolumnie i
pierwszym wierszu do komórki w 3 kolumnie i drugim wierszu.
Wyrównanie
określa w jaki sposób komponent jest lokowany w przeznaczonym dla niego
obszarze. Podajemy wyrównanie w poziomie i wyrównanie w pionie.
Wyrównanie w poziomie może być LEFT, CENTER, RIGHT, w pionie: TOP
CENTER BOTTOM. W obu przypadkach wyrównanie FULL (domyślne, jeśli nie
podamy) oznacza, że komponent ma zając całe wolne miejsce.
Przykład:
add(comp,
"1 1 LEFT CENTER") = komponent dodawany do komórki w 2 kolumnie i 2
wierszu będzie dosunięty do lewego brzegu komórki i pionowo
wycentrowany w obszarze komórki.
add(comp, "1 1 FULL CENTER") - komponent zajmie całą szerokość komórki i będzie pionowo wycentrowany w jej obszarze.
Podając wyrównaie można używać małych liter i tylko początkowych liter słów LEFT, RIGHT, CENTER itp.
W zapisie ograniczeń można też zamiast spacji stosować przecinki.
A zatem możemy pisać tak:
add(comp, "1 1 LEFT CENTER")
i tak:
add(comp, "1 1 L C")
i tak:
add(comp, "1,1, l, c")
TableLayout pozwala też na dynamiczne tworzenie i usuwanie wierszy i kolumn.
Zastosowanie
rozkładu TableLayout jest intuicyjnie proste i daje duże możliwości
układania komponentów. Na przykład dość skomplikowany układ (zob. rys)
można uzyskać za pomocą prostego kodu:
import info.clearthought.layout.*;
import javax.swing.*;
public class TabLay1 extends JFrame {
public TabLay1(String[] labs, int[] fwidth) {
final int N = labs.length, // liczba dynamicznych elementów: etykieta - pole tekstowe
GAP = 10; // odstęp
// Kolumny rozkładu
double[] cols = { GAP, TableLayout.PREFERRED, TableLayout.FILL, TableLayout.PREFERRED, GAP};
// Wiersze rozkładu
double[] rows = new double[N + 5];
rows[0] = GAP; // odstęp górny
for (int i=0; i < N; i++) rows[i+1] = TableLayout.PREFERRED; // etykiety-pola txt
int rhead = N+1; // wiersz nagłówków (etykiet wyjasniajacych)
rows[rhead] = TableLayout.PREFERRED;
int rcomm = N+2; // wiersz dodatk. komentarzy (JTextArea)
rows[rcomm] = TableLayout.FILL;
int rbutt = N+3; // wiersz przycisków
rows[rbutt] = TableLayout.PREFERRED;
rows[N+4] = GAP; // odstęp końcowy
// Utworzenie i ustalenie rozkładu
TableLayout lay = new TableLayout(cols, rows);
lay.setHGap(7); // odstępy pomiędzy komórkami
lay.setVGap(5);
setLayout(lay);
// Dodanie elementów dynamicznych
for (int i=0; i < N; i++) {
int r = i + 1;
add(new JLabel(labs[i]), "1 " + r + " right center" );
add(new JTextField(fwidth[i]), "2 " + r + " left center");
add(new JCheckBox(), "3 " + r + " left center");
}
add(new JLabel("Dodatkowe komentarze"),"2 " + rhead + " 3 " + rhead + " center center");
add(new JLabel("<html><i>Wybór<br>miasta</i></html>"), "1 " + rhead + " center center");
JList list = new JList(new String[] { "Warszawa", "Poznań", "Wrocław", "Gdańsk",
"Lublin", "Katowice", "Szczecin", "Białystok", "Bielsko-Biała" });
add(new JScrollPane(list), "1 " + rcomm + " 1 " + rbutt + " full full ");
add(new JScrollPane(new JTextArea(10, 50)), "2 " + rcomm + " 3 " + rcomm);
add(new JButton("Dodaj"), "2 " + rbutt + " 3 " + rbutt + " left full");
add(new JButton("Znajdź"), "2 " + rbutt + " 3 " + rbutt + " center full");
add(new JButton("Wyślij"), "2 " + rbutt + " 3 " + rbutt + " right full");
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
pack();
setVisible(true);
}
public static void main(String[] args) {
new TabLay1(new String[] {"Imię", "Nazwisko", "Pesel", "Rok urodzenia", "Adres"},
new int[] { 20, 30, 12, 8, 45 });
}
}
Zobacz działanie programu
Jeszcze potężniejszymi możliwościami dysponuje MigLayout.
Jego
zastosowanie jest intuicyjnie proste, a celem autorów było uczynienie
pisania kodu układającego komponenty tak łatwym jak zastosowanie
wizualnych środowisk programowania GUI.
MigLayout dostępny jest na stronie http://www.miglayout.com/
Musimy
też podobnie jak w przypadku innych zewnętrznych bibliotek
skonfigurować BuildPath naszej aplikacji, dodając odpowiedni JAR.
Idea
MigLayot opiera się znowu na "siatce" komórek, o ograniczenia dotyczące
położenia i rozmiarów zadajemy znowu jako napisy (String). Tym razem
jednak nie musimy takiej siatki projektować a priori, ale powstaje ona
w miarę wykonania kodu. Układ ustalamy mniej więcej tak jak przy
pisaniu tekstu:
- komponenty dodawane bez żadnych ograniczeń trafiają do bieżącego wiersza,
- gdy zastosujemy ograniczenie "wrap", następne komponenty będą dodawane w następnym wierszu,
- gdy zastosujemy ograniczenie "span", komponent zajmuje więcej niż jedną komórkę,
- możemy
ustalać bardzo precyzyjnie sposób reagowania na zmiany rozmiarów
kontenera, tu proste ograniczenie "grow" powoduje zwiększanie rozmiarów
komponentu, a "shrink" określa dopuszczalne granice jego zmniejszania
się,
- możemy też za pomocą słówka "gap" określać odstępy.
- wyrównania komponentów w komórkach określane są typowymi słowami: "left", "right" itp.
- można modyfikować minimalne, preferowane i maksymalne rozmiary komponentów przy ich dodawaniu metodą add.
Charakterystyki
rozkładu (ograniczenia) możemy zadawać zarówno na poziomie komponentów
(w metodzie add) jak i dla całego rozkładu (tu ograniczenia dotyczą
wierszy i kolumn) w konstruktorze.
Zobaczymy to na prostym przykładzie "formularza":
tworzonego przez następujący kod:
import javax.swing.*;
import net.miginfocom.swing.*;
public class MigTest extends JFrame {
public MigTest() {
// Ustalenie rozkładu
// drugi argument kosntruktora dotyczy ograniczeń kolumn rozkładu
// tu: komponenty w pierwszej kolumnie będą równane do prawej
// trzecia koleumna może zwiększac swoje rozmiary
setLayout(new MigLayout("", "[right][][grow]"));
add(new JLabel("Imię"));
add(new JTextField(10));
add(new JLabel("Nazwisko"), "right align, gapleft 10"); // równanie do prawej, odstęp 10
add(new JTextField(20), "growx, wrap"); // może rosnąć, przejście do nowego wiersza
add(new JLabel("Adres"));
add(new JTextField(), "growx, span, wrap"); // może rosnąć, zajmuje cały wiersz,
// i zaczynamy nowy wiersz
add(new JCheckBox("Podlega zniżce?"),
"skip 3, right align"); // przeskocz 3 kolumny, wyrównaj do prawej
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
pack();
setVisible(true);
}
public static void main(String[] args) {
new MigTest();
}
}
Aby zobaczyć (intencjonalnie ustalone) zachowanie komponentów przy zmianie rozmiarów okna należy uruchomić demo programu:
Zobacz działanie programu
MigLayout ma ogromne możliwości. Warto się z nimi zapoznać uruchamiając aplikację demonstracyjną twórców tego rozkładu.
Zobacz zaawansowaną demonstrację możlwości MigLayout
Żródło: Mikael Grev, MiG InfoCom AB
9. Działania na kontenerach
W klasie Container znajdziemy użyteczne metody "kontenerowe", m.in. :
- getLayout - zwraca referencję do aktualnie ustalonego zarządcy rozkładu
- getComponents - zwraca tablicę komponentów (obiektów typu Component), które znajdują się w kontenerze
- validate - uaktualnia położenie i rozmiary komponetów w kontenerze
Przykładowe zastosowanie tych metod pokazuje wydruk, przedstawiający metodę
która, po wywołaniu z argumentem typu Component, dowiaduje się w jakim kontenerze
znajduje się przekazany komponent i:
- rozjaśnia tło tego komponentu (metoda brighter z klasy Color)
- przyciemnia tło wszystkich innych zawartych w kontenerze komponentów (metoda darker z klasy Color)
- jesli kontener ma rozkład FlowLayout - dodaje do niego przycisk z napisem "Nowy przycisk"
void chgCont(Component comp) {
Container cont = comp.getParent(); // rodzic komponentu
// w hierarchii zawierania się komponentów
Component[] c = cont.getComponents(); // uzyskanie wszystkich komponentów
for (int i=0; i<c.length; i++) { // przyciemnianie i rozjaśnianie
Color back = c[i].getBackground();
if (c[i] == comp) c[i].setBackground(back.brighter());
else c[i].setBackground(back.darker());
}
LayoutManager lm = cont.getLayout();
if (lm instanceof FlowLayout) { // jeżeli rozkład Flow - dodaj nowy przycisk
cont.add(new JButton("Nowy przycisk"));
}
cont.validate(); // musi być użyte po dodaniu komponentu:
// znak, ze kontener ma byc odswieżony
}
10. Komponenty Swingu a wielowątkowość. Schemat aplikacji Swing
Przypomnijmy, że interakcja użytkownika z aplikacją graficzną odbywa się
poprzez obsługę zdarzeń (takich jak np. kliknięcie w przycisk). Obslugą zdarzeń
zajmuje się specjalny, uruchamiany przez JVM równolegle z głównym wątkiem
aplikacji, wątek obsługi zdarzeń.
Oczywiście, zanim dojdzie do interakcji z użytkownikiem, okno aplikacji
musi
być zbudowane i uwidocznione. Zatem muszą być skonstruowane
komponenty, muszą uzyskać jakieś właściwości (np. kolor, pismo), muszą
być dodane do kontenerów, a w końcu i do okien. Komponenty Swingu
zostały zbudowane w taki sposób, że wszelkie nań nich manipulacje
(tworzenie, odczytywanie i zmiany ich właściwości) muszą się odbywać w
jednym wątku. W związku z interakcją poprzez obsługę
zdarzeń naturalnym wyborem wątku, w którym można odwoływać się do
komponentów Swingu jest właśnie wątek obsługi zdarzeń.
Tworzenie i odwoływanie się do komponentów Swingu
(odczytywanie i zmiana ich właściwości) winno odbywać się wyłącznie w
wątku obsługi zdarzeń.
Niewątpliwie łatwo i naturalnie można spełnić ten warunek w trakcie obsługi
zdarzeń - w metodach obsługujących zdarzenia, bowiem metody te wywoływane
są w wątku obsługi zdarzeń.
We wszystkich innych przypadkach kod, który działa na komponentach Swingu,
powinniśmy umieścić "do wykonania" w wątku obsługi zdarzeń. Służą temu statyczne
metody invokeLater oraz invokeAndWait z klasy java.awt.EventQueue.
Tak
samo nazwane metody znajdują się również w klasie SwingUtilities
(delegują one odwolania do odpowiednich metod z klasy EventQueue).
Argumentem tych jest referencja do obiektu klasy implementującej interfejs Runnable.
W zdefiniowanej w tej klasie metodzie run() umieszczamy kod operujący na
komponentach Swingu. Metoda invokeLater przekaże ten kod do wykonania przez
wątek obsługi zdarzeń (umieści kod w kolejce zdarzeń), wykonanie bieżącego
wątku będzie kontynuowane, natomiast nasz kod umieszczony w kolejce zdarzeń
zostanie wykonany wtedy, gdy wątek obsługi zdarzeń obsłuży zdarzenia znajdujące
się w kolejce przed nim.
Tak samo działa metoda invokeAndWait, z tą różnicą, że po jej wywołaniu działanie
bieżącego wątku zostanie wstrzymane do chwili wykonania przekazanego kodu
przez wątek obsługi zdarzeń.
Nie
należy wywoływać metody invokeAndWait(...) z wątku obsługi zdarzeń,
bowiem może to zablokować GUI (stanie się nierespnsywne).
Sposób wywyołania metody invokeLater(..) prezentuje ilustracyjny schemat:
SwingUtilities.invokeLater( new Runnable() {
public void run() {
// tu działania na komponentach Swingu
}
});
Generalnie przyjmuje się zasadę, że wszelkie działania na GUI
(tworzenie komponentow, dodawanie komponentów do kontenerów, ustalanie właściwości komponentów
wizualnych) winny być wykonywane w wątku obsługi zdarzeń.
Od tej reguły są wyjątki. Mianowicie, niektóre metody działające na komponentach
Swingu są wielowątkowo bezpieczne (mogą być wywoływane z wielu watków, nie
tylko z wątku obsługi zdarzeń). Należy do nich np. metoda repaint().
W dokumentacji metod zawsze zanaczono, czy są one wieloatkowo bezpieczne (thread-safe).
W praktyce - jak widzieliśmy - aplikacje, które stosują osłabioną zasadę Swingu (dopiero
po realizacji komponentów - czyli spakowaniu i/lub uwidocznieniu okna -
wszelkie na nich dzialania muszą odbywać się w wątku obsługi zdarzeń)
działają poprawnie. Trzeba jednak powiedzieć, że jest to coś w
rodzaju przypadku: nie ma gwarancji, że tak będzie zawsze, w każdych
okolicznościach, a tym bardziej w kolejnych nowych wersjach Javy czy
też przy zastosowaniu jakichś bardziej zaawansowanych pakietów typu
pluggable look & feel.
Aby ułatwić budowanie programów GUI przedstawiono typowy szablon aplikacji Swing i typowy sposób postępowania:
- Typowe aplikacje GUI konstruowane są jako okna, zatem klasa aplikacji dziedziczy klasę okna.
- W
kontrsuktorze dokonujemy ew. incjacji pól klasy niezwiązanych z
komponentami GUI, anastępnie w wątku obsługi zdarzeń budujemy GUI
- A zatem ustalany jest (jeśli trzeba) rozkład komponentów i ich właściwości.
- Komponenty dodawane są do okna
- Ustala się rozmiary okna i pokazuje okno.
- Dzialania te można dla lepszej strukturyzacji kodu wyodrębnić jako metodę o nazwie np. createGui i wywołac ją przez invokeLater.
Najprostszy szablon aplikacji Swing
import java.awt.*;
import javax.swing.*;
public class GuiSwing extends JFrame {
public GuiSwing() {
// jakieś dzialania inicjacyjne, nie związane z komponentami Swing
// ...
// budujemy okno;
// ponieważ jest to działanie na komponentach wizualnych
// - zrobimy to w wątku obsługi zdarzeń
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
createGui();
}
});
}
protected void createGui() {
// ustalenie tytułu okna
setTitle("Okno aplikacji");
// ustalenie rozkładu - jeśli trzeba, np:
setLayout(new FlowLayout());
// tworzenie komponentów np.
JLabel lab = new JLabel("Etykieta");
JButton b = new JButton("Przycisk");
// Ustalenie własciwości komponentów,
// np:
lab.setForeground(Color.red);
b.setForeground(Color.blue);
// Dodanie komponentów do okna np.
add(lab);
add(b);
// Ustalenie domyślnej operacji zamknięcia okna
setDefaultCloseOperation(EXIT_ON_CLOSE);
// ustalenie rozmiarów okna, np.:
pack();
// ustalenie położenia okna np. wycentrowanie
setLocationRelativeTo(null);
// pokazanie okna
setVisible(true);
}
public static void main(String[] args) {
new GuiSwing();
}
}
I
jeszcze jedna bardzo ważna rzecz: wszelkie długotrwałe działania (np.
wyszukiwanie w systemie plików, ściąganie danych z Internetu) winny
być wykonywane poza wątkiem obsługi zdarzeń. W przeciwnym razie będą
blokować interakcję z GUI (GUI staje się "zamrożone"). Do uruchamiania
takich działań z kodów wykonujących się w wątku obsługi zdarzeń służy
klasa SwingWorker (zob. w dalszych częściach tekstu).
11. J-komponenty (wspólne właściwości lekkich komponentów Swingu)
Wszystkie
lekkie komponenty Swingu pochodzą od klasy JComponent (będziemy nazywać je J-komponentami).
Dzięki
temu:
- są
komponentami lekkimi o niezależnym od platformy systemowej wyglądzie
(ale zależnym od dobieranych przez programistę ustaleń - "Look
and Feel")
- możemy
ustalać czy mają być przezroczyste, czy nie,
- istnieje
możliwość zewnętrznego ustalania ich preferowanych,
maksymalnych i minimalnych rozmiarów (metody setPreferredSize,
setMaximumSize, setMinimumSize), oraz
ich wyrównania względem innych komponentów
(metody setAlignmentX, setAlignmentY)
- mogą
mieć ramki o dowolnie ustalanej formie,
- mogą
mieć podpowiedzi ( "dymki"
pomocy - fly-over help, tooltips)
- mogą
być uaktywniane z klawiatury
(operowanie na GUI za pomocą klawiatury, nie tylko myszki)
- mogą
mieć przypisaną dodatkową informację (w postaci tablicy
asocjacyjnej, kojarzącej obiekty-klucze i obiekty-wartości)
Teraz
omówimy tylko ramki, przezroczystość i podpowiedzi, inne właściwości (akcje klawiaturowe i tablice asocjacyjne) zostaną
przedstawione dalej.
Ramki
Każdy
J-komponent (również J-panel) może mieć ustaloną przez nas
ramkę.
Do
ustalania ramek służą klasy pakietu javax.swing.border
i/lub klasa BorderFactory z pakietu javax.swing oraz
metoda klasy JComponent setBorder(Border).
Jej argumentem
jest referencja do obiektu implementującego interfejs
Border.
Może to być
referencja do obiektu jednej z klas pakietu javax.swing.border,
albo referencja zwracana przez BorderFactory,
albo referencja do obiektu naszej własnej klasy, odpowiedzialnej za wykreślanie ramki.
Znajdująca
się w pakiecie javax.swing klasa BorderFactory,
która zawiera statyczne metody pozwalające na dobór
prawie
wszystkich standardowych (określanych przez klasy pakietu
javax.swing.borders) ramek, jest niezwykle użyteczna, bowiem:
- ustalenie
ramki za jej pomocą nie wymaga importu klas ramkowych (z pakietu
javax.swing.border)
- nazwy
(statycznych) metod tej klasy są łatwe do zapamiętania
- obiekty-ramki,
dostarczane przez BorderFactory mogą być dzielone (shared)
pomiędzy komponentami, dla których ustalamy ramki (jedna i ta
sama ramka stworzona przez BorderFactory może być używana przez
wiele komponentów równocześnie).
Różnorodność ramek dostarczanych przez Swing jest spora. Tabela opisuje ich podstawowe typy.
Typ ramki
|
Znaczenie
|
Uwagi
|
empty |
obszar pusty | obramowanie (odstęp)
|
line |
ramka liniowa |
może być ustalony kolor linii |
titled |
ramka z tytułem (napisem) |
położenie tytułu może być konfigurowane, dodatkowo można podać ramkę innego typu, do której dodawany jest tytuł |
etched |
ramka akwafortowa |
może być koloryzowana |
bevel |
ramka skośna (brzegi 3D) |
może być wypukła (raised) albo wklęsła (lowered); może być koloryzowana |
softbevel |
j.w. z wygładzonymi brzegami |
dostępna poprzez klasę SoftBevel; BorderFactory jej nie dostarcza |
matte |
ramka matowa (od matowości w fotografii) |
matowość znaczy: dobieranie kolorowych brzegów o dowolnych (np. różnych
z prawej i lewej strony) rozmiarach lub wypełnianie ikoną |
compound |
ramka złożona |
dowolna kombinacja dwóch dowolnych ramek (w tym: ramek złożonych). |
Oto przepis.
Aby ustalić ramkę typu xxx dla komponentu comp należy wywołać
odpowiednią statyczną metodę klasy BorderFactory i zwróconą ramkę podac jako
argument metody setBorder z klasy JComponent:
comp.setBorder(BorderFactory.createXxxBorder(args));
np. ramka liniowa w kolorze niebieskim:
comp.setBorder(BorderFactory.createLineBorder(Color.blue)
Ciekawym rozwiązaniem są ramki złożone, powstające z nałożenia na siebie dwóch ramek.
Możemy przy tym wykorzystać już istniejącą ramkę dowolnego J-komponentu.
Np. dodanie ramki liniowej w kolorze niebieskim do już istniejącej ramki komponentu comp wygląda tak:
comp.setBorder( BorderFactory.createCompoundBorder(
BorderFactory.createLineBorder(Color.blue),
comp.getBorder());
Ale na tym nie koniec: twórcom Swingu przyświecała idea (jeszcze większej) otwartości.
Nic zatem nie stoi na przeszkodzie, by zbudować całkiem własną, całkiem oryginalną ramkę okalającą dowolny J-komponent.
Aby to zrobić, należy:
- stworzyć nową klasę, dziedziczącą klasę AbstractBorder (która abstrakcyjnie implementuje interfejs Border)
- dostarczyć implementacji metod interfejsu Border:
- paintBorder - rysującej ramkę
- getBorderInsets - zapewniającej odpowiednie rozmiary dla ramki
Rysunek pokazuje wybrane rodzaje ramek:
Ramki widoczne na rysunku tworzone są za pomocą następujących odwołań (inicjacji
pól klasy, nazwy zmiennych odpowiadją nazwom ramek pokazanym na rysunku):
import javax.swing.border.*;
//...
private Border
empty = BorderFactory.createEmptyBorder(),
blackLine = BorderFactory.createLineBorder(Color.black),
redLine = BorderFactory.createLineBorder(Color.red),
titled1 = BorderFactory.createTitledBorder("Tytuł"),
titled2 = BorderFactory.createTitledBorder(redLine,"Tytuł"),
etched = BorderFactory.createEtchedBorder(),
etchedC = BorderFactory.createEtchedBorder(Color.red, Color.yellow),
raisedBevel = BorderFactory.createRaisedBevelBorder(),
loweredBevel = BorderFactory.createLoweredBevelBorder(),
matteColor = BorderFactory.createMatteBorder(5, 10, 5, 15, Color.blue),
matteIcon = BorderFactory.createMatteBorder(24,24,24,24,
new ImageIcon("Volume24.gif")),
softBevR = new SoftBevelBorder(SoftBevelBorder.RAISED),
softBevL = new SoftBevelBorder(SoftBevelBorder.LOWERED),
compound1 = BorderFactory.createCompoundBorder(softBevR, softBevL),
compound2 = BorderFactory.createCompoundBorder(redLine, compound1),
compound3 = BorderFactory.createCompoundBorder(
BorderFactory.createLineBorder(Color.blue), matteIcon);
Podpowiedzi (tooltips, fly-over-help)
Każdy J-komponent może mieć przypisany tekst pomocy, który pojawia się w
postaci "dymku" po wskazaniu myszką komponentu i odczekaniu pewnego okresu
czasu.
Aby ustalić tekst pomocy wystarczy użyć metody setToolTipText(String) z klasy JComponent.
Np.
JButton b = new JButton("Commit");
b.setTollTipText("Zapisz zmiany w bazie danych");
Uwaga: tekst podpowiedzi może być tekstem HTML, a więc np. składającym
się z wielu wierszy, w różnym piśmie, o różnych kolorach pisma i tła, zawierającym
obrazki.
Bardziej zaawansowane działania z "dymkami" pomocy mogą polegać na:
- ustalaniu lokalizacji napisu
- ustalaniu różnych napisów dla różnych obszarów komponentu
Ikony
Wiele (choć nie wszystkie) komponentów Swingu może zawierać ikony, dlatego przedstawimy je teraz.
Ikona jest definiowana jako obraz o zadanych rozmiarach
Do pracy z ikonami służy interfejs Icon ("ikonowe" argumenty konstruktorów różnych klas oraz metod setIcon... są właśnie tego typu).
Interfejs Icon dostarcza m.in. następujących metod:
int getIconWidth()
int getIconHeigh()
void paintIcon(Component c, int x, int y)
Klasa ImageIcon implementuje interfejs Icon i umożliwia synchroniczne
ładowanie obrazów z plików (w momencie tworzenia obiektu ImageIcon z podanym
argumentem - nazwą pliku z obrazem, program jest wstrzymywany do chwili,
gdy obraz nie będzie załadowany i gotowy do wyświetlenia). Możemy korzystać
z plików graficznych w formatach GIF, JPEG i PNG.
Np. aby wyświetlić obraz z pliku Obraz.jpg na etykiecie JLabel wystarczy napisać
JFrame f = new JFrame(...);
...
Jlabel l = new JLabel(new ImageIcon("Obraz.jpg"));
f.getContentPane.add(l);
Warto pamiętać, że ikony mają zadane rozmiary: obraz z pliku jest wyświetlany
w oryginalnych rozmiarach. Jeśli chcemy je zmienić, to musimy pobrać obraz
"z ikony" (metoda getImage() z klasy ImageIcon) i go reskalować metodami klasy Image lub poprzez drawImage(...) z klasy Graphics.
Ikona niekoniecznie musi być związana z plikiem graficznym. Może powstawać dynamicznie.
Tworzenie "rysowanych" ikon wymaga zdefiniowania własnej klasy implementującej
interfejs Icon i dostarczenia w niej definicji metod interfejsu Icon. Rysowaniem
ikony zajmuje się tu metoda paintIcon, którą musimy odpowiednio zdefiniować.
Będzie ona automat6cznie wywołana przez JVM, gdy ikona ma być wykreślona.
Zaletą "rysowanych ikon" jest to, że ich rozmiary, kształty, kolory mogą
zmieniać się dynamicznie w zależności od kontekstu komponentu, na którym są
wyświetlane.
Już za chwile zobaczymy prosty przykład takiego rozwiązania.
12. Etykiety i przyciski
Etykiety
są komponentami opisowymi o następujących właściwościach:
- możliwe
jest umieszczania ikon obok (lub zamiast) tekstu etykiety
(tekst lub ikonę podajemy jako argument konstruktora, możemy je też
pobrać za pomoca metod getText() i getIcon()
oraz ustalić za pomocą metod setText(String),
setIcon(Icon),
- możliwe
jest kontrolowanie położenia ikony i/lub tekstu w obszarze etykiety
w pionie i poziomie (metody setHorizontalAlignment(int),
setVerticalAlignment(int))
- mozliwe
jest kontrolowanie położenia tekstu wobec ikony
(setHorizontalTextPosition(int),
setVerticalTextPosition(int))
- możliwe
jest wyświetlanie tekstów HTML na etykiecie (dzięki czemu, na
przykład, można w prosty sposób zrealizować wielowierszowe
etykiety z fragmentami tekstu w różnych stylach),
- z
każdą etykietę za pomocą metody labelFor(Component)
można skojarzyć komponent ("oznaczany" przez tę etykietę).
Jeśli dodatkowo ustalimy skrót klawiaturowy za pomocą metody
setDisplayedMnemonic(char),
to podany znak będzie w etykiecie podkreślony, a naciśnięcie
klawiszy alt-znak spowoduje przejście fokusu do komponentu
oznaczanego przez etykietę.
Przykładowy program:
import java.awt.*;
import javax.swing.*;
import static java.awt.Color.*;
import static javax.swing.SwingConstants.*;
class Labels extends JFrame {
public Labels() {
setLayout(new GridLayout(0, 2));
Icon i = new ImageIcon("red.gif");
add(creLab("Lab 1", i, BLACK, YELLOW, LEFT, TOP, LEFT, CENTER));
add(creLab("Lab 2", i, WHITE, BLUE, CENTER, CENTER, CENTER, BOTTOM));
add(creLab("Lab 3", i, RED, WHITE, RIGHT, BOTTOM, RIGHT, CENTER));
add(creLab("Lab 4", i, YELLOW, BLACK, LEFT, CENTER, CENTER, TOP));
setDefaultCloseOperation(EXIT_ON_CLOSE);
pack();
setVisible(true);
}
JLabel creLab(String txt, Icon icon, Color fc, Color bc, int halign,
int valign, int htxtpos, int vtxtpos) {
JLabel l = new JLabel(txt);
l.setFont(new Font("Dialog", Font.BOLD, 24));
l.setOpaque(true);
l.setIcon(icon);
l.setBackground(bc);
l.setForeground(fc);
l.setHorizontalAlignment(halign);
l.setVerticalAlignment(valign);
l.setHorizontalTextPosition(htxtpos);
l.setVerticalTextPosition(vtxtpos);
return l;
}
public static void main(String args[]) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new Labels();
}
});
}
}
pokaże na ekranie różne formy pozycjonowania napisów wobec ikon w ramach etykiet:
Warto w nim zwrócić uwagę na drobny szczegół dotyczący statycznego importu interfejsu
SwingConstants. Interfejs ten definiuje różne stałe używane w klasach Swingu
(tak, tak interfejsy też - jak pamiętamy - mogą definiowac stałe, a ten tylko
po to stworzono).
Dzięki statycznemu importowi tego interfejsu w powyższym programie możemy posługiwać
się skróconymi nazwami stałych.
Drugi przykład dotyczy tekstów html na etykietach i wykorzystania konstrukcji labelFor.
Do okna dodajemy nagłówek (etykietę z tekstem HTML) oraz trzy etykiety skojarzone z polami edycyjnymi.
Wciśnięcie alt-n wprowadzi kursor do pola obok etykiety "Nazwisko", wciśnięcie
alt-r - do pola "Rok urodzenia", alt-a - do pola "Adres".
Mnemoniki są pokazane poprzez podkreślenie litery w tekście etykiety.
Oto program, który to realizuje:
import java.awt.*;
import javax.swing.*;
class Labels2 extends JFrame {
JPanel panel = new JPanel(new GridLayout(0, 2));
Labels2() {
String html = "<html><center>Proszę<br>"
+ "<b><font color=red>wpisywać</font></b><br>"
+ "<font color=blue>swoje <b>prywatne</b> dane"
+ "</font></center><br></html>";
JLabel head = new JLabel(html);
head.setHorizontalAlignment(JLabel.CENTER);
add(head, BorderLayout.NORTH);
addLabAndTxtFld("Nazwisko", 'n');
addLabAndTxtFld("Rok urodzenia", 'r');
addLabAndTxtFld("Adres", 'a');
add(panel);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
pack();
setVisible(true);
}
void addLabAndTxtFld(String txt, char mnemonic) {
JLabel l = new JLabel(txt);
l.setHorizontalAlignment(JLabel.RIGHT);
JTextField tf = new JTextField(20);
l.setLabelFor(tf);
l.setDisplayedMnemonic(mnemonic);
panel.add(l);
panel.add(tf);
}
public static void main(String args[]) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new Labels2();
}
});
}
}
Uwaga. Etykiety w AWT (klasa Label) są pozbawione większości właściwości
etykiet swingowych. Mogą zawierać tylko tekst, a kontrola jego wyrównania
może odbywać się tylko w poziomie.
Przyciski
W Swingu mamy cztery
rodzaje przycisków:
- zwykły
: JButton
- podstawowy
przełącznik : JToggleButton
(przycisk dwustanowy o wyglądzie normalnego przycisku)
- znacznik : JCheckBox
- radio-przycisk:
JRadioButton - jak JCheckBox, ale z kropką; służy głownie od prezentacji wyłaczających się opcji.
Podstawowową
funkcjonalność wszystkim klasom przycisków dostarcza klasa
AbstractButton.
Przyciski przełącznikowe mają za podstawę JToggleButton.
Hierarchię klas przycisków pokazuje poniższy schemat.
AbstractButton
|
|
+--- JButton
|
|
+--- JToggleButton
|
|
+--- JCheckBox
|
|
+--- JRadioButton
Przy czym prawie wszystkie
metody do operowania na przyciskach
znajdują się w klasie
AbstractButton.
Do
ciekawszych należą:
- ustalanie
różnych ikon (normalnej, w stanie wciśniętego przycisku, w
stanie zaznaczonego przycisku, w stanie wskazania myszką przycisku
itp.).
- ustalanie
położenia tekstu względem ikony (w pionie i poziomie; stałe
interfejsu SwingConstants: TOP, BOTTOM, CENTER, LEFT, RIGHT),
- symulacja
kliknięcia (metoda doClick()...),
- ustalanie czy
przycisk ma być przyciskiem domyślnym w oknie,
-
ustalenie mnemoniki dla przycisku (litery w tekście na przycisku; wciśnięcie
alt-litera będzie równoznaczne z kliknięciem w przycisk).
Przykładowy program pokazuje kilka ciekawych cech przycisków.
Wykorzystamy w nim własną klasę definiującą ikony, jako kółka o zadanym kolorze,
ew. otoczone ramką, jeśli tak wskazuje argument konstruktora.
package buttons;
import javax.swing.*;
import java.awt.*;
import static javax.swing.SwingConstants.*;
class IconA implements Icon {
private Color color;
private int w = 40;
private boolean frame;
IconA(Color c, boolean frame) {
color = c;
this.frame = frame;
}
public void paintIcon(Component c, Graphics g, int x, int y) {
Color old = g.getColor();
g.setColor(color);
w = ((JComponent) c).getHeight() / 2;
int p = w / 4, d = w / 2;
g.fillOval(x + p, y + p, d, d);
if (frame) g.drawRect(x, y, w - 1, w - 1);
g.setColor(old);
}
public int getIconWidth() {
return w;
}
public int getIconHeight() {
return w;
}
}
public class Butt1 extends JFrame {
private Icon[] icons = { new IconA(Color.yellow, false), // normal
new IconA(Color.blue, false), // over
new IconA(Color.red, true), // pressed
new IconA(Color.black, false), // selected
};
// przycisk, który bedzie programistycznie wciśnięty
private JButton bpre = new JButton("Button - pressed");
Butt1() {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
setLayout(new GridLayout(0, 1, 10, 10));
JButton b = new JButton("Button");
setButt(b, icons, RIGHT, CENTER);
JButton bmov = new JButton("Button - mouse over");
setButt(bmov, icons, LEFT, TOP);
setButt(bpre, icons, CENTER, TOP);
JToggleButton tb = new JToggleButton("ToggleButton - selected");
setButt(tb, icons, CENTER, BOTTOM);
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
pack();
setLocationRelativeTo(null);
setVisible(true);
}
});
bpre.doClick(5000);
}
void setButt(AbstractButton b, Icon[] i, int horPos, int vertPos) {
b.setFocusPainted(false);
b.setIcon(i[0]);
b.setRolloverIcon(i[1]);
b.setPressedIcon(i[2]);
b.setSelectedIcon(i[3]);
b.setHorizontalTextPosition(horPos);
b.setVerticalTextPosition(vertPos);
add(b);
}
public static void main(String args[]) {
new Butt1();
}
}
Tablicę ikon icons wykorzystamy przy ustalaniu atrybutów przycisków
w programie przykładowym. Jego działanie ilustruje rysunek. Każdy z czterech
przycisków ma ustalone ikonki dla każdego ze swoich stanów. Każdy przycisk
na rysunku znajduje się w innym stanie (dzięki czemu widzimy różne ikonki).
Aby pokazać stan "wciśnięty" przy jednoczesnym wskazywaniu myszką innego
przycisku (stan "wskazanie kursorem myszki") symulowaliśmy kliknięcie w przycisk
za pomocą metody doClick(int pressTime) z argumentem określającym jak długo
(w milisekundach) przycisk ma być w stanie "wciśniety").
Warto zauważyć, że ikony dynamicznie zmieniają swoje rozmiary przy zmianie
rozmiaru komponentów (np. na skutek zmiany rozmiaru okna) - tak zaimplementowano
interfejs Icon w klasie IconA.
Przycisk
JToggleButton - rudymentarny
przełącznik o dwóch stanach (zaznaczony - nie zaznaczony) -
jest nie tylko bazą dla innych przełączników, ale sam w sobie
może być ciekawym rozwiązaniem graficznym.
Pochodzące
od niego JCheckBox i JRadioButton
oferują inne rodzaje zaznaczeń - znaczniki i radio-przyciski (i oczywiście wszystkie
cechy AbstractButton np. różne ikonki)
Do
grupowania przycisków typu "radio" (a więc wzajemnie
wykluczających się zaznaczeń) służy klasa ButtonGroup.
Po stworzeniu obiektu tej klasy, za pomocą metody add
dodajemy do niego przyciski JRadioButton. Od tej chwili tylko
jeden z dodanych przycisków
może być aktualnie "zaznaczony".
Uwaga:
ButtonGroup nie jest kontenerem!
Dla każdego rodzaju przycisku za pomocą metody
setMnemonic(int c);
możemy ustalić mnemonikę czyli literę (znak), który wciśnięty wraz z klawiszem alt spowoduje "kliknięcie" w przycisk.
Znak ten jest podkreślony w tekście na przycisku.
13. Menu rozwijalne
Hierarchię klas definiujących Swingowe menu pokazuje rysunek, a ich opis przedstawia tabela.
Tabela. Klasy menu
JMenuBar |
pasek menu, dodawany do okna ramowego JFrame lub JInternalFrame lub do apletu (JAplet) |
JSeparator |
Komponent służący do rozdzielania menus na pasku lub elementów (opcji) menu |
JMenu |
Menu rozwijalne. Zawiera elementy, z których każdy może być też menu rozwijalnym. |
JPopupMenu |
Menu kontekstowe (o nim w następnych wykładach) |
JMenuItem |
Element menu (opcja) |
JCheckBoxMenuItem |
Element menu - znacznik (zaznaczony-nie) |
JRadioButtonMenuItem |
Element menu - radio-przycisk (tylko jeden w danej grupie może być zaznaczony;
do grupowania służy omówiona wcześniej klasa ButtonGroup) |
Elementy menu rozwijalnego tworzymy za pomoca konstruktorów klasy JMenuItem i dodajemy do menu za pomocą metody add(JMenuItem) z klasy JMenu
. W klasie JMenu jest dodatkowo wiele innych metod, umożliwiających dodawanie,
usuwanie i przeglądanie komponentów menu rozwijalnego.
To, że klasa JMenu (menu rozwijalne) jest pochodna od JMenuItem
(opcja menu) ma tę ważną konsekwencję, że elementem jakiegokolwiek menu
może być menu rozwijalne, co automatycznie zapewnia możliwość łatwego tworzenia
menu wielopoziomowych.
Menu rozwijalne musi być dodane do paska menu. Pasek ten jest obiektem klasy JMenuBar.
Tworzymy go za pomocą konstruktora JMenuBar() , a za pomocą metod klasy JMenuBar możemy dodawać do niego różne menu rozwijalne (add(JMenu)), przeglądać jego zawartość (int getMenuCount(), JMenu getMenu(int pozycjaOpcji)), usuwać menus (metody remove).
Aby pasek meny pojawił się w oknie - należy użyć metody setJMenuBar(JMenuBar) z klasy JFrame.
Procedura tworzenia menu rozwijalnego
|
1. Utworzyć jedno lub więcej menu rozwijalne
|
JMenu m = new JMenu("NazwaMenu");
|
2. Tworzyć elementy i dodawać je do menu m
|
JMenuItem mi = new JMenuItem("NazwaOpcji");
m.add(mi);
|
3. Jeśli chcemy mieć kolejny poziom, to:
- zamiast elementu (JMenuItem) stworzyć menu (JMenu)
- dodać do niego odpowiednie elementy
- dodać ten poziom menu do menu wyższego poziomu jako element
|
JMenu m2 = new JMenu("NazwaM2");
JMenuItem mmi = new JMenuItem(...)
m2.add(mmi);
m.add(m2);
|
4. Stworzyć pasek menu i dodać do niego wszystkie menus 1-go poziomu
|
JMenuBar mb = new JMenuBar();
mb.add(m);
|
5. Dla danego okna ramowego ustalić pasek menu
|
JFrame f ... ;
f.setJMenuBar(mb);
|
Traktowanie
elementów menu (JMenuItem) jak przycisków umożliwia
umieszczanie w ich opisie tekstów i/lub ikon,kontrolowanie
pozycji tekstu wobec ikony, ustalanie mnemonik etc.
Jeśli
dla danego menu lub elementu menu ustalimy za pomocą metody
setMnemonic(int)
kod znaku, który ma być mnemoniką, to wciśnięcie alt-znak na
klawiaturze spowoduje wybór tego elementu menu,
ale tylko wtedy, gdy dany element jest widoczny.
Mocniejszym narzędziem wyboru są akceleratory
- sekwencje klawiszy, których wciśnięcie na klawiaturze
powoduje wybór opcji menu, niezależnie od tego czy jest ona
widoczna.
Akceleratory ustalamy za pomocą metody
setAccelerator(KeyStroke). Klasa KeyStroke opisuje klawisze, a jej statyczna metoda getKeyStroke(String) pozwala na uzyskanie klawisza z podanego opisu.
Przyklad.
Do tworzenia elementów menu (opcji) wykorzystano następującą metodę:
JMenuItem mi(String t, Icon i, int mnemo, String accel) {
JMenuItem mi = new JMenuItem(t, i); // element menu z tekstem t oraz ikoną i
mi.setMnemonic(mnemo); // ustalenie mnemoniki
mi.setAccelerator(KeyStroke.getKeyStroke(accel)); // ustalenie akceleratora
return mi;
}
Aby stworzyć menu rozwijalne "File" stosujemy następujący kod, wykorzystujący pokazaną metodę mi:
JMenu menu = new JMenu("File");
JMenuItem mi = mi("New", newIcon, 'n', "control N");
menu.add(mi); // dodanie opcji do menu
menu.add(mi("Open", openIcon, 'o', "control O" ));
menu.addSeparator(); // dodaje separator
menu.add(mi("Exit", null, 'x', "control X"));
Zmienne oznaczające ikony wskazują tu na odpowiednie obiekty klasy ImageIcon (wczytany obrazek GIF lub JPEG).
To, że klasa JMenu (menu rozwijalne) jest pochodna od JMenuItem (opcja menu) ma jeszcze inną ważną konsekwencję:
Samo menu rozwijalne - co do opisu - ma cechy JMenuItem, a zatem AbstractButton - i w dalszej kolejności - JComponent.
Korzystając z tej cechy, możemy np. napisać:
JMenu menu1 = new JMenu("Run");
menu1.setIcon( new ImageIcon("red.gif"));
menu1.setMnemonic('r');
menu1.setHorizontalTextPosition(SwingConstants.LEFT);
W ten sposób uzyskamy menu rozwijalne z opisem w postaci tekstu "Run" i ikonką
"red.gif". Tekst znajdzie się z lewej strony ikonki. Wybór tego menu może
być dokonany przez alt-r.
Menu rozwijalne (JMenu) może być dodane do paska menu (JMenuBar), a ten z kolei przyłączony do okna ramowego.
JMenuBar mb = new JMenuBar();
mb.add(menu); // menu "File"
mb.add(menu1); // menu "Run"
f.setJMenuBar(mb); // f - oznacza obiekt typu JFrame
Ale cóż to jest "menu-bar"? Pasek menu (klasa JMenuBar), który jest J- komponentem, a więc i kontenerem (!). Domyślny rozkład w tym kontenerze - to BoxLayout (horizontal). Oczywiście, możemy go zmienić (ciekawe efekty).
Jednak nawet korzystając z tego rozkładu mamy wiele możliwości regulowania położenia opcji (menu rozwijalnych) na pasku menu.
Ilustruje to poniższy fragment programu oraz rysunek:
JSeparator sep = new JSeparator(SwingConstants.VERTICAL);
sep.setMaximumSize(new Dimension(100,100));
mb.add(sep);
JMenu menu2 = new JMenu("View");
menu2.setIcon(new ImageIcon("green.gif"));
mb.add(menu2);
mb.add(Box.createHorizontalGlue());
JMenu menu3 = new JMenu("Help");
mb.add(menu);
Obiekt klasy JSeparator nie ma ograniczonych maksymalnych rozmiarów. Zatem
BoxLayout będzie go rozszerzał, połykając wolne miejsce. To dobre zachowanie.
Możemy go - kiedy indziej - wykorzystać. Ale tu chcemy, by separator miał
ograniczone rozmiary - stąd wywołanie metody setMaximumSize na rzecz separatora.
Z kolei - skoro mamy BoxLayout w JMenuBar - możemy skorzystać z kleju. Dodanie
kleju po menu2 ("View") powoduje, iż menu "Help" będzie zawsze wyrównane
do prawej strony paska menu.
14. Dialogi
Klasa JDialog dostarcza metod tworzenia dialogów "od podstaw".
Jednak wygodniejszym często sposobem oprogramowania dialogów jest wykorzystanie (znanej nam już po częsci) klasy JOptionPane. Klasa ta umożliwia dość łatwe i dość elastyczne tworzenie dialogów modalnych.
"Łatwe "oznacza, iż mamy w tej klasie sporo gotowych metod, które wołane
z jednego wiersza programu ukazują najczęściej używane dialogi.
"Elastyczne" mówi o tym, że przy drobnym wysiłku klasa JOptionPane może nam
posłużyć do tworzenia dowolnie konfigurowalnych dialogów.
Dowolna konfiguracja dialogów za pomocą użycia statycznych metod klasy JOptionPane
jest bardzo łatwa, ale trochę pracochłonna. Cały problem sprowadza się do
prześledzenia wielu ustaleń przyjętych przez twórców tej klasy. Nie będziemy
się tym zajmować szczegółowo - od tego jest dokumentacja (proszę sięgać!).
Przyjrzymy się jednak kilku przykładom użycia statycznych metod tej klasy,
wykraczającym poza nasze dotychczasowe doświadczenia z - używanymi w wielu
prezentowanych wcześniej programach - dialogami.
Przykład 1.
W dialogach informacyjnych (messageDialog) możemy ustalić tytuł dialogu oraz
ikonę pokazywaną obok komunikatu. Komunikat może być tekstem HTML.
Oto fragment programu, realizujący pokazany na rysunku dialog.
String msg = "<html><center><b>Proszę<br>"+
"<font color=blue>WYŁĄCZYĆ KOMPUTER</font><br>" +
"bo się<font color=red> przegrzał</font></b></center></html>";
ImageIcon ikona = new ImageIcon("monitor.jpg");
JOptionPane.showMessageDialog(null, // okno-własciciel
msg, // komunikat
"Uwaga! uwaga!", // tytuł
JOptionPane.WARNING_MESSAGE, // rodzaj komnunikatu
ikona // ikona
);
Przykład 2.
W dialogach dla wprowadzania danych (inputMessage) możemy podać tytuł, własną
ikonę, ograniczyć możliwości wprowadzania informacji do wybranego zestawu
oraz ustalić inicjalną zawartość pola edycyjnego dialogu lub inicjalnyy wybór
z wielu możliwości.
Poniższy fragment programu prosi użytkownika o wprowadzenie dwóch liczb całkowitych,
a jeśli przy wprowadzaniu nastąpi pomyłka (np. to nie będą dwie liczby, albo
któreś ze słów nie daje się zinterpretować jako liczba całkowita) ponawia
dialog z pokazanym błędnie wprowadzonym tekstem (jak na rysunku obok).
String val = "",
msg = "Podaj dwie liczby całkowite";
while ((val = JOptionPane.showInputDialog(msg, val)) != null) {
StringTokenizer st = new StringTokenizer(val);
boolean error = false;
if (st.countTokens() != 2) error = true;
try {
int a = Integer.parseInt(st.nextToken());
int b = Integer.parseInt(st.nextToken());
} catch (Exception exc) { error = true; }
if (error) msg = "<html>Wadliwe dane. " +
"Podaj <font color=red>DWIE liczby CAŁKOWITE</font></html>";
else break;
}
Drugi fragment za pomoca dialogu wejściowego (inputDialog) pozwala wybrać
jedną z możliwości prezentowanych na liście rozwijalnej. Możliwości podajemy
jako elementy tablicy napisów, możemy przy tym zaznaczyć, która z możliwości
będzie inicjalnie wybrana (tu - napis "Polska").
String[] mozliwosci = { "Polska", "Czechy", "Hiszpania", "Tunezja" };
String ans = (String) JOptionPane.showInputDialog(
null, // okno-rodzic
null, // komunikat
"Wybierz kraj", // tytuł
JOptionPane.QUESTION_MESSAGE, // typ komunikatu
new ImageIcon("cross.gif"), // ikona
mozliwosci, // mozliwosci do wyboru
mozliwosci[0] // inicjalnie wybrana
);
Przykład 3.
Uogólnieniem wszystkich rodzajów dialogów jest tzw. dialog opcyjny (optionDialog).
Za jego pomocą można przedstawić dowolny inny typ dialogu, przy czym mamy
wiele możliwości konfiguracyjnych.
Poniższy fragment programu pokazuje jak w prosty sposób stworzyć dialog,
w którym wyboru jakichś działań dokonujemy za pomocą przycisków (tym razem
z napisami mówiącymi wiele więcej niż Ok, Cancel, Yes lub No).
String[] opcje = { "Przywróć", "Zapisz", "Kompiluj"};
int rc = JOptionPane.showOptionDialog(
null, // okno
"Co mam zrobić?", // komunikat
"Program zmodyfikowany", // tytuł
JOptionPane.DEFAULT_OPTION, // rodzaj przycisków u dołu (tu nieważny)
JOptionPane.QUESTION_MESSAGE,// typ komunikatu (standardowa ikona)
null, // własna ikona (tu: brak)
opcje, // własne opcje - przyciski
opcje[1]); // domyślny przycisk
System.out.println("Wybrałeś " + rc + " " + opcje[rc]);
Ciekawą właściwością dialogów jest to, że w miejscu komunikatu, wartości
do wprowadzenia oraz opcji - można podawać dowolne obiekty, nie tylko łańcuchy
znakowe.
Podane łańcuchy znakowe (Stringi) zostaną odpowiednio przekształcone (np.
z napisów-opcji powstaną przyciski), ale równie dobrze możemy podać komponenty
Swingu - i pojawią się one w dialogach, a co więcej będziemy mogli korzystać
z ich właściwości. Pokazuje to przykład, w którym w dialogu umieszcamy edytor
tekstowy, etykietę i pole edycyjne (hipotetyczny prościutki interfejs do
wysyłania emaili - zob. rysunek ).
Oto fragment programu który to realizuje: widzimy tu, że istotnie mamy dostęp
i możemy korzystać z właściwości komponentów umieszczonych w dialogu.
JTextArea list = new JTextArea(10,20); // edytor
list.setBorder(BorderFactory.createLineBorder(Color.blue));
JTextField adres = new JTextField(10); // pole tekstowe
// tablica opcji
Object[] opcje = { new JLabel("Adres"), // etykieta
adres, // pole tekstowe
"Wyślij" // z "Wyślij" powstanie przycisk
};
int rc = JOptionPane.showOptionDialog(
null,
list, // komunikatem może być dowolny obiekt!
"Treść listu", // tytuł
JOptionPane.DEFAULT_OPTION,
JOptionPane.PLAIN_MESSAGE,
null,
opcje, // opcje - to też dowolne obiekty!
opcje[2] // domyślnie - przycisk "Wyślij"
);
if (rc == 2) {
String tekstListu = list.getText();
String adr = adres.getText();
System.out.println("wysyłam na adres " + adr + " :");
System.out.println(tekstListu);
}
Możliwość podawania dowolnych obiektów jako elementów dialogu dotyczy nie
tylko dialogu opcyjnego, ale wszystkich innych rodzajów dialogów (w dokumentacji
metod widzimy typ Object lub Object[]).
Możemy np. napisać metodę, która w dialogu informacyjnym oprócz normalnych
komunikatów pokazuje radio-przyciski i pozwala na wybór za ich pomocą jednej
z opcji. Przy okazji zobaczymy, że istnieje trzeci sposób (oprócz użycia
znaków nowego wiersza lub zastosowania HTML) podziału komunikatu na wiersze
- podanie w miejsce komunikatu tablicy napisów. Naszą metodę - niech nazywa
się choiceMsg - użyjemy w następującym fragmencie programu, zilustrowanym
rysunkiem.
String choiceMsg(String title, // tytuł
String[] normalMsg, // normalne wiersze komunikatów
String[] radioMsg, // radio-przyciski (opcje do wyboru)
Icon icon // ikona
) // metoda zwraca tekst wybranego radio-przycisku
{
Object[] msg = new Object[normalMsg.length + radioMsg.length];
for (int i = 0; i < normalMsg.length; i++) msg[i] = normalMsg[i];
JRadioButton rb[] = new JRadioButton[radioMsg.length];
ButtonGroup bg = new ButtonGroup();
for (int i = 0; i < radioMsg.length; i++) {
rb[i] = new JRadioButton(radioMsg[i]);
bg.add(rb[i]);
msg[normalMsg.length +i] = rb[i];
}
JOptionPane.showMessageDialog(null, msg, title, 0, icon);
for (int i =0; i < rb.length; i++)
if (rb[i].isSelected()) return rb[i].getText();
return null;
}
// ... poniższy fragment pokazuje wywołanie metody choiceMsg
String s = choiceMsg( "Wyjazd nad morze",
new String[] { "Wybierz środek transportu,",
"zaznaczając jedną z opcji",
" "
},
new String[] { "Autokar", "Pociąg", "Samolot" },
new ImageIcon("bview2.jpg")
);
System.out.println("Wybrałeś: " + s);
Tyle o klasie JOptionPane (jeszcze raz warto zachęcić do sięgniecia do jej dokuimentacji).
Dosyć specyficznymi dialogami są w Swingu: dialog wyboru pliku (JFileChooser) i dialog wyboru koloru (JColorChooser).
Obie klasy konstytuują komponenty lekkie. Zatem oba "dialogi" (raczej elementy
GUI) mogą być dodane do dowolnego kontenera i obsługiwane przez nasłuchiwanie
odpowiednich zdarzeń.
Ponieważ jednak utarło się traktować tego typu wybory jako dialogi, Swing
dostarcza metod, które dla wyboru plików i dla wyboru kolorów używają standardowego
(synchronicznego i modalnego) dialogu.
Odpowiednie metody znajdują się w klasach JFileChooser i JColorChooser.
Dla wyboru plików należy:
- utworzyć obiekt (new JFileChooser(...))
- wywołać odpowiednią metodę pokazującą odpowiedni dialog: (showOpenDialog dla otwarcia pliku lub showSaveDialog - zapis pliku). Można też użyć metody showDialog
, podając typ dialogu (otwarcie czy zapis), co znajdzie odzwierciedlenie
w jego tytule oraz tekście na przycisku. Wyniki tych metod (typu int) mówią
nam o tym czy użytkownik wybrał jakiś plik, czy w ogóle zrezygnował z wyboru.
Dalsze informacje (np. plik, nazwa etc.) , możemy uzyskać odpytując dany
obiekt typu JFileChooser
Dialog wyboru koloru korzysta ze statycznej metody klasy ColorChooser.
Sekwencja jest następująca:
- wywołanie metody JColorChooser.showDialog(...) jest synchroniczne (wstrzymuje działanie aplikacji)
- metoda zwraca referencję do wybranego obiektu typu Color.
Oczywiście, jest wiele innych sposobów wyboru pliku (za pomocą klasy JFileChooser)
oraz koloru (JColorChooser). Tutaj poprzestaniemy na - omówionych - podstawowych
i najprostszych.
Fragment programu ilustruje użycie dialogów wyboru pliku i wyboru koloru.
String[] opcje = { "Otwarcie pliku", "Zapis pliku", "Wybór koloru" };
JLabel msgLabel = new JLabel("Wybierz dialog"); // etykieta dialogu
msgLabel.setHorizontalAlignment(JLabel.CENTER);
msgLabel.setOpaque(true);
msgLabel.setBackground(Color.yellow);
msgLabel.setBorder(BorderFactory.createLineBorder(Color.red));
JFileChooser fc = new JFileChooser(new File(".")); // dialog plikowy
// z bieżącym katalogiem
String approveButt = ""; // tekst na przycisku dialogu plikowego
int rc = 0;
while (rc != -1) {
rc = JOptionPane.showOptionDialog(null,
msgLabel,
"Test chooserów",
JOptionPane.DEFAULT_OPTION,
JOptionPane.QUESTION_MESSAGE,
null,
opcje,
opcje[0]
);
switch (rc) {
case 0 :
case 1 : approveButt = opcje[rc];
int retVal = fc.showDialog(null, approveButt);
if (retVal == JFileChooser.APPROVE_OPTION)
System.out.println(approveButt + " " + fc.getSelectedFile());
break;
case 2 : Color nc = JColorChooser.showDialog(null,
"Wybierz kolor",
msgLabel.getBackground() // inicjalny kolor
);
if (nc != null) {
System.out.println("Wybrany kolor: " + nc);
msgLabel.setBackground(nc); // zmiana koloru etykiety
}
break;
default: break;
}
}
Dialog opcyjny tworzony przez ten program daje możliwość wyboru rodzaju dialogu:
Zwróćmy uwagę, że komunikat jest w tym dialogu przedstawiony jako etykieta z żółtym tłem w ramce (msgLabel).
Wybierając którąś z opcji otwarcie lub zapis pliku może wyświetlić dialog plikowy, np.
Wybierając opcję "Wybór koloru" otwieramy JColorChooser z inicjalnie wybranym
kolorem zgodym z kolorem tła etykiety msgLabel. Kolor wybrany w dialogu JColorChooser
zostanie następnie ustalony jako kolor tła etykiety dialogu opcyjnego (msgLabel),
np.
Wybór kolorów w dialogu JColorChooser zorganizowany jest w kilku przekrojach:
bezpośredniego wyboru z palety (zakładka Swatches), wyboru wg RGB (zakładka
RGB) oraz wyboru wg. HSB (hue-saturation-brightness). Poniżej panelu wyboru
(zorganizowanego jako"panel zakładkowy" - JTabbedPane) znajduje się panel
podglądu (Preview) , który natychmiast pokazuje wszelkie efekty wybranego
koloru.
Ilustrują to kolejne rysunki.
Podsumujmy teraz podstawowe informacje o dialogach:
- JOptionPane zawiera proste metody tworzenia dialogów informacyjnych,
ostrzegawczych, informacji o błędach. Ponadto może dawać dialogi dostosowane
do naszych ustaleń (np. teksty i ikony na przyciskach).
- Obiekty typu JFileChooser oraz JColorChooser:
- mogą być wbudowane w dowolne GUI (niekoniecznie muszą być prostymi dialogami)