8. Komponenty GUI II


W tym wykładzie zostaną omówione kontenery i rozkłady - istotne koncepcje programowania GUI w Javie. Przedstawimy także najprostsze komponenty Swingu oraz gotowe komponenty dialogowe.


8.1. 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 nie możemy tego robić. 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 wygląda tak:

     cp.add(comp); // z ew. dodatkowym argumentem

a usuwanie tak:

    cp.remove(comp);

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


8.2. 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  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.3. Rozkłady

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...
Dla komponentów AWT ustalanie tych rozmiarów odbywa się za pomocą odziedziczenia klasy komponentu i zdefiniowania metod get..., co pokazuje poniższy fragment programu.
class MLabel extends Label {
	
	Dimension minSize = new Dimension(100,100);
	Dimension maxSize = new Dimension(400,400);
	Dimension prefSize = new Dimension(300,300);

	MLabel() { super(); }
	MLabel(String s) { super(s); }

	public Dimension getMinimumSize() { return minSize; }
	public Dimension getMaximumSize() { return maxSize; }
	public Dimension getPreferredSize() { return prefSize; }		
} 

Uwaga: nie wszyscy zarządcy rozkładów biorą te parametry pod uwagę.


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.


r


Warto więc pamiętać, że – jak prawie wszystko w Javie - zarządca rozkładu jest obiektem odpowiedniej klasy.
Musimy więc go tworzyć za pomocą wyrażenia new...

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



Charakterystyki rozkładów FlowLayout, BorderLayout i GridLayout przedstawiono w tabeli.

Tabela. Charakterystyki rozkładów

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 rysunke 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
   Container cp = frame.getContentPane();
   cp.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("Przycisk "+(j+1), icon);
         p.add(b, gborders[j]);
       }
       cp.add(p);
   }
   frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
   frame.pack();
   frame.setVisible(true);
 }
}

Proszę skompilować i uruchomić ten program, a następnie zmieniając rozmiary okna zaobserwować jak zmieniają się rozmiary i położenie komponentów.

Do bardziej zaawansowanych rozkładów należą: GridBagLayout  i CardLayout. Prostym w użyciu, a jednocześnie elastycznym jest rozkład BoxLayout (o którym za chwilę). 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ć doinstalowane.

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 konterze posł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.4. 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
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.

W katalogu samples znajduje się program TBox.java, ktory pozwala na obserwowanie zachowania się komponentów w rozkładzie BoxLayout.
Proszę uruchomić ten program i - zmieniając za jego pomocą - charakterystyki komponentów (wyrównanie, rozmiary) oraz dodając kleje i sztywne obszary - zaobserwować zmiany układu komponentów w rozkładzie BoxLayout.


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


8.6. Szablony aplikacji AWT i Swing (bez obsługi zdarzeń)

Aby ułatwić budowanie programów GUI przedstawiono tu typowe szablony aplikacji AWT i Swing i typowy sposób postępowania:


AWT
SWING
import java.awt.*;
...

class GuiAWT extends Frame {


GuiAWT() {
  // ustalenie tytułu okna
  super("Okno aplikacji");
  // ustalenie rozkładu; jeśli trzeba np:
  setLayout(new FlowLayout());
  // tworzenie komponentów np.
  Label lab = new Label("Etykieta");
  Button b = new Button("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 rozmiarów okna, np.:
  pack();
  // pokazanie okna
  show();
}


  public static void main(String[] args) {
  // w metodzie main tworzymy
  //obiekt naszej klasy
  // i wywołujemy konstruktor
  new GuiAWT();
  }
} // koniec klasy GuiAWT

import java.awt.*;
import javax.swing.*;
...
class GuiSwing extends JFrame {

  // uzyskujemy contentPane okna
  Container cp = getContenPane();

GuiSwing() {
  // ustalenie tytułu okna
  super("Okno aplikacji");
  // ustalenie rozkładu; jeśli trzeba np:
  cp.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.
  cp.add(lab);
  cp.add(b);
  // ustalenie rozmiarów okna, np.:
  pack();
  // pokazanie okna
  show();
}


  public static void main(String[] args) {
  // w metodzie main tworzymy
  //obiekt naszej klasy
  // i wywołujemy konstruktor
  new GuiSwing();
  }
} // koniec klasy GuiSwing

Uwaga: często referencje do komponentów deklarowane są jako pola klasy, by mieć do nich dostęp z różnych metod klasy.



8.7. 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 w kolejnych wykładach.


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.


8.8. Etykiety i przyciski

Etykiety są komponentami opisowymi o następujących właściwościach:

Przykładowy program:
import java.awt.*;
import javax.swing.*;

class Labels extends JFrame implements SwingConstants {

    final Color RED = Color.red,
                BLUE = Color.blue,
                YELLOW = Color.yellow,
                WHITE = Color.white,
                BLACK = Color.black;
	
	public Labels() {
		Container cp = getContentPane();
		cp.setLayout(new GridLayout(0,2));
		Icon i = new ImageIcon("red.gif");
		cp.add(creLab("Lab 1",i, BLACK, YELLOW,
		               LEFT, TOP, LEFT, CENTER));
		cp.add(creLab("Lab 2",i, WHITE, BLUE,
		               CENTER, CENTER, CENTER, BOTTOM));
		cp.add(creLab("Lab 3",i, RED, WHITE,
		               RIGHT, BOTTOM, RIGHT, CENTER));
		cp.add(creLab("Lab 4",i, YELLOW, BLACK,
		               LEFT, CENTER, CENTER, TOP));
		setDefaultCloseOperation(EXIT_ON_CLOSE);
		pack();
		show();
	}

    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[]) {
	  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 implementacji 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 implementacji tego interfejsu w powyższym programie możemy posługiwać się skróconymi nazwami stałych: np. zamiast JFrame.EXIT_ON_CLOSE piszemy EXIT_ON_CLOSE).

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.*;
import java.util.*;
import java.io.*;

class Labels2 extends JFrame {

  Container cp = getContentPane();
  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></html>";
  	JLabel head = new JLabel(html);
  	head.setHorizontalAlignment(JLabel.CENTER);
  	cp.add(head, BorderLayout.NORTH);
  	addLabAndTxtFld("Nazwisko", 'n');
  	addLabAndTxtFld("Rok urodzenia", 'r');
  	addLabAndTxtFld("Adres", 'a');
  	cp.add(panel);
  	cp.setBackground(Color.yellow);
  	setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  	pack();
  	show();
  }

  void addLabAndTxtFld(String txt, char mnemonic)
  {
  	JLabel l = new JLabel(txt);
  	l.setHorizontalAlignment(JLabel.RIGHT);
  	JTextField tf = new JTextField(30);
  	l.setLabelFor(tf);
  	l.setDisplayedMnemonic(mnemonic);
  	panel.add(l);
  	panel.add(tf);
  }
  	

public static void main(String args[]) {
	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.

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

class IconA implements Icon {

    Color color;
    int w = 40;
    boolean frame;

    IconA(Color c, boolean frame) { color = c; this.frame = frame; }

    public void paintIcon(Component c, Graphics g, int x, int y) {
        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);
    }

    public int getIconWidth() { return w; }
    public int getIconHeight() { return w; }

}


class Butt1 implements SwingConstants {


JFrame f = new JFrame();
JComponent cp = (JComponent) f.getContentPane();

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


Butt1() {
   cp.setLayout(new GridLayout(0, 1, 10, 10));
   JButton b = new JButton("Button");
   setButt(b, icons, RIGHT, CENTER);
   JButton bmov = new JButton("Button - mouse over"),
           bpre = new JButton("Button - pressed");
   setButt(bmov, icons, LEFT, TOP);
   setButt(bpre, icons, CENTER, TOP);

   JToggleButton tb = new JToggleButton("ToggleButton - selected");
   setButt(tb, icons, CENTER, BOTTOM);

   f.pack();
   f.show();
   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);
      cp.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.



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


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


8.11. Podsumowanie


W wykladzie poznaliśmy zasady budowy GUI w Javie z wykorzystaniem roznorodnych rozkładów komponentów, a także cechy najprostszych komponentów Swingu.
Możemy już tworzyć proste, estetetyczne graficzne interfejsy użytkownika.  Aby dostarzcyć im funkcjonalności musimy jednak poznać reguły rządzące obsługą zdarzeń. I temu będą poświęcone dwa następne wykłady.

8.12. Zadania i ćwiczenia


Zadanie 1. Testowanie prostych rozkładów

Pięć przycisków z napisami "Przycisk 1" - "Przycisk 5" pokazać w oknie:
a) w układzie BorderLayou
b) w układzie FlowLayout
c) w układzie FlowLayout z wyrównaniem do lewej
d) w układzie FlowLayout z wyrównaniem do prawej
e) w układzie GridLayout jako jeden wiersz
f) jako jedną kolumnę
g) jako tablice (3, 2)

Proszę napisać to jako jedną aplikację, w której sposób układania komponentów
określany jest w dialogu wejściowym poprzez  podanie odpowiedniej litery (A-G).


Zadanie 2. Kombinowanie rozkładów

Napisać aplikację, w oknie której znajdzie się edytor tekstowy (JTextArea) oraz umieszone nad nim
trzy przyciski wyrównane do prawej strony okna. Z napisami A, B, C w piśmie "wytłuszczona kursywa".
Przy zmianach rozmiaru okna mają zmieniać się rozmiary edytora, a przyciski mają zachowywać swoje rozmiary i położenie (tuż nad edytorem - wyrównane do prawej strony)

Pokazuje to rysunek:

r


Zadanie 3. Testowanie rozkładu BoxLayout

Napisać aplikację, która czyta z pliku opisy komponentów, tworzy je i umieszcza kolejno w pionowym rozkładzie BoxLayout.
Każdy wiersz pliku zawiera opis jednego komponentu. Komponentami mogą być:     Label  txt icon border minsize maxsize prefsize align
    gdzie:
    Glue  <- ten napis w wierszu pliku oznacza, że mamy stworzyć klej i dodac go do rozkładu

    RigidArea  height  
    gdzie: height - wysokość obszaru