Locked History Actions

Diff for "mgalazka/PRO5"

Differences between revisions 63 and 64
Deletions are marked like this. Additions are marked like this.
Line 411: Line 411:
[[http://ggoralski.pl/?p=1859|JavaFX - Pierwsze starcie]] [[https://1024kb.pl/kjop/wstep-do-javafx/|JavaFX - Pierwsze starcie]]

Java na Desktopy, czy w ogóle warto się uczyć?

Swing

Graficzny interfejs użytkownika czyli GUI (Graphical User Interface)

Jedną z charakterystycznych cech języka Java jest możliwość łatwego tworzenia graficznego interfejsu użytkownika (GUI). Wykorzystuje się do tego dwa podstawowe pakiety: java.awt i javax.swing.

Podstawowym obiektem, z którego zbudowane jest GUI jest okno, w którym umieszczone są wszystkie pozostałe elementy.

W pakiecie javax.swing klasą implementującą okno jest JFrame (https://docs.oracle.com/javase/8/docs/api/javax/swing/JFrame.html)

Przykład 1. Pierwsze okno

W środowisku Eclipse stwórz projekt lab5 a w nim stwórz klasę o nazwie Example1 posiadającą metodę main(). Wewnątrz metody main() wpisz kolejno:

  •      JFrame frame = new JFrame();
         frame.setSize(640, 480);
         frame.setVisible(true);

Uruchomienie takiego programu spowoduje pojawienie się pustego okna o zadanym rozmiarze.

Komponenty i layout managery

Samo okienko jest zaledwie ramką, w której umieszcza się kolejne komponenty, takie jak przyciski, etykiety, pola, listy rozwijane itp. Wymienione elementy można umieszczać bezpośrednio w oknie lub w specjalnym kontenerze implementowanym przez klasę JPanel z pakietu javax.swing.

Przykład 2. Umieszczanie komponentów bezpośrednio w oknie

W pakiecie lab5 stwórz klasę o nazwie Example2 posiadającą metodę main(). Wewnątrz metody main() wpisz kolejno:

  •      JButton button1 = new JButton("Przycisk 1");
         frame.add(button1);
                    
         JButton button2 = new JButton("Przycisk 2");
         frame.add(button2);
    
         frame.setVisible(true);

Po uruchomieniu programu powinno być widoczne okno z jednym dużym przyciskiem pokrywającym całe okno. Dzieje się tak ponieważ do pozycjonowania obiektów w oknie używany jest tzw. layout manager, a domyślny layout manager dla klasy JFrame nazywa się BorderLayout, który przy najprostszym wywołaniu metody add(Component c) umieszcza komponent w centrum ramki i rozciąga go na całe okno. Dodanie drugiego przycisku w ten sam sposób spowoduje zasłonięcie pierwszego. BorderLayout wyróżnia 5 obszarów okna: górny (PAGE_START), dolny (PAGE_END), lewy (LINE_START), prawy (LINE_END) i centralny (CENTER), który jest obszarem domyślnym (zobacz https://docs.oracle.com/javase/tutorial/uiswing/layout/border.html). Aby komponenty nie przekrywały się, konieczne jest przekazanie parametru do metody add(Component c, int index), który będzie informował, w którym obszarze ma zostać umieszczony komponent. Zmodyfikuj metodę main() w klasie Example2:

  •      JButton button1 = new JButton("Przycisk 1");
         frame.add(button1, BorderLayout.PAGE_START);
                    
         JButton button2 = new JButton("Przycisk 2");
         frame.add(button2, BorderLayout.PAGE_END);
    
         frame.setVisible(true);

Rodzaj rozmieszczenia komponentów można zmienić metodą setLayout(), podając jako argument jeden z dostępnych layout managerów. FlowLayout układa elementy jeden obok drugiego, opcjonalnie w kilku rzędach. Aby przetestować jego działanie można umieścić w oknie więcej elementów, np. etykietę z tekstem JLabel lub pole do wpisywania tekstu JTextField. Dodaj kolejne instrukcje do metody main() w klasie Example2:

  •      JLabel label = new JLabel("To jest etykieta");
         frame.add(label);
                    
         JTextField field = new JTextField("A to pole tekstowe");
         frame.add(field);
                    
         frame.setLayout(new FlowLayout());

GridLayout jest bardzo wygodnym managerem, który dzieli okno na siatkę u ustalonej liczbie kolumn i wierszy. Każda komórka takiej siatki ma identyczny rozmiar. W powyższym przykładzie zmień managera layoutu z FlowLayout na GridLayout(2,2) lub GridLayout(4,1). Prosty tutorial dotyczący layout managerów można znaleźć na stronie https://docs.oracle.com/javase/tutorial/uiswing/layout/index.html.

Więcej informacji na temat JLabel i JTextField oraz pełny opis ich metod można znaleźć w dokumentacji https://docs.oracle.com/javase/8/docs/api/.

Uwaga

Zwrot "umieszczanie komponentów bezpośrednio w oknie" jest skrótem myślowym. Uproszczenie to wynika z tego, że komponenty dodaje się do instancji klasy JFrame metodą add(). W rzeczywistości komponenty są umieszczane w kontenerze, który jest dodawany do okna JFrame już w momencie jego inicjalizacji. Kontenerem tym jest instancja klasy Container z biblioteki java.awt (jest to klasa nadrzędna względem klasy JPanel). Aby odwołać się do tego kontenera, np. w celu zmiany koloru tła okna, należy posłużyć się metodą getContentPane().

Dodaj poniższą instrukcję do metody main() klasy Example2 (aby efekt był widoczny trzeba ustawić FlowLayout w ramce):

     frame.getContentPane().setBackground(Color.blue);

Przykład 3. Umieszczanie komponentów wewnątrz panelu

W praktyce bardzo rzadko umieszcza się komponenty bezpośrednio w oknie, które w zamyśle autorów pakietu ma być tylko ramką. Właściwszą metodą jest uprzednie dodanie do okna panelu (JPanel) pełniącego rolę kontenera, w którym następnie umieszcza się elementy. W jednej ramce może być wiele paneli, a każdy może mieć swój własny layout.

W pakiecie lab5 stwórz klasę o nazwie Example3 posiadającą metodę main(). Wewnątrz metody main() należy umieścić instrukcje:

  •      frame.setLayout(new GridLayout(1,2));
                    
         JPanel panel1 = new JPanel();
         panel1.setBackground(Color.white);
         frame.add(panel1);
                    
         JPanel panel2 = new JPanel();
         panel2.setBackground(Color.black);
         frame.add(panel2);
                    
         JLabel leftLabel = new JLabel("Lewy panel");
         panel1.add(leftLabel);
         JLabel rightLabel = new JLabel("Prawy panel");
         rightLabel.setForeground(Color.white);
         panel2.add(rightLabel);
    
         frame.setVisible(true);

Wykorzystano tutaj wygodny GridLayout, który pionowo podzielił ramkę na dwie równe części. W każdej połowie okna umieszczono jeden panel, a w każdym panelu jedną etykietę. Domyślnym layoutem obiektu klasy JPanel jest FlowLayout. Więcej informacji można znaleźć w dokumentacji https://docs.oracle.com/javase/8/docs/api/javax/swing/JPanel.html.

Przykład 4. Proste figury geometryczne wewnątrz panelu

Nadpisując metodę paintComponent() można umieszczać na panelu proste figury geometryczne o zadanym rozmiarze i kolorze.

W pakiecie lab5 stwórz klasę o nazwie DrawablePanel dziedziczącą po klasie JPanel i posiadającą metodę main(). Dodaj do nowej klasy metodę paintComponent() zdefiniowaną poniżej. Wewnątrz metody main() zadeklaruj obiekt klasy DrawablePanel i umieść go wewnątrz okna frame.

  •         public void paintComponent(Graphics g) {
                    super.paintComponent(g);
                    
                    Graphics2D g2d = (Graphics2D) g;
    
                    g2d.setColor(Color.red);
                    g2d.drawRect(50, 50, 150, 100);
                    g2d.fillRect(50, 50, 150, 100);
                    
                    g2d.setColor(Color.blue);
                    g2d.drawOval(250, 250, 150, 150);
                    g2d.fillOval(250, 250, 150, 150);
            }
    
            public static void main(String[] args) {
    
                    JFrame frame = new JFrame();
                    frame.setSize(640, 480);
    
                    DrawablePanel panel = new DrawablePanel();
                    panel.setBackground(Color.white);
                    frame.add(panel);
    
                    frame.setVisible(true);
            }

Klasa JPanel posiada metodę paintComponent(), zatem definicja tej metody w klasie DrawablePanel "nadpisuje" oryginalną definicję. Schemat działania jest tutaj taki sam jak przy konstruktorach.

Jeśli jakaś metoda w klasie dziedziczącej ma rozszerzać metodę klasy oryginalnej to konieczne jest wywołanie oryginalnej metody przy użyciu instrukcji super.paintComponent(g) na początku definicji nowej metody.

W tym konkretnym przykładzie rozszerzona metoda paintComponent() rysuje na panelu dwie figury geometryczne: czerwony prostokąt i niebieskie koło.

Rysowanie odbywa się za pośrednictwem klasy Graphics, która jest automatycznie przekazywana do metody paintComponent(). Jednak trzeba sobie zdawać sprawę, że obecnie jest to tak naprawdę obiekt Graphics2D i jeśli coś rysujemy, to powinniśmy najpierw utworzyć obiekt tej klasy i przypisać mu zrzutowany na tę klasę argument, czyli:

public void paintComponent(Graphics g) {
        Graphics2D g2d = (Graphics2D) g;
         //dalsza część metody, rysowanie itp.
}

Zadanie - Figury w losowych kolorach

W pakiecie lab5 stwórz klasę ThreeShapesPanel, dziedziczącą po klasie JPanel i posiadającą metodę main(). Dodaj do nowej klasy metodę paintComponent() i zdefiniuj ją w ten sposób aby w panelu rysowane były trzy różne figury geometryczne o różnych, losowych kolorach. W tym celu odszukaj w dokumentacji klasę służącą do generowania liczb losowych.

Zmodyfikuj metodę main() w ten sposób, aby w oknie frame umieszczone były dwa panele tej samej wielkości, jeden klasy JPanel a drugi ThreeShapesPanel (i zajmowały całą powierzchnię ramki). W panelu klasy JPanel umieść dowolne cztery komponenty ustawione jeden nad drugim. Upewnij się, że kolory figur nie zmieniają się podczas zmieniania rozmiaru okna.

Obiekty nasłuchujące zdarzeń

Jednym z komponentów zaprezentowanych w Przykładzie 2 był JButton. Jednak samo dodanie przycisku do panelu nie powoduje automatycznie, że będzie on działał zgodnie z wolą programisty. Kliknięcie przycisku jest pewnym zdarzeniem wywołanym przez użytkownika i aby to zdarzenie wywołało jakąkolwiek reakcję muszą być spełnione dwa warunki:

1. Program musi wiedzieć o tym, że ma oczekiwać na kliknięcie przycisku. Innymi słowy potrzebny jest obiekt nasłuchujący zdarzeń (listener) na danym przycisku.

2. Obiekt nasłuchujący musi mieć podane uprzednio instrukcje, które ma wykonać w przypadku kliknięcia w przycisk.

Obiektami nasłuchującymi w Javie są tzw. listenery. Przykładowo, aby obsłużyć zdarzenie kliknięcia na przycisk, należy podłączyć do niego instancję interfejsu ActionListener.

Przykład 5. Obsługa przycisku

W pakiecie lab5 stwórz klasę o nazwie Example5 posiadającą metodę main(). Wewnątrz metody main() należy umieścić instrukcje:

  •      JPanel panel = new JPanel();
         frame.add(panel);
    
         JButton exitButton = new JButton("Zakończ");
         ActionListener exitListener = new ActionListener() {
                 @Override
                 public void actionPerformed(ActionEvent arg0) {
                         System.exit(0);
                                    
                 }  
         };
         exitButton.addActionListener(exitListener);
         panel.add(exitButton);
    
         frame.setVisible(true);

Interfejs ActionListener można definiować i inicjować w różny sposób i w różnych miejscach programu. Powyższy przykład prezentuje użycie tzw. klasy anonimowej, czyli takiej która zostaje zdefiniowana "w locie", w momencie jej inicjalizacji. Instrukcje, które mają zostać wykonane w następstwie kliknięcia na przycisk muszą być umieszczone wewnątrz metody actionPerformed().

Co warto wiedzieć o klasie anonimowej:

  • ma dostęp do atrybutów klasy zewnętrznej (tej w której ją zdefniowano),
  • nie może definiować własnego konstruktora, a jeśli implementuje ona interfejs to tylko używając konstruktora nie przyjmującego argumentów,
  • ma ograniczony dostęp do zmiennych w bloku, w którym powstała (ma dostęp tylko do zmiennych final oraz effective final, czyli takich których wartość nie zmienia się po ich utworzeniu),

  • wewnątrz klasy anonimowej możliwe jest jawne odwołanie do instancji this tej klasy, jak i instancji this klasy zewnętrznej poprzez operator NazwaKlasyZewnętrznej.this.

Zadanie - Okno z trzema przyciskami

W istniejącym pakiecie lab5 stwórz klasę o nazwie ThreeButtonFrame, dziedziczącą po JFrame, zawierającą metodę main(). W konstruktorze nowej klasy zainicjuj trzy przyciski i dodaj je do ramki ThreeButtonFrame. Każdy z przycisków powinien wykonywać inną akcję (np. zamykać program, zmieniać tytuł okna, tekst na etykiecie lub kolor jakiegoś elementu). Wewnątrz metody main() zainicjuj obiekt klasy ThreeButtonFrame i uczyń go widocznym.

Przykład. Implementacja listenera w klasie wewnętrznej

Drugim sposobem na implementację listenera jest użycie klasy wewnętrznej. Polega on na zdefiniowaniu klasy implementującej listener wewnątrz innej klasy, np. klasy rozszerzającej JFrame. Sytuację tę prezentuje poniższy przykład:

Stwórz nową klasę InnerClassListenerFrame dziedziczącą po JFrame i posiadajacą metodę main(). W konstruktorze tej klasy dodaj do okna suwak JSlider i etykietę JLabel. Wewnątrz klasy InnerClassListenerFrame zdefiniuj nową klasę SliderChangeListener implementującą interfejs ChangeListener. Napisz dla niej odpowiedni konstruktor i nadpisz metodę stateChanged(ChangeEvent arg0).

     public class InnerClassListenerFrame extends JFrame {
        
             // Pola klasy InnerClassListenerFrame
             static final int SLIDER_MIN = -100;
             static final int SLIDER_MAX = 100;
             static final int SLIDER_INIT = 0;

             JSlider slider;
             JLabel label;
        
             // Konstruktor klasy InnerClassListenerFrame 
             public InnerClassListenerFrame() throws HeadlessException { 
                     this.setDefaultCloseOperation(DISPOSE_ON_CLOSE);
                     this.setSize(600,50);
                
                     slider = new JSlider(JSlider.HORIZONTAL, SLIDER_MIN, SLIDER_MAX, SLIDER_INIT);
                     this.add(slider, BorderLayout.PAGE_START);
                
                     label = new JLabel(String.format("%d", slider.getValue()));
                     this.add(label);
                
                     slider.addChangeListener(new SliderChangeListener());
             }
        
             // Klasa wewnętrzna SliderChangeListener implementująca ChangeListener
             public class SliderChangeListener implements ChangeListener{
                
                     @Override
                     public void stateChanged(ChangeEvent arg0) {
                             String value = String.format("%d", slider.getValue());
                             label.setText(value);
                     }
                
             }

             public static void main(String[] args) {
                     InnerClassListenerFrame frame = new InnerClassListenerFrame();
                     frame.setVisible(true);
             }

     }

W powyższym przykładzie wartość ustawiona na suwaku wyświetla się na panelu. Do nasłuchiwania wydarzeń na suwaku służy ChangeListener. Klasa SliderChangeListener, która implementuje ChangeListener jest zdefiniowana wewnątrz klasy zewnętrznej InnerClassListenerFrame. Podobnie jak w przypadku klasy anonimowej, klasa wewnętrzna ma dostęp do pól z klasy zewnętrznej. Możliwe jest stworzenie instancji klasy wewnętrznej poza klasą zewnętrzną (na przykład w funkcji main()) w następujący sposób:

     InnerClassListenerFrame frame = new InnerClassListenerFrame();
     SliderChangeListener listener = frame.new SliderChangeListener(); 

Innych powszechnym sposobem na implementację listenera jest stworzenie do tego osobnej klasy zewnętrznej. Należy wtedy zadbać o odpowiednie przekazanie do listenera wskaźników na komponenty, które mają być modyfikowane w metodzie listenera. Najlepiej zrobić to poprzez konstruktor klasy implementującej listener, tak jak w poniższym przykładzie.

Przykład. Implementacja listenera w klasie zewnętrznej

Stwórz nową klasę OuterClassListenerFrame, dziedziczącą po JFrame i posiadajacą metodę main(). W konstruktorze tej klasy dodaj do okna rozwijaną listę (JComboBox) z trzema kolorami tła do wyboru (np. czerwony, zielony, niebieski). Początkowy kolor tła powinien być zgodny z pierwszą pozycją na liście kolorów.

     public class OuterClassListenerFrame extends JFrame { 

                  public OuterClassListenerFrame() throws HeadlessException {
                          this.setDefaultCloseOperation(DISPOSE_ON_CLOSE);
                          this.setSize(300,100); 
                
                          String[] colors = {"red", "green", "blue"}; 
                          JComboBox<String> colorList = new JComboBox<String>(colors); 
                          this.add(colorList, BorderLayout.PAGE_START); 
                          this.getContentPane().setBackground(Color.red);
                  } 

                  public static void main(String[] args) {
                          OuterClassListenerFrame frame = new OuterClassListenerFrame();
                          frame.setVisible(true);
                  } 

          }

Aby obsłużyć akcje generowane na obiekcie klasy JComboBox potrzebny jest ItemListener (lub ActionListener), dlatego stwórz nową klasę o nazwie ComboBoxItemListener implementującą ItemListener. Jedynym polem tej klasy powinien być obiekt klasy JPanel (lub ewentualnie Container), którego tło będziemy zmieniać wewnątrz metody itemStateChanged(ItemEvent arg0). Przekazanie wskaźnika na panel do wewnątrz klasy ComboBoxItemListener powinno się odbywać za pośrednictwem konstruktora tej klasy. Obiekt klasy JComboBox przy każdym kliknięciu na wybraną pozycję z rozwijanej listy generuje dwa wydarzenia ItemEvent: jedno związane z wyborem nowej pozycji (ItemEvent.SELECTED), a drugie z "odkliknięciem" poprzedniej pozycji (ItemEvent.DESELECTED) i dlatego wewnątrz metody itemStateChanged(ItemEvent arg0) należy sprawdzać jakiego rodzaju wydarzenie nastąpiło.

          public class ComboBoxItemListener implements ItemListener {

             JPanel panel; 
        
             public ComboBoxItemListener(JPanel panel) {
                     this.panel = panel;
             }

             @Override 
             public void itemStateChanged(ItemEvent arg0) {
        
                     if(arg0.getStateChange()==ItemEvent.SELECTED) {
                             String color = (String)arg0.getItem();
                               switch(color) {
                                       case "red":
                                               panel.setBackground(Color.red);
                                               break;
                                       case "green": 
                                               panel.setBackground(Color.green);
                                               break;
                                       case "blue":
                                               panel.setBackground(Color.blue); 
                                               break;
                               }
                     }
             }

     }

W powyższym przykładzie, po sprawdzeniu czy została wybrana nowa pozycja z listy następuje przypisanie tej pozycji do zmiennej color. Warto zwrócić uwagę na sposób w jaki rzutowana jest zmienna klasy Object, którą zwraca metoda getItem() na zmienną klasy String. Kiedy klasa ComboBoxItemListener jest gotowa, należy dodać listener do obiektu JComboBox w konstruktorze klasy OuterClassListenerFrame:

     colorList.addItemListener(new ComboBoxItemListener((JPanel)this.getContentPane()));

W powyższej linijce warto zwrócić uwagę na konieczność rzutowania obiektu typu Container zwracanego przez metodę getContentPane() na klasę JPanel. Jest to spowodowane tym, że napisany powyżej konstruktor klasy ComboBoxItemListener oczekuje jako parametru obiektu klasy JPanel.

Przedstawione powyżej metody implementacji listenera wykorzystujące klasy anonimową, wewnętrzną i zewnętrzną mają tę wspólną cechę, że zakładają konieczność zdefiniowania osobnej klasy do nasłuchiwania komponentu. W praktyce nie zawsze jest to konieczne, gdyż można również pozwolić, aby to okno główne programu implementowało listener.

Przykład. Implementacja listenera w oknie głównym programu

Stwórz nową klasę RadioButtonsFrame, dziedziczącą po JFrame, posiadajacą metodę main() i implementującą interfejs ActionListener. W konstruktorze tej klasy dodaj do okna trzy przyciski JRadioButton z trzema kolorami tła do wyboru (np. czerwony, zielony, niebieski). Początkowy kolor tła powinien być zgodny z domyślnie wciśniętym przyciskiem. Nadpisz metodę actionPerformed(ActionEvent arg0) w ten sposób, aby w zależności od wciśniętego przycisku zmieniało się tło okna.

     public class RadioButtonsFrame extends JFrame implements ActionListener {

             static final String[] COLOR_NAMES = {"red", "green", "blue"};
             static final Color[] COLORS = {Color.red, Color.green, Color.blue};
             static final Color INIT_COLOR = COLORS[0];
        
             JRadioButton radioButton1;
             JRadioButton radioButton2;
             JRadioButton radioButton3;
        
             public RadioButtonsFrame() throws HeadlessException {
                     this.setDefaultCloseOperation(DISPOSE_ON_CLOSE);
                     this.setSize(300,100);
                     this.getContentPane().setBackground(INIT_COLOR);
                     this.setLayout(new FlowLayout());
                                
                     radioButton1 = new JRadioButton(COLOR_NAMES[0]);
                     radioButton1.setActionCommand("0");
                     radioButton1.setBackground(INIT_COLOR);
                     radioButton1.addActionListener(this);
                     radioButton1.setSelected(true);
                     this.add(radioButton1);
                
                     radioButton2 = new JRadioButton(COLOR_NAMES[1]);
                     radioButton2.setActionCommand("1");
                     radioButton2.setBackground(INIT_COLOR);
                     radioButton2.addActionListener(this);
                     this.add(radioButton2);
                
                     radioButton3 = new JRadioButton(COLOR_NAMES[2]);
                     radioButton3.setActionCommand("2");
                     radioButton3.setBackground(INIT_COLOR);
                     radioButton3.addActionListener(this);
                     this.add(radioButton3);
                
                     ButtonGroup group = new ButtonGroup();
                     group.add(radioButton1);
                     group.add(radioButton2);
                     group.add(radioButton3);
             }

             @Override
             public void actionPerformed(ActionEvent arg0) {
                     int colorNumber = Integer.parseInt(arg0.getActionCommand());
                     this.getContentPane().setBackground(COLORS[colorNumber]);
                     radioButton1.setBackground(COLORS[colorNumber]);
                     radioButton2.setBackground(COLORS[colorNumber]);
                     radioButton3.setBackground(COLORS[colorNumber]);
             }

             public static void main(String[] args) {
                     RadioButtonsFrame frame = new RadioButtonsFrame();
                     frame.setVisible(true);

             }

    }

W tym przykładzie nowością jest metoda setActionCommand(). Przy jej pomocy możemy nadać każdemu komponentowi unikalny identyfikator, który później, w metodzie actionPerformed() jest wykorzystywany do rozpoznania, który komponent jest źródłem wydarzenia. Kolor tła okna i wszystkich przycisków jest zmieniany w zależności od tego jaki identyfikator jest zwracany przez metodę getActionCommand().

Zadanie - Okno z wieloma komponentami

Do pakietu lab5 dodaj nową klasę o nazwie MultiListenerFrame, która dziedziczy po JFrame i posiada metodę main(). Wewnątrz okna powinny znajdować się co najmniej trzy różne komponenty, z których każdy będzie wykonywał inną akcję. Obsługa zdarzeń generowana przez te komponenty powinna być wykonywana na co najmniej trzy sposoby.

JavaFX

Instalacja e(fx)clipse w Eclipse

Dodanie bibliotek dla JavaFX w Eclipse

Instalacja JavaFX Scene Builder w Eclipse

JavaFX Tutorial for Beginners - Hello JavaFX

JavaFX - Pierwsze starcie