7. Programowanie GUI. Komponenty wizualne


Ten wykład rozpoczyna cykl poświęcony programowaniu graficznych interfejsów użytkownika. Jest to nie tylko ważna i ogólna dziedzina (niewiele jest współcześnie aplikacji, które nie mają graficznych interfejsów użytkownika), ale również jest to jedna z najważniejszych składowych środowiska Javy, a przy tym doskonały przykład zastosowania koncepcji programowania obiektowego (od polimorfizmu poczynając, poprzez delegowanie uprawnień, po architekturę "Model-View-Controller").


7.1.  Ogólne reguły działania z komponentami GUI.  

Standardowe pakiety java.awt (AWT) oraz javax.swing (Swing) zawierają klasy definiujące wiele różnorodnych komponentów wizualnej interakcji programu z użytkownikiem (okna, przyciski, listy, menus, tablice itp.). Są gotowe do wykorzystania w naszych programach.

Można sformułowac następujące reguły dzialania z komponentami GUI.


Przykładowa aplikacja Swing (pokazuje trzy przyciski z obrazkami i tekstem).

r

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

class Przyklad  {

public static void main(String[] args)  {

  Icon[] icon = { new ImageIcon("ocean.jpg"),   // ikony z plików JPEG
                       new ImageIcon("pool.jpg"),
                       new ImageIcon("town.jpg"),
                      };
  String[] opis = { "Ocean", "Pool", "Town" }; //  tekst na przyciskach
  JFrame f = new JFrame();                     // utworzenie okna ramowego
  Container cp = f.getContentPane();           // ... i pobranie jego contentPane
  cp.setLayout(new FlowLayout());              // ustalenie rozkładu FlowLayout

  for (int i=0; i<icon.length; i++) {

     // tworzenie kolejnych przycisków
     JButton b = new JButton(opis[i], icon[i]);

     // Ustalenie pisma i koloru napisu na przyciskach
     b.setFont( new Font("Dialog", Font.BOLD | Font.ITALIC, 18));
     b.setForeground(Color.blue);

    //  Ustalenie pozycji tekstu na przycisku względem ikony
     b.setVerticalTextPosition(SwingConstants.BOTTOM);
     b.setHorizontalTextPosition(SwingConstants.CENTER);

     cp.add(b);   // dodanie przycisku do contentPane
     }

  f.pack(); // spakowanie okna
            // (wymiary okna takie by dokładnie zmieścić komponenty)
  f.show(); // pokazanie okna
  }

}

7.2. Komponenty AWT a komponenty Swingu

AWT (Abstract Windowing Toolkit) – obecny w Javie od samego początku - jest zestawem klas, definiujących proste komponenty wizualnej interakcji.

Problemy AWT:
Odpowiedzią na te problemy oraz ich rozwiązaniem był projekt Swing (ogólniej: Java Foundation Classes - JFC),  początkowo występujący jako dodatek do JDK 1.1.8, a później włączony w sklad Java 2 Platform.

Pakiet Swing (javax.swing i podpakiety)  zawiera dużo więcej niż AWT komponentów - nowych wobec AWT oraz mających rozbudowane właściwości odpowiedników AWT.

Wszystkie komponenty Swingu oprócz kontenerów znajdujących się najwyżej w hierarchii zawierania się  komponentów (kontenerów najwyższego poziomu) są komponentami lekkimi. W przeciwieństwie – gotowe komponenty AWT są komponentami ciężkimi.

Komponenty ciężkie są realizowane poprzez użycie graficznych bibliotek GUI systemu operacyjnego.
Komponenty lekkie są natomiast rysowane za pomocą kodu Javy w obszarze jakiegoś komponentu ciężkiego znajdującego się wyżej w hierarchii zawierania się komponentów (zwykle jest to kontener najwyższego poziomu).


Z tego wynika, że – w przeciwieństwie do komponentów ciężkich: Lekkie komponenty Swingu spełniają oba te warunki, a architektura klas Swingu pozwala wybierać wygląd jego lekkich komponentów (pluggable look and feel).

Uwaga: możliwe jest umieszczanie w jednym kontenerze komponentów lekkich (np. Swingu) i ciężkich (AWT), jednak jest to nie polecane i obarczone pewnymi restrykcjami, dotyczącymi m.in. porządku nakładania się komponentów po osi Z (Z-order). M.in. z tego ostatniego względu architektura okien Swingowych jest złożona (o czym dalej) i standardowo powinniśmy dodawać komponenty Swingu do contentPane okna ramowego Swingu (JFrame).

Podstawową hierarchię klas komponentów GUI przedstawia rysunek.

r

Wnioski, które wynikają z tej hierarchii :



7.3. Podstawowe komponenty AWT i hierarchia klas komponentów AWT

Poniższe rysunki pokazują komponenty AWT oraz hierarchię ich klas.

r

r
Źródło: Java Tutorial.

Bardzo krótko omówimy wybrane komponenty AWT, bowien dalej skupimy się raczej na bardziej bogatych komponentach Swingu.

Przycisk (klasa Button) może być tworziny za pomocą konstruktora Button() lub – z podaniem tekstu na przycisku – Button(String txt). Tekst można pobierać (getLabel()) i zmieniac (setLabel(String)). Z przyciskiem można kojarzyć napis (setActionCommand(String)), który łatwo można pobrać w metodzie obsługującej akcję na przycisku (zdarzenie kliknięcia w przycisk).

Etykieta (klasa
Label) jest w zasadzie komponentem opisowym. Przedstawia tekst. Tekst może być w obszarze etykiety wyrównywane do lewej, do prawej bądź środkowany (odpowiednie argumenty konstruktora lub metody setAlignment(...) – stałe o nazwach Label.LEFT, Label.RIGHT, Label.CENTER).

Oczywiście możemy pobierać (getText()) i ustalać(setText(String)) tekst etykiety.

