PO Graficzny interfejs użytkownika

From Studia Informatyczne

<<< Powrót do przedmiotu Programowanie obiektowe

Spis treści

Wprowadzenie

Podejście obiektowe sprawdza się bardzo dobrze podczas tworzenia graficznego interfejsu użytkownika (ang. Graphical User Interface, GUI). Biblioteki klas służących do tego celu zawierają gotowe do użycia komponenty takie, jak przyciski, pola tekstowe, panele, czy okienka dialogowe. Ich zestawianie razem jest zazwyczaj proste i intuicyjne. Co więcej, dla najpopularniejszych języków istnieją specjalne generatory kodu pozwalające na konstruowanie GUI w sposób graficzny metodą przeciągnij i upuść. W tym wykładzie przedstawiamy szybkie wprowadzenie do wchodzącego w skład JDK graficznego zrębu (ang. framework) Java Foundation Classes (JFC) oraz jego biblioteki komponentów graficznych Swing.

Java Foundation Classes

Java posiada dobrze przemyślany graficzny zrąb (ang. framework) – Java Foundation Classes (JFC) oraz bogatą bibliotekę gotowych komponentów takich, jak przyciski, pola tekstowe, panele czy okienka dialogowe. Mimo że biblioteka Swing jest bardzo rozbudowana, do jej opanowania nie potrzeba dużo czasu. Biblioteka zaprojektowana jest konsekwentnie, a prześledzenie kilku przykładów wystarcza do zapoznania się z podstawowymi konwencjami i wzorcami.

Witaj świecie

import javax.swing.*;

public class WitajŚwiecie {
  private static void utwórzGUI() {
    //tworzenie nowego okna 
    JFrame frame = new JFrame("Okno WitajŚwiecie");
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    
    //dodawanie etykiety z przywitaniem
    JLabel label = new JLabel("Witaj świecie!");
    frame.add(label);
    
    //ustalanie wymiarów i wyświetlanie okna
    //frame.pack();
    frame.setSize(300,150);
    frame.setVisible(true);
  }
  
  public static void main(String[] args) {
    //aby uniknąć zakleszczeń tworzenie GUI zawsze zlecamy dla wątku obsługi zdarzeń
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        utwórzGUI();
      }
    });
  }
}

Kontenery najwyższego poziomu

JFrame frame = new JFrame("Okno WitajŚwiecie");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

Przyjrzyjmy się treści metody utwórzGUI(). Najpierw konstruowany jest nowy obiekty JFrame. Jest to kontener najwyższego poziomu i reprezentuje okno główne aplikacji. Kontener najwyższego poziomu zawiera obszar, w którym będą rysowane pozostałe "zwykłe" komponenty (np. przyciski) oraz umożliwia obsługę zachodzących zdarzeń (np. kliknięcie przycisku). Poza JFrame są jeszcze dwa rodzaje kontenerów najwyższego poziomu: JDialog, który reprezentuje okno dialogowe (zależne od jakiegoś innego okna) oraz JApplet, który działa w oknie przeglądarki WWW. Wszystkie pozostałe komponenty Swing rozszerzają klasę JComponent i muszą być pośrednio lub bezpośrednio umieszczone w kontenerze najwyższego poziomu. Wywołanie metody setDefaultCloseOperation() jest niezbędne, aby program kończył się kiedy użytkownik spróbuje zamknąć okienko. Bez niej takie próby nie przyniosą żadnego efektu.

Dodawanie komponentów do kontenera

JLabel label = new JLabel("Witaj świecie!");
frame.add(label);

W naszym przykładzie w oknie głównym aplikacji umieszczana jest etykieta z tekstem powitania. Każdy komponent można umieścić w kontenerze tylko jednokrotnie. Jeżeli komponent, który już znajdujący się w jakimś kontenerze spróbujesz dodać do innego, automatycznie zostanie usunięty z dotychczasowego kontenera.

Ustalanie rozmiarów i wyświetlanie okna

//frame.pack();
frame.setSize(300,150);
frame.setVisible(true);

Dla okna trzeba jeszcze ustalić odpowiedni rozmiar i wyświetlić go na ekranie. Metoda pack() ustala rozmiar okna tak, aby mieściły się w nim wszystkie widoczne komponenty. Rozmiar okna można również określić samemu przy pomocy metody setSize(int szerokość, int wysokość). Aby okno było widoczne na ekranie, trzeba wywołać jego metodę setVisible() z parametrem true.

Wątek koordynujący zdarzenia

Ze względu na efektywność klasy Swing nie są zabezpieczone na wypadek posługiwania się nimi przez kilka wątków [1] naraz. Za obsługę zdarzeń generowanych przez interfejs i rysowanie komponentów odpowiada specjalny wątek – wątek koordynujący zdarzenia (ang. event-dispatching thread). Dzięki temu dwa zdarzenia nigdy nie są obsługiwane jednocześnie oraz rysowanie nie jest przerywane przez obsługę zdarzenia. Dla bezpieczeństwa cały kod modyfikujący GUI powinien być wykonywany przez wątek koordynujący zdarzenia. Jak się niedługo przekonasz większość operacji na komponentach GUI i tak wykonywanych jest podczas obsługi zdarzeń (przez wątek koordynujący). W pozostałych przypadkach pracę należy zlecić wątkowi koordynującemu przy pomocy metody SwingUtilities.invokeLater(), bądź SwingUtilities.invokeAndWait().

SwingUtilities.invokeLater(new Runnable() {
  public void run() {
    utwórzGUI();
  }
});

W naszym przypadku, jako parametr tej metody przekazujemy obiekt anonimowej podklasy Runnable. Jego metoda run() wykonuje opisaną przez nas metodę utwórzGUI(). Wątek koordynujący przekaże w wolnej chwili sterowanie do tego obiektu Runnable i tym samym zainicjalizuje nasz interfejs.

Działający przykład

Podgląd działającego przykładu
Archiwum JAR Podgląd jako Applet
Żeby uruchomić przykład Po_GUI_WitajSwiecie.jar ściągnij najpierw archiwum jar na swój komputer (możesz to zrobić klikając na odnośniku prawym klawiszem myszy i wybierając "Zapisz jako...") i uruchom jak zwykły program.

Układ komponentów

Pozycje komponentów na formularzu oraz ich rozmiar można określić bezwzględnie, ale zazwyczaj jest to niewskazane. Po pierwsze, w Swingu można bardzo łatwo przełączać wygląd i zachowanie (ang. look and feel) komponentów GUI. Nie jest też trudno napisać program dopasowujący się do domyślnego wyglądu i zachowania platformy, na której jest uruchamiany. Jednak zmiana układu i zachowania zazwyczaj wpływa na rozmiar komponentów. Po drugie, Java jest przystosowana do tworzenia programów z wieloma wersjami językowymi, a napisy prezentowane użytkownikowi różnią się długością w zależności od języka. Dlatego sposób rozmieszczenia komponentów na formularzu, ich rozmiar i proporcje powinno się zlecać zarządcy układu (ang. layout manager).

Zarządca układu to obiekt implementujący interfejs LayoutManager i decydujący o rozmiarze i pozycji komponentów w kontenerze. Mimo, że komponenty mogą podpowiadać, jaki powinny mieć rozmiar i pozycję, ostateczna decyzja należy do zarządcy układu. Jest to cecha wyróżniająca Swing względem innych bibliotek GUI.

BorderLayout

Główne kontenery (JApplet, JDialog i JFrame) domyślnie używają BorderLayout. Przy jego pomocy można rozmieścić do pięciu innych komponentów, w tym inne kontenery, np. panele JPanel. Każdy komponent umieszczany jest w jednym z pięciu rejonów: na dole, na górze, po lewo, po prawo i po środku. Komponenty dodawane do kontenera bez wskazania rejonu domyślnie dodawane są po środku. Dodanie nowego komponentu do rejonu już posiadającego zawartość spowoduje jej podmianę. W poniższym przykładzie okno jest wypełniane pięcioma przyciskami (jak związać z przyciskami jakieś zachowanie wyjaśnimy później).

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

public class BorderLayoutTest extends JFrame {
  BorderLayoutTest() {
    super("Okno BorderLayoutTest");
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    //Główne kontenery (JApplet, JDialog i JFrame) domyślnie używają BorderLayout.
    //Natomiast obiekty JPanel domyślnie używają FlowLayout.
    add(new JButton("CENTER")); //domyślna pozycja to BorderLayout.CENTER
    add(BorderLayout.NORTH, new JButton("NORTH"));
    add(BorderLayout.SOUTH, new JButton("SOUTH"));
    add(BorderLayout.EAST, new JButton("EAST"));
    add(BorderLayout.WEST, new JButton("WEST"));

    setSize(300,150);
    setVisible(true);
  }
 
  public static void main(String[] args) {
    //aby uniknąć zakleszczeń, tworzenie GUI zawsze zlecamy dla wątku obsługi zdarzeń
    javax.swing.SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        new BorderLayoutTest();
      }
    });
  }
}
Podgląd działającego przykładu
Archiwum JAR Podgląd jako Applet
Żeby uruchomić przykład Po_GUI_BorderLayoutTest.jar ściągnij najpierw archiwum jar na swój komputer (możesz to zrobić klikając na odnośniku prawym klawiszem myszy i wybierając "Zapisz jako...") i uruchom jak zwykły program.

Jeżeli rozmiar kontenera ulegnie zmianie, zarządca układu automatycznie poprawia układ komponentów. Wypróbuj to zmieniając rozmiar okna działającego programu.

FlowLayout

Zmianę zarządcy układu wykonuje się przy pomocy metody setLayout(LayoutManager). W poniższym przykładzie prezentujemy działanie FlowLayout, który układa komponenty od lewej do prawej w kolejnych wierszach.

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

public class FlowLayoutTest extends JFrame {
  FlowLayoutTest() {
    super("Okno FlowLayoutTest");
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    
    setLayout(new FlowLayout());
    add(new JButton("Psk."));
    add(new JButton("Przycisk"));
    add(new JButton("Długi przycisk"));
    add(new JButton("B. długi przycisk"));
    add(new JButton("Bardzo długi przycisk"));
    add(new JButton("Bardzo bardzo długi przycisk"));
    setSize(300,150);
    setVisible(true);
  }
 
  public static void main(String[] args) {
    //aby uniknąć zakleszczeń, tworzenie GUI zawsze zlecamy dla wątku obsługi zdarzeń
    javax.swing.SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        new FlowLayoutTest();
      }
    });
  }
}
Podgląd działającego przykładu
Archiwum JAR Podgląd jako Applet
Żeby uruchomić przykład Po_GUI_FlowLayoutTest.jar ściągnij najpierw archiwum jar na swój komputer (możesz to zrobić klikając na odnośniku prawym klawiszem myszy i wybierając "Zapisz jako...") i uruchom jak zwykły program.

Zwróć uwagę, że FlowLayout zezwala komponentom przyjąć preferowany przez nie rozmiar, nawet jeżeli wszystkie komponenty nie mieszczą się w widocznej części kontenera.

GridLayout

GridLayout układa komponenty w komórkach siatki, której rozmiar określamy przy pomocy parametrów konstruktora. Komponenty zajmują kolejne komórki zgodnie z kolejnością dodawania. W poniższym przykładzie do siatki o dwóch kolumnach i trzech wierszach dodawanych jest pięć przycisków. Zwróć uwagę na to, że mimo, iż ostatni przycisk jest bardzo długi, a komórka na prawo od niego jest wolna, będzie zajmował dokładnie tyle samo miejsca co pozostałe przyciski.

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

public class GridLayoutTest extends JFrame {
  GridLayoutTest() {
    super("Okno GridLayoutTest");
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    
    setLayout(new GridLayout(3,2));
    add(new JButton("P1"));
    add(new JButton("P2"));
    add(new JButton("P3"));
    add(new JButton("P4"));
    add(new JButton("Bardzo długi przycisk"));
    setSize(300,150);
    setVisible(true);
  }
 
  public static void main(String[] args) {
    //aby uniknąć zakleszczeń, tworzenie GUI zawsze zlecamy dla wątku obsługi zdarzeń
    javax.swing.SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        new GridLayoutTest();
      }
    });
  }
}
Podgląd działającego przykładu
Archiwum JAR Podgląd jako Applet
Żeby uruchomić przykład Po_GUI_GridLayoutTest.jar ściągnij najpierw archiwum jar na swój komputer (możesz to zrobić klikając na odnośniku prawym klawiszem myszy i wybierając "Zapisz jako...") i uruchom jak zwykły program.

Zagnieżdżanie kontenerów

Omówione dotychczas rodzaje zarządców układu dają jedynie podstawowe możliwości. Trochę więcej można osiągnąć umieszczając jako jeden z komponentów lekki kontener (np. JPanel), który sam będzie zawierał komponenty rozmieszczane przez jakiegoś zarządcę układu. W przypadku JPanel domyślnym zarządcą nie jest BorderLayout, ale FlowLayout.

Pozostałe rodzaje zarządców układu

Swing zawiera jeszcze cztery rodzaje zarządców układu:

  • BoxLayout – komponenty są umieszczane w jednym wierszu lub w jednej kolumnie, zagnieżdżając w sobie kontenery używające BoxLayout można uzyskać bardzo skomplikowane układy; w odróżnieniu od GridLayout, jeżeli jest to możliwe, komponenty będą miały preferowane przez nie rozmiary; Swing zawiera lekki kontener Box, który domyślnie korzysta z BoxLayout oraz posiada kilka bardzo przydatnych udogodnień, takich jak klej (ang. glue), rozpórki (ang. struts) oraz wypełniacze (ang. rigid area),
  • GridBagLayout – rozbudowany zarządca dający bardzo duże możliwości zapanowania nad rozmieszczeniem komponentów; doskonale nadaje się używania przez graficzne narzędzia przeznaczone do budowanie GUI metodą przeciągnij i upuść; ze względu na swoją złożoność rzadko używany przez ludzi,
  • SpringLayout – rozmieszczenie komponentów jest kontrolowane przez definicje więzów, które wyznaczają pionową lub poziomą odległość między krawędziami dwóch komponentów; poszczególne więzy są reprezentowane przez obiekty klasy Spring i można o nich myśleć jak o sprężynach z minimalną, optymalną i maksymalną długością oraz
  • CardLayout – umożliwia proste przełączanie wyświetlanych komponentów w trakcie działania programu; każdy komponent dodany do kontenera używającego CardLayout traktowany jest jak kartka; w danej chwili wyświetlana jest tylko jedna kartka (początkowo ta dodana jako pierwsza); CardLayout udostępnia operacje pozwalające przełączać widoczną kartkę: first(), last(), next() i previous().

Dodatkowe rodzaje zarządców układu można znaleźć w sieci lub napisać samemu. Jeżeli jednak rozmieszczenie komponentów na ekranie robi się zbyt pracochłonne, warto rozważyć użycie jednego z narzędzi, w których taki interfejs projektuje się graficznie, metodą przeciągnij i upuść. JFC został zaprojektowany tak, aby ułatwić konstruowanie tego typu generatorów. Na uwagę zasługuje fakt, że dzięki przemyślanemu projektowi zrębu wygenerowany kod jest zrozumiały dla ludzi i możliwy do ręcznej modyfikacji.

Obsługa zdarzeń

Klikanie rozmieszczanych przez nas przycisków dotychczas nie dawało żadnego efektu. Do komponentów GUI można dołączać kod, który będzie wykonywany w chwili zajścia jednego z dotyczących ich zdarzeń. Przykładowo przycisk może reagować na kliknięcie lub najechanie myszką, a pole tekstowe na wprowadzenie tekstu. Kodu obsługi zdarzenia nie umieszcza się w klasach reprezentujących komponenty, bo stałyby się przez to sprzężone z klasami programu. Zamiast tego komponentom można przekazać do zapamiętania obiekty implementujące specjalny interfejs. Gdy zdarzenie zachodzi komponent wywołuje odpowiednie metody zapamiętanych obiektów. Przykładowo przyciski posiadają metodę addActionListener(ActionListener) przy pomocy której można im przekazywać do zapamiętania obiekty implementujące interfejs ActionListener. W reakcji na kliknięcie przyciski wywołują metodę actionPerformed(ActionEvent) na wszystkich aktualnie pamiętanych obiektach. Metoda removeActionListener(ActionListener) pozwala usunąć jeden z uprzednio zapamiętanych obiektów. Takie rozwiązanie charakteryzuje się bardzo dużą elastycznością i jest znane jako wzorzec Obserwator (ang. Observer), Wydawca-Prenumerator (ang. Publish-Subscribe) oraz Delegowanie obsługi zdarzeń (ang. Delegation Event Model). W poniższym przykładzie liczba kliknięć na przycisku jest zliczana i wyświetlana.

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

public class ZliczanieKliknięć extends JFrame {
  Integer licznikKliknięć = 0;
  JLabel etykieta;
 
  class ZwiększanieLicznika implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      //obiekt klasy wewnętrznej ma dostęp do składowych obiektu klasy otaczającej
      licznikKliknięć++;
      etykieta.setText("Dotychczas kliknąłeś " +
                       licznikKliknięć +
                       (licznikKliknięć == 1 ? " raz" : " razy"));
    }
  }
 
  ZliczanieKliknięć() {
    super("Okno ZliczanieKliknięć");
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    JPanel panel = new JPanel();
    panel.setLayout(new GridLayout(3, 0));

    JButton przycisk = new JButton("Kliknij");
    przycisk.addActionListener(new ZwiększanieLicznika());
    panel.add(przycisk);
   
    etykieta = new JLabel("Jeszcze nie kliknięto ani razu");
    panel.add(new JPanel());//pusty panel zapewnia odstęp
    panel.add(etykieta);
    //puste obramowanie odsuwa komponenty od krawędzi
    panel.setBorder(BorderFactory.createEmptyBorder(30,60,10,60));
    add(panel);
    
    setSize(300,150);
    setVisible(true);
  }
 
  public static void main(String[] args) {
    //aby uniknąć zakleszczeń, tworzenie GUI zawsze zlecamy dla wątku obsługi zdarzeń
    javax.swing.SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        new ZliczanieKliknięć();
      }
    });
  }
}
Podgląd działającego przykładu
Archiwum JAR Podgląd jako Applet
Żeby uruchomić przykład Po_GUI_ZliczanieKlikniec.jar ściągnij najpierw archiwum jar na swój komputer (możesz to zrobić klikając na odnośniku prawym klawiszem myszy i wybierając "Zapisz jako...") i uruchom jak zwykły program.

Kod obsługi zdarzeń wykonywany jest przez wątek koordynujący zdarzenia. Może dzięki temu swobodnie manipulować interfejsem z pominięciem metody SwingUtilities.invokeLater(). Należy zadbać, żeby kod obsługi zdarzeń wykonywał się szybko i nie blokował wątku koordynującego zdarzenia. W przeciwnym przypadku interfejs będzie z opóźnieniem reagował na polecenia użytkownika.

Pola tekstowe

Poniższy przykład pokazuje obsługę ActionEvent dla pola tekstowego JTextField. W tym wypadku zdarzenie zachodzi, gdy wartość wprowadzona w polu zostaje zaakceptowana enterem. Kod obsługi zdarzenia sumuje wartości wpisane do pól tekstowych i wyświetla wynik.

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

import javax.swing.*;

public class Dodawanie extends JFrame {
  JFrame okno = this;
  JTextField wynik = new JTextField(9);
  JTextField pole1 = new JTextField(9);
  JTextField pole2 = new JTextField(9);
  
  //do obsługi zdarzeń często używane są anonimowe klasy wewnętrzne
  //jeden egzemplarz będzie dzielony przez oba pola
  ActionListener sumowanie = new ActionListener() {
    public void actionPerformed(ActionEvent ev) {
      try {
        Integer w = Integer.parseInt(pole1.getText())+
                    Integer.parseInt(pole2.getText());
        wynik.setText(w.toString());
      } catch (NumberFormatException ex) {
        wynik.setText("Błąd");
      }
    }
  };
  
  Dodawanie() {
    super("Okno ZliczanieKliknięć");
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    JPanel panel = new JPanel();
    panel.setLayout(new FlowLayout());

    pole1.addActionListener(sumowanie);
    panel.add(pole1);
    panel.add(new JLabel("+"));
    panel.add(pole2);
    pole2.addActionListener(sumowanie);
    panel.add(new JLabel("="));
    wynik.setEditable(false);
    panel.add(wynik);
    
    //puste obramowanie odsuwa komponenty od krawędzi
    panel.setBorder(BorderFactory.createEmptyBorder(30,30,30,30));
    add(panel);
    
    setSize(300,150);
    setVisible(true);
  }
  
  public static void main(String[] args) {
    //aby uniknąć zakleszczeń, tworzenie GUI zawsze zlecamy dla wątku obsługi zdarzeń
    javax.swing.SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        new Dodawanie();
      }
    });
  }
}
Podgląd działającego przykładu
Archiwum JAR Podgląd jako Applet
Żeby uruchomić przykład Po_GUI_Dodawanie.jar ściągnij najpierw archiwum jar na swój komputer (możesz to zrobić klikając na odnośniku prawym klawiszem myszy i wybierając "Zapisz jako...") i uruchom jak zwykły program.

Najważniejsze rodzaje zdarzeń

Komponenty GUI mogą generować wiele różnych zdarzeń. W każdym przypadku ich obsługa zachodzi w sposób analogiczny a związane klasy, interfejsy i metody przestrzegają przejrzystych konwencji nazewniczych. Do reprezentowania każdego rodzaju zdarzenia istnieje odpowiednia klasa. Jej nazwa przestrzega schematu XxxEvent, np. kliknięcie przycisku reprezentują obiekty klasy ActionEvent. Obiekty zawierające kod obsługi muszą implementować odpowiedni interfejs. Nazwy interfejsów przestrzegają schematu XxxListener, gdzie Xxx jest takie samo jak w przypadku klasy reprezentującej zdarzenie, np. ActionListener. Wyjątkiem jest tu zdarzenie MouseEvent, dla którego istnieją dwa interfejsy MouseListener i MouseMotionListener. Rejestrowanie i wyrejestrowywanie obiektów, które chcą być informowane o zdarzeniach generowanych przez dany komponent odbywa się przy pomocy metod addXxxListenet(XxxListener) oraz removeXxxListener(XxxListener). Interfejs ActionListener zawiera tylko jedną metodę – actionPerformed(ActionEvent), która jest wywoływana przez komponent na wszystkich w danej chwili zarejestrowanych obiektach. Większość interfejsów zawiera takich metod więcej, każdą dla innej sytuacji, w której mogło zajść zdarzenie, np. metoda windowClosed(WindowEvent e) z interfejsu WindowListener jest wykonywana w momencie zamykania okna, a windowIconified(WindowEvent e) w momencie jego minimalizacji. W poniższej tabeli opisane są najważniejsze rodzaje zdarzeń oraz wymienione metody interfejsów, które powinny implementować obiekty zawierające kod obsługi.

Najważniejsze rodzaje zdarzeń, na które można reagować
Interfejs obserwatora i jego metody Opis
ActionListener
  • actionPerformed(ActionEvent e)
Dotyczy komponentów różnego rodzaju. Zachodzi np. gdy użytkownik klika przycisk, zatwierdza enterem tekst wpisany do pola tekstowego lub wybiera pozycję z menu.
MouseListener
  • mouseClicked(MouseEvent e)
  • mouseEntered(MouseEvent e)
  • mouseExited(MouseEvent e)
  • mousePressed(MouseEvent e)
  • mouseReleased(MouseEvent e)
Zdarzenia dotyczą operacji wykonywanych myszką na komponencie jak: wciśnięcie przycisku, puszczenie przycisku, kliknięcie, najechanie i opuszczenie. Do śledzenia ruchów kursora myszki i ruchów myszki podczas przytrzymywania przycisku służy MouseMotionListener. Klasa MouseAdapter dostarcza puste implementacje wszystkich metod z tego interfejsu.
MouseMotionListener
  • mouseDragged(MouseEvent e)
  • mouseMoved(MouseEvent e)
Zdarzenia dotyczą ruchów kursora myszy nad komponentem oraz ruchów kursora myszy podczas przytrzymywania przycisku. Klasa MouseMotionAdapter dostarcza puste implementacje wszystkich metod z tego interfejsu.
KeyListener
  • keyPressed(KeyEvent e)
  • keyReleased(KeyEvent e)
  • keyTyped(KeyEvent e)
Zdarzenia informujące o wciśnięciu, puszczeniu i kliknięciu klawisza na klawiaturze. Klasa KeyAdapter dostarcza puste implementacje wszystkich metod z tego interfejsu.
TextListener
  • textValueChanged(TextEvent e)
Dotyczy komponentów rozszerzających JTextComponent (np. JTextArea i JTextField). Zachodzi gdy zmienił się tekst.
WindowListener
  • windowActivated(WindowEvent e)
  • windowClosed(WindowEvent e)
  • windowClosing(WindowEvent e)
  • windowDeactivated(WindowEvent e)
  • windowDeiconified(WindowEvent e)
  • windowIconified(WindowEvent e)
  • windowOpened(WindowEvent e)
Zdarzenia dotyczące okien. Można m.in. zorientować się kiedy okno staje się/przestaje być aktywne, kiedy jest minimalizowane/przywracane do normalnego rozmiaru oraz kiedy jest otwierane po raz pierwszy/zamykane. Klasa WindowAdapter dostarcza puste implementacje wszystkich metod z tego interfejsu.

Wygląd i zachowanie

Swing daje możliwość łatwego zmieniania wyglądu i zachowania (ang. look and feel) komponentów GUI. Zazwyczaj nie korzysta przy tym z udogodnień systemu operacyjnego, ale symuluje wygląd i zachowanie od zera (komponenty są rysowane przez Javę, a nie system operacyjny). Dzięki temu ten sam wygląd i zachowanie można stosować na wszystkich platformach, dla których jest dostępna wersja JRE. Niestety takie rozwiązanie ma też wady. W większości wypadków symulacja wyglądu i zachowania typowego dla poszczególnych systemów operacyjnych jest daleka od oryginału. Aplikacje w Swingu nie korzystają z efektów graficznych dostępnych w systemie operacyjnych, np. wygładzania czcionek ekranowych. Na ich wygląd nie wpływają również ustawienia systemu operacyjnego dokonane przez użytkownika. Nie jest to jednak regułą. Sama konstrukcja biblioteki nie wyklucza korzystania z rozwiązań systemowych. Dystrybucja Javy dla systemu Mac OS oferuje wygląd i zachowanie ściśle zintegrowane z systemem operacyjnym. Podobne rozwiązanie jest opracowywane dla systemu Windows.

Programowa zmiana wyglądu i zachowania

Jeżeli aplikacja nie spowodowała jeszcze statycznej inicjalizacji żadnej klasy Swing, wystarczy użyć metody UIManager.setLookAndFeel(String) i przekazać jej jako parametr nazwę podklasy LookAndFeel reprezentującej pożądany wygląd i zachowanie. Aby dokonać przełączenia w już działającej aplikacji należy dodatkowo wywołać metodę SwingUtilities.updateComponentTreeUI(Component) i przekazać jej jako parametr kontener główny.

Standardowo dostępne podklasy LookAndFeel, to:

  • javax.swing.plaf.metal.MetalLookAndFeel – domyślne międzyplatformowe wygląd i zachowanie Javy; można go używać na każdej platformie,
  • com.sun.java.swing.plaf.windows.WindowsLookAndFeel – wygląd i zachowanie symulujące te znane z systemu Windows; obecnie można ich używać tylko pod systemem Windows,
  • com.sun.java.swing.plaf.motif.MotifLookAndFeel – wygląd i zachowanie CDE/Motif; domyślne na systemach firmy SUN; można go używać na każdej platformie,
  • com.sun.java.swing.plaf.gtk.GTKLookAndFeel – wygląd i zachowanie GTK+ (nie jest dostępny we wszystkich wersjach JRE).

Klasa UIManager posiada dwie metody statyczne, których wynik można użyć jako parametr setLookAndFeel(). Są to UIManager.getSystemLookAndFeelClassName(), która odgaduje wygląd i zachowanie właściwe dla danej platformy oraz UIManager.getCrossPlatformLookAndFeelClassName(), która wybiera domyślny międzyplatformowe wygląd i zachowanie Javy.

Poniższy przykład pokazuje dynamiczną zmianę wyglądu i zachowania w trakcie działania aplikacji.

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

class ZmianaWygląduIZachowania implements ActionListener {
  JFrame ramka;
  String nazwa;
  ZmianaWygląduIZachowania(JFrame ramka, String nazwa) {
    this.ramka = ramka;
    this.nazwa = nazwa;
  }
  
  public void actionPerformed(ActionEvent e) {
    try {
      UIManager.setLookAndFeel(nazwa);
      SwingUtilities.updateComponentTreeUI(ramka);
    } catch (Exception ex) {
      System.err.println("Nie udała się zmiana na wygląd: "+nazwa);
    }
  } 
}

public class WygladIZachowanie extends JFrame {
  JButton międzyplatformowy, systemu, gtk, windows, motif, metal; 
  
  WygladIZachowanie() {
    super("Okno WyglądIZachowanie");
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    JPanel panel = new JPanel(new GridLayout(0, 1));
    panel.add(new JLabel("Wybierz wygląd i zachowanie:"));
   
    międzyplatformowy = new JButton("Międzyplatformowy");
    międzyplatformowy.addActionListener(
      new ZmianaWygląduIZachowania(this, UIManager.getCrossPlatformLookAndFeelClassName()));
    panel.add(międzyplatformowy);
    
    systemu = new JButton("Systemu operacyjnego");
    systemu.addActionListener(
      new ZmianaWygląduIZachowania(this, UIManager.getSystemLookAndFeelClassName()));
    panel.add(systemu);
    
    metal = new JButton("Metal");
    metal.addActionListener(
      new ZmianaWygląduIZachowania(this, "javax.swing.plaf.metal.MetalLookAndFeel"));
    panel.add(metal);

    windows = new JButton("Windows");
    windows.setToolTipText("Obecnie dostępny jedynie w systemie Windows!");
    windows.addActionListener(
      new ZmianaWygląduIZachowania(this, "com.sun.java.swing.plaf.windows.WindowsLookAndFeel"));
    panel.add(windows);

    motif = new JButton("Motif");
    motif.addActionListener(
      new ZmianaWygląduIZachowania(this, "com.sun.java.swing.plaf.motif.MotifLookAndFeel"));
    panel.add(motif);

    //nie ma go we wszystkich jre
    gtk = new JButton("GTK");
    gtk.setToolTipText("Niedostępny w niektórych JRE!");
    gtk.addActionListener(
      new ZmianaWygląduIZachowania(this,"com.sun.java.swing.plaf.gtk.GTKLookAndFeel"));
    panel.add(gtk);

    panel.setBorder(BorderFactory.createEmptyBorder(20,20,20,20)); 
    add(panel);
    setSize(260,250);
    setVisible(true);
  }
 
  public static void main(String[] args) {
    //aby uniknąć zakleszczeń, tworzenie GUI zawsze zlecamy dla wątku obsługi zdarzeń
    javax.swing.SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        new WygladIZachowanie();
      }
    });
  }
}
Podgląd działającego przykładu
Archiwum JAR Podgląd jako Applet
Żeby uruchomić przykład Po_GUI_WygladIZachowanie.jar ściągnij najpierw archiwum jar na swój komputer (możesz to zrobić klikając na odnośniku prawym klawiszem myszy i wybierając "Zapisz jako...") i uruchom jak zwykły program.

Jeżeli kod aplikacji nie definiuje żadnego wyglądu i zachowania, użytkownik może je jednorazowo wskazać w chwili jej uruchamia:

java -Dswing.defaultlaf=com.sun.java.swing.plaf.gtk.GTKLookAndFeel MyApp

lub zdefiniować na stałe w pliku swing.properties.

Warto zwrócić uwagę, że ramka i listwa górna okna, niezależnie od wyboru wyglądu i zachowania są zapewniane przez menadżera okien systemu operacyjnego. Niektóre wyglądy i zachowania, jak javax.swing.plaf.metal.MetalLookAndFeel potrafią same rysować ramkę i listwę górną okna. Tę funkcjonalność uaktywnia się przy pomocy wywołania JFrame.setDefaultLookAndFeelDecorated(true).

Podsumowanie

Swing jest obszerną biblioteką. Zawiera wiele komponentów, które zazwyczaj posiadają bogate interfejsy. Zdobyta na tym wykładzie wiedza wystarczy ci do samodzielnej pracy z dokumentacją. Dalsze informacje i przykłady możesz znaleźć w przewodniku firmy Sun. Pamiętaj również, że dla Swinga istnieją doskonałe graficznej narzędzia pozwalające tworzyć interfejs metodą przeciągnij i upuść. Godzien polecenia jest generator interfejsu zawarty w pakiecie NetBeans.

Na zakończenie warto wspomnieć, że Swing nie jest jedyną biblioteką do budowy graficznego interfejsu użytkownika. Dość popularną alternatywą jest biblioteka SWT opracowana przez firmę IBM i używana w projekcie Eclipse.