PO Graficzny interfejs użytkownika

Z Studia Informatyczne
Przejdź do nawigacjiPrzejdź do wyszukiwania

<<< Powrót

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 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ść. Materiał poświęcony tworzeniu GUI podzieliśmy na dwa wykłady. Pierwszy zawiera 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. W drugim omawiamy najważniejsze wzorce projektowe oraz podajemy wskazówki jak zorganizować swój kod, aby graficzny interfejs użytkownika nie był nadmiernie sprzężony z resztą systemu i można go było łatwo rozbudowywać lub nawet zastąpić.

Java Foundation Classes

Java posiada dobrze przemyślany graficzny zrąb (ang. framework) – Java Foundation Classes (JFC) oraz bogatą bibliotekę gotowych komponentów jak przyciski, pola tekstowe, panele czy okienka dialogowe. Mimo że Swing jest bardzo rozbudowany, do jego 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 <ref name="PO_wątki"> Omówienie wątków oraz potencjalnych zagrożeń związanych z niewłaściwym ich używaniem nie wchodzi w zakres tego wykładu. W wielkim skrócie wątki dają możliwość wykonywania przez program kilku czynności współbieżnie. W systemach jednoprocesorowych sprowadza się to do nieustannego przełączania procesora pomiędzy wątkami. Tak działające wątki sprawiają wrażenie wykonujących się jednocześnie. Programowanie współbieżne wymaga wiele uwagi. Pechowego poprzeplatanie się kilku operacji wykonywanych przez różne wątki na jednym obiekcie może pozostawić go w stanie niespójnym. Nieprawidłowa rywalizacja o zasoby lub synchronizacja między wątkami może doprowadzić do blokad (ang. deadlock). DOgłębnie te zagadnienia zostaną omówione na programowaniu współbieżnym. </ref> 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.invokeLate(), 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 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. <applet code="applet.WitajSwiecieApplet" archive="images/d/db/PO_GUI_Applety.jar" width="300" height="120"></applet>

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 Singu 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 rozmieszczenie 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 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. <applet code="applet.BorderLayoutApplet" archive="images/d/db/PO_GUI_Applety.jar" width="300" height="120"></applet>

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 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. <applet code="applet.FlowLayoutApplet" archive="images/d/db/PO_GUI_Applety.jar" width="300" height="120"></applet>

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ę, ż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 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. <applet code="applet.GridLayoutApplet" archive="images/d/db/PO_GUI_Applety.jar" width="300" height="120"></applet>

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ń, 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.