Lista (klasa List ) jest komponentem prezentującym listę elementów do wyboru.
Może działać w trybie selekcji pojedyńczej (tylko jeden element na liście może być zaznaczony) lub wieloselekcji (zaznaczonych może być wiele elementów listy).
Podstawowe konstruktory to:

  List() // tworzy listę
  List(int n)   // inicjalnie lista ma n wierszy
  List(int n, boolean b) // jesli b==true możliwa wieloselekcja


Podstawowe metody klasy List
  add(String s)         dodanie elementu s na koniec listy
  add(String s, int i)  dodanie elementu s na pozycji i
  int getRows()  na ile wierszy widoczna
  int getItemCount()  ile ma elementów
  String[] getItems() pobierz wszystkie elementy
  String getItem(int i) co na pozycji i
  int getSelectedIndex()      pozycja zaznaczonego
  int[] getSelectedIndexes()  pozycje zaznaczonych
  String getSelectedItem()    zaznaczony element
  String[] getSelectedItems() zaznaczone elementy
  boolean isIndexSelected(int i) czy element o indeksie i zaznaczony
  boolean isMultipleMode()    czy w trybie wielozaznaczania
  select(int i) zaznacz element o indeksie i
  deselect(int i) usunąć zaznaczenie elementu o indeksie i
  remove(int i) usunąć element na pozycji i
  removeAll() usunąć wszystkie elementy
  setMultipleMode(boolean) wielozaznaczanie czy nie
 
Najprostszy przykład tworzenia listy i dodawania elementów może wyglądac tak:
        
List l = new List();       
for (int i = 1; i <= 10; i++) 
  l.add("Element listy " + i); 
Podobna do zwykłej listy jest lista rozwijalna. Składa się ona z tekstu opatrzonego z prawej strony przyciskiem oraz listy, która ukazuje się (rozwija) pod spodem pola tekstowego po kliknięciu w przycisk..
Wybrany z listy element pojawia się w polu tekstowym.
Lista rozwijalna (niezbyt szczęśliwie) nazywa się w AWT Choice.
Metody klasy Choice są podobne do metod klasy List: musimy tylko pamiętać, że może być zaznaczony tylko jeden element i że pojawia się on jako napis w polu tekstowym.
Lista rozwijalna pozwala oszczędzać miejsce w GUI.

Kolejnym komponentem jest przycisk-znacznik (klasa Checkbox). Pozwala on zaznaczać opcje: zaznaczenie obrazowane jest "ptaszkiem" z lewej strony tekstu przycisku. Kolejne kliknięcie w przycisk zmienia jego stan na "niezazaczony" i "ptaszek" znika.
Właściwości znacznika: napis na przycisku, stan: zaznaczony-niezaznaczony mogą być ustalane w konstruktorze oraz w metodach setLabel(String) i setState(boolean) oraz pobierane odpowiednimi metodami getLabel() i getState().
Stany przycisków-znaczników dodanych do GUI są niezależne od siebie. Jeśli chcemy opisywać wyłączające się opcje (symulować działanie tzw. radio-przycisków) to znaczniki powinny być dodane do obiektu klasy CheckboxGroup. Wtedy graficzne zaznaczenie zmieni się na kropeczkę i automatycznie będzie spełniony warunek, że tylko jeden ze znaczników w grupie może być zaznaczony.

Dwie klasy reprezentujące w AWT komponenty tekstowe.
Klasa TextField oznacza tekstowe pole edycyjne (jeden wiersz), a klasa TextArea wielowierszowe pole edycyjne (niczym prosty edytor tekstów).
Obie te klasy pochodzą od klasy TextComponent, w której zawarto większość metod manipulowania na komponentach tekstowych. Nie możemy tworzyć obiektów tej klasy, ale za to możemy korzystać z jej metod wobec obiektów klas TextField i TextArea.

Podstawowe metody klasy TextComponent
 int getCaretPosition() pozycja kursora
 String getSelectedText()  zaznaczony tekst
 int getSelectionEnd()  gdzie koniec zaznaczenia
 getSelectionStart()  gdzie początek zaznaczenia
 getText()           cały tekst
 isEditable()       czy edytowalny
 setCaretPosition(int)  ustawia kursor na podanej pozycji
 setEditable(boolean)  czy ma być edytowalny
 setSelectionEnd(int)  ustawia koniec zaznaczenia
 setSelectionStart(int) ustawia początek zaznaczenia
 setText(String)  ustla tekst


Klas TextField umożliwia tworzenie pól edycyjnych o podanej liczbie widocznych kolumn tekstu (konstruktor TextField(int)) i/lub zawiearających podany tekst (TextField(String)).
Obsługuje również właściwośc Columns:
    int getColumns() // ile widocznych kolumn tekstu
    setColumns(int) // ustal liczbę widocznych kolumn
 

W klasie TextArea oprócz liczby kolumn możemy kontrolować liczbę wierszy, Poza tym dostępna jest metoda dopisująca podany tekst na końcu tekstu już zawartego w wielopolu edycyjnym :
                append(String tekst) // dodaje tekst na końcu .



7.4. Hierarchia klas komponentów Swingu. Krótkie omówienie wybranych komponentów.


Swing dostarcza wiele nowych komponentów, których nie było w AWT. Natomaist te komponenty, które były w AT obecne (np. przyciski, czy listy) zyskały nowe, rozbudowane możliwości, czasem bardzo różniące je od "starych" komponentów.

Dla efektywmnego posługiwania się komponentami wizualnymi użyteczna jest znajomość ich hierarchii dziedziczenia. Poniższe rysunki przedstawiając te hierarchie, stanowią informację, która być może w tej chwili nie będzie bezpośrednio użyteczna, ale do której warto wracać przy analizowaniu działania konkretnych komponentów.

r

Rys. Komponenty Swingu o rozbudowanych możliwosciach w stosunku do AWT
Żródło: Magellan Institute Swing Short Course.

r
Rysunek .  Hierarchia klas komponentów Swingu, nie mających odpowiedników w AWT
Żródło: Magellan Institute Swing Short Course.

W poniższej tabeli przedstawiono przegląd komponentow Swingu z krótkimi komentarzami co od ich właściwości. Konkretne kompoenenty będziemy poznawać dokladnie w toku dalszego wykladu.


Komponenty terminalne Swingu

Przyciski: klasy JButton, JToggleButton, JCheckBox, JRadioButton

Możliwości:

  • tekst i/lub ikona na przycisku z dowolnym pozycjonowaniem
  • różne ikony dla różnych stanów (wciśnięty, kursor myszki nad przyciskiem etc)
  • ustalanie tekstu w HTML
  • programistyczne symulowanie kliknięć (metoda doClick())
  • ustalanie mnemoniki (metoda setMnemonic())

Etykieta: klasa JLabel

Możliwości:

  • tekst i/lub ikona z dowolnym pozycjonowaniem
  • tekst HTML
  • ustalenie mnemoniki i związanie jej z innym komponentem np. polem edycyjnym (wciśnięcie alt-mnemonika powoduje przejście fokusu do danego komponentu np. pola edycyjnego)

Menu rozwijalne: klasy JMenu, JMenuItem, JCheckBoxMenuItem, JRadioMenuItem.

Ponieważ pochodzą od AbstractButton - wszystkie właściwości przycisków!

Menu kontekstowe: klasa JPopupMenu

Suwak: klasa JSlider

Ustalanie wartości za pomocą suwaka. W pełni konfigurowalny, jako etykiety może zawierać ikony.

Dialog wyboru koloru: JColorChooser

łatwy w użyciu w wersji standardowej. W pełni konfigurowalny - możliwość tworzenia wersji niestandardowych i wbudowywania ich w inne kompoennetu GUI.

Inny dialog wyboru - JFilaChooser - wybór plików.

Pole edycyjne: JTextField

do wprowadzania hasła: JPasswordField

weryfikacja tekstu: za pomocą dziedziczenia klasy abstrakcyjnej InputVerifier i zdefiniowania metody verify(Component).

W JDK 1.4 - nowa klasa JFormattedTextField - z wbudowaną weryfikacją tekstu:

Wielowierszowe pole edycyjne: JTextArea

Uwaga: wszystkie komponenty tekstowe pochodzą od klasy JTextComponent, która zapewnia bardzo elastyczne możliwości tworzenia różnych edytorów. Komponenty tekstowe są bardzo rozbudowane, jesli chodzi o architekturę. Procesory dokumentów:. JEditorPane i JTextPane

Lista: klasa JList

  • oparta na współpracy modelu danych listy z widokiem tych danych
  • elementy: teksty i/lub obrazki, a nawet inne komponenty GUI (wygląd)
  • rózne elementy listy mogą mieć różny wygląd (kolor, pismo, obrazek lub nie etc).

Lista rozwijalna: JComboBox

oszczędność miejsca w GUI

te same właściwości co lista + możliwość przechodzenia do elementu tekstowego po wciśnięciu pierwszej litery napisu, który on reprezentuje

Tabela: klasa JTable

Ogromne możliwości konfiguracyjne, przestawianie kolumn (myszką i programistycznie), różny wygląd kolumn (teksty, obrazki, komponenty interakcyjne), sortowanie wierszy, wielozaznaczanie (po wierszach i po kolumnach)

Drzewo: klasa JTree

Reprezentowanie hierarchii. Węzły drzewa mają te same właściwości co elementy tabeli (tzn. mogą być reprezentowane w postaci napisów i/lub ikon oraz innych komponentów)

  

Lekkie i wyspecjalizowane kontenery Swingu

 

Panel: klasa JPanel

służy do grupowania komponentów

Panel dzielony: klasa JSplitPane

podział kontenera na dwie części (poziomo lub pionowo) z możliwością przesuwania belki podziału dla ustalenia rozmiarów widoków części

Panel zakładkowy: JTabbedPane

zakładki służą do wybierania komponentów, które mają być uwidocznione w panelu

Panel przewijany: JScrollPane

służy do pokazywania komponentów, które nie mieszczą się w polu widoku; suwaki umożliwiają "przewijanie" widoku komponentu, tak, by odsłaniać kolejne jego fragmenty.

JTextArea i JList powinny być umieszczane w JScrollPane, jeśli ich zawartość (wiersze tekstu, elementy listy) może się powiększać.

Pasek narzędzi: JToolBar

Źródło rysunków: Swing Connection, Sun.
Uwaga: okna (w tym wewnętrzne) zostaną omówione oddzielnie.

7.5. Wspólne właściwości komponentów (AWT i Swing)

Wszystkie komponenty wywodzą się z abstrakcyjnej klasy Component, która definiuje metody, m.in. ustalające właściwości komponentów. Mamy dwa rodzaje komponentów: komponenty-kontenery (takie, które mogą zawierać inne komponenty) oraz komponenty terminalne (nie mogą zawierać innych komponentów).

Właściwości mogą być pobierane za pomocą metod getNNN() lub (dla właściwości zero-jedynkowych, typu boolean) isNNN() i ustalane (jeśli to możliwe) za pomocą metod setNNN(...)., gdzie NNN – nazwa własciwości.

Do najogólniejszych właściwosci wszystkich komponentów należą.

Właściwość

Komentarz

rozmiar (Size)

ustalanie i pobieranie tych właściwości ma ograniczone zastosowanie (zob. dalej)





szerokość (Width)

wysokość (Height)

położenie (Location)

rozmiar i położenie (Bounds)

minimalny rozmiar (MinimumSize)

Właściwości ważne dla niektórych zarządców rozkładu (zob. dalej). Mogą być ustalane tylko dla komponentów Swingu. W AWT – ustalanie poprzez zdefiniowanie metod get... w klasie dziedziczącej klasę komponentu standardowego.


preferowany rozmiar (PreferredSize)

mksymalny rozmiar (MaximumSize)

wyrównanie po osi X (AlignmentX)

określa położenie komponentów wobec innych.

Uwagi j.w.


wyrównanie po osi Y (AlignmentY)

pismo (Font)

pismo jest obiektem klasy Font

kolor tła (Background)

kolor jest obiektem klasy Color

kolor tła ma znaczenie tylko dla nieprzezroczystych komponentow


kolor pierwszego planu (Foreground)

rodzic (Parent)

kontener, który zawiera dany komponent.

Właściwość tylko do odczytu.

nazwa (Name)

Komponenty otrzymują domyślne nazwy. Można je zmieniać.

Właściwości typu 0-1

widzialność (Visible)

czy widoczny? można zmieniać

lekkość (LigthWeight)

czy komponent lekki?

przezrosczystość (Opaque)

zmiany tylko dla komponentów Swingu

dostępność (Enabled).

czy możliwa interakcja z komponentem?


Rozmiary i położenie komponentów nie są znane dopóki komponenty nie zostaną zrealizowane (uwidocznione lub okno w którym się znajdują nie zostanie spakowane).

Położenie komponentu określane jest przez współrzędne (x,y), a punkt (0,0) oznacza lewy górny róg obszaru w którym znajduje się komponent (jest to ten obszaru kontenera, do którego mogą być dodane komponenty, a więc np. za wyłączeniem paska menu w oknie).
Zmiana położenia i rozmiarów za pomocą metod set ma w zasadzie sens tylko dla komponentów znajdujących się w kontenerach bez zarządcy rozkładu (o zarządcach rozkładu powiemy za chwilę) lub dla komponentów-okien.

Aby ustalić rozmiar okna frame piszemy np.:

    frame.setSize(200, 200);

Szczególna metoda w klasie Window (dziedziczonej przez Frame i JFrame) – pack() pozwala ustalić rozmiary okna, tak by były dokladnie takie (i nie większe) żeby zmieścić wszystkie znajdujące się w nim komponenty:

    frame.pack();



Pismo jest obiektem klasy Font, tworzonym za pomocą konstruktora

   Font(nazwa_pisma, styl, rozmiar)


gdzie:
nazwa_pisma - jest łańcuchem znakowym, określającym rodzaj pisma (np. "Dialog")

styl - jest jedną ze stałych statycznych typu int z klasy Font:
        Font.BOLD
        Font.ITALIC
        Font.PLAIN

        (kombinacje uzyskujemy poprzez sumę logiczną np. Font.BOLD | Font.ITALIC)
rozmiar - liczba całkowita określająca rozmiar pisma w punktach.


Podstawowe, logiczne, nazwy pisma to: Serif, SansSerif, Dialog i MonoSpaced.

Zatem, aby np. dla przycisku b ustalić pismo,
piszemy

JButton b = new JButton("Tekst na przycisku");
b.setFont(new Font("Dialog", Font.PLAIN, 14);


Kolor jest obiektem klasy Color, która ma kilka konstruktorów oraz udostępnia stałe statyczne typu Color z predefiniowanymi kolorami, np. Color.red, Color.blue, Color.white...

Kolory przycisku możemy więc ustalić za pomocą takich konstrukcji:

    b.setBackground(Color.blue);
    b.setForeground(Color.white);

albo:

   int r, g, b;
   r = 200; g = 200; b = 255;
   b.setBackground(new Color(r,g,b));

 
 

Zablokowanie/odblokowanie komponentu

Komponenty gotowe do interakcji są odblokowane (enabled). Zablokowanie komponentu uniemożliwia interakcję z nim.

Np. jeśli b jest przyciskiem

b.setEnabled(false); // zablokowanie; kliknięcia w przycisk nie będą "przyjmowane"
// odblokowanie:

if (!b.isEnabled()) b.setEnabled(true);
 

Uwidacznianie komponentów:

Wszystkie komponenty, oprócz tych wywodzących się z klasy Window, są domyślnie widoczne. Pojawiają się one na ekranie, gdy zostały dodane do jakiegoś kontenera i kontener jest/został uwidoczniony.

W trakcie działanie programu można czynić komponenty niewidocznymi i przywracać ich widzialność, np:

JButton b = new JButton(...);
....
b.setVisible(false); // stanie się niewidoczny
...
if (!b.isVisible()) b.setVisible(true); // gdy niewidoczny, uwidaczniamy


Przezroczystość komponentów


Wszystkie komponenty AWT (jako ciężkie) są nieprzezroczyste (isOpaque() zwróci true).


Komponenty lekkie mogą być przezroczyste lub nie.


Domyślnie większość lekkich komponentów Sw
ingu jest nieprzezroczysta.

Wyjątkiem jest etykieta JLabel.

Zatem aby ustalić tło etykiety Swingu musimy napisać:

JLabel l = new JLabel("Jakaś etykieta");
l.setOpaque(true);
l.setBackground(Color.yellow);


7.6 Własne komponenty i rysowanie

Warto zastanowić się nad tym, w jaki sposób komponenty pojawiają się na ekranie?

Otóż odpowiada za to metoda paint. To ona "maluje" komponenty na ekranie,
Klasa każdego komponentu zawiera definicję metody public void paint(Graphics).
Metoda ta jest zdefiniowana w klasie Component (od której pochodzą wszystkie komponenty). W klasach konkretnych komponentów AWT jest ona przedefiniowana.  W przypadku komponentów Swingu - metoda jest przedefiniowana w klasie JComponent, a z jej wnętrza wywoływane są polimorficznie inne metody, odpowiedzialne za rysowanie

Metoda paint(..) jest wywoływana przez JVM (na zasadzie callback, czyli "jestem i czekam, aż ktoś mnie wywoła" ) zawsze wtedy, gdy graficzny kontekst komponentu wymaga odświeżenia tj:
W metodzie paint(...) dostarczany jest kod, który powoduje wyrysowanie komponentu.
Ten kod będzie wywołany przez system - gdy trzeba odświeżyć komponent.

Wykreślanie ciężkich i lekkich komponentów różni się nie tylko pod względem odwołań do natywnego systemu graficznego (ciężkie się odwołują, lekkie – rysują bezpośrednio w obszarze "pożyczonym" od ciężkiego kontenera z wyższego poziomu hierarchii ), ale również  gdy chodzi  o wewnętrzne mechanizmy wykreślania (o czym więcej w przyszłym semestrze  w wykładzie o zaawansowanej  grafice).

Lekkie komponenty Swingu definiują, oprócz metody paint(...), trzy inne metody:

protected void paintComponent(Graphics g)   // wykreśla sam komponent
protected void paintBorder(Graphics g)          // wykreśla ramkę komponentu (jeśli jest)
protected void paintChildren(Graphics g)       // wykreśla hierarchię zawartych  komponentów

Są one wywoływane "z wnętrza" metody paint().
Ostatnia z trzech metod wymaga komentarz: jak można zauważyć ze schematu dziedziczenia klas, wszystkie lekkie komponenty Swingu są kontenerami. Dlatego dla każdego definiowana jest metoda paintChildren(...), wykreślająca komponenty zawarte w odrysowywanym komponencie.

Metody paint(...) nie wolno wołać z poziomu aplikacji.
Jeśli istnieje konieczność odrysowania komponentu przez aplikację, to należy użyć metody repaint(...).



Opisany skrótowo mechanizm pozwala na wykonywanie własnych rysunków na gotowych komponentach.

Aby przedefiniować sposób wykreślania komponentów dziedziczymy ich klasy i przedefiniowujemy:


Ten sposób jest użyteczny szczególnie w odniesieniu do "upiększania" gotowych komponentów AWT. Chociaż możemy go zastosować wobec komponentów Swingu, to istnieją lepsze podejścia związane z wykorzystaniem mechanizmów pluggable lookk & feel (o czym parę słów powiemy w wykładzie 11).

W istocie, dziedziczac klasy komponentów i przedefiniowując metody paint... tworzymy własne komponenty.
Te własne komponenty możemy budować całkiem od podstaw (wykorzystując "minimalne" komponenty, np. JComponent lub JPanel), albo też możemy skorzystać z gotowej funkcjonalności bardziej rozbudowanych komponentów (np. JButton), dziedzicząc ich klasy.

Niewątpliwie najczęściej będziemy wykorzystywać opisany sposób do prezentowania jakichś rysunków. Dlatego musimy wiedzieć w jaki sposób rysować podstawowe kszałty, napisy oraz wykreślać obrazy z plików graficznych.

Kluczem jest tu klasa Graphics. Referencja do obiektu tej klasy stanowi parametr metod paint...
Obiekt ten określa kontekst graficzny komponentu. Cóż to takiego?

Kontekst graficzny jest swoistym logicznym "urządzeniem wyjściowym". Zwykle jest to ekran komputera, ale może  to być np. wydruk lub bufor w pamięci. Logiczny kontekst graficzny może więc być związany z różnymi "urządzeniami wyjściowymi" na których "rysowany" jest komponent.


W klasie Graphics zdefiniowano wiele metod umożliwiających m.in.: I właśnie dzięki temu mamy pełną kontrolę nad tym co ma być rysowane w obszarze naszego komponentu.

M.in. dostępne są następujące metody (proszę przejrzeć zestaw metod klasy Graphics w dokumentacji):
 voiddrawLine(int x1, int y1, int x2, int y2)
          Rysuje linię prostą pomiędzy punktami  (x1, y1) i (x2, y2) 
 voiddrawOval(int x, int y, int width, int height)
          Rysuje okrąg (elipsę).
 voiddrawRect(int x, int y, int width, int height)
          Rysuje prostokąt 

Analogiczne metody fill... pozwalają na rysowanie wypełnionych kształtów (figur).
Rysowanie i wypełnianie odbywa się bieżącym kolorem kontekstu graficznego, który jest domyślnie kolorem pierwszego planu (Foreground) komponentu, ale może być dla potrzeb każdego rysunku zmieniany za pomocą metody setColor(Color) z klasy Graphics. Metoda getColor() zwraca bieżący (ustalony) kolor.

W metodach rysowania, wypisywania, malowania obrazów posługujemy się współrzędnymi.
Przy rysowaniu figur należy pamiętać o tym, że górny lewy róg komponentu ma współrzędne (0, 0). Zwiększanie wspólrzędnych następuje w prawo i w dół.

Współrzędne określają punkty POMIĘDZY odpowiednimi pikselami "urządzenia wyjścia".

Przy rysowaniu kształtów trzeba rozumować tak:
Zatem zrobienie ramki wokół komponentu wygląda tak:
public void paintComponent(Graphics g) {
  g.drawRect(0, 0, getWidth()-1, getHeight()-1);
}
Nieco inaczej wygląda sytuacja przy wypełnianiu figur:
public void paint(Graphics g) {
  g.fillRect(0,0,getWidth(),getHeight());
}
Tutaj wypełniane jest WNĘTRZE ścieżki po której idzie pióro wyimaginowanego plotera.


r Zobaczmy pierwszy przykład - proste  "upiększenie" przycisku małymi czerwonymi kwadracikami umieszczonymi w jego rogach (zob. rysunek)
Aby to osiągnąć, odziedziczymy klasę JButton i przedfiniujemy w niej metodę paintComponent. W tej metodzie wyrysujemy narożne kwadraciki. Uzyskamy w ten sposób w pełni funkcjonalny przycisk (mający wszystkie cechy JButton) z dodatkiem (raczej ilustracyjnym, bo jego celowość jest mało sensowna) w postaci czerwonych "narożników".

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

class MyButton extends JButton {

  public MyButton(String txt) {
    super(txt);
  }

  public void paintComponent(Graphics g) {
    super.paintComponent(g);
    int w = getWidth();       // aktualna szerokość ...
    int h = getHeight();      // i wysokość komponentu
    g.setColor(Color.red);    // ustalenie kolru rysunku
    // rysowanie kwadracików
    g.fillRect(0, 0, 10, 10);
    g.fillRect(w-10, 0, 10, 10);
    g.fillRect(0, h-10, 10, 10);
    g.fillRect(w-10, h-10, 10, 10);
  }
}

class MyButtonTest extends JFrame {
	
	Container cp = getContentPane();
	
	public MyButtonTest() {
	  MyButton mb = new MyButton("To jest przycisk");
	  cp.add(mb);
	  pack();
	  show();
	}

	public static void main(String args[]) {
	  new MyButtonTest();	
	}
} 
Zwróćmy uwagę na ważną kwestię.

Aby zagwarantować odpowiedni wygląd i funkcjonalność gotowych komponentów, których klasy dziedziczymy, w przedfiniowywanej metodzie paintComponent należy na początku wywołać metodę paintComponent z nadklasy



Budując własne komponenty calkowicie od podstaw (np. obszary do rysowania), wykorzystujemy zwykle możliwie "minimalne" klasy. 

Budowanie komponentów wizualnych od podstaw


Komponenty terminalne
Komponenty-kontenery
Ciężkie
komponenty
AWT
class NewComp extends Canvas {
 ...
}
class NewComp extends Panel {
...
}
Lekkie
komponenty
AWT
class NewComp extends Component {
...
}         
class NewComp extends Container {
...
}
Lekkie
komponenty Swingu
class NewComp extends JComponent {
...
}
class NewComp extends JPanel {
...
}

Przy budowaniu "od podstaw" ważne jest nie tylko przedefiniowanie metody paint, ale również metod określających minimalne, maksymalne i preferowane rozmiary komponentów. W przeciwnym razie nasze komponenty mogą być niewidoczne (klasy "minimalne", takie jak Canvas czy JComponent, dają komponenty o zerowych rozmiarach).

r Przykład: stworzyć rysunek siatki niebieskich linii (zob. rysunek). Obszarem rysowania będzie własny, zbudowany od podstaw, komponent dziedziczący klasę JComponent.
Musieliśmy zatem przedefiniwoać metody:

 public Dimension getMinimumSize();
 public Dimension getPrefferedSize();
 public Dimension getMaximumSize();

Uwaga: zwracają one referencję do obiektu klasy Dimension, który opisuje wymiary - szerokość i wysokość. Również w metodach set.. dla preferowanych, minimalnych i maksymalnych rozmiarów jako argument podajemy referencje do obiektu tej klasy. Obiekt taki możemy stworzyć za pomocą konstruktora Dimension(szerokośc, wysokośc).


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

class ObszarRysunku extends JComponent {
  Dimension d;

  public ObszarRysunku(int w, int h) {
    d = new Dimension(w, h);
  }

  public Dimension getMinimumSize() { return d; }
  public Dimension getPreferredSize() { return d; }
  public Dimension getMaximumSize() { return new Dimension(1000, 1000); }

  public void paintComponent(Graphics g) {
    super.paintComponent(g);
    int w = getWidth();
    int h = getHeight();
    g.setColor(Color.blue);
    g.drawRect(0,0,w-1,h-1);
    int hor = 10, vert = 10;
    while (hor < h) {
       g.drawLine(1, hor, w-1, hor);
       hor += 10;
    }

    while (vert < w) {
       g.drawLine(vert, 1 , vert, h-1);
       vert += 10;
    }
  }
}


class Rysunek extends JFrame {
	
	Container cp = getContentPane();
	
	public Rysunek() {
	  ObszarRysunku or = new ObszarRysunku(100, 100);
	  cp.add(or);
	  setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
	  pack();
	  show();
	}

	public static void main(String args[]) {
	  new Rysunek();	
	}
}

Czasami na rysunkach będziemy potrzebowali napisów.
Zwykle uzyskujemy je za pomocą metody void drawString(String s, int x, int y)

r Lokalizacja tekstu (x, y) - INACZEJ NIŻ PRZY RYSOWANIU KSZTAŁTÓW i FIGUR -  oznacza POŁOŻENIE LINII BAZOWEJ TEKSTU (base line). Różnicę obrazuje rysunek.




Aby dobrze rozplanować położenie tekstu należy uzyskać charakterystyki pisma i wykorzystać je przy układaniu tekstu. Służy temu klasa FontMetrics np.

FontMetrics fm;
fm = g.getFontMetrics() // zwraca metrykę dla bieżącego pisma kontekstu g
 
W klasie FontMetrics mamy np. metodę

  stringWidth(String s)

która zwraca szerokość zajmowaną przez napis s wyrażony w konkretnym piśmie (tej metryce).
Można ją wykorzystać np. do wycentrowania napisu w poziomie:
    public void paintComponent(Graphics g) {
        String s = "jakis tekst";
        int y = ... ; // położenie w pionie
        int w = getWidth();
        int h = getHeight();
        g.drawString(s, (w - g.getFontMetrics().stringWidth(s))/2, y);
        ...
        }

r Inne metody klasy FontMetrics pozwalają na rozmieszczanie napisów w pionie.
Mamy tu takie metody jak:
Ich znaczenie pokazuje rysunek (źródło: Java Tutorial).

Za pomocą metod klasy Graphics możemy także wykreślać obrazy z plików graficznych (typów JPEG, GIF i PNG).
Mówiąc ściślej, dostępne są metody wykreślania obrazów, które są obiektami klasy Image.
Aby uzyskać obiekt Image, reprezentujący obraz z pliku, możemy zastosować dwa podejścia: użyć klasy ImageIcon (o czym w następnym wykładzie, przy okazji omawiania interfejsu Icon) albo użyć metody getImage() z klasy Toolkit.
Głównym zadaniem klasy Toolkit jest zapewnienie współpracy pomiędzy komponentami AWT, a środkami platformy systemowej, szczególnie komponantami realizowanymi przez graficzne API systemu. Niewiele z metod tej klasy można i należy wykorzystywać w programach użytkowych. Do tych niewielu należy  np. metoda pobierania infrmacji o rozdzielczości ekranu oraz - właśnie - metoda getImage(), stosowana do uzsyakania obrazu z pliku graficznego.
Aby użyć metody getImage() musimy najpierw uzyskać obiekt klasy Toolkit. Można to zrobić m.in. za pomocą statycznej metody getDefaultToolkit() z klasy Toolkit.

Dostęp do obrazu z pliku możemy uzyskać za pomocą odwołania:

        Image img = Toolkit.getDefaultToolkit().getImage(nazwa_pliku);

Wykreślaniem obrazu w obszarze komponentu wizualnego zajmuje się metoda drawImage z klasy Graphics



Uzyskanie odpowiedniej referencji do obrazu z pliku za pomocą metody getImage()  nie powoduje załadowania obrazka z pliku
Ładowanie następuje w trakcie wyświetlania przez metodę drawImage. Zwraca ona sterowanie, gdy tylko część obrazu zostanie zaladowana i może być wyświetlona. Metoda paint  wołana jest przez system ponownie i ponownie, dopóki caly obraz nie zostanie załadowany i wyświetlony. Takie  ładowanie (i wyświetlanie) etapami - przy dużych obrazach - może trwać długo (za każdym razem odrysowywany jest od nowa cały komponent wizualny).

Dlatego w Javie zapewniono dwa sposoby, umożliwiające załadowanie obrazu przed wyświetleniem.
Pierwszy polega na dostarczeniu obiektu typu ImageObsrever. Referencja do tego obiektu jest ostatnim argumentem przekazywanym metodzie drawImage, a sam obiekt może zajmować się szczegółowym śledzeniem postępów ladowania obrazu, co pozwala na jednokrotne wykreślenie komponentu zawierającego obraz wtedy, gdy cały obraz jest załadowany. Nie będziemy (na razie) korzystać z tego sposobu, wspomnimy tylko, że ImageObserver jest interfejsem i że klasa Component implememntuje ten interfejs, zatem jako ostatni argument wywołania metody drawImage możemy podać referencję do dowolnego komponentu (w szczególności tego, w obszarze którego obraz jest wyświetlany).

Drugim sposobem na "zaczekanie na załadowanie obrazka" jest użycie klasy MediaTracker.
Nie daje on tak precyzyjnych informacji jak ImageObserver, ale za to jest łatwiejszy w użyciu i umożliwia śledzenie ładowania wielu obrazków (co jest użyteczne np. przy animacji).

Konstruktor klasy MediaTracker ma jeden argument - komponent na którym ewentualnie (ale niekoniecznie) będzie malowany obrazek
W zasadzie możemy tu użyć dowolnego komponentu np. aplikacji dziedziczącej okno
Po stworzeniu obiektu MediaTracker dodaajemy do nego obrazy (do śledzenia).

 MediaTracker mt = new MediaTracker(this);
 Toolkit tk = Toolkit.getDefaultToolkit();
 Image img = tk.getImage(nazwaPliku);
 mt.addImage(img, 1);         // drugi argument - identyfikaor, pozwalający dzielić
                              // obrazy na grupy z różnymi priorytetami ładowania
Następnie żądamy od MediaTrackera, by rozpoczął ładowanie i  zaczekał na załadowanie wszystkich dodanych obrazków:
 try {
      mt.waitForAll();
 } catch(InterruptedException e) { System.exit(1); }
Albo – by ładował obrazy konkretnej grupy (waitForID(...))
Wywołanie metod waitForAll lub waitFor... wstrzymuje dzialanie aplikacji, dopóki wszystkie obrazy dodane do MediaTrackera (lub obrazy podanej grupy) nie zostaną załadowane.
To oczekiwanie na załadowanie może przerwane z zewnątrz - stąd potrzeba obsługi wyjątku InterruptedException.

Gdy obrazek jest już zaladowany możemy go wykreślić za pomocą metody drawImage np. w metodzie paintComponent klasy komponentu, który ma stanowić obszar prezentacji.

Jedna z przeciążonych wersji tej metody wygląda następująco.

    boolean drawImage(Image img,                    // obraz do wykreślenia
                                  int x, int y,                    // górny lewy róg w obszarze komponentu
                                  int width, int height,       //  szerokość i wysokość (skalowanie)
                                  ImageObserver observer)


r Jako przyklad zbudujmy klasę ImagePanel, której obiekty będą stanowić kontenery z obrazkiem z podanego pliku jako tłem. Do takiego kontenera możemy dodawać inne komponenty i będą prezentowane na tle obrazu. Inicjalne rozmiary kontenera będą równe rozmiarom obrazka, a przy zmianach rozmiarów kontenera obrazek stanowiący jego tło będzie reskalowany. Rysunek obok pokazuje taki kontener z dodanym przyciskiem "Jakiś przycisk".

Kod programu przedstawiono poniżej.
import javax.swing.*;
import java.awt.*;

class ImagePanel extends JPanel {

  Image img;
  boolean loaded = false; // czy obrazek został załadowany?

  public ImagePanel(String imgFileName) {
    loadImage(imgFileName);
  }

  public void paintComponent(Graphics g) {
    super.paintComponent(g);
    if (img != null && loaded)
      g.drawImage(img, 0, 0, getWidth(), getHeight(), this);
    else
      g.drawString("Bez obrazka", 10, getHeight() - 10);
  }

  private void loadImage(String imgFileName) {
    img = Toolkit.getDefaultToolkit().getImage(imgFileName);
    MediaTracker mt = new MediaTracker(this);
    mt.addImage(img, 1);
    try {
      mt.waitForID(1);
    } catch (InterruptedException exc) { }
    int w = img.getWidth(this);       // szerokość obrazka
    int h = img. getHeight(this);     // wysokość obrazka
    if (w != -1 && w != 0 && h != -1 && h != 0) {
      loaded = true;
      setPreferredSize(new Dimension(w, h));
    }
    else setPreferredSize(new Dimension(200,200));
  }

}

class ImagePanelTest extends JFrame {
	
	Container cp = getContentPane();
	
	public ImagePanelTest(String fname) {
	  ImagePanel p = new ImagePanel(fname);
          p.add(new JButton("Jakiś przycisk"));
	  cp.add(p);
	  setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
	  pack();
	  show();
	}

	public static void main(String args[]) {
	  new ImagePanelTest(args[0]);	// argument - nazwa pliku graficznego
	}
}
Komentarze i uwagi.

Kończąc to syntetyczne "wprowadzenie do rysowania" trzeba powiedzieć, że w Javie istnieją dużo bardziej rozbudowane możliwości graficzne. W zakresie grafiki dwuwymiarowej dostarcza ich klasa Graphics2D oraz szereg innych związanych z nią klas. Z zaawansowanymi możliwościami graficznymi, również w obszarze przetwarzania obrazów i multimediów, zapoznamy się w przyszłym semestrze.

 

7.8. Komponenty Swingu a wielowątkowość

Przypomnijmy, że interakcja użytkownika z aplikacją graficzną odbywa się poprzez obsługę zdarzeń (takich jak np. kliknięcie w przycisk). Obslugą zdarzeń zajmuje się specjalny, uruchamiany przez JVM równolegle z głównym wątkiem aplikacji graficznej, wątek obslugi zdarzeń.
Oczywiście, zanim dojdzie do interakcji z użytkownikiem, okno aplikacji musi być uwidocznione. Uwidoczneinie okna uwidacznia wszystkie zawarte w nim komponenty wizualne. Powiada się, że wtedy komponenty są realizowane.

Komponent uznaje się za zrealizowany wtedy, gdy zostaje uwidoczniony lub gdy okno, w którym się znajduje zostaje spakowane


Komponenty Swingu zostały zbudowane w taki sposób, że wywołanie metod na ich rzecz po realizacji powinny być dokonywane wyłącznie w wątku obsługi zdarzeń.

Niewątpliwie łatwo i naturalnie można spełnić ten warunek w trakcie obsługi zdarzeń - w metodach obsługujących zdarzenia, bowiem metody te wywoływane są w wątku obsługi zdarzeń.
We wszystkich innych przypadkach kod, który działa na komponentach Swingu, powinniśmy umieścić "do wykonania" w wątku obsługi zdarzeń. Służą temu statyczne metody invokeLater oraz invokeAndWait z klasy SwingUtilities.
Ich argumentem jest referencja do obiektu klasy implementującej interfejs Runnable.
W zdefiniowanej w tej klasie metodzie run() umieszczamy kod operujący na komponentach Swingu. Metoda invokeLater  przekaże ten kod do wykonania przez wątek obsługi zdarzeń (umieści kod w kolejce zdarzeń),  wykonanie bieżącego wątku będzie kontynuowane, natomiast nasz kod umieszczony w kolejce zdarzeń zostanie wykonany wtedy, gdy wątek obslugi zdarzeń obsłuży zdarzenia znajdujące się w kolejce przed nim.

Tak samo działa metoda invokeAndWait, z tą różnicą, że po jej wywołaniu dzialanie bieżącego watku zostanie wstrzymane do chwili wykonania przekazanego kodu przez wątek obsługi zdarzeń.

Prezentuje to ilsutracyjny schemat:

    SwingUtilities.invokeLater( new Runnable() {
       public void run() {
         // tu działania na komponentach Swingu
       }
    });


Od tej reguły są wyjątki. Mianowicie, niektóre metody działające na komponentach Swingu są wielowątkowo bezpieczne (mogą być wywoływane z wielu watków, nie tylko z wątku obsługi zdarzeń). Należy do nich np. metoda repaint().

7.9. Podsumowanie


Wykład stanowił przegląd komponentów wizualnej interakcji użytkownika z programem. Niejako z lotu ptaka poznaliśmy dostępne w Javie komponenty. Przyjrzeliśmy się także wspólnym, dla wszystkich bez wyjątku komponentów, właciwościom i sposobom ich pobierania i ustalania. Ostatni punkt ktrótko wprowadził nas w elementy grafiki, co umożliwia wykonywamnie prostych rysunków i umieszcanie obrazów "na komponentach".

7.10. Zadania i ćwiczenia


Zad 1. Właściwości komponentów

Stworzyć okno ramowe JFrame z tytułem "Prosty edytor", zawierające komponent JTextArea (wielowierszowe pole edycyjne).
Zapewnić możliwość ustawiania z wiersza poleceń (przy uruchamianiu aplikacji) rodzaju i rozmiaru pisma oraz kolorów tła i pisma.

Argumenty wywołania:

Jeśli w wywołaniu nie podano argumentów, domyślne wartości winny być takie:

typ pisma = "Dialog"
rozmiar pisma = 14
kolor tła = niebieski
kolor pisma = biały

Wywołanie aplikacji powinno być możliwe:

Np.
java Zad1 -> niebieski edytor z pismem Dialog 14 w kolorze białym
java Zad1 Monospaced -> niebieski edytor z pismem Monospaced 14 w kolorze białym
java Zad1 Dialog 18  -> niebieski edytor z pismem Dialog 18 w kolorze białym
java Zad1 Dialog 18 0 0 0 -> czarny edytor z pismem Dialog 18 w kolorze białym
java Zad1 Dialog 18 255 255 255 0 0 0 -> biały edytor z pismem jw.


 
Zad. 2. Wykres

Napisać aplikację, która wczytuje plik tekstowy.
Aplikacja ma zliczać częstotliwość wystąpienia poszczególnych znaków w pliku i podać wynik graficznie - w postaci wykresu słupkowego, na którym szerokość słupków jest proporcjonalna do częstości występowania znaków, przy czym minimalna częstość jest oznaczana kolorem sarym, maksymalna - czerwonym, a posrednie - niebieskim. Za słupkami na wykresie pokazać liczby, oznaczające odpowiednie częstości. Obrazuje to poniższy rysunek.
r



Zad. 3.  Obrazki

Napisać program, który wyświetla kolejno co 5 sekund pliki graficzne znajdujące się w podanym katalogu.
Np. jeśli w podanym katalogu znajdują się pliki pool1.jpg, pool2.jpg i bview2.jpg, to w oknie aplikacji mają pojawiać się co 5 sekund kolejne obrazki z tych plików:

r
r
r