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.

Przykładowa aplikacja Swing (pokazuje trzy przyciski z obrazkami i tekstem) ilustruje większość z podanych wyżej zasad..

r

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

r

Wnioski, które wynikają z tej hierarchii :



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.


r

Rys. Komponenty Swingu o rozbudowanych możliwosciach w stosunku do AWT (AWT ma swoje odpowiedniki)
Żródło: Magellan Institute Swing Short Course.

r
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

Źródło: SwingSet3,  Sun Microsystem 2008

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


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.: 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):
 voiddrawLine(int x1, int y1, int x2, int y2)
          Rysuje linię prostą pomiędzy punktami  (x1, y1) i (x2, y2) 
 voiddrawOval(int x, int y, int width, int height)
          Rysuje okrąg (elipsę).
 voiddrawRect(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:
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.


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

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

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

r Inne metody klasy FontMetrics pozwalają na rozmieszczanie napisów w pionie.
Mamy tu takie metody jak:
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)


r



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.

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.

r


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


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.

r


Aby ustalić rozkład komponentów musimy:

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)
  1. Komponenty ułożone są w wierszu.
  2. Przy zmianie rozmiarów kontenera rozmiary komponentów nie zmieniają się
  3. Jeśli szerokość kontenera jest za mała, pojawiają się dodatkowe wiersze.
  4. 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
  5. 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)

  1. Komponenty ułożone są "geograficznie": "North", "East", "South", "West", "Center"
  2. Używa się metody cont.add(comp, loc), gdzie loc - napis oznaczający miejsce lub stała całkowitoliczbowa BorderLayout.NORTH, BorderLayout.CENTER, etc.
  3. Komponent dodany w miejscu "Center" wypełnia całe pozostawiane przez inne komponenty miejsce.
  4. 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
  5. Można podać odstępy między komponentami (pion, poziom)
       new BorderLayout(odst_poz, odst_pion)

GridLayout

  1. Siatka (tablica) komponentów
  2. Rozmiary wszystkich komponentów będą takie same
  3. 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.

r

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


W klasie Box zdefiniowano też wygodne metody statyczne do wprowadzania "wypełniaczy" przestrzeni  w rozkładzie BoxLayout. Te wypełniacze to:

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:
Przykładowe rozkłady typu BoxLayout pokazano w tabeli.

Tabela. Przykładowe rozkłady typu BoxLayout

r
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
r
Zmiana wyrównania komponentów po osi X: setAlignmentX(Component.CENTER_ALIGNMENT)
r
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

r
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ą.
r
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:
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:
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)

r


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

r

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.

rZobacz 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. :

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:
  1. rozjaśnia tło tego komponentu (metoda brighter z klasy Color)
  2. przyciemnia tło wszystkich innych zawartych w kontenerze komponentów (metoda darker z klasy Color)
  3. 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:

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: 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: Różnorodność ramek dostarczanych przez Swing jest spora. Tabela opisuje  ich podstawowe typy.

Typ ramki
Znaczenie
Uwagi
empty obszar pustyobramowanie (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:
Rysunek pokazuje wybrane rodzaje ramek:

r


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:

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

1

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.

r

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: 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żą:
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();
  }

}
r1 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.

r

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

r


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

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.

r

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;
  }


r


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.

r 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]);

r 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[]). 

r 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:
Dialog wyboru koloru korzysta ze statycznej metody klasy ColorChooser.

Sekwencja jest następująca: 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:

r

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.

2

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.

1

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.

1


1



Podsumujmy teraz podstawowe informacje o dialogach: