Rozbudowane możliwości Swingu



Zajmiemy się teraz dodatkowymi, rozbudowanymi, ciekawymi możliwościami komponentów Swingu. Nie są one bardzo skomplikowane i na pewno warto je stosować w codziennym programowaniu

1. Architektura okien

 
Prawie wszystkie komponenty Swingu są komponentami lekkimi, co oznacza m.in., że:

mają taki sam wygląd niezależny od platformy systemowej (zależny wyłacznie od wybranego przez programistę "stylu" - "look and feel")

Jednak komponenty lekkie nie mogą istnieć bez jakiegoś komponentu ciężkiego (czyli takiego, który jest realizowany przez odwołanie do natywnego komponentu graficznego API danej platformy systemowej). Taki komponent (choć jeden) musi znaleźć się u początku hierarchii zawierania się komponentów, tak aby komponenty lekkie mogły być na czymś rysowane.
Dlatego w Swingu kontenery najwyższego poziomu (okna i dialogi) zrealizowane są jako komponenty ciężkie (są to jedyne ciężkie komponenty Swingu).

Hierarchię klas realizujących okna w Swingu przypomina rysunek.

r

Jak widać okno ramowe, dialog i aplet są komponentami ciężkimi, bowiem pochodzą od odpowiednich klas AWT.
Klasa JInternalFrame jest natomiast komponentem lekkim. Umożliwia ona tworzenie "okien wewnętrznych" (zawartych w obszarze określonego pulpitu).

Praca z kontenerami najwyższego poziomu (oknami) w Swingu zdecydowanie różni się od  pracy z oknami w AWT ze względu na inną architekturę budowy tych komponentów.



Wszystkie w/w typy okien zbudowane są z części.
Każde okno zawiera przede wszystkim kontener rootPane  (typu JRootPane).
Ten z kolei zawiera kontenery glassPane (szyba - obiekt typu JComponent) i layeredPane (kontener warstwowy, obiekt typu JLayeredPane).
Kontener layeredPane zawiera contentPane (kontener do którego zwykle dodajemy komponenty) oraz ew. pasek menu (JMenuBar).
Zatem struktura okna jest złożona, co pokazują rysunki

r r


Złożoność ta wynika po części ze zmieszania komponentów ciężkich (którymi są same okna) z lekkimi (komponenty Swingu), po części zaś - z chęci dostarczenia programiście metod wyszukanej konstrukcji GUI.

Warstwy

Bezpośrednim potomkiem (w hierarchii zawierania się komponentów) każdego okna jest rootPane, który z kolei ma swoje "dzieci": glassPane i layeredPane.  Ten ostatni dopiero zawiera  contentPane (i ew. pasek  menu).
Kontener layeredPane umożliwia operowanie warstwami komponentów.
W ten sposób możliwe jest zarządzanie nakładaniem się na siebie komponentów (Z-order).

Mamy tu dwa porządki:
Klasa JLayeredPane definiuje wygodne stałe, oznaczające często używane warstwy (tabela) 

Nazwa WartośćOpis
FRAME_CONTENT_LAYERnew Integer(-30000)Na tym poziomie dodawane są contentPane i pasek menu.
DEFAULT_LAYERnew Integer(0)Warstwa domyślna: na tym poziomie dodawane są do layeredPane komponenty, jeśli nie podano inaczej. 
PALETTE_LAYERnew Integer(100)Warstwa pasków narzędzi i palet. 
MODAL_LAYERnew Integer(200)Warstwa dialogów modalnych
POPUP_LAYERnew Integer(300)Warstwa menu kontekstowego
DRAG_LAYERnew Integer(400)Dla przeciągania komponentów (operacja drag).

Taka kolejność zapewnia np. że paski narzędzi będą ponad zwykłymi komponentami, dialog modalny - jeszcze wyżej, menu kontekstowe może pojawić się ponad dialogiem modalnym, a operacja przeciągania komponentów będzie zawsze wizualnie obrazowana.

Można też używać własnych warstw i dodawać do nich komponenty.
Służą temu przeciążone metody add, wywoływane wobec kontenera typu JLayeredPane.

Odniesienie do kontenera layeredPane okna frame możemy uzyskać poprzez:

    JLayeredPane lp = frame.getLayeredPane();

Dodanie komponentu comp do warstwy n (liczba całkowita):

    lp.add(comp, new Integer(n));

Dodanie komponentu comp do warstwy n na pozycji m (liczba całkowita):

    lp.add(comp, new Integer(n), m);

Oprócz tego dostępne są metody klasy JLayeredPane, które dynamicznie zmieniają położenie komponentów w ramach warstwy i  pomiędzy warstwami:

    lp.moveToFront(comp)
    lp.moveToBack(comp);
    lp.setLayer(comp, n);
    lp.setLayer(comp, n, m);




Program na wydruku  pokazuje jak można korzystać z klasy JLayeredPane.
package layers1;

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

public class Layer extends JFrame implements ActionListener {


  private JLayeredPane l = null;

  public Layer() {
    l = getLayeredPane();
    getContentPane().setBackground(Color.white); // widzimy contentPane
    int x = 10, y = 10;
    for (int i = 1; i <= 5; i++) {
      JButton b = new JButton("Przycisk " + i);
      b.setHorizontalAlignment(JButton.CENTER);
      b.setVerticalAlignment(JButton.TOP);
      b.setBounds(x, y, 150, 100);
      b.addActionListener(this);
      l.add(b, new Integer(i));
      x += 30;
      y += 30;
    }
    JButton b = new JButton("P(5,1)");
    b.addActionListener(this);
    b.setHorizontalAlignment(JButton.RIGHT);
    b.setVerticalAlignment(JButton.BOTTOM);
    b.setBounds(x + 50, y, 100, 100);
    b.setBackground(Color.yellow);
    l.add(b, new Integer(5), 1);
    setSize(400, 300);
    setVisible(true);
  }

  public void actionPerformed(ActionEvent e) {
    JComponent c = (JComponent) e.getSource();
    l.moveToFront(c);
  }
  
  public static void main(String[] args) {
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        new Layer();
      }
    });
  }

}


Dodajemy tu pięc przycisków - każdy w innej warstwie (od 1 do 5).
W piątej warstwie dodajemy jeszcze jeden - żółty przycisk  z etykietą P(5,1)  - na pozycji wyższej od już znajdującego się w tej warstwie przycisku (który ma pozycję 0).
Ustalamy lokalizację przycisków tak, by wzajemnie nakładały się na siebie (zob. rys.); wyższe numery warstw oznaczają "bliżej nas", wyższy numer pozycji w warstwie powoduje schowanie przycisku P(5,1) pod przyciskiem Przycisk 5. Obsługa kliknięcia w przycisk powoduje wywołanie metody moveToFront().
Metoda ta działa tylko wobec przycisków znajdujących się w tej samej warstwie zmieniając ich położenie (przesuwając dany przycisk na pierwszy plan). Po kliknięciu w przycisk P(5,1) znajdzie się on na wierzchu.

r 















 





Warstwy można wykorzystać np. przy tworzeniu przezroczystych przycisków, nakładających się na inne komponenty.


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

r public class Opaq {

   public static void main(String[] args) {
     JFrame f = new JFrame();
     f.setDefaultCloseOperation(3);
     JLabel img = new JLabel(
                                 new ImageIcon("pool2.jpg"));
     f.getContentPane().add(img);
     JButton b = new JButton("Zrób coś");
     b.setFont(new Font("Dialog", Font.BOLD, 16));
     b.setForeground(Color.red);
     b.setOpaque(false);
     b.setBounds(10,10, 100, 50);

     Border rbevel = BorderFactory.createRaisedBevelBorder(),
            lbevel = BorderFactory.createLoweredBevelBorder();

    

      b.setBorder(BorderFactory.createCompoundBorder(rbevel,lbevel));
     f.getLayeredPane().add(b);
     f.pack();
     f.setVisible(true);
    }
}


Szyba

Innym kontenerem zawartym w rootPane jest   "szyba", przykrywająca cały rootPane.
Inicjalnie szyba jest niewidoczna.
Możemy ją uaktywnić poprzez uwidocznienie (setVisible(true)).
Wtedy oddziela ona nas od okna.

Na szybie możemy:

Dostęp do szyby uzyskujemy za pomocą metody getGlassPane();

    Component g = frame.getGlassPane();
    g.setVisible(true); // w tej chwili szyba oddziela nas od okna.


Możemy też stworzyć własną szybę.
Jest to szczególnie użyteczne wtedy gdy na szybie chcemy coś rysować (zatem powinniśmy odziedziczyć klasę JComponent i dostarczyć w naszej klasie metody paintComponent()).
"Własną" szybę podstawiamy na miejsce standardowej za pomocą metody setGlassPane().
Prosty  przykład zastosowania szyby poznamy przy okazji przykładu okien wewnętrznych.

2. Okna wewnętrzne


Okna wewnętrzne (JInternalFrame) są lekkimi komponetami o funkcjonalności okien ramowych.
Podstawowe różnice wobec zwykłych okien ramowych (JFrame):
Ponieważ okna wewnętrzne zawarte są zawsze w jakimś kontenerze, to stanowią one okna na wirtualnym pulpicie (nie mogą "wyjść" poza pulpit).

Pulpitem (kontenerem zawierającym okna wewnętrzne) jest zwykle obiekt typu JDesktopPane, choć może to być i jakiś inny kontener.

JDesktopPane zapewnia jednak pewne dodatkowe metody (np. uzyskanie listy okien na pulpicie).


Przy tworzeniu obiektów typu JInternalFrame możemy podać (w postaci wartości boolowskich) czy okno ma właściwości:
Właściwości te można także pobierać za pomocą metod is.. i ustalać za pomocą metod set..
Przy minimalizacji okna na pulpicie (kontenerze w którym jest zawarte okno) pojawia się ikonka przedztawiająca zminimalizowane okno. Ikonę tę możemy ustalić za pomocą odpowiedniej metody set..., jak również możemy ustalić ikonę pojawiającą się w górnym lewym rogu okna.

Przedstawiony dalej przykład użycia okien wewnętrznych powraca do tematyki wykorzystania JLayeredPane.
Zauważmy: JDesktopPane jest klasą pochodną od JLayeredPane.
Zatem okna wewnętrzne używające JDesktopPane zawsze korzystają z "warstwowości".
Przy okazji zobaczymy jak od JDesktopPane uzyskać listę okien i jak można poslugiwać się szybą.
Funkcje prezentowanej dalej drobnej aplikacji ilustują rysunki.

1

Przyciski "To front" i "To back" zmieniaja uporządkowanie okien po osi Z, przycisk z napisem HTML "Active glass..." uwidacznia szybę. Ponadto kliknięcie prawym klawiszem myszki na pulpicie otworzy listę okien wewnętrznych (również tych niewidocznych). Z listy możemy wybrać okno do schowania lub ponownego otwarcia, jak również dodać nowe okno do pulpitu.

r

Kliknięcie w przycisk "Active GLASS..." uwidoczni szybę. Od tej chwili interakcja z aplikacją za pomoca kliknięć w pulpit będzie przechwytywana na szybie i klinięcia będą rysowac na niej czerwone kropki.

r

W tej chwili mamy już wystarczającą wiedzę, by zrozumieć tekst programu. Dlatego jego analizę pozostawiam jako samodzielne ćwiczenia.

package internframes;

import java.awt.*;
import java.awt.event.*;

import javax.swing.*;
import java.util.*;

public class InternalWin extends JFrame implements ActionListener {

  private static ImageIcon toFront = 
          new ImageIcon(InternalWin.class.getResource("images/arrnorth.gif"));
  private static ImageIcon toBack = 
          new ImageIcon(InternalWin.class.getResource("images/arrsouth.gif"));

  private JDesktopPane desk = new JDesktopPane();
  private Component glass;
  private final int MAXC = 4;

  InternalWin() {
    super("Desktop");

    // Menu kontekstowe będziemy dynamicznie tworzyć tuz przed pokazaniem 
    desk.addMouseListener(new MouseAdapter() {
      
      public void mousePressed(MouseEvent e) {
        createAndShowPopup(e);
      }

      public void mouseReleased(MouseEvent e) {
        createAndShowPopup(e);
      }
      
      void createAndShowPopup(MouseEvent e) {
        if (e.isPopupTrigger()) {
          int x = e.getX(), y = e.getY();
          JPopupMenu pm = makePopup(x, y);
          pm.show(e.getComponent(), x, y);
        }
      }
    });
    
    // Tworzymy okna wewnętrzne
    int x = 0, y = 0;
    for (int i = 0; i < MAXC; i++) {
      x += 50;
      y += 50;
      makeInternalWindow(i, x, y);
    }
    
    // Szyba
    glass = getGlassPane();
    glass.addMouseListener(new MouseAdapter() {

      public void mousePressed(MouseEvent e) {
        if (e.isMetaDown()) {
          glass.setVisible(false);
          desk.revalidate();
        } else {
          Graphics glassGraphics = glass.getGraphics();
          glassGraphics.setColor(Color.red);
          glassGraphics.fillOval(e.getX() - 25, e.getY() - 25, 50, 50);
          desk.revalidate();
        }
      }
    });

    add(desk);
    setDefaultCloseOperation(DISPOSE_ON_CLOSE);
    setSize(600, 600);
    setLocationRelativeTo(null);
    setVisible(true);
  }

  public void actionPerformed(ActionEvent e) {
    JButton c = (JButton) e.getSource();
    JRootPane rp = c.getRootPane();
    JInternalFrame w = (JInternalFrame) rp.getParent();
    final String cmd = e.getActionCommand();
    if (cmd.equals("To front")) {
      desk.setLayer(w, desk.highestLayer());
      w.toFront();
    } else if (cmd.equals("To back")) {
      desk.setLayer(w, desk.lowestLayer());
      w.toBack();
    } else {
      glass.setVisible(true);
      JOptionPane
          .showMessageDialog(desk,
              "Next mouse press will draw red oval, use right button to exit this mode");
    }
  }

  private JPopupMenu makePopup(final int x, final int y) {
    JPopupMenu pm = new JPopupMenu();
    JPanel p = new JPanel(new BorderLayout());
    JLabel lab = new JLabel(
        "<html><center><b>Window list</b><br>Select one to close/open<br>"
            + "or add new window option</center></html>");
    lab.setBackground(Color.yellow);
    lab.setOpaque(true);
    p.add(lab);
    p.setBorder(BorderFactory.createLineBorder(Color.blue, 5));
    pm.add(p);
    final JInternalFrame[] jif = desk.getAllFrames();
    for (int i = 0; i < jif.length; i++) {
      JMenuItem mi = new JMenuItem(jif[i].getTitle());
      pm.add(mi);
      mi.putClientProperty("WinToClose", jif[i]);
      mi.addActionListener(new ActionListener() {

        public void actionPerformed(ActionEvent e) {
          JComponent c = (JComponent) e.getSource();
          JInternalFrame wclo = (JInternalFrame) c
              .getClientProperty("WinToClose");
          if (!wclo.isVisible())
            wclo.setVisible(true);
          else
            wclo.doDefaultCloseAction();
        }
      });
    }
    pm.addSeparator();
    JMenuItem mi = new JMenuItem("Add new window");
    mi.addActionListener(new ActionListener() {

      public void actionPerformed(ActionEvent e) {
        makeInternalWindow(jif.length, x, y);
      }
    });
    pm.add(mi);
    return pm;
  }

  void makeInternalWindow(int i, int x, int y) {
    JInternalFrame w = new JInternalFrame("Okienko " + i, true, true, true,
        true);
    
    w.setLayout(new BorderLayout(5, 5));
    JPanel controls = new JPanel();
    controls.setBorder(BorderFactory.createRaisedBevelBorder());
    JButton b = new JButton("To front", toFront);
    b.addActionListener(this);
    controls.add(b);
    b = new JButton("To back", toBack);
    b.addActionListener(this);
    controls.add(b);
    w.add(controls, "North");
    b = new JButton("<html><center><b><font color=red>Active</font><br>"
        + "<font color=blue>GLASS</font><br>"
        + "<b>will prevent interaction</b></center></html>");
    b.addActionListener(this);
    w.add(b, "Center");
    w.setDefaultCloseOperation(JInternalFrame.HIDE_ON_CLOSE);
    w.pack();
    desk.add(w, i);
    w.setLocation(x, y);
    w.setVisible(true);
  }

  public static void main(String[] args) {
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        new InternalWin();
      }
    });
  }

}

Zobacz działanie programu







Okna klasy JInternalFrame nie generują zdarzeń typu WindowEvent.
Zamiast tego dostępne są zdarzenia typu InternalFrameEvent, a do ich obsługi służą metody z interfejsu InternalFrameListener

3. Wyspecjalizowane kontenery Swingu

Swing wprowadził bardzo ciekawe i użyteczne rozwiązanie: wszystkie jego komponenty (J-komponenty) są kontenerami.
Wynika to bezpośrednio z hierarchii dziedziczenia klas:

Object
|
|
Component
|
|
Container
|
|
JComponent

Nic np. nie stoi na przeszkodzie, aby przycisk potraktować jako kontener, do którego dodajemy panel z innymi przyciskami. Przycisk-kontener będzie działał jak zwykły przycisk (wywołując przy kliknięciu skojarzoną z nim akcję), a mniejsze przyciski dodane do przycisku-kontenera mogą mieć swoje akcje.
Na wydruku  przedstawiono program testowy, sprawdzający taką możliwość:
 
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class Test1 extends JFrame implements ActionListener {
  
  public Test1() {
    setLayout(new FlowLayout(0, 0, FlowLayout.LEFT));
    ((JComponent) getContentPane()).setBorder(BorderFactory
                                   .createEmptyBorder(10, 10, 10, 10));
    JButton mb = new JButton("Główny przycisk");
    mb.setHorizontalAlignment(AbstractButton.RIGHT);
    mb.setPreferredSize(new Dimension(200, 150));
    mb.addActionListener(this);
    JPanel p = new JPanel(new GridLayout(0, 1));
    p.setBorder(BorderFactory.createLineBorder(Color.blue));
    for (int i = 1; i <= 5; i++) {
      JButton bb = new JButton("" + i);
      bb.addActionListener(this);
      p.add(bb);
    }
    mb.setLayout(new FlowLayout(0, 0, FlowLayout.LEFT));
    mb.add(p);
    add(mb);
    setDefaultCloseOperation(DISPOSE_ON_CLOSE);
    pack();
    setLocationRelativeTo(null);
    setVisible(true);
  }

  public void actionPerformed(ActionEvent e) {
    System.out.println(e.getActionCommand());
  }

  public static void main(String[] args) {
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        new Test1();
      }
    });
  }

}

Tabela pokazuje działanie programu.

r
r
r
Start programu
Przyciśnięto główny przycisk
Na konsolę wyprowadzony
zostanie napis
"Główny przycisk"
Przyciśnięto jeden z dodanych
przycisków. Na konsolę
wyprowadzony będzie jego numer

Warto zauważyć, że domyślnym  rozkładem dla J-komponentu (który nie jest Swingowym kontenerem) jest rozkład OverlayLayout (ogólnie niezbyt użyteczny). W programie (zwykłymi środkami, stosowanymi wobec kontenerów) zmieniliśmy go na FlowLayout, dzięki czemu panel z przyciskami nie przykrył napisu na głównym przycisku.
Ogólnie, nie należy nadużywać możliwości traktowania wszystkich komponentów Swingu jako kontenerów.
Swing dostarcza bowiem łatwiejszych i bardziej użytecznych sposobów grupowania komponentów w wyspecjalizowanych panelach (panelach Swingu).
Mamy do dyspozycji:

Warto pamiętać, że wszystkie te panele są jednocześnie J-komponentami, a więc można wobec nich używać metod klasy JComponent (np. ramki, przeźroczystość itp.).


3.1. Panel dzielony - JSplitPane


JSplitPane jest panelem podzielonym na dwie części, w których znajdują się różne komponenty.
Części rozdzielone są paskiem podziału, który możemy przesuwać myszką, zmieniając wielkość obszarów, w których widoczne są  komponenty. Jeśli ustalona jest właściwość boolowska continousLayout, to wraz ze zmianą wielkości obszarów zmieniają się rozmiary komponentów.
Ustalenie boolowskiej właściwości oneTouchExpandable dodaje do paska podziału dwie ikonki-strzalki. Kliknięcie w odpowiednią ikonkę maksymalnie rozszerza (lub zwęża) jeden z obszarów.

W zależności od ustalonej orientacji podział na obszary jest poziomy lub pionowy. Orientację określają stałe: JSplitPane.HORIZONTAL_SPLIT  (uwaga: pasek podziału jest pionowy!) i  JSplitPane.VERTICAL_SPLIT (pasek podziału jest poziomy).
Dla właściwego działania JSplitPane należy ustalić (za pomocą metod setMinimumSize i setPreferredSize) minimalne i preferowane rozmiary komponentów zawartych w obu jego obszarach.
 
W klasie JSplitPane znajdziemy kilka konstruktorów oraz wiele metod  zmieniających zawartość i działanie tego panelu (m.in. lokalizację paska podziału i inne właściwości).
Wygodnym konstruktorem jest:

JSplitPane(orientacja, continousLayout, lewy | górny komponent, prawy | górny komponent);
 
Split-panele można wkładać jeden-w-drugi, tworząc panele dzielone na wiele obszarów.

Przykład:
Mamy trzy panele p1, p2, p3  z ramkami o tytułach "1 panel", "2 panel", "3 panel" i z odpowiednio ustalonymi rozmiarami.
Panele p1 i p2 dodajemy do split-panelu sp1 podzielonego pionowym paskiem.
Następnie do drugiego split-panelu sp2, podzielonego poziomym paskiem, dodajemy u góry split-panel sp1, a u dołu panel p3.
Do paska podziału split-panelu sp1 dodajemy ikonki natychmiastowego rozszerzenia (własność oneTouchExpandable).

 boolean continousLayout = true;
 JSplitPane sp1 = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT,
                                 continousLayout, p1, p2);
 JSplitPane sp2 = new JSplitPane(JSplitPane.VERTICAL_SPLIT,
                                 continousLayout, sp1, p3);
 sp1.setOneTouchExpandable(true);

Wygląd panelu sp2 przedstawia rysunek

1


3.2. Panel zakładkowy - JTabbedPane


Panel zakładkowy udostępnia zakładki (tekst, tekst i ikona lub sama ikona, możliwe jest również ustalenie jako zakładki dowolnego komponentu). Z każdą zakładką związany jest inny komponent, który po kliknięciu w zakładkę wypełnia panel.
Zakładki mogą być ulokowane u góry, u dołu, po lewej lub po prawej stronie panelu.
Mogą być kolorowane, można z nimi związać podpowiedzi (fly-over-help).
Klasa JTabbedPane dostarcza wielu metod operowania na zakładkach i związanych z nimi komponentach.
M.in. można uzyskiwać wszelkie informacje o zakładkach i komponentach i dynamicznie zmieniać ich własności: wybrana zakładka i komponent, właściwości zakładek (kolory, teksty, ikony, lokalizacja).
Kilka takich metod poznamy przy okazji przykładowego programu.

Tworzymy panel zakładkowy z pięcioma różnie pokolorowanymi zakładkami.
Z każdą zakładką związany jest JPanel, w każdym z JPaneli umieszczono pięć przycisków.
Przyciski opisują położenie zakładek. Kliknięcie w przycisk ma spowodować:

r r












Przyjrzyjmy się programowi. 

Utworzenie JTabbedPane jest proste: wystarczy użyć konstruktora bezparametrowego.
Alternatywnie można wywołać konstruktor z argumentem określającym położenie zakładek.
Argument specyfikuje się jako odpowiednią stałą określoną w interfejsie SwingConstants.
Klasa JTabbedPane implementuje ten interfejs, można więc używać nazw JTabbedPane.TOP, JTabbedPane.LEFT, JTabbedPane,RIGHT, JTabbedPane.BOTTOM.
Lepszym jednak rozwiązaniem jest implementacja interfejsu we własnej klasie (nie trzeba wtedy podawać nazwy klasy przy odwołaniu do stałych statycznych). A skoro tak - można też rozszerzyć interfejs o własne stałe.
W omawianym przykładzie stworzono interfejs Constants rozszerzający SwingConstants i uzupełniający go o stałe oznaczające kolory (RED, BLUE itp.).

W konstruktorze przykładowej klasy są tworzone J-panele, dodawane do nich przyciski, po czym dla każdego J-panelu tworzone są zakładki w panelu zakładkowym. Ponieważ są to czynności powtarzające się umieszczono je w pętlach, korzystając z tablic kolorów, tekstów i lokalizacji.

Dla sprawnej obsługi kliknięcia w przycisk skorzystamy z możliwości przypisywania J-komponentom dodatkowej informacji w postaci par: klucz - wartość (metoda putClientProperty(Object key, Object value) z klasy JComponent). Tą informacją będzie wartość stałej (przekształconej do Integer, bo musi być obiektem), określająca położenie zakładek.

Po stworzeniu zakładek (metoda addTab(tekst, komponent)) ustalamy kolory ich tła i napisów (metody setBackgroundAt(...) i setForegroundAt(...)).
Wszystko to pokazuje pierwszy fragment programu.( fragment 1)

fragment 1

public class Tabbed1 extends JFrame
                     implements ActionListener, Constants {

JTabbedPane tp = new JTabbedPane();

Tabbed1() {
 Color[] back = { BLUE, YELLOW, RED, WHITE, BLACK };
 Color[] fore = { WHITE, BLACK, YELLOW, BLACK, WHITE };
 String[] txt = { "Top", "Left", "Right", "Bottom", "Default" };
 String[] loc = { "North", "West", "East", "South", "Center" };
 int[] place = { TOP, LEFT, RIGHT, BOTTOM, TOP };
 JButton b = null;
 JPanel  p = null;
 for (int i=0; i<back.length; i++) {
     p = new JPanel(new BorderLayout());
     for (int j=0; j<txt.length; j++) {
         b = new JButton(txt[j]);
         b.addActionListener(this);
         b.putClientProperty("Place", new Integer(place[j]));
         p.add(b, loc[j]);
         }
     tp.addTab("Tab"+(i+1), p);
     tp.setBackgroundAt(i, back[i]);
     tp.setForegroundAt(i, fore[i]);
     }
 getContentPane().add(tp);
 setSize(300, 200);
 setVisible(true);
}
...
}

Mając jako pole klasy zmienną tp, oznaczającą utworzony JTabbedPane, przy obsłudze kliknięcia w przycisk:
  1. pobieramy informację o żądanym położeniu zakładek (metoda getClientProperty(...) z klasy JComponent)
  2. ustalamy nowe położenie zakładek (tp.setTabPlacement(...))
  3. dowiadujemy się, który komponent-panel jest akurat wybrany przez zakładkę (tp.getSelectedComponent())
  4. w tym panelu dezaktywujemy wszystkie przyciski
  5. dowiadujemy się ile jest w ogóle zakładek (tp.getTabCount())
  6. dowiadujemy się jaki jest indeks wybranej zakładki (tp.getSelectedIndex())
  7. jeżeli indeks nie wskazuje na ostatnią zakładkę, przechodzimy do zakładki z indeksem o 1 większym; w przeciwnym razie - do pierwszej zakładki (tp.setSelectedIndex(...).
Te czynności wykonywane są w metodzie actionPerformed zaimplementowanej w omawianej przykładowej klasie ( zob. wydruk  fragment 2)


Fragment 2

public void actionPerformed(ActionEvent e) {
  JComponent c = (JComponent) e.getSource();
  Integer prop = (Integer) c.getClientProperty("Place");
  tp.setTabPlacement(prop.intValue());
  JComponent p = (JComponent) tp.getSelectedComponent();
  Component[] b  = p.getComponents();
  for (int i=0; i<b.length; i++) b[i].setEnabled(false);
  int tabs = tp.getTabCount();
  int index = tp.getSelectedIndex();
  if (index == tabs-1) index = 0;
   else index++;
  tp.setSelectedIndex(index);
}

Mamy też dodatkową możliwość ustalania "polityki rozkładu zakładek" w konstruktorze lub za pomocą metody setTabLayoutPolicy, używając przy tym argumentu – stałej całkowitoliczbowej:

JTabbedPane.SCROLL_TAB_LAYOUT – przewijany wiersz/kolumna zakładek
JTabbedPane.SCROLL_TAB_LAYOUT  - zakładki (jeśli się nie mieszczą w widocznym obszarze układane są w kilku wierszach lub kokumnach).

1 Rysunki pokazują ułożenie 10 zakładek w stylu SCROLL_TAB_LAYOUT u góry (rys. A) i WRAP_TAB_LAYOUT z prawej (rys B).













Kody dla konstrukcji i dynamicznej zmiany układu zakladek:

JTabbedPane jtp = new JTabbedPane(JTabbedPane.TOP,
                            JTabbedPane.SCROLL_TAB_LAYOUT);
jtp.setLayout(JTabbedPane.WRAP_TAB_LAYOUT);

1


Poczynając od wersji JDK 1.6 zakładki mogą być dowolnymi komponentami, co np. pozwala na różnego rodzaju zaawansowane efekty graficzne.



3.3. Panel przewijany - JScrollPane


r Służy do przedstawiania komponentów, których rozmiar jest większy niż widoczny w panelu obszar. 


Do przewijania widoku komponentu przeznaczone są suwaki.
W Swingu pełni bardzo ważną rolę, gdyż wiele komponentów, które w AWT miały wbudowane suwaki (takich jak np wielowierszowe pole edycyjne czy lista) w Swingu musi być umieszczona w panelu przewijania i dopiero wtedy pojawią się suwaki.




Stąd najprostsze zastosowanie JScrollPane: "obudowanie" nim J-komponentu, który powinien mieć suwaki np.
   public static void main(String[] args) {
     JScrollPane scroll = new JScrollPane(new JTextArea(25,80));
     scroll.setPreferredSize(new Dimension(200,200));
     scroll.setBorder(BorderFactory.createTitledBorder("Edytor"));
     JFrame f = new JFrame();
     f.getContentPane().add(scroll);
     f.pack();
     f.setVisible(true);
   }
Uwaga: dla uproszczenia pominięto invokeLater
 

Ale użyteczność JScrollPane na tym się nie kończy. Ma on dużo bardziej skomplikowaną budowę niż ScrollPane z AWT i dostarcza znacznie większych możliwości.

JScrollPane zarządza dziewięcioma komponentami:

r

Rysunek pokazuje schemat budowy JScrollPane.


Obecność pasków przewijania regulowana jest przez tzw. politykę przewijania. Możemy zadekretować np., by pojawiały się zawsze, albo tylko wtedy gdy jest taka potrzeba (przewijalny klient jest większy od rozmiarów widoku w danym kierunku).


Do ustalania widoku klienta służy metoda setViewportView(Component).
Nagłówki ustalamy za pomocą metod setRowHeaderView(Component) i setColumnHeaderView(Component).
Zawartość rogów możemy  definiować za pomocą metody setCorner(int jaki_róg, Component), gdzie jaki róg - stała satyczna typu String określająca do którego rogu dodawany jest komponent.

Przykładowy program na wydruku  wykorzystuje niektóre możliwości JScrollPane.

Tworzymy duży panel "komórek" - etykiet, rozmieszczonych w układzie GridLayout.
Jak w arkuszach kalkulacyjnych, wiersze rozkładu są oznaczane liczbami, a kolumny - literami.
Każda komórka-etykieta zawiera napis  obrazujący jej "adres" (np. A1 lub V11).
Panel jest tak duży, że nie mieści się  w oknie o zadekretowanych rozmiarach.
Zatem będzi on umieszczony w JScrollPane jako widok przewijalnego klienta.
Ustalimy dla naszego przykładowego JScrollPane naturalne nagłówki kolumn (litery) i wierszy (liczby).
Dodatkowo w górnych rogach (lewym i prawym) umieścimy przycisk obrazkowy (z ikonką "UP"), którego kliknięcie spowoduje przejście do komórki A1. W tym celu w obsłudze kliknięcia w przycisk (actionPerformed) użyto metody z klasy JComponent scrollRectToVisible(Rectangle), która przewija JScrollpane w ten sposoób, by uwidocznić podany prostokąt komponentu (tego przewijalnego). Działanie programu ilustruje rysunek.


r public class Scroll2 extends JFrame implements ActionListener {

  int cellW = 50, cellH = 20, rows = 30, cols = 26;
  JPanel cont = new JPanel(new GridLayout(rows, cols, 0, 0));
  JPanel colHead = new JPanel(new GridLayout(1, cols, 0, 0));
  JPanel rowHead = new JPanel(new GridLayout(rows, 1, 0, 0));

Scroll2() {
  Color lYellow = new Color(255,255,240),
        lBlue   = new Color(219,232,255);
  String[] lit = new String[cols+1];
  JLabel l = null;
  for (int j = 1; j <=cols; j++) {
      lit[j] = "" + (char) ('A'+(j-1));
      l = createLabel(lit[j], Color.black, cellW, cellH);
      colHead.add(createLabel(lit[j], Color.black, cellW, cellH));
      }
  for (int i = 1; i<=rows; i++) {
      rowHead.add(createLabel(""+i, Color.black, cellH, cellH));
      for (int j = 1; j<=cols; j++)
          cont.add(createLabel(lit[j]+i, Color.blue, cellW, cellH));
      }
   JScrollPane sp = new JScrollPane();
   cont.setBackground(lYellow);
   sp.setViewportView(cont);
   rowHead.setBackground(lBlue);
   sp.setRowHeaderView(rowHead);
   colHead.setBackground(lBlue);
   sp.setColumnHeaderView(colHead);
   ImageIcon up = new ImageIcon("Up16.gif");
   JButton leftUp = new JButton(up);
   leftUp.addActionListener(this);
   JButton rightUp = new JButton(up);
   rightUp.addActionListener(this);
   sp.setCorner(JScrollPane.UPPER_LEFT_CORNER, leftUp);
   sp.setCorner(JScrollPane.UPPER_RIGHT_CORNER, rightUp);
   JComponent cp = (JComponent) getContentPane();
   cp.add(sp);
   cp.setPreferredSize(new Dimension(300,300));
   pack();
   setVisible(true);
}

JLabel createLabel(String s, Color c, int w, int h) {
  JLabel l = new JLabel(s, JLabel.CENTER);
  l.setBorder(BorderFactory.createLineBorder(c));
  l.setPreferredSize(new Dimension(w, h));
  l.setOpaque(false);
  return l;
}

public void actionPerformed(ActionEvent e) {
 cont.scrollRectToVisible(new Rectangle(0,0,cellW,cellH));
}
}

Zawartość panelu przewijalnego (przewijalny klient, nagłówki kolumn i wierszy , rogi) można zmieniać dynamicznie  (w trakcie działania programu).



3.4. Pasek narzędzi - JToolBar


Klasa JToolBar definiuje  tzw. pasek narzędzi.
Przykładowy pasek narzędzi pokazuje rysunek.

1

JToolBar jest kontenerem, do którego możemy dodawać dowolne komponenty.
Np. aby uzyskać pasek jak na rysunku stworzono obiekt JToolBar:

    JToolBar tb = new JToolBar();

i za pomocą metody add dodana do niego cztery przyciski obrazkowe, etykietę ("Szukaj"), pole tekstowe oraz panel z przyciskami ponumerowanymi 1, 2, 3,
Następnie dodano JToolBar do okna o rozkładzie BorderLayout na północy: frame.getContentPane().add("North");

Od zwykłych kontenerów  JToolBar różni się tym, że:


4. Akcje

Jeśli mamy kilka komponentów (np. przycisk na pasku narzędzi, element menu rozwijalnego lub element menu kontekstowego), które powinny mieć tę samą funkcjonalność, to możemy w łatwy sposób, jednokrotnie, funkcjonalność tę zdefiniować i łatwo przypisać ją tym komponentom.
Służą temu obiekty typu Action.

Obiekty typu Action:



Atrakcyjność obiektu-akcji polega na tym, iż po jego utworzeniu może on być równolegle dodany:

Ponadto dla każdego abstrakcyjnego przycisku (wszystkich przycisków, elementów menu) możemy dynamicznie zmieniać właściwości związane z akcją za pomocą metody setAction(Action) z klasy AbstractButton.

Wywołanie setAction(...) dynamicznie zmienia właściwości przycisku (napis, ikonę, podpowiedź itd.) oraz słuchacza akcji (definiowanego przez obiekt typu Action) nie ruszając innych słuchaczy, przyłączonych za pomocą addActionListener.


Action jest interfejsem. Abstrakcyjna klasa AbstractAction implementuje część metod tego interfejsu i dostarcza konstruktora z argumentami: napis, ikona.

Aby utworzyć własny obiekt-akcję wystarczy:
  1. stworzyć klasę dziedziczącą AbstractAction,
  2. dostarczyć  w niej  implementacji metody actionPerformed(...) - co ma się dziać, gdy pojawi się zdarzenie akcji,
  3. stworzyć obiekt nowej klasy.
Przykładowy program na wydruku tworzy dwa obiekty-akcje, które będą związane z elementami menu rozwijalnego, paska narzędzi oraz menu kontekstowego. Użytkownik może dokonać wyboru każdej z dwóch akcji albo klikając w przycisk na pasku narzędzi, albo wybierając opcję z menu rozwijalnego albo z kontekstowego, uruchamianego prawym kliknięciem myszki na wielopolu edycyjnym.
Tworząc akcję podajemy napis i ikonę, które pojawią się na przyciskach (paska narzędzi, menus).
Oprócz tego specyfikujemy obsługę akcji - implementację metody actionPerformed(..)
Samo dodanie obiektów-akcji (metoda add) do paska narzędzi lub do menu automatycznie tworzy ich wizualny opis (napis i ikonka na przycisku paska narzędzi lub w menu) oraz zapewnia, że po kliknięciu w przycisk lub opcję menu powstanie odpowiednie zdarzenie akcji i zostanie obsłużone przez metodę actionPerformed implementowaną w klasie obiektu-akcji.

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

public class Akcja extends JFrame {

  JTextArea ta = new JTextArea(10, 20);
  JScrollPane sp = new JScrollPane(ta);

  Action newAct = new AbstractAction("New", new ImageIcon("New24.gif")) {
    public void actionPerformed(ActionEvent e) {
      newFile();
    }
  };

  Action openAct = new AbstractAction("Open", new ImageIcon("Open24.gif")) {
    public void actionPerformed(ActionEvent e) {
      openFile();
    }
  };

  JPopupMenu popup = new JPopupMenu();

  public Akcja() {
    JToolBar tb = new JToolBar();
    JMenu menu = new JMenu("File");
    tb.add(newAct);
    tb.add(openAct);
    menu.add(newAct);
    menu.add(openAct);
    popup.add(newAct);
    popup.add(openAct);
    ta.setComponentPopupMenu(popup);
    JMenuBar mb = new JMenuBar();
    mb.add(menu);
    setJMenuBar(mb);
    add(tb, "North");
    add(sp);
    setDefaultCloseOperation(DISPOSE_ON_CLOSE);
    pack();
    setLocationRelativeTo(null);
    setVisible(true);
  }

  private void newFile() {
    // utworzenie pliku
  }

  private void openFile() {
    // otwarcie pliku    
  }

  public static void main(String[] args) {
     SwingUtilities.invokeLater(new Runnable() {
      public void run() {
       new Akcja();
      }
    });
  }
}



Atrybuty akcji (określające właściwości przycisku) są przechowywane w obiekcie typu akcja i możemy je uzyskać lub ustalić (przed ustaleniem akcji dla przycisku) za pomocą metod getValue i putValue interfejsu Action.

Standardowo zdefiniowano m.in. następujące rodzaje atrybutów, jako stałe interfejsu Action:


Przykładowo, jeśli obiekt a jest klasy implementującej interfejs Action, to po:
a.putValue(Action.SMALL_ICON, new ImageIcon("jakis.gif"));
przyciski (np. na pasku narzędzi lub w menu), dla których ustalimy akcję a zyskają ikonę z pliku jakis.gif.

Można definiować (w klasach własnych komponentów) nowe/inne atrybuty akcji i zapewnić mechanizm dynamicznych zmian akcji za pomocą użycia metody configurePropertiesFromAction() oraz nasłuchu zmian właściwości "action".

5. Mapy akcji i mapy klawiaturowe

W Javie istnieje możliwość skojarzenia z każdym J-komponentem - akcji klawiaturowych (czyli akcji wykonywanych na skutek naciśnięcia klawiszy na klawiaturze).

Powiązanie skrótów klawiaturowych z akcją odbywa się za pomocą dwóch map: mapy akcji (obiekt klasy ActionMap) i mapy klawiatury (obiekt klasy InputMap).
Mapa akcji zawiera pary: nazwa akcji - akcja (obiekt typu Action), mapa klawiatury zawiera pary: klucz (obiekt typu KeyString) - nazwa akcji (mówiąc ściślej, "nazwa akcji" może być dowolnym obiektem, zwykle jednak jest to napis).

Każdy komponent "posiada" swoją mapę akcji oraz swoją mapę klawiaturową (a nawet trzy takie mapy, ale o tym za chwilę).

Akcje wiązane są z klawiszami w następujący sposób: gdy wciśnięto klawisz, w mapie klawiaturowej komponentu wyszukiwane jest odzwzorowanie: klawisz - nazwa akcji. Jeśli istnieje takie odwzorowanie, to nazwa akcji staje się kluczem wyszukiwania samej akcji w mapie akcji komponentu. Odnaleziona akcja jest wykonywana, to znaczy wywoływana jest metoda actionPerformed z klasy akcji, przy czym źródłem zdarzenia ActionEvent  jest dany komponent.

Przykładowe powiązanie obu map pokazuje rysunek.

r


Mechanizm akcji klawiaturowych jest ogólniejszy i silniejszy niż obsługa klawiatury za pomocą KeyListenera czy  też użycie mnemonik lub akceleratorów, gdyż źródłem akcji klawiaturowych mogą być dowolne J-komponenty i to niezależnie od tego czy mają akurat fokus czy nie.

Z każdym J-komponentem związane są bowiem trzy mapy klawiaturowe, oznaczane stałymi całkowitoliczbowymi:

Mapę klawiaturową J-komponentu comp uzyskujemy za pomocą odwolania:

        InputMap imap = comp.getInputMap(int rodzaj);

gdzie:
        imap - mapa klawiaturowa,
        rodzaj - rodzaj mapy:
            WHEN_FOCUSED,
            WHEN_ANCESTOR_OF_FOCUSED_COMPONENT,
            WHEN _IN_FOCUSED_WINDOW.

Mapę akcji komponentu comp uzyskujemy poprzez:

        ActionMap amap = comp.getActionMap();


Możemy też użyć odpowiednich metod setInputMap(...) i setActionMap(...). do ustalania map dla komponentów.       


Dodawanie powiązań: klawisze - nazwa akcji do mapy klawiaturowej odbywa się za pomocą metody put(KeyStroke, Object).
Pierwszy jej argument określa "skrót klawiaturowy", drugi - nazwę (identyfikację) akcji.

Aby uzyskać obiekt klasy KeyStroke, reprezentujący skrót klawiaturowy stosujemy statyczmną metodę tej klasy:

        KeyStroke getKeyStroke(String)

której argumentem jest napis, oznaczający skrót klawiaturowy, np. "control D".



Dodawanie powiązań: nazwa akcji - akcja  do mapy akcji odbywa się za pomocą analogicznej metody put(Object, Action). z argumentami: nazwa akcji, obiekt klasy implementującej interefejs Action.

Oczywiście, możemy używać dowolnych innych metod interfejsu Map, np. dla uzyskania informacji o powiązaniach w mapach akcji i mapach klawiaturowych jakichś komponentów.

Uwaga: usunięcie powiązania klawisz - akcja odbywa się poprzez związanie klawisza ze specjalną nazwą akcji - "none":  
       
        InputMap imap;
        KeyStroke key;
        //...
        imap.put(key, "none");


Przykładowy program ilustruje użycie map akcji i map klawiaturowych.
W programie tworzymy mapę akcji i pod nazwą "write" zapisujemy do niej akcję, która zastępuje zaznaczony tekst w komponencie tekstowym tekstem pobranym z własciwości clientProperty żródła zdarzenia.
Ustalamy tę mapę jako mapę akcji dla trzech etykiet, a do pobranych od nich map klawiaturowych (typu WHEN_IN_FOCUSED_WINDOW) dopisujemy odpowiednie skróty klawiaturowe powodujące wywołanie akcji "write".

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

class Writer extends AbstractAction {

  JTextComponent tc;

  public Writer(JTextComponent t) {
    super("write");
    tc = t;
  }

  public void actionPerformed(ActionEvent e) {
    Object src = e.getSource();
    JComponent c = (JComponent) e.getSource();
    String txt = (String) c.getClientProperty("text");
    tc.replaceSelection(txt);
  }
}

public class KMap extends JFrame {

  String[] txt = { "Pies", "Kot", "Tygrys" };
  String[] keys = { "control P", "control K", "control T" };

  ActionMap amap = new ActionMap();

  public KMap() {
    JTextArea ta = new JTextArea(20, 20);
    add(new JScrollPane(ta));
    amap.put("write", new Writer(ta));
    add(new JScrollPane(ta));
    JPanel p = new JPanel();
    p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS));
    p.setBorder(BorderFactory.createLineBorder(Color.blue));

    for (int i = 0; i < txt.length; i++) {
      JLabel l = createLabel(txt[i], keys[i]);
      l.putClientProperty("text", txt[i]);
      l.setAlignmentX(JLabel.RIGHT);
      p.add(l);
      JSeparator js = new JSeparator();
      js.setMaximumSize(new Dimension(1200, 7));
      p.add(js);
    }

    add(p, "West");
    setDefaultCloseOperation(DISPOSE_ON_CLOSE);
    pack();
    setLocationRelativeTo(null);
    setVisible(true);
  }

  JLabel createLabel(String txt, String key) {
    JLabel l = new JLabel(txt + "  ");
    l.setPreferredSize(new Dimension(100, 50));
    l.setToolTipText("Wciśnij : ");
    InputMap imap = l.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
    imap.put(KeyStroke.getKeyStroke(key), "write");
    l.setActionMap(amap);
    return l;
  }

  public static void main(String[] args) {
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        new KMap();
      }
    });
  }
}


6. Obieralny wygląd (pluggable look & feel)

Do chwili pojawienia się Swingu komponenty Javy wyglądały zawsze tak, jak dekretowała platforma systemowa.
Przyciski na platformach Win, Motif czy OS/2 - wyglądają inaczej. A Java ma być językiem wieloplatformowym! Zatem, powinna mieć propozycję  unifikacyjną: jednakowy wygląd na róznych platformach, I to w Swingu zrealizowano.
I więcej: wygląd i zachowanie (look and feel)  elementów GUI może być ustalane dynamicznie.
Można także definiować nowe rodzaje "wyglądów".

Zarządzaniem wyglądem komponentów zajmuje się klasa UIManager.

Aby ustalić "look and feel" (L&F) należy wywołać  metodę setLookAndFeel (statyczna metoda klasy UIManager) z argumentem specyfikującym pełną kwalifikowaną nazwę klasy określającej wygląd i zachowanie komponentów.

Standardowo  dostępne są następujące klasy, określające "look and feel" . 
Oprócz tego w klasie UIManager dostępne są statyczne metody:

Ustalenie L&F powinno odbyć się przed stworzeniem jakichkolwiek komponentów.
Jeśli chcemy dynamicznie zmienić L&F w trakcie działania programu, to należy:
  1. wywołać UIManager.setLookAndFeel(...)
  2. następnie powiadomić wszystkie komponenty o zmianie za pomocą statycznej metody updateComponentTreeUI z klasy SwingUtilities; argumentem tej metody powinna być referencja do kontenera najwyższego poziomu (zwykle okna ramowego aplikacji).
Na wydruku  pokazano przykład ustalania "Look and Feel" zarówno na początku programu, jak i dynamicznie w trakcie jego działania.


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

import net.miginfocom.swing.*;

public class Test extends JFrame implements ActionListener {

  final static String MotifLF = "com.sun.java.swing.plaf.motif.MotifLookAndFeel",
                      JavaLF = UIManager.getCrossPlatformLookAndFeelClassName(),
                      WindowsLF = "com.sun.java.swing.plaf.windows.WindowsLookAndFeel",
                      NimbusLF = "com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel";
                                 
  
  Test(String tit) {
    super(tit);
    setLayout(new MigLayout());
    add(new JCheckBox("CheckBox"));
    add(new JRadioButton("Radio"));
    JTextField tf = new JTextField(10);
    add(new JLabel("textField"), "align right"); 
    add(new JTextField(10), "span, wrap");
    add(new JLabel("Slider"), "align right"); 
    add(new JSlider(0, 100), "span, wrap");
    
     
    add(createButton("Motif", MotifLF), "align right");
    add(createButton("Java", JavaLF));
    add(createButton("Nimbus", NimbusLF));
    add(createButton("Windows", WindowsLF));
     
    setDefaultCloseOperation(DISPOSE_ON_CLOSE);
    pack();
    setLocationRelativeTo(null);
    setVisible(true);
  }

  JButton createButton(String txt, String lafClass) {
    JButton b = new JButton(txt);
    b.setActionCommand(lafClass);
    b.addActionListener(this);
    return b;
  }

  public void actionPerformed(ActionEvent e) {
    String laf = e.getActionCommand();
    try {
      UIManager.setLookAndFeel(laf);
      setTitle(laf.substring(laf.lastIndexOf('.') +1));
    } catch (Exception exc) {
      System.out.println("Nie umiem ustalić L&F = " + laf);
    }
    SwingUtilities.updateComponentTreeUI(this);
    pack();
  }
  
  public static void main(String[] args) {
    try {
      UIManager.setLookAndFeel(JavaLF);
    } catch (Exception excp) {
      System.out.println("Nie umiem ustalić L&F");
    }
    
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        new Test("MetalLookAndFeel");
      }
    });
  }

}

Zobacz działanie programu







Architektura "Pluggable Look & Feel" jest bardzo rozbudowana i elastyczna. Umożliwia
Niewątpliwie najłatwiej jest ingerować w pewne wybrane charakterystyki, określające wygląd i (po części) zachowanie komponentów. Każdy look&feel ma tzw. ustawienia. Właśnie one określają wspomniane charakterystyki komponentów. Domyślne ustawienia możemy uzyskać od aktualnego wyglądu (obiektu klasy LookAndFeel) w postaci obiektu klasy UIDefaults (który tak naprawdę jest mapą, z kluczami będącymi nazwami atrybutów).
Pokazuje to poniższy programik.
public class PlafDefaults {

  public static void main(String[] args) {
    UIDefaults defaults = UIManager.getLookAndFeel().getDefaults();;
    for (Object key : defaults.keySet()) {
      System.out.println(key + " = " + defaults.get(key));
    }
  }

}
Program pozwala zorientować się jakie atrybuty, określające wygląd (a po częsci i zachowanie) komponentów są dla nas dostępne i zarazem jakie mają domyślne wartości.
Otrzymamy domyślne właściwości dla wszystkich komponentów. Cały wydruk programu liczy ponad 500 wierszy, możemy zatem przedstawić tylko jego częśc (np. właściwości przycisków klasy JButton).

Button.background = javax.swing.plaf.ColorUIResource[r=204,g=204,b=204]
Button.border = javax.swing.plaf.BorderUIResource$CompoundBorderUIResource@cd2c3c
Button.darkShadow = javax.swing.plaf.ColorUIResource[r=102,g=102,b=102]
Button.disabledText = javax.swing.plaf.ColorUIResource[r=153,g=153,b=153]
Button.focus = javax.swing.plaf.ColorUIResource[r=153,g=153,b=204]
Button.focusInputMap = javax.swing.plaf.InputMapUIResource@1d99a4d
Button.font = javax.swing.plaf.FontUIResource[family=Dialog,name=Dialog,style=bold,size=12]
Button.foreground = javax.swing.plaf.ColorUIResource[r=0,g=0,b=0]
Button.highlight = javax.swing.plaf.ColorUIResource[r=255,g=255,b=255]
Button.light = javax.swing.plaf.ColorUIResource[r=255,g=255,b=255]
Button.margin = javax.swing.plaf.InsetsUIResource[top=2,left=14,bottom=2,right=14]
Button.select = javax.swing.plaf.ColorUIResource[r=153,g=153,b=153]
Button.shadow = javax.swing.plaf.ColorUIResource[r=153,g=153,b=153]
Button.textIconGap = 4
Button.textShiftOffset = 0
ButtonUI = javax.swing.plaf.metal.MetalButtonUI


Widzimy tutaj  ustawienia dotyczące m.in. kolorów, pisma, ramek itp. dla przycisków JButton (w nazwach klas, określających wygląd komponentów Swingu standardowo pomija się literę J). Wartości ustawień są reprezentowane jako obiekty klas xxxxUIResource (np. ColorUIResource albo FontUIResource). Dlaczego nie po prostu Color albo Font?
Otóż, przy dynamicznych (w trakcie wykonania programu) zmianach look&feel Java musi odróżniać ustawienia związane z look&feel od tych właściwości które zostały nadane przez programistę bez zmiany samego look & feel. Np. jeżeli kolor tła przycisku określony został przez programistę za pomoca metody setBackground(...) i jest obiektem klasy Color, to ew. zmiana look&feel nie powinna zmienić koloru tła tego przycisku. Może to natomiast zrobić, gdy kolor jest określony przez obiekt klasy ColorUIResource.

Aktualne ustawienia możemy zmienić za pomocą metodu put(...) z klasy UIManager.
Poniższy przykładowy program zmienia ustawienia kolorów selekcji w menu rozwijalnym (JMenu) i jego elementach (JMenuItem) oraz pismo JMenu.

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


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

public class MenuPlaf extends JFrame {
  
  private static int x = 300, y = 300;

  public MenuPlaf() {

    JMenu menu = new JMenu("File");

    String[] labels = { "New", "Open", "Save", "Save as..", "Properties" };
    for (int i=0; i<labels.length; i++) {
      JMenuItem mi = new JMenuItem(labels[i]);
      menu.add(mi);
    }

    JMenuBar mbar = new JMenuBar();
    mbar.add(menu);

    String[] addMenus = { "Edit", "View", "Tools" };
    for (int i=0; i<addMenus.length; i++) {
      menu = new JMenu(addMenus[i]);
      mbar.add(menu);
    }


    setJMenuBar(mbar);
    setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
    setSize(200, 100);
    setLocation( x+=100, y += 100 );
  }

  public static void main(String[] args) {
    ColorUIResource selBack = new ColorUIResource(Color.black);
    ColorUIResource selFore = new ColorUIResource(Color.white);
    UIManager.put("Menu.selectionBackground", selBack);
    UIManager.put("Menu.selectionForeground", selFore);
    UIManager.put("Menu.font",
                   new FontUIResource(new Font("Dialog", Font.BOLD, 20)));
    UIManager.put("MenuItem.selectionBackground", selBack);
    UIManager.put("MenuItem.selectionForeground", selFore);

    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        new MenuPlaf().setVisible(true);
        new MenuPlaf().setVisible(true);
      }
    });
  }
}

Od momentu ustalenia tych właściwości w metodzie main(), każde menu będzie już je miało. Program ilustracyjnie tworzy dwa okna z przykładowymi menu, które wyglądaja jak na rysunku.
1

Zobacz działanie programu







Nasza ingerencja w wygląd i działanie komponentów może być posunięta i znacznie dalej.
Można np. zmieniać nie tylko poszczególne własności komponentów. ale całe zachowanie i wygląd konkretnych komponentów. W tym celu należy przedefiniować klasę delagata UI dla danego komponentu i ustalić ją jako aktualną przy danym Look And Feel.

Tak naprawdę najczęściej będziemy korzystac z gotowych pakietów PLAF.  Biblioteki obieralnych wyglądów (pluggable look and feels) są rozwijane coraz bardziej, a w Internecie można znaleźć ich całe mnóstwo. Do najbardziej eleganckich nalezą wyglądy JGoodies (autor,  Karsten Lentzsch, jest wybitnym znawcą zasad tworzenia GUI).
Ilustruje to rysunek:

r


Aby uzyskać pokazany efekt wystarczyło napisać:
      UIManager.setLookAndFeel(
          "com.jgoodies.looks.plastic.PlasticXPLookAndFeel");
Natomaist do najbardzeij elastycznych i  konfigurowalnych co do efektów graficznych nalezy Substance Kirila Grouchnikow'a, wspierany przez pakiet laf-widgets tego samego autora.
Nie sposób wymienić wszystkich możliwości Substance i laf-widgets (zresztą laf-widgets mogą być stosowane i wobec innych wyglądów). Przedstawię tylko dwa przykłady: podgląd zakładek panelu JTabbedPane i  animowane przyciski z efektami typu ghost/spring.

Program na wydruku  tworzy panel zakładkowy z automatyczniie dodawanym przyciskiem. otwierającym podgląd zakładek.

Tworzenie panelu zakładkowego z podglądem zakładek.
import java.awt.*;
import javax.swing.*;
import org.jvnet.lafwidget.*;
import org.jvnet.lafwidget.tabbed.*;
import org.jvnet.substance.*;
import org.jvnet.substance.theme.*;

public class TPview extends JFrame {

  public TPview() {
    super("Przegląd zakładek");

    Color[] c = { Color.BLUE, Color.RED, Color.YELLOW };

    JTabbedPane tp = new JTabbedPane();
    for (int i = 0; i < c.length; i++) {
      JPanel p = new JPanel();
      p.setBackground(c[i]);
      int n = i + 1;
      p.add(new JButton("Przycisk " + n));
      tp.add("Tab " + n, p);
    }

    add(tp, BorderLayout.CENTER);

    // Ustalenie podglądu zakładek
    tp.putClientProperty(LafWidget.TABBED_PANE_PREVIEW_PAINTER,
        new DefaultTabPreviewPainter());

    setSize(500, 300);
    setLocationRelativeTo(null);
    setDefaultCloseOperation(EXIT_ON_CLOSE);
    setVisible(true);
  }

  public static void main(String[] args) throws Exception {
    UIManager.setLookAndFeel(new SubstanceLookAndFeel());
    SubstanceLookAndFeel.setCurrentTheme(new SubstanceSteelBlueTheme());
     SwingUtilities.invokeLater( new Runnable() {
        public void run() {
         new TPview();
        }
     });
  }
}

Poniższy rysunek przedstawia panel zakładkowy z przyciskiem przeglądu zakładek

r


a podgląd zakładek widoczny jest poniżej.

r


Doskonalenie wyglądów nie omija i standardu Javy. Już w wersji 5 wprowadzono dający niezwykle duże możliwości konfiguracyjne mechanizm SynthLookAndFeel. Można odnieść wrażenie, że pozostał on niedoceniony, a naprawdę wart jest uwagi.  Szósta wersja Javy znacząco poprawiła różne aspekty grafiki, szczególnie dla Windows a w update 10 pojawił się nowy elegancki wygląd Nimbus. W wersji siódmej ma on zastąpić dotychczasowy Ocean jako wygląd typu "cross-platform" . 

Przykładowa aplikacja pokazująca wygląd Nimbus.
public class NimbusTest {

  public static void main(String[] args) throws Exception {
      UIManager.setLookAndFeel("com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel");
 
      SwingUtilities.invokeLater( new Runnable() {
        
        public void run() {

          JFrame frame = new JFrame("Nimbus look and feel");
          frame.add(new JScrollPane(new JTable(
             new Object[][] { { "Reksio", "Pies", 10, 20  },
                              { "Saba", "Tygrys", 5, 70 },
                              { "Mruk", "Kot", 1, 5 },
                              { "Krówka", "Żubr", 6, 100 },
                              { "Puchacz", "Niedźwiedź", 7, 120 },
                              { "Śpioch", "Borsuk", 2, 10 },
             }, 
             new String[] { "Imię", "Rodzaj", "Wiek", "Waga" }
             )
          )); 
      
          JPanel p = new JPanel();
          p.add(new JLabel("Opis"));
          p.add(new JTextField(10));
          p.add(new JSlider(0, 10));
          p.add(new JButton("Ok"));
          p.add(new JButton("Cancel"));
          frame.add(p, "South");
          frame.pack();
          frame.setLocationRelativeTo(null);
          frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
          frame.setVisible(true);
        }
      });
    }
}
Wygląd Nimbus (pole tekstowe ma zaznaczony ramką fokus, przycisk Ok jest w stanie "rollover")

r


Zobacz działanie programu







7. Integracja z pulpitem

Jakiś czas temu w ramach SwingLab powstał projekt Java Desktop Integration Components (JDIC). Jego celem było (i jest) zintegrowanie Javy z elementami powłoki graficznej systemu operacyjnego, w tym z pulpitem. Chodzi o umożliwienie wykonywania z poziomu aplikacji Javy podobnych operacji jak na pulpicie m.in. uruchamianie aplikacji przez wybór dokumentów (plików) skojarzonych z tymi aplikacjami, otwieranie przeglądarki i klienta pocztowego, umieszczanie ikon w obszarze powiadamiania (tray).
JDIC rozwija się w dalszym ciągu, jednocześnie koncepcja trafiła do standardu Javy w wersji 6 w postaci udostępnienia podobnych możliwości. Niestety, wygląda na to, że nie było ścisłej współpracy pomiędzy projektem JDIC a implementacją rozwiązań "pulpitowych" w Javie 6. Ogólnie można powiedzieć, że rozwiązania JDIC są bardziej zaawansowane (dają więcej możliwości). Ale ponieważ standard jest standardem przyjrzymy się mu w pierwszej kolejności, o niektórych rozszerzeniach JDIC wspominając przy okazji.

7.1. Dokumentocentryczne uruchamianie rodzimych aplikacji. Przeglądarka i poczta.

Bardzo przyjemna (bo niewielka) klasa Desktop z pakietu java.awt pozwala na:
Aby korzystać z tych metod należy najpierw upewnić się czy na danej platformie dostępna jest integracja Javy z pulpitem (statyczna metoda Desktop.isDektopSupported()) i uzyskać obiekt klasy Desktop za pomocą statycznej metody Desktop.getDesktop().

Zastosowanie klasy Desktop ilustruje przykładowa aplikacja )zob. tysunek i listing).
Jest to swoista atrapa jakiejś części "przeglądarki" hoteli.

r

Oprócz obejrzenia informacji o hotelu, możemy otworzyć broszurę PDF, obejrzeć film o hotelu, zobaczyć jego stronę czy wreszcie otworzyć klienta pocztowego z adresem hotelu (by np. zarezerwować miejsce).
Wszystkie te operacje w łatwy sposób wykonuje się za pomocą klasy Desktop (zob. poniższy listinh).


public class HotelView extends JFrame implements ActionListener {

  // Obiekt klasy Dekstop
  private static Desktop desk;
  
  public HotelView() {
    // Teksty na przyciskach
    String[] txt = { "<html><center>Informacja<br>o hotelu</center></html>", 
                     "Broszura", "Film", "Web", "Mail" };

    // ActionCommand przycisków
    String[] cmd = { "Details", "roband.pdf", "45sec.mpg", 
                     "http://oberoibali.com/", 
                     "mailto:oberoi@bali.com" };
    // Ikony na przyciskach
    Icon[] icon = {
        new ImageIcon("oberoi.jpg"), new ImageIcon("pdf_icon.gif"),
        new ImageIcon("player.jpg"), new ImageIcon("firefox.png"), 
        new ImageIcon("Email3.gif") 
       };
    
    setLayout(new FlowLayout(FlowLayout.LEFT));

    for (int i = 0; i < icon.length; i++) {
      JButton b = new JButton(txt[i], icon[i]);
      b.setActionCommand(cmd[i]);
      b.setVerticalTextPosition(SwingConstants.BOTTOM);
      b.setHorizontalTextPosition(SwingConstants.CENTER);
      b.addActionListener(this);
      add(b);
    }
    setDefaultCloseOperation(EXIT_ON_CLOSE);
    pack();
    setVisible(true);
  }
  
  // Obsługa przycisków
  public void actionPerformed(ActionEvent e) {
    String cmd = e.getActionCommand();
    if (cmd.equals("Details")) {
      showDetails();
    } else if (!cmd.startsWith("http:") && ! cmd.startsWith("mailto:")) {
        try {
          desk.open(new File(cmd));  // otwarcie aplikacji skojarzonej z plikiem
        } catch (IOException exc) {
          exc.printStackTrace();
        }
    } else try {  // przeglądarka lub poczta
          URI uri = new URI(cmd);
          if (cmd.startsWith("http:")) 
            desk.browse(new URI(cmd)); // przeglądarka z podanym adresem
          else desk.mail(uri);         // klient pocztowego z podanym mailto:
        } catch (IOException exc) {
          exc.printStackTrace();
        } catch (URISyntaxException exc) {
          exc.printStackTrace();
        }
  }

  private void showDetails() {
    // ...
  }

  public static void main(String[] args) {
    if (!Desktop.isDesktopSupported()) {
      System.out.println("Aplikacja nie działa na tej platformie");
      System.exit(1);
    }
    desk = Desktop.getDesktop();
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        new HotelView();
      }
    });

  }

}
Klasa Desktop w Javie 6 jest bardzo podobna do klasy Desktop z JDIC.
Może najistotniejszą różnicą jest to, że w Javie 6 adresy internetowe (dla przeglądarki i dla klienta pocztowego) traktuje się jednolicie jako URI. W JDIC natomiast mamy URL dla przeglądarki i specjalny obiekt klasy Message dla poczty. Na obiekcie klasy Message możemy wykonywać operacje ustalania adresata, przedmiotu, treści itp. Jest to łatwiejsze niż działanie z URI typu "mailto:", choć - być może - mniej ogólne.

Warto podkreślić, że projekt JDIC ma dużo większe możliwości niż elementy integracji z pulpitem zawarte w Javie 6. Należy do nich m.in. operowanie na asocjacjach (typy danych - aplikacje). Szczególnie istotna jest jednak zdolność do wbudowywania  działania rodzimej (domyślnej) przeglądarki w aplikacje Javy. Obiekt klasy WebBrowser, który jako komponent może być umieszczony np. w panelu w oknie aplikacji Javy, pokazuje wyniki działania przeglądarki. Stwarza to możliwość budowania bardzo elastycznych wolnostojących aplikacji, w prosty sposób korzystających z bogactwa Internetu.


7.2. Ikonki obszaru powiadomień (tray)


Poczynając od wersji 6 aplikacja Javy może dodawać ikonki do obszaru powiadamiania (SystemTray).
Ikonki reprezentowane są przez klasę TrayIcon, przy czym możemy:

Przykładowa aplikacja wykorzystująca w/w możliwości symuluje przeszukiwanie dysku w poszukiwaniu plików. Naturalnym jest uruchomienie takiej aplikacji w postaci ikony w obszarze powiadomień (lewa ikonka na rys. a).

Rys. a. Aplikacja jako ikona w obszarze powiadomień Windows

r

Podpowiedź (tooltip) mówi o tym co to za aplikacja (rys. b)

Rys. b. Podpowiedź pokazuje postępu przeszukiwania

r

Tooltip się zmienia wraz z postępami przeszukiwania (rys. c).

Rys. c. Postępy przeszukiwania

r

Po zakończeniu szukania zostanie wyświetlony "balonowy" komunikat (rys. d).

Rys. d. Komunikat o zakończeniu przeszukiwania

r


Z menu kontekstowego ikony możemy wybrać opcje działania aplikacji (rys. e).

Rys. e. Menu kontekstowe ikonki
r

Program (wraz z komentarzami wyjaśniającymi jego elementy) przedstawia poniższy wydruk

public class TrayDemo1 implements ActionListener {
  
  // Ikonka aplikacji
  private TrayIcon ticon;
  // Menu kontekstowe
  private PopupMenu pm = new PopupMenu();
  // Obszar powiadomień (tray)
  private SystemTray tray = SystemTray.getSystemTray();
  // Wątek przeszukiwania
  private volatile Thread finderThread;
  
  // Wynik przezukiwania
  private String result;
  
  // Zadanie wykonywane w wątku przeszukiwania
  // (symulacja przeszukiwania dysku)
  Runnable finder = new Runnable() {
    
    String[] msg = { "Search finished.\nClick to view results.",
                      "Search canceled.\nClick to view partial results."
                    };

    public void run() {
      int mnr = 0, k;
      for (k=0; k<1000; k+=100) {  // tu winno być pzreszukiwanie
        try {                      // symulujemy pętlą ze sleepem
          Thread.sleep(1000);
        } catch (InterruptedException exc) {
          mnr = 1;
          break;
        }
        ticon.setToolTip(k + " files searched.");  // aktualne info  
      }
      // Po zakończeniu przeszukiwania
      result = (k-1) + " files found";   // wynik
      ticon.setToolTip("Search files");  // przywracamy oryginalny tooltip
      
      // Pokazujemy dymek z info, że szukanie zakończone
      ticon.displayMessage("Finder", msg[mnr], TrayIcon.MessageType.INFO);

      finderThread = null;
    }
  };
  
  
  public TrayDemo1() {
    
    // Ustalenie menu kontekstowego
    String[] cmds = { "Suspend", "Cancel", "Start new search", "Exit" };
    for (int i = 0; i < cmds.length; i++) {
      MenuItem mi = new MenuItem(cmds[i]);
      mi.addActionListener(this);
      pm.add(mi);
    }  
    
    // Stworzenie ikonki aplikacji
    ticon = new TrayIcon(new ImageIcon("jsearch.gif").getImage(),
                         "Search files",
                         pm);
    ticon.setImageAutoSize(true);
    
    // Reakcja na kliknięcie w "balon"
    ticon.addActionListener( new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        JOptionPane.showMessageDialog(null, result);
      }
    });

    // Dodanie ikonki do obszaru powiadomień
    try {
      tray.add(ticon);
    } catch (AWTException exc) {
      exc.printStackTrace();
    }
    
    // Start wątku przeszukiwania
    finderThread = new Thread(finder);
    finderThread.start();
    
  }

  // Obsługa wyborów z menu kontekstowego
  public void actionPerformed(ActionEvent e) {
    String cmd = e.getActionCommand();
    if (cmd.equals("Exit")) System.exit(0);
    else if (cmd.equals("Cancel")) {
      if (finderThread != null) {
        finderThread.interrupt();  // zakończenie wątku
        finderThread = null;
      }
    }
    else if (cmd.equals("Start new search")) {
      if (finderThread == null) {
        finderThread = new Thread(finder);
        finderThread.start();
      }
    }
    else {
      System.out.println("Command not implemented.");
    }
  }


  public static void main(String[] args) {
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        new TrayDemo1();      }
    });
  }

}

Należy zwrócić uwagę na to, że menu kontekstowe w TrayIcon jest typu PopupMenu z pakietu java.awt.
Jest to duża niedogodność, ponieważ PopupMenu ma całkowicie przestarzałą konstrukcję. Nie umieścimy w nim ikon i nie dodamy wygodnych akcji (obiektów klas implementujących interfejs Action). Swingowe JPopupMenu już dawno zmieniło tę sytuację: opcje menu są pochodne od klasy AbstractButton i mają wszystkie właściwości przycisków Swingu, do menu można też bezpośrednio dodawać akcje.

Mniej więcej rok temu Artem Ananiev przedstawił w swoim blogu proste rozwiązanie problemu: nie należy ustalać PopupMenu dla TrayIcon, ale w obsłudze zdarzeń myszki otworzyć swingowe JPopupMenu. Rozwiązanie okazało się niewystarczające pod wieloma względami, w szczególności menu nie znikało przy kliknięciu poza jego obszarem, co więcej menu i jego elementy nie miały fokusu (brak możliwości działania z klawiatury, np. przemieszczania zaznaczeń klawiszami kursora).
Tymczasem projekt JDIC dostarcza nam właśnie JPopupMenu dla TrayIcon. Dlaczego tego nie uwzględniono w Javie 6? Być może ograniczeniem było to, że JPopupMenu w żaden sposób nie chce nakładać się na TaskBar z ustawieniem "OnTop", a wobec tego zachowuje się nieco inaczej niż rodzime "popupy" ikon.
Jeśli jednak ta różnica nam nie przeszkadza, możemy używać JDIC i mieć prawdziwy JPopupMenu na ikonach obszaru powiadamiania.
A czy można coś zrobić, pozostając przy standardzie Javy 6 (który zresztą pod pewnymi względami jest lepszy od JDIC - np. możliwość obsługi zdarzeń myszki na tray-ikonach)?
Kłopoty "wczesnych" rozwiązań "JPopup na TrayIcon" wynikały z niewłaściwego ustalenia komponentu na którym menu jest otwierane (właściwość "invoker").
Otóż musi to być komponent widoczny (a zatem zawarty w widocznym oknie najwyższego poziomu). Właściwym wyborem będzie JDialog. Okno jest nam potrzebne tylko do uwidocznienia menu, dlatego nie chcemy, aby jego ikonka był a widoczna na pasku zadań (stąd JDialog). Ustalimy mu również rozmiary na (0, 0).
Nie można jednak ustalić dialogu jako komponentu na którym pojawia się menu, ponieważ jest on komponentem ciężkim i stracimy fokus. Naturalnym wyborem będzie contenPane tego dialogu. Ponadto, przy uwidacznianiu dialogu należy go przesunąć na pierwszy plan (toFront).
Wreszcie TrayIcon nie pochodzi od klasy Component. Wobec tego - jeśli uwidacznianie menu kontekstowego zwiążemy wyłącznie z obsługą dość wysokopoziomowego zdarzenia mouseReleased czy mousePressed, to zdarzenia niższego poziomu (rodzime, fizyczne zdarzenia myszki) będą docierać do BasicPopupUI (delegata odpowiedzialnego za zachowanie menu kontekstowego) i powodować powstanie wyjątku ClassCastException (z TrayIcon do Component). Powierzchowne i szybkie rozwiązanie tego problemu polega na przechwyceniu tego wyjątku. Jednak powstaje on w wątku obsługi zdarzeń (EDT) i nasz kod nie może go normalnie obsłużyć. Wyjściem jest ustanowienie handlera nieprzechwyconych wyjątków EDT, co w tej chwili można zrobić ustalając  właściwość systemową "sun.awt.exception.handler".

Podsumowaniem tych rozważań jest poniższy listing, przedstawiający narzędziową klasę "TrayIcon z JPopupMenu".


package traypopup;

import java.awt.*;
import java.awt.event.*;

import javax.swing.*;
import javax.swing.event.*;

public class TrayIconPopup extends TrayIcon  {
  
  // okno potrzebna dla fokusu (dialog, aby nie bylo widoczne na pasku)
  private static JDialog frame;
  
  private JPopupMenu menu = new JPopupMenu();

  // Trochę wygodniejszy konstruktor
  public TrayIconPopup(ImageIcon icon, String tooltip) {
    this(icon.getImage(), tooltip);
  }

  // Standardowy konstruktor
  public TrayIconPopup(Image image, String tooltip) {
    super(image, tooltip);

    // aby uniknąc od czasu do czasu pojawiającego się błędu z EDT
    // spowodowanego zbyt wysokim poziomem (u nas) obsługi zdarzeń myszki
    System.setProperty("sun.awt.exception.handler",
                       "traypopup.EdtUncaughtExcHandler");
    configureMenu();
  
    // Pokazuje menu kontekstowe z TrayIcom
    addMouseListener(new MouseAdapter() {

      @Override
      public void mouseReleased(MouseEvent e) {
        if (e.isPopupTrigger()) {
          if (frame == null) {
            frame = new JDialog((Frame)null);
            frame.setAlwaysOnTop(true);  // nie zwalczymy tym WinTaskBaru on top!
            frame.setSize(0, 0);
          }
          frame.setVisible(true);
          int shift = menu.getPreferredSize().width;
          menu.show(frame.getContentPane(), e.getX() - shift, e.getY());
          frame.toFront();
        }
      }
    });

  }

  private void configureMenu() {
    menu.setLightWeightPopupEnabled(false);
    menu.addPopupMenuListener( new PopupMenuListener() {

      public void popupMenuCanceled(PopupMenuEvent e) {}
      
      public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
        frame.setVisible(false);
        frame.toBack();
      }
      public void popupMenuWillBecomeVisible(PopupMenuEvent e) { }
    });
  }

  // Dodanie do menu kontekstowegp
  public void addToPopup(JMenuItem mi) {
    menu.add(mi);
  }

  // Dodanie do menu kontesktowegp
  public void addToPopup(Action a) {
    menu.add(a);
  }
  
  // Ustalenie innego menu kontekstowego
  public void setPopup(JPopupMenu popup) {
    menu = popup;
    configureMenu();
  }

  // Zwraca JPopupMenu, możemy na nim działać dynamicznie
  public JPopupMenu getPopup() {
    return menu;
  }

  public void addToPopup(JSeparator separator) {
    menu.add(separator);
  }

}
Klasę handlera nieprzechwyconych wyjątków EDT pokazano niżej.

Handler nieprzechwyconych wyjątków EDT.

package traypopup;

public class EdtUncaughtExcHandler {
  
  public void handle(Throwable t) {
    
    if (t.toString()
        .indexOf("TrayIconPopup cannot be cast to java.awt.Component") == -1) {
      t.printStackTrace();
    }
  }

}

Możemy sprawdzić działanie tej klasy dla przypadku prostego przykładowego JPopupMenu (ryunek i listing poniżej(

Rys.  TrayIcon z JPopupMenu

r

Test TrayIcon z JPopupMenu
package traypopup;

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import com.jgoodies.looks.windows.*;

public class Test {

  public static void main(String[] args) {
    
    // Aby menu kontekstowe wyglądało ładnie uzyjemy JGoodies
    try {
      UIManager.setLookAndFeel(new WindowsLookAndFeel());
    } catch (Exception e) {
      e.printStackTrace();
    }

    ImageIcon[] icons = {
        new ImageIcon("jsbook.gif"), new ImageIcon("jsearch.gif"),
        new ImageIcon("middle.gif"), new ImageIcon("error.gif"),
    };
    
    TrayIconPopup ti = 
      new TrayIconPopup(icons[0],  "Test JPopupMenu with TrayIcon");
     
    for (int i=1; i< icons.length; i++) {
      ti.addToPopup(new JMenuItem("Element " + i, icons[i]));
    }
    ti.addToPopup(new JSeparator());
    ti.addToPopup(new JMenuItem("Wykonanie operacji x"));
    ti.addToPopup(new AbstractAction("Quit") {
      public void actionPerformed(ActionEvent e) {
        System.exit(0);
      }
    });
    
   SystemTray tray = SystemTray.getSystemTray();
    try {
      tray.add(ti);
    } catch (AWTException exc) {
      exc.printStackTrace();
    }
    
    JFrame f = new JFrame("Okno aplikacji");
    f.add(new JButton("Może coś robi, jest jakieś GUI, itd. itp."));
    f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    f.setLocationRelativeTo(null);
    f.pack();
    f.setVisible(true);
  }

}



8. Dlugotrwałe zadania i SwingWorker


Co się stanie, jeśli z wątku obsługi zdarzeń (ang. event dispatching thread, w skrócie EDT) uruchomimy jakieś długo wykonujące się zadanie?
Cóż, nasze GUI będzie zamrożone, niereaktywne, dopóki zadanie się nie skończy.
Przekonuje o tym następujący programik, w którym metoda longTask() spędza ok. 5 sekund na jakichś obliczeniach (co symulujemy przez sleep(..)).

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

public class LongTask extends JFrame {

  LongTask() {
    JButton b = new JButton(
        "Uruchamiam długie zadanie. Kliknij i spróbuj coś wpisać w pole tekstowe");
    final JTextField tf = new JTextField(40);
    b.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        int wynik = longTask();
        tf.setText("Wynik: " + wynik);
      }
    });

    add(tf, "Center");
    add(b, "South");
    setDefaultCloseOperation(DISPOSE_ON_CLOSE);
    pack();
    setLocationRelativeTo(null);
    setVisible(true);
  }
  
  public int longTask() {
    try {
      Thread.sleep(5000);
    } catch (InterruptedException exc) {
      exc.printStackTrace();
    }
    return 100;    
  }

  public static void main(String[] args) {
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        new LongTask();
      }
    });
  }
}

Zobacz działanie programu







Ważny wniosek.


Długotrwałe czynności nie mogą być wykonywane w wątku obsługi zdarzeń (EDT).

Należy takie czynności uruchamiać jako odrębne wątki.
Ale robiąc to całkiem "ręcznie" natkniemy się na kilka problemów. Po pierwsze, uzyskania wyniku działań (on przecież będzie dostępny asynchronicznie, w  przyszłości, nie wiemy kiedy). Po drugie, komunikacji z GUI (zmiany w GUI - jak wiemy - zawsze muszą być dokonywane w EDT).

Rozwiązaniem tych problemów jest zastosowanie klasy SwingWorker.
Klasę tę wykorzystujemy przez dziedziczenie.

Działania (te właśnie długotrwałe czynnośic) wykonywane w odrębnym od EDT wątku określamy w przedefiniowanej metodzie doInBackground(...).

Aby uruchomić zadanie stosujemy metodę execute() klasy SwingWorker.

Działania, które mają być wykonane po zakończeniu zadania określamy w przedefiniowanej metodzie done(). Jest ona wywoływana po zakończeniu "długotrwałych czynności" i wykonywana w EDT, a więc z jej poziomu możemy swobodnie operować na GUI (np. w celu prezentacji wyników).

Wynik zaś uzyskujemy za pomocą metody get() z klasy SwingWorker (w metodzie done() zadanie jest zakończone, więc get() nie blokuje wątku; inne jego użycie jest blokujące albo do uzyskania wyniku albo przez określony jako argument czas).

Zobaczmy na zmodyfikowanym przykładzie "dlugiego zadania".
import java.awt.event.*;
import javax.swing.*;

public class SwingWorker1 extends JFrame {
  
  private JTextField tf = new JTextField(40);
  
  class Worker extends SwingWorker<Integer, Integer> {

    @Override
    protected Integer doInBackground() throws Exception { // działa poza EDT
      return longTask();   
    }
    
    @Override
    protected void done() {  // działa w EDT, wołane po zakończeniu zadania
      try {
        int wynik = get();  // uzyskanie wyniku
        tf.setText( tf.getText() + " Wynik: " + wynik );
      } catch (Exception exc) {
        exc.printStackTrace();
      }
    }
  }

  SwingWorker1() {
    
    JButton b = new JButton(
        "Uruchamiam długie zadanie. Teraz GUI będzie responsywne");
    
    b.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        new Worker().execute();
      }
    });

    add(tf, "Center");
    add(b, "South");
    setDefaultCloseOperation(DISPOSE_ON_CLOSE);
    pack();
    setLocationRelativeTo(null);
    setVisible(true);
  }
  
  public int longTask() {
    try {
      Thread.sleep(5000);
    } catch (InterruptedException exc) {
      exc.printStackTrace();
    }
    return 100;    
  }

  public static void main(String[] args) {
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        new SwingWorker1();
      }
    });
  }
}
Uwagi:

Zobacz działanie programu







No włanie, drugi parametr typu klasy SwingWorker określa typ wyników posrednich. Często bowiem istnieje potrzeba uzyskiwania wyników pośrednich (jeszcze przed zakończeniem długo wykonujących się czynności), choćby po to, aby pokazywac w GUI postępy zadania.

Wyniki pośrednie udastępniamy w metodzie doInBackground za pomocą wywołania publish(...). To się wykonuje poza EDT, ale wyniki pośrednie stają się dostępne dla metody process(List), która jest wykonywana w EDT.
W ten sposób na bieżąco możemy w GUI odzwierciedlać postępy wykonania zadania.

Obrazuje to poniższy kod, w którym obliczenia składają się z kolejnych kroków (ich liczbę podajemy przy tworzeniu instancji SwingWorkera) i  używamy w metodzie doInBackground, która podaną liczbę razy wywołuje longTask(...) (tym razem co 2 sek. losująca kolejną liczbę z przedziału 100-199).

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

public class SwingWorker2 extends JFrame {
  
  private JTextArea ta = new JTextArea(20, 20);
  
  
  class Worker extends SwingWorker<List<Integer>, Integer> {
    private int steps;
    public Worker(int n) {
      steps = n;
    }

    @Override
    protected List<Integer> doInBackground()  {
      List<Integer> nums = new ArrayList<Integer>();
      int wyn = 0;
      for (int i=1; i <= steps; i++) {
        wyn =  longTask();
        nums.add(wyn);
        publish(wyn);  // publikacja pośredniego wyniku - do wykorzystania przez process w edt
      }
      return nums;
    }
    
    
    // Metoda process() działa w EDT, a otrzymuje jako argument pośrednie wyniki od publish
    // Może je więc pokazywać w GUI
    @Override
    protected void process(List<Integer> chunks) {
      for (Integer n : chunks) {
        ta.append("\n"+ n);
      }
    }
    
    @Override
    protected void done() {
      List<Integer> nums = null;
      try {
        nums = get();
      } catch (Exception exc) {
        exc.printStackTrace();
      }
      String liczby = "";
      for (Integer n : nums) {
        liczby += n + " ";
      }
      ta.append("\nGotowe:\n" + liczby );
    }
    
  }

  SwingWorker2() {
    
    JButton b = new JButton(
        "Uruchamiam długie zadanie. Teraz GUI będzie pokazywało wyniki pośrednie");
    
    b.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        new Worker(10).execute();
      }
    });

    add(new JScrollPane(ta), "Center");
    add(b, "South");
    setDefaultCloseOperation(DISPOSE_ON_CLOSE);
    pack();
    setLocationRelativeTo(null);
    setVisible(true);
  }
  
  private Random rand = new Random();
  
  public int longTask() {
    try {
      Thread.sleep(2000);
    } catch (InterruptedException exc) {
      exc.printStackTrace();
    }
    return rand.nextInt(100) + 100;    
  }

  public static void main(String[] args) {
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        new SwingWorker2();
      }
    });
  }
} 

Zobacz działanie programu











Klasa SwingWorker ma jeszcze sporo innych możliwości: np. przerywania zadań czy asynchronicznego powiadamiania o postępach na zasadzie nasłuchu zdarzeń. Dobre ich opanowanie wymaga jednak wiedzy o narzędziach pakietu java.utilc.oncurrent oraz o nasłuchu zmian właściwości w modelu JavaBeans. O tym będziemy mówić w niedalekiej przyszłości, w ramach nieco bardziej zaawansowanego kursu.